SGDK学習メモ:No.6、画面のモード、状態、解像度などについて確認してみる
*以下SGDKは記述時点で最新版のSGDK 2.00 (january 2024)を使用しています
画面のモード等についての知識が曖昧なままなので、実際にコードを書きつつ確認してみます。WINDOW(後述)のコード記述方法も確認します。確認にはSGDK同梱のサンプルも使用します。
今回(も)かなり冗長ですが、比喩や卑下ではなく「私は不器用」ということでご了承ください(無類の不器用/免罪符)。
今回学習のベースにするのはメガドライブ版ダライアスなどで有名なHidecadeさんの
メガドライブのゲームの作り方 その2 - 画面モードとVRAMのマッピング | Arcade Cabinet
と
GENESIS Technical Overview (*PDF)
です。
GENESIS Technical Overviewは表紙に"CONFIDENTIAL"、"PROPERTY OF SEGA"とありますが、セガオフィシャルでゲームをリリースしたHidecadeさんのサイトでもこの資料(または類似資料)からの引用が行われており、「怒られは発生しないだろう」と判断しました。引用時には可読性を上げるためBold強調などを行っている場合があります。
使用される用語について
Sega Technical OverviewにTERMINOLOGY(用語集)という項目があります。
今回に関連する項目としては
「3. "CELL" は8 (pixel) x 8 (pixel)です」
「6 . VDPはVideo Display Processorを表します(の省略です)」
「8. VRAMはVDP RAMを表します、64K bytesのRAMはVDP経由でのみアクセスできます」(*SGDKではVDPアクセス用の関数が用意されている)
あたりです。
画面モード、WINDOW、背景のPLANEと優先度、最背面の背景色について
上図モードVの説明だとトータル4画面(4 planes)存在するように見えますが
とあり実際には3画面です。これは
WINDOWはSCROLL Aの一部で、WINDOWはSCROLL Aの一部分をスクロールしない(させない)ためのPLANEである
となっています(と私は認識しています)。
テスト用に作成したプログラムで確認します。
今回テスト用プログラムではリソースファイルを取り込まず、SGDKが用意している(取り込んでいる)デフォルトのフォント(res/image/font_default.png)を背景用データとして使用しています。
SGDKでは
WINDOW(上図青文字0~1) → VDP_setTileMapXY(WINDOW,tile,x,y);
SCROLL A(上図青文字3~5) → VDP_setTileMapXY(BG_A,tile,x,y);
とWINDOWとSCROLL Aにはそれぞれに値を設定する必要があります(*VRAMも別に割り当てる必要がある)。
SCROLL Aに指定したタイルがVDP_setWindowVPos()(またはVDP_setWindowHPos())によってWINDOWになるわけではない(←当初私はこう思っていた)、ということになります。
WINDOWは画面の上下左右のいずれかに設定可能です。
画面上 → VDP_setWindowVPos(FALSE、固定するCELL数);
画面下 → VDP_setWindowVPos(TRUE、固定するCELL数);
画面左 → VDP_setWindowHPos(FALSE、固定するCELL数);
画面右 → VDP_setWindowHPos(TRUE、固定するCELL数);
となります。
画面分割表示は「0爆弾」「IRQ」や「MMC5でなければ縦分割スクロールできない」なファミコンと比べるとSGDKの手助けもあって劇的に楽です。
背景及びスプライトのPLANEはプライオリティを設定可能です。背景は通常
SCROLL B < SCROLL A/WINDOW
なのですが、SCROLL Bのプライオリティを上げることで
SCROLL B > SCROLL A/WINDOW
となります。
最背面の背景はパレットの番号を指定することで任意の色に変更可能です。SGDKでは VDP_setBackgroundColor(パレットの番号(0~63)) を実行します。初期値は0です。
画面解像度について
*本項目は基本NTSCについての話となります、ちなみにフランスにはSECAMモデルのメガドライブが存在したそうです
メガドライブの解像度には大別して以下の2つ("THERE ARE TWO MOEDS")
32 cell wide mode(H32) → 32*28 CELL (256*224 PIXEL)
40 cell wide mode(H40) → 40*28 CELL (320*224 PIXEL)(SGDKのデフォルトはこれ)
が存在します。
横の解像度が8 CELL(64 PIXEL)増減し、縦の解像度は28 CELL(224 PIXEL)固定です。
PALの場合は縦解像度30 CELL(240 PIXEL)も選択可能だそうですが("a vertical size of 30 cells (240 dots) is selectable")が、今回これは無視しておきます。
(PALにしたい場合は要レジスタへの設定?←未確認)
SGDKでは
VDP_setScreenWidth320() → 40 cell wide mode
VDP_setScreenWidth256() → 32 cell wide mode
で切り替え可能です。今回ボタン押下により解像度を切り替えできるようにしましたが、これが実機の場合にどのような挙動になるかは未確認です。またエミュレータにより挙動も異なりました。
解像度によって表示できる最大スプライト数、横にちらつかずに並べられるスプライト数も異なります。
32 cell wide mode → 最大 64 sprites、16 sprites per scanline
40 cell wide mode → 最大 80 sprites、20 sprites per scanline
となっています。
ちなみにファミコン、セガ・マークIII/マスターシステムだとちらつき無しで横に並べられるスプライト数は最大8です(=32 cell wide modeはマスターシステム互換ではない)。
ラエルさんのサイトによると
だそうです。
32 cell wide modeで表示できるスプライト数が減るのは「解像度が低いときはクロックを下げているから」(意訳)ということになるのでしょうか?
ついでに32 cell wide modeについての考察動画と、動画中で言及されている32 cellを使用しているゲームリストも紹介しておきます。
How to Use 256 Low Resolution Mode on Sega Genesis & Mega Drive - Beginners Game Dev Tutorials - YouTube
Genesis USA games that use 256x224 mode during main gameplay. List created & tested by Firebrandx (wolff@firebrandx.com) Updated March 27, 2019.
https://www.firebrandx.com/downloads/Genesis-256-Mode-List.txt
基本的にはデフォルトの40 cell wide modeを使用する、ということで良さそうです。
上記動画を見ても32 cell wide modeのメリットがいまいち感じられませんでした(携帯機でもないのに解像度を「下げる」とクロックが下がり表示可能スプライト数が減る/ちなみにメガジェットはACアダプタ駆動)が、動画のコメントにある
「32 cell wide modeだとコンポジット接続、RF端子接続で画像が鮮明になる」(意訳)
が個人的には「なるほど」と感じました。BlastEmのように拡大表示されるのであれば、特にブラウン管だと効果がありそうです。
スクロール面のサイズの設定とVRAMのマッピング(一部)について
メガドライブのVRAM容量は64K Bytes(0x0000-0xFFFF)です(*CRAM/パレットは別領域)。
VRAM中のマッピングは自由に配置可能ですが、背景とスプライトに使用する画像データ(PATTERN GENERATOR TABLE and SPRITE GENERATOR TABLE)はVRAM中の先頭(0x0000)から固定で確保されます。
スクロール面(SCROLL PLAYFIELD)の設定可能なサイズについては、先に引用済みのDISPLAY SPECIFICATION OUTLINEに記載があります。該当部分をもう一度引用します。
32 CELL、64 CELL、128 CELLの組み合わせでトータル6パターンあります。
また別のページに以下のような説明があります。
スクロール面は1 CELL("char is 8 x 8 pixels")毎に"each char position takes 2 Bytes"ということなので、SCROLL A、Bが(それぞれ)必要とするメモリは
(1) 幅32CELL * 高さ32CELL * 2Bytes = 2048Bytes(0x800) = 2KiB
(2) 幅32CELL * 高さ64CELL * 2Bytes = 4096Bytes(0x100) = 4KiB
(3) 幅32CELL * 高さ128CELL * 2Bytes = 8192Bytes(0x200) = 8KiB
(4) 幅64CELL * 高さ32CELL * 2Bytes = 4096Bytes(0x100) = 4KiB
(5) 幅64CELL * 高さ64CELL* 2Bytes = 8192Bytes(0x200) = 8KiB
(6) 幅128CELL * 高さ32CELL* 2Bytes = 8192Bytes(0x200) = 8KiB
となります。
スクロール面のサイズ変更には
VDP_setPlaneSize ()
を使用します。
VDP_setPlaneSize()は引数setupVramにTRUEを渡すことで、SGDKがVRAMマッピング+タイルの再配置をしてくれます(*タイル再配置については今回未確認)。
今回VDP_setPlaneSize(幅,高さ,TRUE)を実行してどの様にVRAMがマッピングされるのか、Gensのデバッグ機能VDP Registersを使用して確認してみました。VDPの各レジスタの説明はGENESIS Technical Overview ,PP.22-26 に記載があります。以下の画像では今回の内容に関係ある部分を赤で囲みました。
パターン1) スクロール面ごとに必要なVRAMが2KiB
・(1) 幅32CELL * 高さ32CELL
SCROLL AにはVRAM 2KiBが割り当てられていますが、WINDOWとSCROLL Bには同一のアドレス(0xC000-0xDFFF)がマッピングされ、更にVRAMの割り当てが8KiBとなっています。容量的に余裕があるはずなのになぜWINDOWとSCROLL Bで同じアドレスがマッピングされるのか、また2KiBより大きい容量(8KiB)が設定されている理由は現時点で理解できていません。
とりあえずSCROLL AとSCROLL Bには容量2KiB、またはそれ以上が割り当てられていることがわかりました。
今回WINDOWとSCROLL Bに同一のアドレスが設定されていますが、このような場合の挙動については次回以降の宿題としておきます。
とあり、スクロール面とWINDOWに同一(重複)アドレスを割り当てることは仕様上問題ないとされています。
解像度 幅32CELL * 高さ32CELLをどのような状況で使用したらよいのか理解できていませんが、固定画面系のゲームで多めに画像を使いたい(PATTERN GENERATOR TABLE and SPRITE GENERATOR TABLEにデータを多く置きたい)場合とかでしょうか?
↓
*2024/05/10追記
sgdk Documentation及びソースコード(vdp.c)を軽く追ってわかったのですが、VRAMのマッピング時に指定するアドレス(の開始位置)は特定の値の倍数を指定することというルールがありました。
SCROLL Bに(必要な2KiBに対して過剰とも言える)8KiBが設定されているのはこれが理由でした(と今のところ理解しておきます)。
SCROLL A、SCROLL B以外でも倍数値は異なりますが同様のルール(縛り)が存在するため、それを踏まえてマッピングする必要があります。
SCROLL A、SCROLL Bのアドレスには0x2000(8192/8KiB)の倍数を指定するルールのため、実質的に0xC000または0xE000のいずれかを指定せざるを得ないでしょう。スクロールする背景が1面で足りる場合はSCROLL A、SCROLL Bに同一のアドレスを設定する、とか。
パターン2) スクロール面ごとに必要なVRAMが4KiB
・(2) 幅32CELL * 高さ64CELL
・(4) 幅64CELL * 高さ32CELL
先のパターン1)と異なり、今回はSCROLL A、SCROLL B、WINDOWのそれぞれに4KiBが割り当てられています。
SGDKのデフォルトでは (4) 幅64CELL * 高さ32CELL が設定されており、Hidecadeさんのサイトでも
とあります。
パターン3) スクロール面ごとに必要なVRAMが8KiB
・(3) 幅32CELL * 高さ128CELL
・(5) 幅64CELL * 高さ64CELL
・(6) 幅128CELL * 高さ32CELL
SCROLL A、SCROLL Bに8KiB、WINDOWに4KiBが割り当てられています。今回WINDOWには4KiBが割り当てられていますが、「とりあえずWINDOWには4KiBを用意した」「WINDOWにもっとメモリが必要なら自分で設定してくれ」ということでしょうか?
↓
*2024/05/11追記
Sega Technical Overview P.55に"WINDOW PATTERN NAME TABLE MAX 4K BYTES"とありました。P.79には"There are 2K bytes for WINDOW PATTERNNAME TABLE in H32 cell mode, and 4K byte area in H 40 cell mode."とあり、PP.80-81のVRAMマッピングサンプルでもWINDOWにはH32で2KiB、H40で4KiBの割り当てです(=通常H40なので、WINDOWを使用するのであれば4KiBを割り当てることになる)。
とあるのですが、これはつまり「背景面の画像が背景面に指定したサイズ以下ならスクロール時に画像(再)読み込みが不要」となります。
SGDKのサンプルにconsole(sample\sys\console)が存在するのですが、これのmain.cで
VDP_setPlaneSize(128, 32, TRUE);
が行われており、つまり解像度1024PIXEL*256PIXEL(=(128*8)*(32*8))以下の画像であれば再読み込みが不要となります。
consoleで使用している背景用の画像ファイル
sample\sys\console\res\R-Type_FG.png
はPLANEに指定しているサイズと同値の1024 PIXEL * 256 PIXELで
GensのPlane Explorerで確認すると、スクロールしても再読み込みが行われていないことがわかります。
今回かなり長くなってしまったので、VRAMのマッピングについての残りは次回にします。
今回作成したソースファイルも載せておきます。
#include <genesis.h>
//VRAMアドレス
#define HSCROLL_TABLE_ADDR 0xA800
#define SPRITE_LIST_ADDR 0xAC00
#define WINDOW_ADDR 0xB000
#define BGA_ADDR 0xC000
#define BGB_ADDR 0xE000
bool isBgBPriority=FALSE;
bool isScreen256=FALSE;
bool isSetAddress=false;
static const int sciSlash = 0x05AF; //文字"/"、1455
static const int sciA = 0x05C1; //文字"A"、1473
static const int sciOne = 0x05B0; //文字"1"、1456
s16 currentScreenX=0;
s16 currentScreenY=0;
u8 currentBackgrouondColor=0;
int currentPlaneSizesIndex=0; //0-5が設定される想定
static const int PLANE_SIZES[2][6] = {{32,32,32,64,64,128}, {32,64, 128,32,64,32}};
//背景初期化
void bgInit(){
//BG_B、A
VDP_fillTileMapRect(BG_B,TILE_ATTR_FULL(PAL0,FALSE,FALSE,FALSE,sciSlash),0,0,39,27);
//BG_B,AとB
VDP_fillTileMapRect(BG_B,TILE_ATTR_FULL(PAL1,FALSE,FALSE,FALSE,sciA),39,0,1,27);
VDP_fillTileMapRect(BG_B,TILE_ATTR_FULL(PAL1,FALSE,FALSE,FALSE,sciA+1),0,27,39,1);
//BG_B,C
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,FALSE,FALSE,FALSE,sciA+2),39,27);
//0~2はWINDOW(BG_A)
VDP_setTileMapXY(WINDOW,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne),0,0);
VDP_setTileMapXY(WINDOW,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne+1),31,0);
VDP_setTileMapXY(WINDOW,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne+2),39,0);
//3~5はBG_A
VDP_setTileMapXY(BG_A,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne+3),0,27);
VDP_setTileMapXY(BG_A,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne+4),31,27);
VDP_setTileMapXY(BG_A,TILE_ATTR_FULL(PAL3,FALSE,FALSE,FALSE,sciOne+5),39,27);
}
//背景 BG_B のpriorityを設定
void setBGBPriority(){
isBgBPriority = !isBgBPriority;
//Slash
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL0,isBgBPriority,FALSE,FALSE,sciSlash),0,0);
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL0,isBgBPriority,FALSE,FALSE,sciSlash),31,0);
//A
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,isBgBPriority,FALSE,FALSE,sciA),39,0);
//B
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,isBgBPriority,FALSE,FALSE,sciA+1),0,27);
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,isBgBPriority,FALSE,FALSE,sciA+1),31,27);
//C
VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,isBgBPriority,FALSE,FALSE,sciA+2),39,27);
//VDP_setTileMapXY(BG_B,TILE_ATTR_FULL(PAL1,isBgBPriority,FALSE,FALSE,sciA+2),39,27);
}
//VDP アドレスログ出力、未使用
void logVramAddresses(){
kprintf("VDP_getBGAAddress(): %u",VDP_getBGAAddress());
kprintf("VDP_getBGBAddress(): %u",VDP_getBGBAddress());
kprintf("VDP_getWindowAddress(): %u",VDP_getWindowAddress());
kprintf("VDP_getSpriteListAddress(): %u",VDP_getSpriteListAddress());
kprintf("VDP_getHScrollTableAddress(): %u",VDP_getHScrollTableAddress());
}
//VDP アドレスセット、未使用
void setVramAddresses(){
VDP_setBGBAddress ( BGB_ADDR );
VDP_setBGAAddress ( BGA_ADDR );
VDP_setWindowAddress ( WINDOW_ADDR );
VDP_setSpriteListAddress ( SPRITE_LIST_ADDR );
VDP_setHScrollTableAddress( HSCROLL_TABLE_ADDR );
}
//キーイベント(コールバック)
void myJoyHandler( u16 joy, u16 changed, u16 state)
{
if (joy == JOY_1){
//Aボタン、WINDOWの位置設定
if (state & BUTTON_A){
VDP_setWindowVPos(FALSE, 1);
}
//Bボタン、PLANE Bのプライオリティ変更
if (state & BUTTON_B){
setBGBPriority();
}
//Cボタン、解像度変更
if (state & BUTTON_C){
if(isScreen256){
VDP_setScreenWidth320();
}else{
VDP_setScreenWidth256();
}
isScreen256 = !isScreen256;
}
//Xボタン、最背面の色
if (state & BUTTON_X){
if(currentBackgrouondColor < 63 ){
currentBackgrouondColor++;
}else{
currentBackgrouondColor=0;
}
//パレットのindexを指定する
kprintf("(Before)VDP_getBackgroundColor(),%u",VDP_getBackgroundColor());
VDP_setBackgroundColor(currentBackgrouondColor);
kprintf("(Current)VDP_setBackgroundColor(),%u",VDP_getBackgroundColor());
}
//Yボタン、PLANEサイズの変更
if (state & BUTTON_Y){
// int currentPlaneSizesIndex=0; //0-5が設定される想定
// static const int PLANE_SIZES[2][6] = {{32,32,32,64,64,128}, {32,64, 128,32,64,32}};
VDP_setPlaneSize(PLANE_SIZES[0][currentPlaneSizesIndex],PLANE_SIZES[1][currentPlaneSizesIndex],TRUE);
if(currentPlaneSizesIndex >= 5){
currentPlaneSizesIndex=0;
}else{
currentPlaneSizesIndex++;
}
}
//Z
if (state & BUTTON_Z){
PAL_setColor(0,0xF00);
}
//スタートボタン、リセットによる初期化処理(*PLANEサイズの初期化はしない)
if (state & BUTTON_START){
VDP_setWindowVPos(FALSE, 0);
currentScreenX=0;
currentScreenY=0;
VDP_setVerticalScroll(BG_A,currentScreenY);
VDP_setHorizontalScroll(BG_A,currentScreenX);
VDP_setVerticalScroll(BG_B,currentScreenY);
VDP_setHorizontalScroll(BG_B,currentScreenX);
currentBackgrouondColor=0;
VDP_setBackgroundColor(currentBackgrouondColor);
}
}
}
//キーイベント(VBlankごと)
static void handleInput()
{
u16 value = JOY_readJoypad(JOY_1);
if (value & BUTTON_UP){
currentScreenX++;
}
if (value & BUTTON_DOWN){
currentScreenX--;
}
if (value & BUTTON_RIGHT){
currentScreenY++;
}
if (value & BUTTON_LEFT){
currentScreenY--;
}
VDP_setVerticalScroll(BG_A,currentScreenX);
VDP_setVerticalScroll(BG_B,currentScreenX);
VDP_setHorizontalScroll(BG_A,currentScreenY);
VDP_setHorizontalScroll(BG_B,currentScreenY);
}
// main
int main()
{
JOY_init();
JOY_setEventHandler( &myJoyHandler );
bgInit();
while(1)
{
handleInput();
SYS_doVBlankProcess();
}
return (0);
}
【了】