Buho No.218 目次

Visual J++ 6.0 で DirectDraw を使う試み(前編)

-1ParaGRAPH


本来は高速な画面表示のためにある DirectDraw を, 速度からほど遠いイメージを持つ Java から使おうという間違ったお話です。 ちなみに C++ で {DirectDraw を使うことは, 部報207号でわたるさんが解説されています。

VJ6.0発売その後

Visual J++ 6.0 で DirectInput を使用するための記事は TACT 氏が 部報214号で書いていますが, ここでは DirectDraw を使用してみたいと思います。

ところで,部報214号で TACT 氏が Visual J++ 6.0 の特徴を挙げていますが, ここで裏返して書いておきますと,

ということになります。MSDN ライブラリが突然壊れたりしますし。 でも Visual C++ 5.0 の統合環境とかよりは二桁上なので 十分に使いやすいです 1

DirectDraw を使う

ここでは,ゲームなどで使用する排他モードではなく, ウィンドウモードを扱うことにします。 Visual J++ でゲームを作るというのも酔狂で良いのですが:-)

解説の都合上,先にソースファイルを載せることにします。

/** ***************************************************************************
 *
 *                 Visual J++ で DirectDraw を使用する試み
 * 
 * ****************************************************************************
 */
import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;

import com.ms.directX.*;
import com.ms.com.*;

/**
 * このクラスはコマンド行から任意の個数の引数を受けとることができま
 * す。プログラムの実行は main() メソッドから始まります。このクラス
 * のコンストラクタは, main() メソッドで 'Form2' 型のオブジェ
 * クトが生成された際に初めて呼び出されます。
 */
public class Form2 extends Form implements DirectXConstants
{
  
  DirectDraw  dd;
  DirectDrawSurface pddsPrime, pddsBack, pddsTemp, pddsFore;
  DDSurfaceDesc displayMode;

  void repaint_surfaces()
  {
    // 背景 Bitmap ファイルのロード
    DirectDrawBitmap  bmBack = new DirectDrawBitmap();
    try{
      bmBack.filename("back.bmp");           /* 適当な BMP ファイル(640x480) */
      bmBack.initWidth(640); bmBack.initHeight(480);
      bmBack.loaded();
    } catch (ComFailException e) {                     /* BMP ファイルがない */
      MessageBox.show("背景 Bitmap ファイルがありません", "", MessageBox.OK);
      System.exit(-1);
    }
    // 前景 Bitmap ファイルのロード
    DirectDrawBitmap  bmFore = new DirectDrawBitmap();
    try{
      bmFore.filename("fore.bmp");            /* 黒地に白文字の BMP ファイル */
      bmFore.initWidth(640); bmFore.initHeight(480);
      bmFore.loaded();
    } catch (ComFailException e) {
      MessageBox.show("前景 Bitmap ファイルがありません", "", MessageBox.OK);
      System.exit(-1);
    }
    // Bitmap をオフスクリーンサーフェスに転送
    try{
      pddsBack.copyBitmap(bmBack, 0, 0, 0, 0);
      pddsFore.copyBitmap(bmFore, 0, 0, 0, 0);
    } catch (Exception e) {
      MessageBox.show("BMP をオフスクリーンに転送できませんでした", "", MessageBox.OK);
      System.exit(-1);
    }
    // 合成
    Rect topRect = new Rect();
    topRect.left = 0; topRect.top = 0; topRect.right = 640; topRect.bottom = 480;
    // bltFast を使う場合は定数が変わる
    pddsTemp.bltFast(0, 0, pddsBack, topRect, DDBLTFAST_WAIT);
    pddsTemp.bltFast(0, 0, pddsFore, topRect, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);
  }

