スプライトダブラとは、VCOUNT割り込み(ラスタ割り込み)でスプライト情報を書き換える方法です。この技術を用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代でも使われていました。仮にスプライト0を画面の左上に表示したとします。赤線は現在のVCOUNTラインです。
スプライト0が表示された後、VCOUNT割り込みでスプライト0の位置を変更します。結果、画面に2つ表示することが出来ます。
スプライト0〜スプライト127までに対して行えば256個を表示することが出来るという寸法です。
GBAではスプライト情報を以下のような形で持っています。このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではVCOUNT割り込み処理でOAMを書き換えることになります。
typedef struct { u16 attr0; // Y 座標などのデータ u16 attr1; // X 座標などのデータ u16 attr2; // タイル情報、パレット情報などのデータ u16 dummy; } OBJATTR;
割り込み中はあまり時間のかかる処理を行うことが出来ません。ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。そこで事前に書き換え後のスプライト情報を用意しておいて、それらのデータをDMA転送させる方法を取ります。
最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータを用意します。割り込みの際に少しづつデータをずらして上書きコピーしていけば、表示することが出来きるわけです。VCOUNT基準、つまりOBJATTRデータはy軸に沿ってソートされている必要があります。
y軸を4ライン毎に区切って、それらを一塊のブロックとしてDMA転送します。正直日記さんのソースコードを流用しました。
//--------------------------------------------------------------------------- #define BULLET_MAX_CHR_CNT 512 #define BULLET_MAX_IDX_CNT 42 // (縦画面160+非表示8) / 4 #define INT2FIX(A) ((A)<<7) #define FIX2INT(A) ((A)>>7) //--------------------------------------------------------------------------- typedef struct { bool is; s16 x; s16 y; s16 mx; s16 my; } ST_BULLET_CHR; typedef struct { s16 maxCnt; // 最大数 s16 idxCnt[BULLET_MAX_IDX_CNT]; // yラインを41分割したカウント数 ST_BULLET_CHR chr[BULLET_MAX_CHR_CNT]; // 弾情報 } ST_BULLET;
//--------------------------------------------------------------------------- IWRAM_CODE void BulletExec(void) { _Memset(&Bullet.idxCnt, 0x00, sizeof(Bullet.idxCnt)); s32 i; for(i=0; i<BULLET_MAX_CHR_CNT; i++) { if(Bullet.chr[i].is == FALSE) { continue; } Bullet.chr[i].x += Bullet.chr[i].mx; Bullet.chr[i].y += Bullet.chr[i].my; s32 xx = FIX2INT(Bullet.chr[i].x); s32 yy = FIX2INT(Bullet.chr[i].y); if(xx <= -8 || xx >= 240 || yy <= -8 || yy >= 160) { Bullet.chr[i].is = FALSE; Bullet.maxCnt--; continue; } s32 idx = (yy + 8) / 4; Bullet.idxCnt[idx]++; } }
まず4ライン毎に区切られた各ブロックにそれぞれ幾つスプライト(弾)があるかカウントします。例えばBullet.idxCnt[0]は-7〜-5ラインに含まれる数。Bullet.idxCnt[1]は-4〜-1ラインに含まれる数といった形です。次に、512個分のOBJATTRに対してソートを行います。
//--------------------------------------------------------------------------- IWRAM_CODE void SprExec(void) { SprInitItem(); s32 i, cnt = 0; ST_SPR_ITEM* pW[SPR_MAX_IDX_CNT]; for(i=0; i<SPR_MAX_IDX_CNT; i++) { // 各VCOUNTでDMA転送するバッファ位置を格納 Spr.idx[i] = cnt; // ソートで使うワークポインタを格納 pW[i] = &Spr.item[cnt]; cnt += Bullet.idxCnt[i]; } // 弾情報ポインタを取得 ST_BULLET_CHR* pS = (ST_BULLET_CHR*)&Bullet.chr; // 最初の弾情報を取得 while(pS->is == FALSE) { pS++; } // ソート開始 s32 max = Bullet.maxCnt; for(i=0; i<max; i++) { s32 x = FIX2INT(pS->x); s32 y = FIX2INT(pS->y); u32 y4 = (y + 8) / 4; pW[y4]->attr0 = OBJ_16_COLOR | OBJ_SQUARE | (y & 0x00ff); pW[y4]->attr1 = OBJ_SIZE(0) | (x & 0x01ff); pW[y4]->attr2 = (1+Spr.chrNo); pW[y4]++; // 次の弾情報を取得 do { pS++; } while(pS->is == FALSE); } // スプライトアニメーション Spr.chrNo++; Spr.chrNo &= 0x03; }
4ライン毎に区切られた各ブロック数は決まっているので、各ブロック先頭の開始ワークポインタも確定することができます。関数の後半では弾情報を頭から順番に見ていって、各ワークポインタを通してソートを行います。
ソートが決まったら次は表示についてです。まずVBlank割り込みでOAMの0クリアを行います。その後にVCOUNT毎に弾情報を流し込んでいきます。
//--------------------------------------------------------------------------- IWRAM_CODE void SprVBlank(void) { // 0クリア OAM REG_DMA0SAD = (u32)&Spr.zero; REG_DMA0DAD = (u32)OAM; REG_DMA0CNT = (u32)128*2 | (DMA_SRC_FIXED | DMA_DST_INC | DMA32 | DMA_ENABLE); Spr.oamCnt = 0; Spr.vCnt = 0; REG_DISPSTAT = (REG_DISPSTAT & STAT_MASK) | LCDC_VCNT | VCOUNT(SprVmap[Spr.vCnt]); }
注意点としては、n番目のブロックを転送するには、4ライン前のVCOUNT、つまり(n - 1) * 4ライン目で割り込みを発生させる必要があります。このタイミングで転送をスタートしておけば、実際にnブロック目が描写されるタイミング(n * 4ライン目) にはOAMへのDMA転送が終了している計算になります。転送ウェイトを考慮して-4ライン分も加えることにしました。
// VCOUNT割り込みライン s32 SprVmap[SPR_MAX_IDX_CNT+1] = { 218, // 0 218 = 228 - 8 - 4 222, // 1 226, // 2 0, // 3 4, // 4 8, // 5 12, // 6 16, // 7 20, // 8 24, // 9 28, // 10 32, // 11 36, // 12 40, // 13 44, // 14 48, // 15 52, // 16 56, // 17 60, // 18 64, // 19 68, // 20 72, // 21 76, // 22 80, // 23 84, // 24 88, // 25 92, // 26 96, // 27 100, // 28 104, // 29 108, // 30 112, // 31 116, // 32 120, // 33 124, // 34 128, // 35 132, // 36 136, // 37 140, // 38 144, // 39 148, // 40 152, // 41 0, // vblank待ち用 };
残りはVCOUNT毎の割り込みで、対応しているブロックのOBJATTRを転送していくだけです。DMA転送が2つあるのはリングバッファになっています。OAM領域の末端まで行ったら、転送の残りは先頭から開始する仕組みです。
//--------------------------------------------------------------------------- IWRAM_CODE void SprVCount(void) { s32 idx = Spr.idx[Spr.vCnt]; // 転送するSpr.itemのインデックス s32 cnt = Spr.idxCnt[Spr.vCnt]; // 転送する数 s32 all = Spr.oamCnt + cnt; // OAMインデックス + 転送する数 s32 sad0 = idx; s32 dad0 = Spr.oamCnt; s32 cnt0 = cnt; s32 sad1 = 0; s32 dad1 = 0; s32 cnt1 = 0; if(all >= SPR_MAX_OAM_CNT) { cnt0 = all - SPR_MAX_OAM_CNT; cnt1 = cnt - cnt0; sad1 = idx + cnt0; } OBJATTR* pS = (OBJATTR*)&Spr.item; OBJATTR* pD = (OBJATTR*)OAM; if(cnt0 != 0) { REG_DMA0SAD = (u32)&pS[sad0]; REG_DMA0DAD = (u32)&pD[dad0]; REG_DMA0CNT = (u32)cnt0*2 | (DMA_SRC_INC | DMA_DST_INC | DMA32 | DMA_ENABLE); } if(cnt1 != 0) { REG_DMA1SAD = (u32)&pS[sad1]; REG_DMA1DAD = (u32)&pD[dad1]; REG_DMA1CNT = (u32)cnt1*2 | (DMA_SRC_INC | DMA_DST_INC | DMA32 | DMA_ENABLE); } Spr.oamCnt += cnt; Spr.oamCnt &= (SPR_MAX_OAM_CNT - 1); Spr.vCnt++; REG_DISPSTAT = (REG_DISPSTAT & STAT_MASK) | LCDC_VCNT | VCOUNT(SprVmap[Spr.vCnt]); }
実際にはコピー先がOAMの最後に達した際には、wrap処理を行うなど細かい制御が必要になります。基本的には上記のようなロジックで割り込み処理を行います。
上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。そこで40個のOBJATTRからなるブロックを割り込みの毎にコピーしていく形にします。コピー先のOAMアドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。ただし、この構造の場合、一度に扱えるスプライト数が40個に制限されてしまうので、表示数を限界まで高めたい場合には上記の構造を取る必要があります。