スプライトダブラとは、VCOUNT割り込みでスプライト情報を書き換える方法です。この技術をGBAで用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代に限られたスプライトを有効利用するために使われていました。単純な例ですと以下のようにスプライト0を画面の左上に表示していたとします。赤線は現在の描画済みVCOUNTラインです。

その際にスプライト0が表示された後のVCOUNT割り込みでスプライト0の位置を変更すると、スプライト0を画面に2つ表示することが出来ます。

これをスプライト0〜スプライト127までに対して行えば、256個のスプライトを表示することが出来ます。
GBAではスプライトの情報を以下のような形で持っています。
typedef struct {
u16 attr0; // Y 座標などのデータ
u16 attr1; // X 座標などのデータ
u16 attr2; // タイル情報、パレット情報などのデータ
u16 dummy;
} OBJATTR;
このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではVCOUNT割り込みの処理でOAMを書き換えることになります。
スプライトダブラを実装するにあたっては、割り込みの処理を工夫する必要があります。というのも割り込み処理はあまり時間のかかる処理を行うことが出来ません。ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。そこで事前に書き換え後のスプライト情報を用意しておいて、ラスタ割り込みではそれらのデータをDMA転送させる方法を取ります。
スプライトダブラで最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータをあらかじめ用意しておき、ラスタ割り込みの際に少しづつデータをずらして上書きコピーしていけば512個のスプライトを表示することが出来ます。あらかじめ用意する 512個のOBJATTRはy軸に沿ってソートされている必要があります。
前提として、y軸を4ライン毎に区切って、それらを一塊のブロックとして扱います。これはOAMへのDMA転送が最大2ライン分の時間がかかると考え、最高でも4ライン毎でしかDMA転送をスタートしないことに起因しています。VCOUNTが0,4,8,12, ..., 152の場合の割り込みでDMA転送をスタートさせます。ちなみに4ラインに区切るということは
class SpriteDoubler {
static const u32 LINEBLOCK = 4;
static const u32 MAXITEM = 512;
static const u32 NUM_BLOCK = 160 / LINEBLOCK;
class CompiledObjattr {
OBJATTR sortedOBJATTR[MAXITEM]; // 512 個分のソート済み OBJATTR
u32 itemNumInBlock[NUM_BLOCK]; // 各ブロックに含まれるスプライトの数
OBJATTR *objattrStartPosInBlock[NUM_BLOCK]; // sortedOBJATTR に対するポインタ。 各ブロックに対応するコピー先エリア。
};
};
上で見たように、4ラインを一塊として見るので、OBJATTRのソートも正確にy軸に沿っていなくても4ライン分は同一視してかまわないことになります。
そこで、まずは4ライン毎に区切られた各ブロックにそれぞれ幾つのスプライトが属するかをカウントします。これらの個数がitemNumInBlockに格納されます。 例えばitemNumInBlock[0]は0〜3ラインに含まれるスプライトの数、itemNumInBlock[1]は4〜7ラインに含まれるスプライトの数になります。
次に、512個分のOBJATTRに対して、各ブロックに対応するコピー先エリアを設定します。 下記のmakeObjAttrStartPosInBlock関数で設定を行います。各ブロックに所属するスプライトについて*objattrStartPosInBlockにattributeを設定すれば良いことになります。
void makeObjAttrStartPosInBlock() {
objattrStartPosInBlock[0] = sortedOBJATTR;
for (u32 i = 0; i < NUM_BLOCK - 1; ++i) {
objattrStartPosInBlock[i + 1] = objattrStartPosInBlock[i] + itemNumInBlock[i];
}
}
このように事前準備を行った上でソート済みOBJATTRを作成します。大雑把に下記のような処理でsortedOBJATTR を作成していきます。
void registObjAttr(u32 attr0, u32 attr1, u32 attr2) {
u32 idx = attr0 / LINEBLOCK;
OBJATTR *p = objattrStartPosInBlock[idx];
p->attr0 = attr0;
p->attr1 = attr1;
p->attr2 = attr2;
++(objattrStartPosInBlock[idx]);
}
こうすることで、ソート済みOBJATTRがsortedOBJATTR 上に構築されました。実際にこれらの処理を呼び出す処理は、GameEngine::compileBulletなどで行われています。
void
GameEngine::compileBullet()
{
SpriteDoubler::CompiledObjattr *p = SpriteDoubler::getIncurrentCompiledObjattr();
p->initialize();
BulletInfo *bi;
// ------------------------------------------------------------
bi = ListBullets::getFirstItem();
for (;;) {
if (bi == NULL) {
break;
}
p->registItemNumInBlock(bi->posy);
bi = ListBullets::iterator(bi);
}
// ------------------------------------------------------------
p->normalizeItemNumInBlock();
p->makeObjAttrStartPosInBlock();
// ------------------------------------------------------------
bi = ListBullets::getFirstItem();
for (;;) {
if (bi == NULL) {
break;
}
p->registObjAttr(bi->posy, bi->posx, OBJ_PALETTE(0) | bi->type);
bi = ListBullets::iterator(bi);
}
}
まずはVBlank割り込みでOAMの初期設定を行います。その後に、ラスタ割り込みを行うべきVCOUNTの値を設定します。
注意点としては、n番目のブロックを転送するには、4ライン前のVCOUNT、つまり(n-1)*4ライン目で割り込みを発生させる必要があります。このタイミングで転送をスタートしておけば、実際にnブロック目が描写されるタイミング(n*4 ライン目) にはOAMへのDMA転送が終了している計算になります。
void
SpriteDoubler::irq_vblank()
{
p->srcStart = p->sortedOBJATTR;
p->dstStart = OAM + DOUBLER_SPRITE_START;
u32 size = p->firstCopySize;
DMA1COPY(p->srcStart, p->dstStart, DMA32 | DMA_IMMEDIATE | (size * sizeof(OBJATTR) / 4));
p->nextIdx = p->itemContainIdx;
u32 vcount = (*(p->nextIdx) - 1) * LINEBLOCK;
REG_DISPSTAT = (REG_DISPSTAT & 0xff) | VCOUNT(vcount);
}
あとはVCOUNTでの割り込みで対応しているブロックのOBJATTRを転送していくだけです。割り込みの最後で次の割り込みの設定を行うことで次々とVCOUNT割り込みを発生させています。
void
SpriteDoubler::irq_vcount()
{
u32 num = p->itemNumInBlock[*(p->nextIdx)];
DMA1COPY(p->srcStart, p->dstStart, DMA32 | DMA_IMMEDIATE | (num * sizeof(OBJATTR) / 4));
p->srcStart += num;
p->dstStart += num;
++(p->nextIdx);
u32 vcount = (*(p->nextIdx) - 1) * LINEBLOCK;
REG_DISPSTAT = (REG_DISPSTAT & 0xff) | VCOUNT(vcount);
}
実際にはコピー先が OAM の最後に達した際には、wrap処理を行うなど細かい制御が必要になりますが、基本的には上記のようなロジックでラスタ割り込み処理を行います。
制約の話書く
上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。 そこで、下記のような構造を取ると簡単にダブラを実装できます。
この場合、40個のOBJATTRからなるブロックをラスタ割り込みの毎にコピーしていく形になるので、コピー先のOAM アドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。
ただし、この構造の場合、一度に扱えるスプライト数が40 個に制限されてしまうので、スプライトの表示数を限界まで高めたい場合には上記の構造を取る必要があります。 フレーム開始時 oam-another01.png ラスタ割り込み(1) oam-another02.png ラスタ割り込み(2) oam-another03.png