  void init_DirectDraw()
  {
    // DirectDraw オブジェクトの生成
    /** 今どき DirectDraw がインストールされていないなどということは想定しない
     *  また,GVRAM は 640x480 のオフスクリーンサーフェスを二枚確保できるだけの
     *  空き容量 (HiColor で 約 1.3MB) が必要 */

    dd = new DirectDraw();
    
    // 協調レベルの設定
    dd.setCooperativeLevel(null, DDSCL_NORMAL);     /* ウィンドウモード */
    
    // Primary サーフェスを生成 (現在表示されている画面がそのまま渡される)
    DDSurfaceDesc ddsdPrime = new DDSurfaceDesc();
    ddsdPrime.flags = DDSD_CAPS; ddsdPrime.ddsCaps = DDSCAPS_PRIMARYSURFACE;
    try{
      pddsPrime = dd.createSurface(ddsdPrime);
    } catch (ComFailException e) {
      MessageBox.show("Primary サーフェスを生成できませんでした。", "", MessageBox.OK);
      throw e;
    }

    // Bitmap 用の DirectDraw サーフェスの設定
    DDSurfaceDesc ddsd = new DDSurfaceDesc();
    ddsd.flags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
    ddsd.width = 640; ddsd.height = 480;

    // 背景用サーフェスはメインメモリに確保
    ddsd.ddsCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY;
    // まず背景用サーフェスを確保
    try{
      pddsBack = dd.createSurface(ddsd);
    } catch (ComFailException e) {
      MessageBox.show("背景 Bitmap 用サーフェスの確保に失敗しました", "", MessageBox.OK);
      throw e;
    }
    
    // 前景・バックバッファはビデオメモリ上に確保
    ddsd.ddsCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;
    // 次に前景用サーフェスを確保
    try{
      pddsFore = dd.createSurface(ddsd);
    } catch (ComFailException e) {
      MessageBox.show("前景 Bitmap 用サーフェスの確保に失敗しました", "", MessageBox.OK);
      throw e;
    }
    // バックバッファを確保
    try{
      pddsTemp = dd.createSurface(ddsd);
    } catch (ComFailException e) {
      MessageBox.show("バックバッファの確保に失敗しました", "", MessageBox.OK);
      throw e;
    }
    // 前景の透過色を RGB = (0, 0, 0) に設定
    DDColorKey ddTcol = new DDColorKey();
    ddTcol.colorSpaceLowValue = ddTcol.colorSpaceHighValue = 0;
    pddsFore.setColorKey(DDCKEY_SRCBLT, ddTcol);

    // panel1 の領域外に描画されないようクリッピングを設定
    DirectDrawClipper ddclip = new DirectDrawClipper();
    ddclip.setHWnd(0, panel1.getHandle());
    pddsPrime.setClipper(ddclip);

    // バックバッファに描画
    repaint_surfaces();
  }
  
  public Form2()
  {
    super();

    // Visual J++ フォーム デザイナのために必要
    initForm();   

    // TODO: 構築用のコードを追加します。initForm の呼び出し前には追加しないでください。

    init_DirectDraw();
    
  }

  /**
   * Form2 は,コンポーネント リストを削除するために dispose メソッドを
   * オーバーライドします。
   */
  public void dispose()
  {
    super.dispose();
    components.dispose();
  }

  private void panel1_paint(Object source, PaintEvent e)
  {
    // Rectangle のフィールド width/height は実際には x2/y2 である
    Rectangle winRect = panel1.rectToScreen(panel1.getClientRect());
    Rectangle wmpRect = e.clipRect;
    // 再描画領域が 640x480 より外なら修正
    if (wmpRect.x >= 640) wmpRect.x = 640;
    if (wmpRect.y >= 480) wmpRect.y = 480;
    if (wmpRect.width >= 640) wmpRect.width = 640;
    if (wmpRect.height >= 480) wmpRect.height = 480;
    Rect      destRect = new Rect(), srcRect = new Rect();
    srcRect.left = wmpRect.x; srcRect.right = wmpRect.width;
    srcRect.top = wmpRect.y; srcRect.bottom = wmpRect.height;
    destRect.left = winRect.x + srcRect.left; destRect.top = winRect.y + srcRect.top;
    destRect.right = winRect.x + srcRect.right; destRect.bottom = winRect.y + srcRect.bottom;
    if (pddsPrime.blt(destRect, pddsTemp, srcRect, DDBLT_WAIT) != 0){
      // サーフェスが失われている
      try{
        pddsBack.restore(); pddsFore.restore(); pddsTemp.restore(); pddsPrime.restore();
        repaint_surfaces();
      } catch (ComFailException cfe) {
        // サーフェスの回復に失敗 → DirectDraw 再初期化を試みる
        try{ init_DirectDraw(); panel1_paint(source, e); } catch (ComFailException cfex) {}
      }
    }
  }

