ホームチュートリアルプログラム
1-セットアップ 4-サウンド 7-FIFO インベーダー
2-フレームバッファ 5-SRAM 8-割り込み DSEmu
3-キー入力 6-ファイルシステム 9-マイク  
10-拡張回転バックグラウンド

Homebrew任天堂DS開発 Part 2

このチュートリアル シリーズのPart 1 ではスクリーンにテキストを表示しながら簡単な'hello World'スタイルプログラムをコンパイルして、どう動かすかについて解説しました。

このPart 2ではフレームバッファモードを使用して、任天堂DSスクリーンの1つをどのように利用するかを解説します。 DSの上の各スクリーンは各々さまざまな異なったモードに入れることができます。 各モードには利点と欠点がありますが、私はフレームバッファモードが直接最も描きやすいので、ここではフレームバッファモードを使用します。

フレームバッファ

'フレームバッファ'はスクリーンがメモリの部分に写像されるスクリーンモードです。 このメモリ領域にデータを書くと、そのデータはスクリーンに表示されます。 私が使用するつもりのモードでは、スクリーンの各画素は2バイトのデータによって表されます。 これはC言語タイプでは16ビットの符号なし整数に対応します。 私たちがこの記憶域に書くデータは555形式で表現される画素の色です。

私たちは手動で色を555形式に変換する方法を持っていないので、そうは私たちが赤・緑・青の画素の量を定義することができる'RGB15'と呼ばれる便利なマクロを使用します。 それぞれの赤・緑・青の成分を0から31した数です。 0は色が無いことを意味し、31はその色の最大を意味します。 ここに、いくつかの例があります:

RGB15
RGB15(31,0,0)
RGB15(0,31,0)
RGB15(0,0,31)
RGB15(0,0,0)
RGB15(31,31,31)

以下のコードは、フレームバッファの開始ポインタを計算し、青でスクリーンをどう満たすかを示します:

  uint16* framebuffer = ...;
  for(int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; ++i) 
    *framebuffer++ = RGB15(0,0,31);

フレームバッファに書くと、すぐに画素が描画されます。 フレームバッファモードに関して良いことはあなたがスクリーンでどこでも欲しいものを描くことができるということです、そして、それは任天堂DSで利用可能な2Dモードのハードウェア アクセレーションを直接使用します。

フレームバッファモードの欠点はあなたが自分ですべてをしなければならないということです。 あなたが自分でそれをするためにコードを書かないなら、'スプライト'、タイルド マップ、スクロールなどはありません。 このタイプのものにはDSの他のモードのほうがよいでしょう。そして、それらは後のチュートリアルでカバーする予定です。 差し当たり、フレームバッファは、多くの柔軟性を許容して、ハードウェアに私たちがなじみ深いです。

スクリーン

ハードウェアから見ると、DSには上部と下部に2つのスクリーンがあります。下部のスクリーンはタッチスクリーンになっています。

また、プログラミングの観点から見ると、'main'スクリーンと'sub'スクリーンの2つがあります。これらの2つのプログラミングスクリーンのどちらかを2つのハードウェアスクリーンにマップできます。 このサンプルではメインスクリーンの1つのスクリーンだけを使用し、ハードウェアの上部スクリーンに表示します。

メインスクリーンのモードを設定するために、私たちは'videoSetMode'と呼ばれる機能を使用します。 スクリーンに1個以上のフレームバッファをマップすることができます。 これで、二重バッファリングとページ切替のようなことをすることができます。 今回は1個のフレームバッファしか使用しないつもりですので、私たちが使用するモードは MODE_FB0 です。

  videoSetMode(MODE_FB0);

フレームバッファのための'VRAM'と呼ばれるメモリ領域は、前方より各領域は文字'A'から順番に名付けられています。 私たちがフレームバッファにどのVRAM領域を使用するつもりであるかをビデオシステムに設定する必要があります。 私たちは一番最初の VRAM_A を使用します:

  vramSetBankA(VRAM_A_LCD);

図形の描画

私たちがスクリーンに描画する予定の形は単色で簡単な正方形です。 これをするために、スクリーンに描画する形の x と y 座標、それを描く色、フレームバッファの開始位置ポインタにより描画します:

void draw_shape(int x, int y, uint16* buffer, uint16 color)
{
  buffer += y * SCREEN_WIDTH + x;
  for(int i = 0; i < shape_height; ++i) {
    uint16* line = buffer + (SCREEN_WIDTH * i);
    for(int j = 0; j < shape_width; ++j) {
      *line++ = color;
    }
  }
}

フレームバッファは列形式メモリでレイアウトされています。 したがって、スクリーンが200ピクセル幅ですので、フレームバッファの中の最初の200個の uint16 (400バイト)は最上段の最初のスクリーン列へのピクセルです。次の200個の uint16 (400バイト)は2番目の列に対応します。

