Archives

November 2009 の記事

AMD Z430 は PowerVR と同じようにタイルベースのレンダリング (TBR) を行うことで
知られています。TBR も Unified Shader の仕様も AMD は Xbox360 の GPU がベース
だと説明していますが、その両者の性能差は桁違いです。
一方は強力な GPU が要求するバス帯域に応えるため、もう一方はモバイル向けでタイトな
メモリ速度でもそれなりの性能を維持するため。同じ技術でも応用先の GPU が真逆なのは
面白いところです。

AMD Next-Gen Tile-Based GPUs (pdf)
AMD Next-Generation OpenGLR ES 2.0 Graphics Technology Achieves Industry Conformance

NetWalker に用いられている i.MX515 は GPU core として AMD (ATI) imageon Z430
を搭載しています。
ここ最近 NetWalker で OpenGL ES 2.0 を触っていますが、まだ Z430 の特性がうまく
つかめておらずあまり速度が出ていません。調べている最中です。
偶然 TBR のタイル領域の大きさを知ることが出来ました。


●タイルサイズ

NetWalker_OpenGLES03.jpg

フレームバッファをクリアしないで描画した場合の画面がこれです。GPU のタイルバッファも
クリアせずに上書きされているようです。
1024x600 の画面なので、上の写真のケースだと 1タイルは 256x128 ドット。
以下バックバッファの組み合わせ毎にタイルサイズをまとめてみました。

Color   Depth/Stencil   Tile
-----------------------------------
16bit   none            512x128
16bit   16bit           256x128
32bit   16bit           128x128
16bit   24bit/8bit      128x128
32bit   24bit/8bit      128x128

◎タイルバッファの容量

 少なくても 128KByte 存在しています。予想より大容量です。

◎Depth/Stencil との組み合わせ

 使用可能な depth の幅はカラーの影響を受けることが多いのですが、こちら
 書いたとおり Z430 では任意の組み合わせを選べます。PowerVR もそうだったので
 やはり TBR 専用のバッファを持っているおかげだと思われます。

◎32bit Color or 24bit Depth 利用時の速度低下

 Color か Depth どちらかを 16bit より増やすと速度に影響が出ます。
 その原因はバス帯域だと思っていましたが、この結果を見るとタイル分割数の増加が
 負担になっているのかもしれません。


● Extension

まだ試していませんが TBR を制御する API があります。

GL_VERSION: OpenGL ES 2.0
GL_RENDERER: AMD Z430
GL_VENDOR: Advanced Micro Devices, Inc.
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL ES 1.00
GL_EXTENSIONS:
    GL_AMD_compressed_3DC_texture
    GL_AMD_compressed_ATC_texture
    GL_AMD_performance_monitor
    GL_AMD_program_binary_Z400
    GL_AMD_tiled_rendering
    GL_EXT_texture_filter_anisotropic
    GL_EXT_texture_type_2_10_10_10_REV
    GL_EXT_bgra
    GL_OES_compressed_ETC1_RGB8_texture
    GL_OES_compressed_paletted_texture
    GL_OES_depth_texture
    GL_OES_depth24
    GL_OES_EGL_image
    GL_OES_EGL_image_external
    GL_OES_element_index_uint
    GL_OES_fbo_render_mipmap
    GL_OES_fragment_precision_high
    GL_OES_get_program_binary
    GL_OES_packed_depth_stencil
    GL_OES_rgb8_rgba8
    GL_OES_standard_derivatives
    GL_OES_texture_3D
    GL_OES_texture_float
    GL_OES_texture_half_float
    GL_OES_texture_half_float_linear
    GL_OES_texture_npot
    GL_OES_vertex_half_float
    GL_OES_vertex_type_10_10_10_2
    GL_NV_fence


● NetWalker の速度比較

描画面積を出来るだけ小さくして頂点速度を測定しようとしたもの。

AMD Z430          48000x1 =  48000 Tris/s 30fps = 約 1.44M Tris/s  (1024x600)
PowerVR SGX 535   48000x4 = 192000 Tris/s 45fps = 約 8.64M Tris/s  ( 480x320)

フレームバッファのサイズが極端に違うし、どちらも Unified Shader なので頂点に
ピクセルの影響が全くないとは言い切れません。Clear → Swap だけでもあまり速度が
出ないので、広い画面が仇になっている可能性があります。
タイル+転送だけである程度の負荷がかかり、頂点も圧迫しているのでしょうか。
1024x600 は 480x320 のちょうど 4倍の面積です。

