文字描画関連その1

CPUとGPU半導体なので、すごい勢いで技術が進歩している。主に製造プロセスの進歩と、それに伴い増えたトランジスタをどう使うかという部分での性能強化だ。ただし、CPUに対してGPUのほうが性能の伸びがずいぶん大きい。この違いは載せている回路の使い方の違いだ。
大雑把に言うと、GPUは単純な演算を行う大量の回路を載せて並列動作させるから、トランジスタ数にほぼ比例するような性能が得られる。対してCPUは少ないスレッドをそれぞれ高速に動かそうとするもので、この性能強化はコストが高い。なぜかというとシングルスレッドのプログラムを細かく分割しても並列に動かせる部分は少ないからだ。例えばVIEWで100命令同時発行とかのCPUを作ってもそれを活かせるコードを出力するようなコンパイラは作れないだろう。

GPUも最近の流行はGPGPU用途で、演算コアの複雑化が進んでいる。Intelの派手に討ち死にしたLarrabeex86コアを多数並べてGPUとして動作させるアーキテクチャだったが、GPUもこのまま進歩すれば似たようなものになっていくだろう。いずれ現在でいうARMとx86の比較のように、命令セットと搭載する機能の若干の違いによる電力効率とパフォーマンスの違いでしか無くなる日がくる。そのとき、Intelはそれならx86でいいんじゃね?て感じで小さなx86コアを大量に積んだ統合CPUを作るだろうと予測している。アプリがスレッドを作っても、DirectXでシェーダを実行しても、CPU内の小さなコアに割り当てられて必要なぶんだけ動作するという、非常に効率のよいCPUとなる。Larrabeeは早すぎたのだ。

そういう時代になったとき、RubyDirectXライブラリであるところのDXRubyはどうなるのか、というのをよく考えているのだが、まあ、ほんとにそうなるかどうかもよくわからんし、Rubyがどうなっているかもわからんわけで、そんときになってみないと何とも言えないというお約束のオチである。

それはさておき今回は文字描画まわりについて。


Windowsで文字を描画しようとすると、っていうかWindowsに限らず、文字を扱うのはかなり厄介な話である。そもそも文字は日本語だけではないし、WindowsのようなOSは他国語対応なので否応無しにそのへんはヤヤコシクなる。PC98時代は日本語を扱うのも割とラクだったものだ。
気をつけないといけないのはまず文字コード。いわゆるキャラセットとエンコードである。それらをWindowsがどう扱っているか、と、Rubyがどう扱っているか、というのを理解しないといけない。それから文字を描画するためのフォント情報をどう扱うか。どうやって目に見える形にして描画するか。

とりあえずDXRubyのRenderTarget#draw_fontを見てみよう。長いけど。

/*--------------------------------------------------------------------
   フォント描画
 ---------------------------------------------------------------------*/
