- 追加された行はこの色です。
- 削除された行はこの色です。
#author("2023-05-10T22:51:12+09:00","","")
#author("2023-05-12T14:41:21+09:00;2023-05-12T12:14:57+09:00","","")
* スプライトダブラ [#e3a083e3]
スプライトダブラとは、VCOUNT割り込みでスプライト情報を書き換える方法です。この技術をGBAで用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代に限られたスプライトを有効利用するために使われていました。単純な例ですと以下のようにスプライト0を画面の左上に表示していたとします。赤線は現在の描画済みVCOUNTラインです。
スプライトダブラとは、VCOUNT割り込み(ラスタ割り込み)でスプライト情報を書き換える方法です。この技術をGBAで用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代に限られたスプライトを有効利用するために使われていました。単純な例ですと以下のようにスプライト0を画面の左上に表示していたとします。赤線は現在の描画済みVCOUNTラインです。
#ref(1.png,nolink)
その際にスプライト0が表示された後のVCOUNT割り込みでスプライト0の位置を変更すると、スプライト0を画面に2つ表示することが出来ます。
#ref(2.png,nolink)
これをスプライト0〜スプライト127までに対して行えば、256個のスプライトを表示することが出来ます。
スプライト0〜スプライト127までに対して行えば256個のスプライトを表示することが出来ます。このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではVCOUNT割り込みの処理でOAMを書き換えることになります。
** 始めに [#w8cb0b00]
GBAではスプライトの情報を以下のような形で持っています。
typedef struct {
u16 attr0; // Y 座標などのデータ
u16 attr1; // X 座標などのデータ
u16 attr2; // タイル情報、パレット情報などのデータ
u16 dummy;
} OBJATTR;
このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではVCOUNT割り込みの処理でOAMを書き換えることになります。
** 方針 [#tc656ca4]
スプライトダブラを実装するにあたっては、割り込みの処理を工夫する必要があります。というのも割り込み処理はあまり時間のかかる処理を行うことが出来ません。ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。そこで事前に書き換え後のスプライト情報を用意しておいて、ラスタ割り込みではそれらのデータをDMA転送させる方法を取ります。
実装するにあたっては、割り込みの処理を工夫する必要があります。というのも割り込み処理はあまり時間のかかる処理を行うことが出来ません。ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。そこで事前に書き換え後のスプライト情報を用意しておいて、割り込みでそれらのデータをDMA転送させる方法を取ります。
** 基本的な構造 [#x16dece7]
スプライトダブラで最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータをあらかじめ用意しておき、ラスタ割り込みの際に少しづつデータをずらして上書きコピーしていけば512個のスプライトを表示することが出来ます。あらかじめ用意する 512個のOBJATTRはy軸に沿ってソートされている必要があります。
スプライトダブラで最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータをあらかじめ用意しておき、割り込みの際に少しづつデータをずらして上書きコピーしていけば512個のスプライトを表示することが出来ます。あらかじめ用意する 512個のOBJATTRはy軸に沿ってソートされている必要があります。
- ソートされた 512 個の OBJATTR を用意する。
- ラスタ割り込みで、それらを上書きコピーする。
- ソートされた 512個のOBJATTRを用意する。
- 割り込みで、それらを上書きコピーする。
** 実装方式 [#hc944e96]
前提として、y軸を4ライン毎に区切って、それらを一塊のブロックとして扱います。これはOAMへのDMA転送が最大2ライン分の時間がかかると考え、最高でも4ライン毎でしかDMA転送をスタートしないことに起因しています。VCOUNTが0,4,8,12, ..., 152の場合の割り込みでDMA転送をスタートさせます。ちなみに4ラインに区切るということは
前提として、y軸を8ライン毎に区切って、それらを一塊のブロックとして扱います。
** ソートされた512個のOBJATTRの用意 [#u7a70178]
- bullet,arm.h(抜粋)
//---------------------------------------------------------------------------
#define BULLET_MAX_CHR_CNT 512
#define BULLET_MAX_IDX_CNT 21 // (縦画面160+非表示8) / 8
#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ラインを21分割したカウント数
ST_BULLET_CHR chr[BULLET_MAX_CHR_CNT]; // 弾情報
} ST_BULLET;
class SpriteDoubler {
static const u32 LINEBLOCK = 4;
static const u32 MAXITEM = 512;
static const u32 NUM_BLOCK = 160 / LINEBLOCK;
- bullet.arm.c(抜粋)
//---------------------------------------------------------------------------
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) / 8;
Bullet.idxCnt[idx]++;
}
}
class CompiledObjattr {
OBJATTR sortedOBJATTR[MAXITEM]; // 512 個分のソート済み OBJATTR
まずは8ライン毎に区切られた各ブロックにそれぞれ幾つあるかカウントします。個数はBullet.idxCnt[]に格納されます。 例えばBullet.idxCnt[0]は-7〜-1ラインに含まれる数。Bullet.idxCnt[1]は0〜8ラインに含まれる数になります。次に、512個分のOBJATTRに対してソートを行います。
u32 itemNumInBlock[NUM_BLOCK]; // 各ブロックに含まれるスプライトの数
//---------------------------------------------------------------------------
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++)
{
// 8ラインごとの弾数を取得
Spr.idxCnt[i] = BulletGetIdxCnt(i);
// 各VCOUNTでDMA転送するバッファ位置を格納
Spr.idx[i] = cnt;
// ソートで使うワークポインタを格納
pW[i] = &Spr.item[cnt];
cnt += Spr.idxCnt[i];
}
// 弾情報ポインタを取得(高速化の為、モジュール化度外視)
ST_BULLET_CHR* pS = BulletGetChrPointer();
// 最初の弾情報を取得
while(pS->is == FALSE)
{
pS++;
}
// ソート開始
s32 max = BulletGetMaxCnt();
for(i=0; i<max; i++)
{
s32 x = FIX2INT(pS->x);
s32 y = FIX2INT(pS->y);
u32 y8 = (y + 8) / 8;
pW[y8]->attr0 = OBJ_16_COLOR | OBJ_SQUARE | (y & 0x00ff);
pW[y8]->attr1 = OBJ_SIZE(0) | (x & 0x01ff);
pW[y8]->attr2 = (1+Spr.chrNo);
pW[y8]++;
// 次の弾情報を取得
do {
pS++;
} while(pS->is == FALSE);
}
// スプライトアニメーション
Spr.chrNo++;
Spr.chrNo &= 0x03;
}
OBJATTR *objattrStartPosInBlock[NUM_BLOCK]; // sortedOBJATTR に対するポインタ。 各ブロックに対応するコピー先エリア。
};
};
8ライン毎に区切られた各ブロック数は決まっているので、ブロック先頭の開始ワークポインタを作ります。関数の後半ではソートされていない弾情報を頭から順番に見ていって、各ワークポインタに弾情報とスプライトを作成しています。
上で見たように、4ラインを一塊として見るので、OBJATTRのソートも正確にy軸に沿っていなくても4ライン分は同一視してかまわないことになります。
** 割り込みによるOAMのDMA転送 [#a76c368e]
まずはVBlank割り込みでOAMの初期設定を行います。その後に割り込みを行うべきVCOUNTの値を設定します。
そこで、まずは4ライン毎に区切られた各ブロックにそれぞれ幾つのスプライトが属するかをカウントします。これらの個数がitemNumInBlockに格納されます。 例えばitemNumInBlock[0]は0〜3ラインに含まれるスプライトの数、itemNumInBlock[1]は4〜7ラインに含まれるスプライトの数になります。
//---------------------------------------------------------------------------
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]);
}
次に、512個分のOBJATTRに対して、各ブロックに対応するコピー先エリアを設定します。 下記のmakeObjAttrStartPosInBlock関数で設定を行います。各ブロックに所属するスプライトについて*objattrStartPosInBlockにattributeを設定すれば良いことになります。
注意点としては、n番目のブロックを転送するには、8ライン前のVCOUNT、つまり(n-1)*8ライン目で割り込みを発生させる必要があります。このタイミングで転送をスタートしておけば、実際にnブロック目が描写されるタイミング(n*8ライン目) にはOAMへのDMA転送が終了している計算になります。こちらでは-2分の安全マージンを取っています。実機では動作が異なるかもしれません。
void makeObjAttrStartPosInBlock() {
objattrStartPosInBlock[0] = sortedOBJATTR;
for (u32 i = 0; i < NUM_BLOCK - 1; ++i) {
objattrStartPosInBlock[i + 1] = objattrStartPosInBlock[i] + itemNumInBlock[i];
}
}
// VCOUNT割り込みライン
const s32 SprVmap[SPR_MAX_IDX_CNT+1] =
{
218, // 0 218 = 228 - 8 - 2
226, // 1
6, // 2
14, // 3
22, // 4
30, // 5
38, // 6
46, // 7
54, // 8
62, // 9
70, // 10
78, // 11
86, // 12
94, // 13
102, // 14
110, // 15
118, // 16
126, // 17
134, // 18
142, // 19
150, // 20
0, // 21 // SprVBlank待ち用
};
このように事前準備を行った上でソート済み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);
}
}
** ラスタ割り込みによるOAMのDMA転送 [#a76c368e]
まずは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;
//---------------------------------------------------------------------------
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]);
}
++(p->nextIdx);
u32 vcount = (*(p->nextIdx) - 1) * LINEBLOCK;
REG_DISPSTAT = (REG_DISPSTAT & 0xff) | VCOUNT(vcount);
}
実際にはコピー先が OAM の最後に達した際には、wrap処理を行うなど細かい制御が必要になりますが、基本的には上記のようなロジックでラスタ割り込み処理を行います。
制約の話書く
** オマケ単純な構造のダブラ [#n80470da]
上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。 そこで、下記のような構造を取ると簡単にダブラを実装できます。
上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。 そこで、下記のような構造を取ると簡単にダブラを実装できます。この場合、40個のOBJATTRからなるブロックをラスタ割り込みの毎にコピーしていく形になるので、コピー先のOAM アドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。ただし、この構造の場合、一度に扱えるスプライト数が40 個に制限されてしまうので、スプライトの表示数を限界まで高めたい場合には上記の構造を取る必要があります。
この場合、40個のOBJATTRからなるブロックをラスタ割り込みの毎にコピーしていく形になるので、コピー先のOAM アドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。
ただし、この構造の場合、一度に扱えるスプライト数が40 個に制限されてしまうので、スプライトの表示数を限界まで高めたい場合には上記の構造を取る必要があります。
フレーム開始時 oam-another01.png
ラスタ割り込み(1) oam-another02.png
ラスタ割り込み(2) oam-another03.png
** 512個まで表示 [#xf0b528a]
-[[github:https://github.com/akkera102/gbadev-ja/tree/main/doc23%20%E3%82%B9%E3%83%97%E3%83%A9%E3%82%A4%E3%83%88%E3%83%80%E3%83%96%E3%83%A9]]
#ref(3.png,nolink)
** 出典元 [#pb4ac969]
- GBA で学ぶ古典的プログラミング (スプライトダブラ)
** 履歴 [#j0abcb62]
- 2023/05/11
- 2023/05/12