1024x600 = 614400 pixels  8 倍  NetWalker
 640x480 = 307200 pixels  4 倍  VGA
 480x320 = 153600 pixels  2 倍  iPhone
 320x240 =  76800 pixels  1 倍  QVGA


● VRAM 容量

PC のチップセット内蔵 GPU と同じように、VRAM はメインメモリから確保していると
考えられます。NetWalker の RAM は 512MB ですが 480MB しか見えないので、残りの
32MB が VRAM の取り分かもしれません。

1024x600 x16bit は 1.2MB (fb は 2.5MB) なので、32MB もあると少々無駄に感じる
かもしれません。ところが OpenGL ES 2.0 を使っていたらあっという間に VRAM が
溢れました。(GL_OUT_OF_MEMORY を返す)

その原因は自分のローダーでした。Z430 が DXT に対応していないため、圧縮された
テクスチャを 8888 に展開していたからです。あらかじめ ATC/3Dc/ETC へ変換して
おけば解決するはずです。計算したらテクスチャを 23MB も読み込んでいました。

もう一つ考えられる理由は TBR による遅延レンダリングです。
システムメモリからの逐次転送できず、シーンに必要なリソースを全部 VRAM に乗せて
おかないと描画できないのかもしれません。何らかの確証があるわけではなくあくまで
想像です。

どちらにせよ、設定を変えられるなら VRAM 容量はもうちょっと増やしたいところです。


●立ったまま OpenGL ES 2.0 プログラミング

帰りの電車の中では立ったまま、EGLConfig のパラメータを変えて make したりタイルの
データ取りをしてました。この大きさで開発+実行できるのは良いですね。


関連エントリ
NetWalker PC-Z1 i.MX515 OpenGL ES 2.0 (3)
NetWalker PC-Z1 i.MX515 OpenGL ES 2.0 (2)
NetWalker PC-Z1 i.MX515 OpenGL ES 2.0
OpenGL ES 2.0 Emulator


ComputeShader はかなり便利なことがわかってきました。
ポストフィルタなどのピクセル処理だけでなく、VertexShader の前段に頂点演算として
挿入したり、頂点シェーダーの代わりに使ったりも出来ます。
標準描画パイプラインへのちょっとした機能追加が簡単にできるようになった印象です。

これまでは Stream Output や PixelShader を使っていた処理が ComputeShader
だけで済むわけです。


●ここが簡単

・読み込み位置、書き込み位置が固定されない

  与えるリソースはどれも任意アドレスに対してアクセス可能で、特別扱いする
  ものがありません。

・設定が項目が少ない

  ConstantBuffer, ShaderResourceView, UnorderedAccessView のわずか 3つだけ。
  サンプラーを使う場合は + 1。

・設定が独立している

  描画用のステートをいじる必要がないので、パラメータを保存したり復帰させなくて
  済みます。たとえば Viewport とか、RenderTarget とか、
  DepthStencilState とか!! 描画に影響与えないし、戻さなくてもいいんです。
 
特に 3番目。いちいち DepthStencilState を作ったり、Depth を disable にしたり
しなくても良いだけで CS ありがとう、といった感じ。


● CS の制限

今までの Shader から見れば制限無しに扱いやすい ComputeShader ですが、
VertexShader 代わりに使おうとするといくつか制限も生じます。

 ・グループ内の実行スレッド数はシェーダー内に記述し、実行時に書き換えできない。
 ・Dispatch() に与えられる実行回数は x,y,z それぞれ 65535 まで。
 ・グループ内のスレッド数は 1024 まで。

よって VertexShader のように、1~100万回 など任意の実行回数が与えられた場合に
どのように Compute Shader を呼び出せばよいのか悩みます。

1. 65535 回を超える場合

x, y, z に分けるにしろ、CS 内のアトリビュートで Group 内スレッド数を増やすにしろ、
実行回数が常に定数で割り切れるとは限らない。

2. 速度

ある程度グループ内のスレッド数を大きめの値にしなければ ComputeShader の実行
速度が落ちます。65535 回未満だからと言って、Dispatch() だけで回数を指定して
下記のようなシェーダーを走らせると非常に低速になります。

[numthreads(1,1,1)]
void cmain_loop1( uint3 threadid : SV_DispatchThreadID )
{
    lmain( threadid.x );
}


●解決案

二通りの手段を考えてみました。

(1) 複数のシェーダーに分ける