  /**
   * 注意: 以下のコードは Visual J++ フォームデザイナのために
   * 必要です。フォーム デザイナで変更することができます。コード
   * エディタで変更しないようにしてください。
   */
  Container components = new Container();
  Panel panel1 = new Panel();

  private void initForm()
  {
    this.setText("Form2");
    this.setAutoScaleBaseSize(new Point(5, 12));
    this.setClientSize(new Point(640, 480));

    panel1.setDock(ControlDock.FILL);
    panel1.setSize(new Point(640, 480));
    panel1.setTabIndex(0);
    panel1.setText("panel1");
    panel1.addOnPaint(new PaintEventHandler(this.panel1_paint));

    this.setNewControls(new Control[] {
                        panel1});
  }

  /**
   * アプリケーションのメイン エントリ ポイントです。 
   *
   * 引数 args に,コマンドラインからアプリケーションに渡された引数が
   * 配列として設定されます。
   */
  public static void main(String args[])
  {
    Application.run(new Form2());
  }
}

フォームデザイナで Panel コントロールを 貼り付けただけのフォームです。 GDI を使わず,わざわざ DirectDraw を使用して この Panel に画像を表示します。

ソースを見れば解説は要らないような気もしますが,順に説明します。

init_DirectDraw()

まず,DirectDraw クラスのオブジェクトを生成します。 以後,このオブジェクトのメソッドを使用して DirectDraw を操作します。

次に,setCooperativeLevel メソッドで 協調レベルの設定を行います。 協調レベルとは,DirectDraw アプリケーションが 要求する制御レベルのことです。 ここではウィンドウモードを使用しますので, 第1パラメータに null を, 第2パラメータに DDSCL_NORMAL を渡します。 この辺は C++ で DirectDraw を使用するときと同じです。

実画面に描画するため,createSurface メソッドで Primary サーフェスを取得します。 ウィンドウモードですので現在表示されている GDIサーフェスが返ってきます。

その後の createSurface では, 表示する画像やバックバッファ用のサーフェスを確保しています。 ちなみに,このプログラムでは以下のファイルが 実行ファイルと同じディレクトリにあることが必要です:

back.bmp
640x480 の BMP ファイルです。適当にそのへんから 拾ってきてください。
fore.bmp
RGB = (0, 0, 0) の黒背景に他の色で 文字か何かが描かれた画像ファイルです。 back.bmpの上に重ね合わせて表示します。

