LECTURE
Oeffentliches Kunst Nr.10
H18/02/06記述
PC-9801series 高速描画の話1
 ゲームアプリケーションに限らず、描画の高速性は大きなアドバンテージになります。インターネットサイト閲覧用ソフト(ブラウザ)でも、表示の高速性をウリにしているものがあるように、これは昔も今も変わりません。ただ、CPUの処理速度が十分になかった時代、高速描画は今以上に大きな意味を持っていました。ゲームの場合、それは「遊べるか、遊べないか」を決定するほど重要なことだったのです。
 今回はPC-9801全盛時代、CPUがV30-10MHz〜i486-40MHzくらいの頃の高速テクニックを紹介していきます。もちろん、それ以降でも有効なのですが、Pentium(60MHz〜)になるとCPUの内部構造が大きく変わり、また別の技法が有効になったりするため、敢えて時代を限定してお話ししてまいります。
 01:高速画面クリア
 これはもう、しょっちゅう使用する処理のひとつです。画面全体(640×400pixel)を単一色で塗りつぶす処理をいいます。マシンによっては、ハードウェアでクリアできるものもありますが、PC-9801にはそのような機能はありません。ソフトウェアでV-RAMにクリアデータを書き込んでいく必要があります。ごく単純に考えてCでコーディングすると、リスト1のようになります。ちなみに、コンパイラはBorland C++ 3.1を想定しています。
 一般的な画面クリアルーチン
0001
void ClearScreen1(unsigned int clrdata)
0002
{  
0003
  unsigned int far * pVram,;
0004
  unsigned int cnt, seg;
0005
    
0006
  seg = 0xA800;    // 青プレーンセグメント
0007
  do {
0008
    pVram = MK_FP(seg, 0);    // VRAM farポインタ作成
0009
    for(cnt = 0; cnt < 80 * 400 / sizeof(unsigned int); cnt++) {
0010
      *(pVram + cnt) = clrdata;
0011     }
0012      if(seg == 0xB800) {
0013       seg = 0xE000;    // 輝度プレーンセグメント
0014     } else {
0015       seg += 0x0800;    // VRAMセグメント更新
0016     }
0017   } while(seg <= 0xE000);
0018 }  
【リスト1】
 もはや組み込み用途以外ではお目にかかれなくなった16ビットのプログラムです。intelのCPU用になっているので、セグメントの切り替えでややこしくなっています。
 このように、順次4枚のV-RAMプレーンに16ビット単位(unsigned int)でクリアデータを書き込んでいます。かなりの作業量であることは推測できると思います。しかし、これをさらに高速化しろと言われるとキツいものがあります。
 そこで出番の回ってくるのがGRCGグラフィックチャージャーです。Nr.2のビートバイスのところで少し説明しましたが、1プレーン分のアクセスで、4プレーンにアクセスしてくれるハードウェアです。
 GRCGを利用した高速画面クリアルーチン
