RenderTargetの構造と描画予約

DXRubyは1.2からRenderTargetを導入したおかげで、内部的にはWindowモジュールが一つのRenderTargetオブジェクトを持つようにして、そいつをスクリーンバッファに関連付けて使うように変更した。こうすることでWindowモジュールのdraw系メソッドは内部RenderTargetのdraw系メソッドの呼び出しという形で実装できてラクになる。どうせ同じ動きだし。なので基本的には描画まわりの処理はRenderTargetに一本化されている。
そういう都合で描画処理を説明するためにはRenderTargetを説明することになる。


RenderTargetオブジェクトはCの構造体ではこうなっている。

/* RenderTargetオブジェクトの中身 */
struct DXRubyRenderTarget {
    struct DXRubyTexture *texture;
    float x;     /* x始点位置      */
    float y;     /* y始点位置      */
    float width; /* イメージの幅   */
    float height;/* イメージの高さ ここまでImageと共通*/
    IDirect3DSurface9 *surface;

    int PictureCount;                  /* ピクチャの登録数 */
    int PictureAllocateCount;          /* ピクチャ登録のメモリ確保数 */
    int PictureSize;                   /* ピクチャデータの使用済みサイズ */
    int PictureAllocateSize;           /* ピクチャデータのメモリ確保サイズ */
    char *PictureStruct;

    struct DXRubyPictureList *PictureList;

    int minfilter;      /* 縮小フィルタ */
    int magfilter;      /* 拡大フィルタ */

    int a;              /* 背景クリア色 α成分 */
    int r;              /* 背景クリア色 赤成分 */
    int g;              /* 背景クリア色 緑成分 */
    int b;              /* 背景クリア色 青成分 */

    VALUE array; /* drawFontExによる自動生成Imageの置き場所 */

    int PictureDecideCount;            /* ピクチャの登録確定数 */
    int PictureDecideSize;             /* ピクチャデータの登録確定サイズ */
};

上の5つはImageオブジェクトと同じ構造になっていて、描画データとして指定されたときはstruct DXRubyImageにキャストして使う。surfaceはテクスチャと同じデータを違うオブジェクトでも持っているだけで、DirectX APIの都合で両方あったほうがラクだから存在している。その下のPictureなんちゃらというやつは描画予約情報の管理用変数で、これはたぶんあとで別の説明記事になる。その後ろはフィルタの設定、背景の色、フォント画像保持配列、描画予約確定処理用変数となる。まあ、ようするに画像データと描画情報を持っているわけだ。RenderTargetが持っているテクスチャは前にも書いたように、CPUから編集できるタイプではないので、Imageのように画像編集対象にはできない。すごく遅くていいならできるけども。

さて、描画処理のほうなのだが、RenderTarget#drawで呼ばれる関数はとりあえずこんな感じになっている。

/*--------------------------------------------------------------------
   描画設定(通常描画)
 ---------------------------------------------------------------------*/
static VALUE RenderTarget_draw( int argc, VALUE *argv, VALUE obj )
{
    struct DXRubyImage *image;
    struct DXRubyPicture_draw *picture;
    float z;
    struct DXRubyRenderTarget *rt = DXRUBY_GET_STRUCT( RenderTarget, obj );
    DXRUBY_CHECK_DISPOSE( rt, surface );

    if( argc < 3 || argc > 4 ) rb_raise( rb_eArgError, "wrong number of arguments (%d for %d..%d)", argc, 3, 4 );

    /* 引数のイメージオブジェクトから中身を取り出す */
    DXRUBY_CHECK_IMAGE( argv[2] );
    image = DXRUBY_GET_STRUCT( Image, argv[2] );
    DXRUBY_CHECK_DISPOSE( image, texture );

    picture = (struct DXRubyPicture_draw *)RenderTarget_AllocPictureList( rt, sizeof( struct DXRubyPicture_draw ) );

    /* DXRubyPictureオブジェクト設定 */
    picture->func = RenderTarget_draw_func;
    picture->x = NUM2INT( argv[0] );
    picture->y = NUM2INT( argv[1] );
    picture->value = argv[2];
    picture->alpha = 0xff;
    picture->blendflag = 0;

    /* リストデータに追加 */
    rt->PictureList[rt->PictureCount].picture = (struct DXRubyPicture *)picture;
    z = argc < 4 || argv[3] == Qnil ? 0.0f : NUM2FLOAT( argv[3] );
    rt->PictureList[rt->PictureCount].z = z;
    picture->z = z;
    rt->PictureCount++;

    return obj;
}

これを基本にして、他のはちょっと毛が生えたりしている程度だ。
ここでのミソはまずrb_scan_argsを使わず、argvを直接アクセスしているところだろう。ほんの少しでも実行速度を上げようと涙ぐましい努力の跡だ。いまどきのPC用に拡張ライブラリを作る人は決して真似してはいけない。
真ん中やや上にあるRenderTarget_AllocPictureListというのは描画予約情報を格納するメモリを必要なら確保して、そのポインタを返す。もともと描画ごとにmallocしていたのだがそれだと遅いから独自メモリ管理を作ったものだ。中身はそのうち説明するかもしれない。このへんにも苦労の跡がある。いまどきならmalloc/freeしとけばいいんじゃねーの、みたいな。
こいつが返すDXRubyPictureオブジェクトというのは、

/* Picture系基底構造体 */
struct DXRubyPicture {
    void (*func)(void*);
    VALUE value;
    unsigned char blendflag; /* 半透明(000)、加算合成1(100)、加算合成2(101)、減算合成1(110)、減算合成2(111)のフラグ */
    char reserve1;
    char reserve2;
    char reserve3;
};

という構造体のデータで、これを基本にして必要な追加情報を後ろにつけることになる。

struct DXRubyPicture_draw {
    void (*func)(void*);
    VALUE value;
    unsigned char blendflag; /* 半透明(000)、加算合成1(100)、加算合成2(101)、減算合成1(110)、減算合成2(111)のフラグ */
    unsigned char alpha;     /* アルファ(透明)値 */
    char reserve1;           /* 予約3 */
    char reserve2;           /* 予約4 */
    int x;
    int y;
    float z;
};

このような感じだ。
funcは関数ポインタで、これが指す関数が実際の描画時に呼ばれる。描画パターンごとに最適化した関数に分かれている。valueは基本的にはImageオブジェクトが設定されて、あとブレンド情報と座標とzとなっている。これらを設定したうえで、RenderTargetが持つ描画予約リストにその情報を書き込んで、終わる。描画そのものはここではなく、Window.loopがまわるときやupdateメソッドを呼んだときに実行されるので、そこはまたいずれ。

描画予約リストは

/* ピクチャ配列 */
static struct DXRubyPictureList {
    float z;                        /* ピクチャのZ座標 */
    struct DXRubyPicture *picture;    /* ピクチャ構造体へのポインタ */
};

という構造体の配列になっていて、さっきのRenderTarget_AllocPictureListの中で領域が足りなければこれも自動的に拡張される。描画優先順ソートをするためだけに存在していて、最低限の情報に絞ることでCPUキャッシュを効率よく使ってマージソートすることができる。はずだ。
描画時にはここに並んでいるピクチャ構造体を順番に描画することになる。

ちなみにNUM2INTはRubyのマクロだがNUM2FLOATは自分で作った。

#define NUM2FLOAT( x ) ((float)( FIXNUM_P(x) ? FIX2INT(x) : NUM2DBL(x) ))

FixnumをNUM2DBLマクロに食わせるとFloatオブジェクトを生成してからdouble型の値を取り出して返してくるので無駄だったのだ。地味に細かい努力をしているつもり。