draw_sharp関数は、描画する'y'座標および'x'座標に基づいて、スクリーンバッファの最初のピクセル位置を計算します。 SCREEN_WIDTHとSCREEN_HEIGHTがスクリーン幅と高さを返すために'ndslib'によって提供されたマクロであるのに注意してください。

この関数は、フレームバッファの適切な位置にピクセル色のデータをセットすることにより書く列の形を描画しています。

shape_height と shape_width はテスト目的のために変更するのに便利な静的変数です:

static int shape_width = 10;
static int shape_height = 10;

図形の移動

スクリーン内を動くような図形を描画するためには、現在の位置で図形を消して、新しい位置でそれを描き直す必要があります。 移動の前後に'x'と'y'位置の動向をおさえることによって、これを実行します。

static int old_x = 0;
static int old_y = 0;
static int shape_x = 0;
static int shape_y = 0;

簡単な方法は、背景色(この場合は黒)、古い'x'座標、'y'座標を指定して'draw_shape'関数を呼び出して図形を消去します。そして新しい'x'座標、新しい'y'座標、図形の色(個の場合は赤)で関数を呼び出して描画します。

  draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
  draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));

'draw_shape'関数に渡されたフレームバッファ VRAM_Aは、先ほど指定したメインスクリーンであることに注意してください。

完全には正しくない...

図形の位置を計算し描画する単純な'main'関数は、以下のようなものになります。

int main(void)
{
  powerON(POWER_ALL);
  videoSetMode(MODE_FB0);
  vramSetBankA(VRAM_A_LCD);

  while(1) {
    old_x = shape_x;
    old_y = shape_y;
    shape_x++;
    if(shape_x + shape_width >= SCREEN_WIDTH) {
      shape_x = 0;
      shape_y += shape_height;
      if(shape_y + shape_height >= SCREEN_HEIGHT) {
	shape_y = 0;
      }
    }      
    draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
    draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
  }

これを実行すると、非常に早い '傾斜した' 図形を見ることができます。 Dualisでのスクリーンショット.

垂直ブランク割り込み

前のサンプルで図形が傾斜している理由はスクリーン表示が動作していることにあります。ハードウェアは1/60秒ごとにスクリーンを再描画しています。この単位でフレームバッファのピクセルはハードウェアスクリーンのピクセルに複写されます。

これが起こっている間に、main関数内でスクリーンに描画されるフレームバッファの内容を変更しています。従って、図形を消去する直前にハードウェアが図形を描画してしまうと、それはすぐには消去されません。次にハードウェアがそれを描画する直前に新しい図形を描画すると、古い図形と新しい図形が混在して描画されることになります。

幸い、ハードウェアには、それがいつスクリーンを書き終えたかを伝える方法があります。それは'垂直ブランク割り込み'と呼ばれています。私達はこれが起きた時に呼び出される関数を指定することができます。

'割り込み'は現在実行中の処理(この例では、'main'関数内をループしている)を割り込んで即時に他の何かを実行する関数を呼び出すハードウェアメカニズムです。 割り込み関数から復帰した場合は、まるでそれが割り込みされていないかのように前の処理を継続できます。

先ほどの描画の問題を解決するためには、ハードウェアがフレームバッファの内容をスクリーンに描画していない間に、フレームバッファに描画します。これを実行する最も良い時間が垂直ブランク割り込みなのです。

割り込みのセットアップ

A 割り込みのチュートリアルで、割り込みがどのように動作するかに関して、詳細に立ち入ります。差し当たり、簡潔に割り込みコードで起こっている事に関して概説します。

最初にどんな割り込みが起きた時に割り込んでほしいかを任天堂DSに設定します。

void InitInterruptHandler()
{
  IME = 0;
  IRQ_HANDLER = on_irq;
  IE = IRQ_VBLANK;
  IF = ~0;
  DISP_SR = DISP_VBLANK_IRQ;
  IME = 1;
}

このコードでは、私達は VBlank 割り込みだけが発生して欲しいと思っています。そしてそれが発生した時に 'on_irq' 関数が呼び出されます。

on_irq 関数では以前に main 関数で実行していたフレームバッファへの描画を実行します。

void on_irq() 
{	
  if(IF & IRQ_VBLANK) {
    draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
    draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));

    // VBLANK割り込みをハンドルしたことをDSに通知
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    IF |= IRQ_VBLANK;
  }
  else {
    // 他の割り込みを無視
    IF = IF;
  }
}

この関数の主な部分は以前に 'main'関数内の 'while' ループで実行していた描画機能です。残りの部分としては '割り込み' ハンドリングに関する機能です。

また、私達は VBLANK 割り込みをハンドルしたことをDSハードウェアについ位置する必要があります。これは私達が後で使用する 'swiWaitForVBlank' 呼び出しに必要です。後で、理由について解説します。それをする部分は以下の通りです。

    // VBLANK割り込みをハンドルしたことをDSに通知
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    IF |= IRQ_VBLANK;

まだ完全ではない...

私達は 'main' 関数から描画のためのコードを取り除きます。そして、以下のようになります。