numthreads = 32 などグループスレッド数を増やしたものと、numthreads = 1 の
端数を処理するスレッドに分けて実行します。
下のプログラムは 32 で割り切れる回数分 cmain_loop32 を実行し、端数を
cmain_loop1 で処理しています。

例えば 75 個のデータを処理するなら、cmain_loop32 を 2回、cmain_loop1 を 11回分
実行します。

// Compute Shader 5.0
cbuffer offset_T : register( b0 ) {
    uint    threadid_offset;
    uint    thread_total;
    uint    r0;
    uint    r1;
};

[numthreads(32,1,1)]
void cmain_loop32( uint3 threadid : SV_DispatchThreadID)
{
    lmain( threadid.x );
}

[numthreads(1,1,1)]
void cmain_loop1( uint3 threadid : SV_DispatchThreadID )
{
    lmain( threadid.x + threadid_offset );
}


// C++
    const int ThreadGroup= 32;
    int dcount1= data_count/ThreadGroup;
    int offset= dcount1*ThreadGroup;
    int dcount2= dcount - offset;

    offset_T cparam;
    cparam.threadid_offset= offset;
    cparam.thread_total= data_count;
    context.UpdateSubresource( CB_Offset.iBuffer, 0, NULL, &cparam, 0, 0 );
    context.CSSetConstantBuffers( 0, 1, &CB_Offset.iBuffer );

    if( dcount1 > 0 ){
        context.CSSetShader( LoopShader_1.iCS, NULL, 0 );
        context.Dispatch( dcount1, 1, 1 );
    }
    if( dcount2 > 0 ){
        context.CSSetShader( LoopShader_32.iCS, NULL, 0 );
        context.Dispatch( dcount2, 1, 1 );
    }


(2) 動的分岐を用いる

端数込みで 32 の倍数分実行します。スレッド番号が実行したい回数より多ければ、
動的分岐で処理を省きます。

例えば 75 個のデータを処理するなら、cmain_loop_dis を 3 回 (96回分) 実行し、
id が 75 以上なら何もしないで終了します。

// Compute Shader 5.0
cbuffer offset_T : register( b0 ) {
    uint    threadid_offset;
    uint    thread_total;
    uint    r0;
    uint    r1;
};

[numthreads(32,1,1)]
void cmain_loop_dis( uint3 threadid : SV_DispatchThreadID )
{
    if( threadid.x < thread_total ){
        lmain( threadid.x );
    }
}


// C++
    const int ThreadGroup= 32;
    int dcount1= (data_count+ThreadGroup-1)/ThreadGroup;

    offset_T cparam;
    cparam.threadid_offset= 0;
    cparam.thread_total= data_count;
    context.UpdateSubresource( CB_Offset.iBuffer, 0, NULL, &cparam, 0, 0 );
    context.CSSetConstantBuffers( 0, 1, &CB_Offset.iBuffer );

    context.CSSetShader( CSSubDShaderDis.iCS, NULL, 0 );
    context.Dispatch( dcount1, 1, 1 );


●実行結果

RADEON HD 5870 で試してみました。
1 フレームあたり 7セット Compute Shader の実行を繰り返しています。
それ以外の描画は fps などのフォントのみ。

上のプログラムはグループ内スレッド数 32 固定でしたが、16~320 まで変更して
試しています。

GroupThread= 16
(1)   476 fps
(2)   482 fps

GroupThread= 32
(1)   840 fps
(2)   852 fps

GroupThread= 48
(1)   973 fps
(2)  1006 fps

GroupThread= 64
(1)  1102 fps
(2)  1145 fps

GroupThread= 96
(1)   984 fps
(2)  1026 fps

GroupThread= 128
(1)  1090 fps
(2)  1140 fps

GroupThread= 256
(1)  1083 fps
(2)  1128 fps

GroupThread= 320
(1)  1009 fps
(2)  1065 fps

GroupThread が小さいと低速です。あまり小さいと Dispatch() の 65535 制限にも
ひっかかります。

どのケースでも、分岐を用いた (2) の方が高速でした。
端数分とはいえ GroupThread=1 で実行しているスレッドがあるため効率が悪い、
7セットの実行中に毎回シェーダー切り替えが発生しているから、切り替えないで済む
(2) の方が条件的に有利、等の理由が考えられます。


関連エントリ
DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
Direct3D11/DirectX11 ComputeShader 4.0 を使う
Direct3D11/DirectX11 (6) D3D11 の ComputeShader を使ってみる