RenderTargetの生成と解放

DXRubyの描画予約順ソートはマージソートを採用している。初期の頃はシンプルなバブルソートだったのだが、さすがにアレなのでマージソートに変更したのだ。なぜマージソートを選択したのかというと、描画優先順が同一だった場合、メソッドを発行した順に描画される必要があるからソートは安定ソートであるのがまず前提で、それなりに速く、実行時間の変動が少ないからだ。
1フレームという規定時間内に処理が完了する必要がある場合、ある処理の実行時間が想定できないと手痛いことになる。最も遅い時間にあわせて他を切り詰めないといけなくなるからだ。ソートの場合、データの並び方のパターンで実行時間が大きく変わってくる方式が多いから、そういうのを採用すると速いときは速いが遅いときは遅い、しかも並び方のパターンによるから微妙に予測ができない、ということになる。ソートアルゴリズムまで考えたうえでソートが速くなるように描画順を考えるぐらいなら、いっそのことソートが必要ないように描画順を制御したほうがいい。
ソート処理については真面目にマージソートを実装しただけなのでソースは割愛、マージソートアルゴリズムも調べてもらえばいくらでもでてくるので割愛する。

今回はRenderTargetの生成と解放について。


まずはRenderTargetのallocateから。

/*--------------------------------------------------------------------
   RenderTargetクラスのallocate。メモリを確保する為にinitialize前に呼ばれる。
 ---------------------------------------------------------------------*/
static VALUE RenderTarget_allocate( VALUE klass )
{
    VALUE obj;
    struct DXRubyRenderTarget *rt;

    /* DXRubyRenderTargetのメモリ取得&RenderTargetオブジェクト生成 */
    rt = malloc(sizeof(struct DXRubyRenderTarget));
    if( rt == NULL ) rb_raise( eDXRubyError, "メモリの取得に失敗しました - RenderTarget_allocate" );
    obj = Data_Wrap_Struct(klass, RenderTarget_mark, RenderTarget_release, rt);

    /* とりあえずテクスチャオブジェクトはNULLにしておく */
    rt->texture = NULL;
    rt->surface = NULL;

    /* ピクチャ構造体の初期値設定 */
    rt->PictureCount = 0;
    rt->PictureAllocateCount = 128;
    rt->PictureSize = 0;
    rt->PictureAllocateSize = 128*32;
    rt->PictureList = malloc( rt->PictureAllocateCount * sizeof(struct DXRubyPictureList) );
    rt->PictureStruct = malloc( rt->PictureAllocateSize );
    rt->PictureDecideCount = 0;
    rt->PictureDecideSize = 0;

    rt->minfilter = D3DTEXF_LINEAR;
    rt->magfilter = D3DTEXF_LINEAR;

    rt->a = 0;
    rt->r = 0;
    rt->g = 0;
    rt->b = 0;
    rt->array = Qnil;
    rt->array = rb_ary_new();

    return obj;
}

RenderTarget構造体は前の記事に上げたので無し。メモリ確保などは特に新しいことは無いが、構造体の初期設定をしている部分についても別に珍しいことはない。強いていうならarrayがRubyオブジェクトになっているところぐらいか。わざわざnilを入れてからrb_ary_newしているのは奇妙だが、rb_ary_newは新しいRubyオブジェクトを生成するから、このタイミングでGCが発生する可能性がある。するとarrayはGC内でマークされるものなのにこのタイミングでは値を設定していなくて不定になってしまう。ということでとりあえずnilを設定してから配列を生成しているのだ。
ちなみに同様の理由で、Data_Wrap_Structを最後のreturnのときにやるのもよろしくない。Rubyオブジェクト化するタイミングでGCが発生すると、そのオブジェクトはまだ作られていないからGCからマーク関数が呼び出されず、ここで生成したRubyオブジェクトが全部破棄されるハメになる。DXRubyではそれで何度かバグって苦労した。
マーク関数というのはData_Wrap_Structで設定してる2番目の引数のことで、この関数がGCのマーク処理の中から呼ばれることになっている。そもそもマークってなんぞやっていう人はGCのマーク&スウィープというアルゴリズムを勉強してください。とりあえずそれ知らないとRubyの拡張ライブラリは作れない。

/*--------------------------------------------------------------------
   RenderTargetクラスのmark
 ---------------------------------------------------------------------*/
static void RenderTarget_mark( struct DXRubyRenderTarget *rt )
{
    int i;

    for( i = 0; i < rt->PictureCount; i++ )
    {
        rb_gc_mark( rt->PictureList[i].picture->value );
    }

    rb_gc_mark( rt->array );
}

この中では描画予約リストが保持しているImageオブジェクト(じゃないこともあるけど)と、arrayで持っているArrayオブジェクトをマークしている。CRubyにはこの仕掛けがあるのでC構造体にVALUE型を保持することが可能だが、推奨はされていない。mrubyにはこの仕掛けが無いのでC構造体にRubyオブジェクトを保持することはできない。関係ないけど。

続いてGCで回収されたときの処理。

/*--------------------------------------------------------------------
   参照されなくなったときにGCから呼ばれる関数
 ---------------------------------------------------------------------*/
static void RenderTarget_free( struct DXRubyRenderTarget *rt )
{
    RELEASE( rt->surface );
    if( rt->texture != NULL )
    {
        RELEASE( rt->texture->pD3DTexture );
        free( rt->texture );
        rt->texture = NULL;
    };
    free( rt->PictureList );
    free( rt->PictureStruct );
    DeleteRenderTargetList( rt );
    rt->PictureCount = 0;
    rt->PictureSize = 0;
    rt->PictureDecideCount = 0;
    rt->PictureDecideSize = 0;
    rt->array = Qnil;
}

void RenderTarget_release( struct DXRubyRenderTarget *rt )
{
    /* テクスチャオブジェクトの開放 */
    if( rt->texture )
    {
        RenderTarget_free( rt );
    }
    free( rt );
    rt = NULL;

    g_iRefAll--;
    if( g_iRefAll == 0 )
    {
        CoUninitialize();
    }
}

freeとreleaseの関係はImageと同じようなもんで、特に目新しいことは無いのだが、freeのほうの真ん中あたりにDeleteRenderTargetListという関数がいる。これは、存在するRenderTargetオブジェクトへの参照をすべて保持する配列が別に存在していて、そこから当該RenderTargetオブジェクトへの参照を削除する関数だ。なぜそんなものがあるのかというとデバイスロストの対処に必要だからで、デバイスロスト時にはDirectXに管理されていないテクスチャなどのオブジェクトをすべて解放して、デバイスをリセットして、それから再作成する。そのためにはRenderTargetオブジェクトの完全なリストが必要になる。なのでそのリストをRenderTargetのinitializeやdispose、GCによる回収などで地道に管理しているのだ。
DXRuby1.2ではRenderTargetだけが追加されたが、これだけでも意外に結構な手間がかかっているシロモノだったりする。描画系はほとんど作り直したしね。