背景用サーフェスを DDSCAPS_SYSTEMMEMORY でメインメモリ上に, 前景・バックバッファ用サーフェスを DDSCAPS_VIDEOMEMORY で VRAM 上に確保しています。 特に指定しなかった場合,DirectDraw は先に VRAM 上へ確保を試み, 空き容量が足りなければメインメモリに確保します。 ちなみに,背景用サーフェスをメインメモリに確保するという 倹約をしているのは, VRAM to VRAM と System to VRAM とであまり速度が変わらないからです 2。 これに対し前景用サーフェスがVRAMなのは透過色処理を伴う転送が System to VRAM でサポートされていないことが多いからです 3。 本当は DirectDraw.getCaps して 調べるべきなんですが (^^;

C++ で開発している場合は DirectDraw の CreateSurface メソッドが返り値で 成功(DD_OK)/失敗(それ以外)を返しますが, com.ms.directX パッケージのメソッドは 一般的に成功/失敗を示す返り値を返しません。 失敗するといきなり ComFailException が飛んできます。 これはActiveXコンポーネントを使用すると起こる一般的な現象ですが, ComFailException からエラーの原因が判らないのは なんとかしてほしいものです 4

前景を背景に重ね合わせるため, setColorKey メソッドで前景サーフェスに透過色を設定します。 colorSpaceLowValue から colorSpaceHighValue までが 透過色として扱われますので単一色でなく色の範囲を透過色として 指定することもできますが, その場合はハードウェアによるアクセラレーションが 効かなくなる可能性があります。

最後に,Primary サーフェスにクリッピング領域を設定します。 クリッピング領域を設定するのは, 自分のウィンドウ外に描画して他のウィンドウを破壊しないためです。 自分で領域の座標を指定することもできますが,ここではお手軽に setHwndpanel1 コントロールから 外への描画をクリッピングします。 setHwnd は非常に便利で, 指定したウィンドウをリサイズしても移動しても, 上に別のウィンドウが重なっても自動的にクリッピング領域を変更してくれます。 なお,クリッピング領域を設定すると bltFast は 使えなくなりますので blt を使用します。

repaint_surfaces()

DirectDraw で Bitmap を貼り付けるのに便利なのが DirectDrawBitmap クラスです。 new でとりあえず生成し, filename メソッドでファイル名を指定し, initWidth/initHeight で大きさを決め loaded メソッドで実際にロードします。 失敗すると例によって ComFailException が発生します。

こうして生成した DirectDrawBitmap オブジェクトを DirectDrawSurfacecopyBitmap メソッドで 貼り付ければ,Bitmap の DirectDrawSurface が出来上がります。

その後,バックバッファに転送します 5。 バックバッファはクリッピングの必要がないので, 自称高速な bltFast メソッドで転送しています 6blt メソッドと bltFast メソッドで 引数に使う定数が違うので注意が要ります(常識らしいですが)。

でもって WM_PAINT

panel1WM_PAINT メッセージ処理をする panel1_paint メソッドで実画面に転送します。 似て非なる Rectangle クラスと Rect クラスが錯綜して 非常にうっとうしいです。 しかも Rectangle クラスの width フィールドと height フィールドが 実際には x2/y2 として使われているので混乱の極みです。 どういう仕様設計をしたのか是非教えていただきたい!!TM

それからついでに,Rect クラスにコンストラクタが ないため new Rect(x1, y1, x2, y2) を いきなり渡すといったことができず不便このうえないです。 これもなんとかしていただきたい!!TM

また,ここで指定する x2/y2 は,実際の座標+1 です。 例えばVGA画面は (0,0)-(639,479) ではなく,(0,0)-(640,480) になります。 間違って実際の座標を指定すると, 「なんか黒い線が出る」とか「微妙に縮んだ気がする」とかいう 怪奇現象が発生します。

blt メソッドは,サーフェスが失われていた場合に ComFailException を飛ばすのではなく返り値でエラーを示します。 このため,解像度の変更などで DirectDrawSurface が 無効になっている場合はわかります 7

サーフェスが失われた場合は restore メソッドで復旧を行います。 しかしながら DirectDraw を使用するゲーム等が動作したことで 解像度が変更された場合は失敗して ComFailException が発生します。 したがって restore メソッドが例外を発生した場合は DirectDraw を初期化し直さなければなりません。

本来解像度が変更されると WM_DISPLAYCHANGE が 飛んでくるので分かるのですが, Visual J++ のフォームだと簡単にはトラップできないので困りものです。 メッセージの横取りは出来るようなのですが 「速度が落ちる可能性があります」警告付きなので採用しませんでした。

DDBLT_WAIT 回避

上のプログラムでは bltDDBLT_WAIT を, bltFastDDBLTFAST_WAIT を渡し, ビデオチップによる転送が終わるまで 8 CPUを無意味に占有します。 ゲームならともかく, 他のプログラムも動作しているウィンドウモードでは CPUの無意味な占有は望ましくありません。

blt/bltFast メソッドは, DDBLT_WAIT/DDBLTFAST_WAIT 指定しなければ, ビデオチップがBUSYのときすぐに返ってきます。 このとき一時的にCPUを解放すれば良いわけです。 CPUを解放するにはWindows 3.1よろしく PeekMessage() APIを使ってもいいですし, お手軽に Sleep(1) でもいいでしょう :D

……と,ここまで書いていうのもなんですが, 実際に実験してみると,手元のMillenium IIでは DDBLT_ASYNC 9を 指定しようが連続で巨大な転送をしようが糸色文寸にBUSYにならず, 必ず DD_OK が返ってきました。 なんででしょう?(^^; 変だなぁ,人徳が足りなかったのに違いない。

何にせよ,blt を乱発するときに MS-DOS時代のINT 28hよろしく Sleep(1) なんかを発行すれば バックグラウンドプロセスにCPUが回りますし, ウィンドウモードにはいいんじゃないでしょうか(無責任)。

CPU 描画の壁

CPU で描画するためにサーフェスへのポインタを取得する 10には, C++ では Lock メソッドを使うのでした。しかし,

DirectDrawSurface の lock は似非 Lock

なので全然使えないのでした。 どういうことかというと, DirectDrawSurfacelock メソッドの仕様は

public void lock(Rect r, DDSurfaceDesc d, int flags, int hnd, byte[] memory);

なのですが,最後の byte[] memory が曲者です。つまり,

lock でサーフェスの内容を memory[ ] にコピーし
unlock でサーフェスに書き戻す

のです。 そんなのDirectじゃね〜 o(--;)/Θ

もちろん DDSurfaceDesc から lpSurface に相当するフィールドは削除されています 11。 理由はおそらく,byte[] にサーフェスへの参照を格納してしまうと Java VM のガーベッジコレクタが誤動作するからとかでしょう。 J/Direct の仕様って怪しげだし……

というわけで,次回はJavaでCPU描画をする方法になります(予定)。

おまけ

Visual Studio 6.0 を持っている方へ,お薦めの MSDN トピック。

ていうか,MSDN が読み物になるとは知りませんでした(ぉ 12


1) Visual C++ 6.0 の統合環境は使いやすくなっています
2) Millenium II では数割しか違いませんが,RIVA 等の新しいカードではVRAM to VRAM のほうがかなり速いかもしれません。
3) 少なくともMilleniumではサポートされていません(x_x)
4) com.ms.directX を使わずに自前でJ/Direct を使えば返り値を取得できるのでしょうが……
5) 別にこんな使い方ならバックバッファなんていらないんですが,サンプルとして一応 :-)
6) というか,引数の設定が楽 :D たぶん bltbltFast で速度なんて変わらん :D
7) DirectDrawSurfaceisLost メソッドで調べるのが正道です
8) DirectDraw ではビデオチップにCPUと非同期で転送作業を行わせることができます
9) 非同期転送を指示する定数
10) DirectDraw は仮想メモリ機構を酷使して,たとえバンク切替方式のVRAMでもリニア配列としてサーフェスの実体を返してくれます。VRAMそのものなので書き換えれば即座に表示へ反映されます。
11) C++ ではこのポインタがサーフェスメモリの実体を指すのですが……
12) 【壱註】名簿が遅れたのは,これを読みながら電話越しに爆笑合戦をやっていたからですな。すまん。

今野 俊一 (こんの, knn) <toknn@ijk.com>, <knn@ebony.plala.or.jp>
東京大学 工学部 計数工学科(内定), TSG(理論科学グループ)