static VALUE RenderTarget_drawFont( int argc, VALUE *argv, VALUE obj )
{
    struct DXRubyPicture_drawFont *picture;
    struct DXRubyFont *font;
    VALUE vcolor;
    int cr, cg, cb;
    VALUE vz, vangle, vscalex, vscaley, valpha, vcenterx, vcentery, vblend;
    VALUE voption;
    struct DXRubyRenderTarget *rt = DXRUBY_GET_STRUCT( RenderTarget, obj );
    VALUE vsjisstr;

    DXRUBY_CHECK_DISPOSE( rt, surface );

    if( argc < 4 || argc > 5 ) rb_raise( rb_eArgError, "wrong number of arguments (%d for %d..%d)", argc, 4, 5 );
    Check_Type(argv[2], T_STRING);

    if( argc < 5 || argv[4] == Qnil )
    {
        voption = rb_hash_new();
    }
    else
    {
        Check_Type( argv[4], T_HASH );
        voption = argv[4];
    }

    vblend = hash_lookup( voption, symbol_blend );
    vangle = hash_lookup( voption, symbol_angle );
    valpha = hash_lookup( voption, symbol_alpha );
    vscalex = hash_lookup( voption, symbol_scale_x );
    vscalex = vscalex == Qnil ? hash_lookup( voption, symbol_scalex ) : vscalex;
    vscaley = hash_lookup( voption, symbol_scale_y );
    vscaley = vscaley == Qnil ? hash_lookup( voption, symbol_scaley ) : vscaley;
    vcenterx = hash_lookup( voption, symbol_center_x );
    vcenterx = vcenterx == Qnil ? hash_lookup( voption, symbol_centerx ) : vcenterx;
    vcentery = hash_lookup( voption, symbol_center_y );
    vcentery = vcentery == Qnil ? hash_lookup( voption, symbol_centery ) : vcentery;
    vz = hash_lookup( voption, symbol_z );
    vcolor = hash_lookup( voption, symbol_color );

    DXRUBY_CHECK_TYPE( Font, argv[3] );
    font = DXRUBY_GET_STRUCT( Font, argv[3] );
    DXRUBY_CHECK_DISPOSE( font, pD3DXFont );

    picture = (struct DXRubyPicture_drawFont *)RenderTarget_AllocPictureList( rt, sizeof( struct DXRubyPicture_drawFont ) + RSTRING_LEN(argv[2]) + 2 );
    if( picture == NULL )
    {
        rb_raise( eDXRubyError, "フォント用メモリの確保に失敗しました" );
    }

    /* DXRubyPictureオブジェクト設定 */
    picture->func = RenderTarget_drawFont_func;
    picture->x = NUM2INT( argv[0] );
    picture->y = NUM2INT( argv[1] );
    picture->angle   = (vangle   == Qnil ? 0.0f : NUM2FLOAT( vangle ));
    picture->scalex  = (vscalex  == Qnil ? 1.0f : NUM2FLOAT( vscalex ));
    picture->scaley  = (vscaley  == Qnil ? 1.0f : NUM2FLOAT( vscaley ));
    picture->centerx = (vcenterx == Qnil ? 0.0f : NUM2FLOAT( vcenterx ));;
    picture->centery = (vcentery == Qnil ? 0.0f : NUM2FLOAT( vcentery ));;
    picture->alpha   = (valpha   == Qnil ? 0xff : NUM2INT( valpha ));
    picture->blendflag = (vblend == Qnil ? 0 :
                         (vblend == symbol_add ? 4 :
                         (vblend == symbol_none ? 1 :
                         (vblend == symbol_add2 ? 5 :
                         (vblend == symbol_sub ? 6 :
                         (vblend == symbol_sub2 ? 7 : 0))))));
    picture->value = argv[3];

#ifdef HAVE_RB_ENC_STR_NEW
    if( rb_enc_get_index( argv[2] ) != 0 )
    {
        vsjisstr = rb_funcall( argv[2], rb_intern( "encode" ), 1, rb_str_new2( sys_encode ) );
    }
    else
    {
        vsjisstr = argv[2];
    }
#else
    vsjisstr = argv[2];
#endif
    lstrcpy( picture->str, RSTRING_PTR( vsjisstr ) );  /* 文字列の保存 */
    picture->str[RSTRING_LEN(vsjisstr)] = ' ';    /* イタリック対策にスペース追加 */
    picture->str[RSTRING_LEN(vsjisstr)+1] = 0;

    if( vcolor != Qnil )
    {
        Check_Type( vcolor, T_ARRAY );
        if( RARRAY_LEN( vcolor ) == 4 )
        {
            picture->alpha = NUM2INT( rb_ary_entry( vcolor, 0 ) );
            cr = NUM2INT( rb_ary_entry( vcolor, 1 ) );
            cg = NUM2INT( rb_ary_entry( vcolor, 2 ) );
            cb = NUM2INT( rb_ary_entry( vcolor, 3 ) );
        }
        else
        {
            cr = NUM2INT( rb_ary_entry( vcolor, 0 ) );
            cg = NUM2INT( rb_ary_entry( vcolor, 1 ) );
            cb = NUM2INT( rb_ary_entry( vcolor, 2 ) );
        }
    }
    else
    {
        cr = 255;
        cg = 255;
        cb = 255;
    }
    picture->color = D3DCOLOR_XRGB(cr, cg, cb);
    picture->value = argv[3];
    picture->z = 0;

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

    return obj;
}

上のほうに並んでいるhash_lookupはRubyの関数ではなく、自分で作った高速ハッシュ参照関数である。古いRubyに無かったので互換性のために自分で作った。
最初のポイントは通常描画のところにもあったRenderTarget_AllocPictureList関数の引数。

    picture = (struct DXRubyPicture_drawFont *)RenderTarget_AllocPictureList( rt, sizeof( struct DXRubyPicture_drawFont ) + RSTRING_LEN(argv[2]) + 2 );

最後の「 + RSTRING_LEN(argv[2]) + 2」というのが何かという話なのだが、RSTRING_LENはStringオブジェクトの文字の長さを取得するRubyマクロだ。参照しているargv[2]はdraw_fontに渡したStringオブジェクトで、+2は後で説明する1文字と終端文字の長さとなる。つまりstruct DXRubyPicture_drawFontのサイズに文字列のサイズを足していて、これはどういうわけなのかと言うと、構造体を見るとわかる。

struct DXRubyPicture_drawFont {
    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;
    int z;
    float scalex;
    float scaley;
    float centerx;
    float centery;
    float angle;
    int color;                  /* フォントの色 */
    char str[0];                /* 文字列オブジェクト */
};

最後のchar str[0]である。これが描画しようとしている文字列なのだが、配列のサイズを0と定義してあって、mallocするときに余分にサイズを取ってやれば任意のサイズの配列が作れるよね?みたいな小手先テクニックなのだ。昔はサイズ0で配列を作れないコンパイラがあって、そういう場合は1を指定していた。今そういうのがあるかどうかは知らない。