0001
void ClearScreen2(void)
0002
{  
0003
  unsigned int far * pVram,;
0004
   unsigned int cnt,;
0005
   
0006
  outportb(0x7C, 0x80);    // GRCGをTDWモードで起動
0007
  for(cnt = 0; cnt < 4; cnt++) {
0008
    outportb(0x7E, 0x00);    // 全タイルレジスタに0を設定
0009
  }
0010
  pVram = MK_FP(0xA800, 0);    // VRAM farポインタ作成
0011   for(cnt = 0; cnt < 80 * 400 / sizeof(unsigned int); cnt++) {
0012      *(pVram + cnt) = cnt;
0013    }
0014   outportb(0x7C, 0x0);    // GRCGを解除
0015 }  
【リスト2】
 GRCGには3種類の動作モードがあるのですが、ここではその中のTDW(Tile Direct Write)モードを使用します。このモードは1バイト×4プレーン分のタイルレジスタに登録されているデータを、書き込みアクセスされたアドレスにそのまま書き込むという動作をします。仮に画面をパレット番号0(通常は黒)で消去したい場合、すべてのタイルレジスタに0x00を設定しておき、V-RAM1面分(赤・青・緑・輝度、どのプレーンでもOK)に書き込みアクセスするだけです。書き込むデータは何でも構いません。Cで記述すると【リスト2】のようになります。V-RAMに16ビット単位でアクセスしていますが、この場合GRCGは自動的にタイルを16ビットに拡張して書き込んでくれます。つまり、下位バイトと上位バイトの両方にタイルレジスタの内容を書き込んでくれるわけです。2重ループがなくなり、すっきりとしたプログラムになりました。当然ですが、リスト1のプログラムに比べて圧倒的に高速です。ちなみに12行目で代入している値は何でもよいのですが、最適化で無駄なくレジスタが割り振られやすいようにカウンタ変数を代入しています。
 ここではわかりやすいようにCで記述してありますが、アセンブラで最適化すればさらに高速化します。コンパイラの最適化性能に大きく左右されますが、8086系CPUにはOUTSSTOSWといったストリング命令があるので、これらを使用するとよいでしょう。
 と、ここで終わるとありきたりな話になってしまいます。これで満足せずに粘って考えてみます。GRCGを使用する場合、書き込むデータは何でも構わないというところに着目します。とにかく高速に書き込み動作さえできればよいわけですから、CPUのメモリに対しての書き込み命令を総チェック。すると、80186以降のCPU(V30含む)には、全汎用レジスタ(16bit×8本)を一度にスタックへ退避する命令が見つかります(ニモニックでPUSHA
 ここから80286CPUのリアルモード(8086互換)を例にお話しします。80286では、16ビットのデータを連続してメモリに書き込む命令STOSWの消費クロックはPUSHA17です。PUSHAは8本の16ビットレジスタを一気にスタックに退避してくれますから、16バイトの書き込み動作と書き込みポインタの更新を17クロックで実行してくれることになります。対してSTOSWはREPプリフィクスと共に使用することによって、16バイトの書き込みとポインタの更新と回数のカウントを3×8=24クロックで実行します。つまり、24−17=7クロック未満で回数のカウント処理を実現できれば、PUSHA命令を利用することによって、さらなる高速化が実現することになります。
 PUSHAを使った超高速画面クリアルーチン
0001
  MOV DX,80*400 2
0002
  MOV BX,SS 2
0003
  MOV AX,A800H 2
0004
MOV CX,40 2
0005
LTOP:      
0006
  CLI   3
0007
  MOV SS,AX 2
0008
  XCHG SP,DX 3
0009
  REPT 50  
0010
  PUSHA   17*50
0011   ENDM    
0012   XCHG SP,DX 3
0013   MOV SS,BX 3
0014   STI   2
0015   LOOP LTOP 8

 STOSWを使った高速画面クリアルーチン
0001   MOV CX,A800H 2
0002   MOV ES,CX 2
0003   MOV DI,0 2
0004
  MOV CX,40*400 2
0005   CLD   2
0006
  REP STOSW 3*40*400
【リスト3】
 しかし、80286の回数カウント命令のLOOPの消費クロックはです。これでは逆に遅くなってしまいます。万事休す?!・・・いえいえ、まだ諦めてはいけません。PUSHAは1バイトの命令なのです。つまり、連続して並べてもたいしたサイズにはならないのです。
 【リスト3】が実際のコーディング例です。さすがにCだとわかりづらいのでアセンブラニモニックで記述してあります。事前にGRCGは設定してあると考えてください。対比用にストリング命令STOSWを使ったものも示します。PUSHAを使用した方はプログラムサイズが大きくなりますが、3割強高速になります。単純計算した消費クロック数だと、上が34,968クロック、下が48,010クロックとなります。実際には命令のプリフェッチキューの状態も影響するため、差はもう少し小さくなると思われます。また、スタックがV-RAMになるようにスタックポインタをセグメントごと書き換えているため、ループの中にその切り替えが必要になっています。当然、スタックを書き換えている間に割り込みが入ってくるとマズい(GRCG作動中のVRAMリードは保証されていない)ので、スタック書き換え中は割り込みを禁止しています。(スタックポインタ'SP'はアドレスの低い方向へ自動更新されるため初期値が80*400になります。)
 この例ではPUSHAを50回、画面では10ライン分ごとのループになっていますが、このあたりが無難なところでしょう。ループ回数を増やすと高速性が落ちますし、逆に減らすと割り込み禁止期間が長くなって、BGMが途切れたり、マウスの移動がぎこちなくなったりします。
 ちなみに、CPUが80386になると、この差は一気に顕著になり、2倍近くなります。実際にテストプログラムを作って調べてみましたが(80386-25MHz)、明らかに体感できる高速性を得られました(正確なデータが残っていないので、当時の記憶のみなのですが)
 【リスト3】で紹介したような荒技ルーチンは、市販された風雅の製品には使用されていません。なぜなら、STOSW命令を使ったルーチンで十分に高速だからです。では、なぜこのような高速クリアルーチンを考え出したのかというと、企画段階で中止になったリアルタイム3Dアクションゲーム(「ソリッド・ブラスター」という名称もついていました)用に必要だったからなのです。この企画はうりぼう塚原がリーダーになって進めていたもので、80386CPUを搭載したPC-9801シリーズをターゲットにしていました。ポリゴンで描かれた街の中をモビルスーツで駆け回るタイプのロボットアクションで、プロトタイプまでできていたのです。杉之原名人はこの高速クリアルーチンの他にも、ポリゴン描画ルーチンなども作っていました。
 ところが、「80286でも遊べなきゃダメ!」との社長命令もあり、そのまま保留となってしまったのです。そうこうしているうちにWindows95&Direct3Dが登場し、幻の企画となりました。
 02:高速矩形領域塗り潰し(BOX FILL)
 処理的には先の画面クリアと似ていますが、いろいろと考慮すべき点が出てくるのが、この矩形の塗り潰し、BOX FILLです。
 ごく普通に考えられるのが、同じ長さの水平直線を垂直方向に連続して描画する方法です。これならアルゴリズムも非常に単純ですし、高速に処理できるでしょう。・・・・・ビットマップ型のV-RAMであれば。PC-9801シリーズのグラフィックV-RAMは1ビットが1ピクセルに対応したプレーン型のみなので、この方法が最も高速であるとは一概に言えないのです。
PC-9801シリーズのグラフィックV-RAM
 【図・1】のように、PC-9801のグラフィックV-RAMは1ビットが1ピクセルに対応したモノクロ画面が4プレーン分重なっています。もうお分かりのように、水平直線を描画するとき、左端と右端の画面座標によって、書き込むバイト(ワード)パターンを適宜変化させる必要があるのです。
 例えば、左上座標( 3, 0) 右下座標(517, 199)の塗り潰し矩形を描画する場合を考えてみます。左上X座標が3ということは、【図・2】のようにA800:0000H番地の1バイトのbit4〜0のみを'1'または'0'にする必要があります。また、右下X座標が517ですから、同様に対応するアドレス(A800:0040H番地)の1バイト内のbit7〜2のみを'1'または
AktieのNYダウの動きを示すチャート
 【図・2】 PC-9801青画面上の水平ライン描画例
'0'にする必要もあります。その間の63バイト(=504ドット分)はベタで'FFH'または'00H'書き込みます。
 ベタ書きできる部分は8086のストリング命令'STOSB/W'を使えば高速処理が実現できるので問題ありませんが、両端のバイトは内部のビット処理が必要です。それも書き込む前のV-RAMのデータと論理演算を行わねばなりません。描画しない部分のビットはそのままにしておかねばなりませんから、適宜特定のビットパターンと'OR'または'AND'をとってやる必要があるわけです。これを描画する色に合わせて4プレーン分繰り返すことになります。
 さて、ここでまず考えられる高速化がGRCGを利用する方法です。画面クリアのときはTDWモードを使用しましたが、この場合はRMW(リード・モディファイ・ライト)モードを合わせて使用します。このモードでは、あらかじめタイルレジスタに色パターンを登録しておくと、V-RAMに描き込むバイトのビットが'1'になっている部分はタイルレジスタの内容が描き込まれ、'0'になっている部分は変化しません。つまり、1バイト(8ドット)分の“透過合成表示”ができるわけです。(【図・3】)
GRCG RMWモードの動き
 【図・3】 GRCGのRMWモードの動き
 仮に今回はパレット番号1の色で塗りつぶそうと思えば、GRCGRMWモードに設定し、タイルレジスタにFFH-00H-00H-00Hと設定しておき、A800:0000H番地に1FH(00011111B)を書き込めばよいのです。次いでGRCGTDWモードに設定し直し、任意の値を63バイト分連続アドレスに書き込み、再度RMWモードに設定してFCHを書き込めば、1ライン分の描画の終了です。これをY方向に200回繰り返せば塗り潰された矩形描画の完了となります。これだけでかなり高速な描画が実現できます。また、先に述べたように、水平直線描画ルーチンを作っておけば、それを任意回数繰り返し呼び出すだけで実現できるので、とても単純で簡単な方法でもあります。
 では、ここから高速化を考えます。
 先の方法だと、水平ラインを描画する間にGRCGのモードを3回設定しています。これには無駄があります。ここは、次の行を描画するときも最初はRMWモードに設定するので、2行目以降の最初のGRCGモード設定は省略が可能です。ただ、これは単純に水平ラインルーチンを繰り返し呼び出す方法では実現できません。専用のルーチンとして書き下ろす必要があります。
 さらに考えてみると、実はGRCGモードの設定は、全体を通して2回だけで済むことに気付きます。そう、左右両端のみを最初にすべて描画してしまうのです。この方法だと両端のビットパターンをレジスタに設定し、Y方向に一気に書き込んでしまえるので無駄がありません。そして、残った中央部分をTDWモードとストリング命令でまとめて描画するのです。実際、風雅システムの製品に使用されているBOXFILLルーチンはこのアルゴリズムです。
CPUとV-RAMの接続図
 【図・4】 CPUとV-RAM周辺の接続概念
 そしてさらに考えます。PC-9801シリーズのV-RAMは16ビットのバスで接続されています(図・4)。つまり、8ビット単位で書き込んでも、16ビット単位で書き込んでも、かかる時間は同じなのです。ただし、16ビット値を書き込む場合は偶数アドレスである必要があります(奇数アドレスから16ビット値を書き込むと倍近く時間がかかる)。そうとなったら、中央部分はSTOSW命令を使って16ドット単位で描画しない手はありません。GRCGも自動的に16ビットに拡張して動いてくれます。ちょっと面倒ですが、偶数アドレスから偶数バイト分か、奇数アドレスから偶数バイトか、偶数アドレスから奇数バイトか、そして奇数アドレスから奇数バイトか、4通りに場合分けを行うのです。この部分で多少手間がかかっても、2倍の速度でベタ描画ができるのですから、描画するX幅にもよりますが、多くの場合は元が取れるでしょう。
 しかし、今述べたように、この手法ではX幅の狭い矩形の場合はデメリットはあってもメリットはありません。場合分けのチェックに要する時間がある分だけ、わずかですが遅くなってしまうからです。かといって、そのチェックの必要の有無自体をチェックする(ベタ描画部分が無かったり、1バイト分だけだったりしないか)というのも本末転倒な話です。
 このようなときは、敢えて描画ルーチン内でチェックせず、呼び出し側で判断して、X幅の狭い塗り潰し矩形描画専用のルーチンを実行するようにすると効率的です。つまり、「常にX幅の狭い矩形であるとわかっている場合は、それ専用に特化した描画ルーチンを呼び出す」ということなのです。これはグラフィックコントーラチップの“ショートベクタ描画機能”の考え方に似ています。ショートベクタ描画機能とは、最近のグラフィックコントーラチップ(GPU)にまだ残っているか否かは不明ですが、例えば長さ15ドット以下で水平か垂直か斜め45度の直線描画の場合だけ、始点と終点ではなく、始点と方向と長さの情報で超高速に描画する機能をいいます。このような線分はハードウェアだけ(ワイヤードロジック)で簡単に超高速に描画できるため、通常の直線描画とは別の機能としてインプリメントされていた(る)のです。これと同じように、X幅が8ドット以下の矩形描画専用のルーチンや、バイトバウンダリの(左右端のビット操作の必要のない)矩形描画専用のルーチンなどを用意しておき、適宜これらを使い分けるというわけです。
AktieのNYダウの動きを示すチャート
 【画面1】 「株式投資Aktie」の株価チャート画面例
 もちろん、このような専用ルーチンを使うべき場面というのは、大量に同様のケースが連続している場合のみです。例えば、Aktieのような株式投資シミュレーションなどで必要になる株価チャートの描画(【画面1】)などです。一定X幅の矩形を大量に高速に描画する必要があります。
 逆にアマランス1&2の戦闘画面の体力ゲージの描画などには通常の矩形描画ルーチンを常に使用すべきです。X幅が8ドット以下の場合だけ別ルーチンを呼び出すようなチェックをいちいち行っていたのでは、巨視的にはわずかながら遅くなることも考えられます。という前に、そもそも頻度が低く、わずかな高速化の意味自体がありません。株価チャートの場合は、タラタラ表示されるか、パッと表示されるかという‘差’を感じられる可能性があります。
 これは矩形塗り潰しに限ったことではないのですが、特に小さな繰り返し処理(=ループ)があるプログラムを記述する場合は、飛び先が偶数アドレスになるようにすると高速化する場合があります。アセンブラで記述する際に、ディレクティブの"EVEN"をループの先頭などに入れておくのです。つまり、ジャンプ命令でプリフェッチキュー(実行されるであろう命令を'先読み'して溜めておく場所)がクリアされた際に、効率良く再充填されるようにするわけです。ごく小さなループの場合、かなり効果のある場合があります。
 この手法はごく一般的なもので、32bitCPUにも有効です。もちろん、この場合は「偶数アドレス」ではなく、「4の倍数アドレス」になりますが。また、ループの前に入れると大量のNOP命令(何もするなという命令)が挿入されてしまうので、よく使うサブルーチン(関数)の前に入れておくのが無難でしょう。追補しますが、PentiumIII/IVのように強力な分岐予測機能を備えたCPUではこの手法の効果はほとんどありません。
 高速描画の話。今となっては懐かしい過去の話かもしれません。しかし、温故知新という言葉があるように、中には何かのヒントになったりするものがあるかもしれません。何気なく、おもしろおかしく(?)読んでいただけたら幸いです。ちなみに次回の高速描画の話は「高速ライン描画」の予定です。突然気が変わって、高速文字表示や高速ビットマップ表示になる可能性もあります。なんか、気まぐれなブログみたいなんでですけど・・・・・・(もともとその傾向はありますが)

特別講義メニューページへ
ディン:「涙ぐましい努力ね・・・・」