int main(void)
{
  powerON(POWER_ALL);
  videoSetMode(MODE_FB0);
  vramSetBankA(VRAM_A_LCD);
  InitInterruptHandler();
  while(1) {
    old_x = shape_x;
    old_y = shape_y;
    shape_x++;
    if(shape_x + shape_width >= SCREEN_WIDTH) {
      shape_x = 0;
      shape_y += shape_height;
      if(shape_y + shape_height >= SCREEN_HEIGHT) {
	shape_y = 0;
      }
    }      
  }

残念ながら、これを実行しても問題が発生します。Dualisでのスクリーンショット を見るとチェッカー盤のように複数の図形がスクリーンに出現しています。

理由は明確です。ハードウェアは垂直ブランク割り込みが起きる1/60秒毎に on_irq 関数を呼び出します。 これ毎に図形は消去され、再描画されます。

残念ながら、'main' 関数内の 'while' ループはこのフレームレートとは同期していません。 それは図形が描画される座標を増加しながら、すばやくループします。 'on_irq' 関数が呼び出される前に50回実行されるかもしれません。 その結果、描画ルーチンは図形が最後に描画されてから50回も更新されている古い'x'座標と'y'座標をに基づいて図形を消去します。従って、間違った領域が消去されてしまいます。

正解は

中断が発生するまで'スリープ'するように'while'ループで設定することによって、私たちはこれを修正することができます。 これには作成の副作用があり、ループは'忙しい'状態にはなりません。中断が発生するまで、事実上、ARM9プロセッサを減速することができます。 また、それは、私たちのフレームレート1秒あたり60フレームになることを可能にします。 これをする関数が'swiWaitForVBlank'です。

'on_irq' 内部でいくつかのレジスタに VBLANK 割り込みをハンドルしたことを通知する設定をしたことを覚えているでしょうか。 これが 'swiWaitForVBlank' が動作するのに必要です。これらのレジスタを設定しないと 'swiWaitForVBlank' は、けして起こらない割り込みがハンドルされたという通知を待ち続けてしまいます。

以下の1行を追加することにより、このサンプルプログラムは Dualisエミュレータで適切に動作します。

int main(void)
{
  powerON(POWER_ALL);
  videoSetMode(MODE_FB0);
  vramSetBankA(VRAM_A_LCD);
  InitInterruptHandler();
  while(1) {
    old_x = shape_x;
    old_y = shape_y;
    shape_x++;
    if(shape_x + shape_width >= SCREEN_WIDTH) {
      shape_x = 0;
      shape_y += shape_height;
      if(shape_y + shape_height >= SCREEN_HEIGHT) {
	shape_y = 0;
      }
    }      
    swiWaitForVBlank();
  }

  return 0;
}

アプリケーションが動作しているDualisのスクリーンショットです。

スクリーンのスワップ

いくつかのバージョンの Dualis では上下のスクリーンが逆に表示される場合があります。このケースでは Dualis は下部のスクリーンをメインスクリーンとして扱っています。

以前 下部または上部のハードウェアスクリーンをメインスクリーンにマップできるかに関して説明しました。メインスクリーンが上部スクリーンにマップされている状態で、現在のデモは動作します。'lcdSwap'関数を使用することにより、これを変更することができます。 これを呼び出すと、メインスクリーンは下部タッチスクリーンにマップされます。いつでもスクリーンを交換するためにこの関数を呼び出すことができます。以下のmain関数の変更でタッチスクリーンにアプリケーションが表示されます。

int main(void)
{
  powerON(POWER_ALL);
  videoSetMode(MODE_FB0);
  vramSetBankA(VRAM_A_LCD);
  InitInterruptHandler();
  lcdSwap();
  while(1) {
    old_x = shape_x;
    old_y = shape_y;
    shape_x++;
    if(shape_x + shape_width >= SCREEN_WIDTH) {
      shape_x = 0;
      shape_y += shape_height;
      if(shape_y + shape_height >= SCREEN_HEIGHT) {
	shape_y = 0;
      }
    }      
    swiWaitForVBlank();
  }

  return 0;
}

ビルド

アプリケーションをビルドする手順は最初のチュートリアルで概説しています。私はデフォルトの arm7_main.cpp とARM9コード arm9_main.cppを使用します。コンパイルコマンドを実行させるための簡単な Makefile ファイルを提供します。

完全なソースコードはframebuffer_demo1.zip にあり、エミュレータまたは実機で実行する framebuffer_demo1.ndsframebuffer_demo1.nds.gba をダウンロードすることができます。

結論

このチュートリアルからテクニックを得ながら、フレームバッファを使用して、あなたは単純なアニメーションとゲームを開発することができます。 私達が チュートリアル1でスクリーンに表示したタッチスクリーンコードを使用して、あなたはスクリーン内でアイテムをドラッグすることも可能です。以降のチュートリアルでは、ダブルバッファリング、サウンド、および他のスクリーンモードをカバーする予定です。いろいろなコメントや提案は歓迎します。 以下の私の連絡先を見てください。