では次、HAVE_RB_ENC_STR_NEWマクロをifdefで囲んでいるところ。このマクロはRuby拡張ライブラリを作るときにextconf.rbで「have_func("rb_enc_str_new") 」とするとこのCの関数がRubyに存在するときにMakefileで定義される。この関数はRuby1.9以降に存在するものだから、ようするに簡単に言うとRuby1.8と1.9の切り分けに使える。こういう手でDXRubyでは1.8と1.9のコンパイルをわけて、ソース互換に無理矢理しているのだ。
そもそもなんで1.8と1.9でわける必要があったのかと言う話だが、まあ、この関数の中ではじめて出現したのだから想像できるだろうけども、文字コード関連の処理だ。もともとDXRubyは日本語WindowsSJIS(Windows-31J)を扱うOSという理由でSJIS限定のライブラリだったのだが、Ruby1.9ではCSI方式になって文字列がエンコード情報を持つようになったから、例えばダイアログを開いてファイル名を取得した場合に、そいつのエンコード情報をどうするか、というのが問題になった。1.8なら放っておけばいいが、1.9だとエンコードをつけてやらなければならない。ソース互換をどうやって取ろうか、という話だ。
結果としてこういう手段でなんとかしたわけだが、それだったらせっかくだし1.9の場合はユーザが使うエンコードは自由にしてしまおう、と言うことで、文字のエンコード変換を入れたわけだ。Ruby1.9では文字列がエンコード情報を持っているから、システムエンコード(日本語WindowsならSJIS)になぁれって言うだけで勝手になってくれる。実にラクである。CSIさまさまだ。

あと微妙なのは文字列をDXRubyPicture_drawFontの最後にコピーしてから空白を入れているところ。イタリック体にすると文字が傾いて右にはみ出してしまうのだが、DirectXの文字描画は予め文字数にあわせた矩形を作って描画しているらしく、右にはみ出したぶんが描画されないという問題があって、それの対策だ。この1文字を入れるために構造体のサイズ+文字数+「2」をしていたのだな。終端用だけなら1で足りる。

んで今回初登場、色配列から色を取り出す処理があって、3要素と4要素で分岐している。ちょっとしょぼい。これを見るとやっぱりARGBじゃなくてRGBAにしとけばよかったかなーShaderのデータもRGBAだしなーと思うのだが、互換性的問題で変更は不可だ。

最後にこのデータの描画処理は以下のようになる。初期に作った描画処理なのでD3DXFontを使っていて、これもなんとかしたいところなのだがやはり互換性的な問題で簡単にはどうにもできない。すごくラクではあるのだが。

static void RenderTarget_drawFont_func( struct DXRubyPicture_drawFont *picture )
{
    D3DVECTOR vector;
    D3DXMATRIX matrix;
    D3DXMATRIX matrix_t;
    RECT rect;
    struct DXRubyFont *font = DXRUBY_GET_STRUCT( Font, picture->value );
    float angle = 3.141592653589793115997963468544185161590576171875f / 180.0f * picture->angle;
    DXRUBY_CHECK_DISPOSE( font, pD3DXFont );

    /* D3DXSpriteの描画開始 */
    g_pD3DXSprite->lpVtbl->Begin( g_pD3DXSprite, D3DXSPRITE_ALPHABLEND );

    /* 回転及び拡大縮小 */
    D3DXMatrixScaling    ( &matrix_t, picture->scalex, picture->scaley, 1 );
    D3DXMatrixRotationZ  ( &matrix  , angle );
    D3DXMatrixMultiply   ( &matrix  , &matrix_t, &matrix );

    /* 平行移動 */
    D3DXMatrixTranslation( &matrix_t, (float)picture->x + picture->centerx, (float)picture->y + picture->centery, 0 );
    D3DXMatrixMultiply   ( &matrix  , &matrix, &matrix_t );

    g_pD3DXSprite->lpVtbl->SetTransform( g_pD3DXSprite, &matrix );

    rect.left   = -picture->centerx;
    rect.top    = -picture->centery;
    rect.right  = picture->centerx;
    rect.bottom = picture->centery;
    font->pD3DXFont->lpVtbl->DrawText( font->pD3DXFont, g_pD3DXSprite, picture->str, -1, &rect, DT_LEFT | DT_NOCLIP,
                                       ((int)picture->alpha << 24) | picture->color & 0x00ffffff);
    g_pD3DXSprite->lpVtbl->Flush( g_pD3DXSprite );

    /* ピクチャの描画終了 */
    g_pD3DXSprite->lpVtbl->End( g_pD3DXSprite );
}

DXRubyの文字描画はこれとは別にRenderTarget#draw_font_exとImage#draw_font、Image#draw_font_exがあるので、それはまた後日。Fontオブジェクトのほうもいずれ説明せねばならないだろう。