月別アーカイブ: 2007年10月

Direct3D 10 HLSL の asint/asuint/asfloat 命令

D3D10/DX10 の ShaderModel4.0 では整数演算を行うことができます。
この整数演算系の命令出力を見ていると、従来の浮動少数演算と
全く同じレジスタを使っていることがわかります。
Buffer やレジスタ、リソースアクセスは TYPELESS 宣言しなければ
型付けされますが、Shader の命令的には float だろうが int だろうが
特に区別が無いようです。

これは SSE 等の CPU 命令でも同じで、32bit ×4 の 128bit の値と
いうだけで特に動的な型情報はなさそうです。データの中身を利用する
側が便宜上区別しているだけに過ぎません。

例えば条件判定の結果によって 0.0f か 1.0f を代入したい場合、
HLSL コンパイラはこんなコードを出力します。

// HLSL
v.z= v.x < v.y ? 1.0f : 0.0f;

// asm
lt r0.x, v0.x, v0.y
and o0.z, r0.x, l(0x3f800000)

ここでは直後が ret なので o0 に代入されています。
上の v は float4 で宣言しています。

0x3f800000 は 32bit 浮動少数の 1.0f を bit 表現したものです。
r0.x が 0 なら o0.z もそのまま 0.0f になり、r0.x の bit が
すべて 1 なら 1.0f が代入されます。

つまり実数の 0.0f か 1.0f か選択するために整数演算の bit mask を
活用して最適化をはかっています。演算上整数データと浮動少数データの
区別がないことがわかります。

またこのことから lt 命令は結果を整数値で返し、さらに成立すれば
bit が全部 1 (-1)、不成立なら 0 を返すのだと予想できます。
(昔の basic 言語のように)

ちなみに、代入先の 0 と 1.0 を交換するとこんなコードになります。

// HLSL
v.z= v.x < v.y ? 0.0f : 1.0f;

// asm
lt r0.x, v0.x, v0.y
movc o0.z, r0.x, l(0), l(1.000000)

movc は条件付転送命令のようです。
条件成立時の値を 1.0f 以外にしてみます。

// HLSL
v.z= v.x < v.y ? 4095.0f : 0.0f;

// asm
lt r0.x, v0.x, v0.y
and o0.z, r0.x, l(0x457ff000)

条件式の結果からマスクで値を生成しているだけなので、どんな値でも
ペナルティが無いようです。
条件結果を整数で返す場合は次のようになりました。

// HLSL
uint c= v.x < v.y;

// asm
lt r0.x, v0.x, v0.y
and o0.x, r0.x, l(1)

HLSL/C言語の仕様的には条件式は -1 or 0 ではなく 1 or 0 なので、
こちらでも and 演算が入ってしまいます。

このような型を無視したデータの取り扱いを HLSL 上で行うためには
asint(), asuint(), asfloat() 命令を使います。
型変換ですがデータは変更しないので型キャストとは異なります。

キャストでは実際にデータを変換する命令に置き換わりますが、
これらの命令は実行時には何もしません。コンパイラに対して型が
変わったことを通知しているだけです。

例えば asfloat() の動作を C/C++ で書くとこんな感じです。

float asfloat( int b )
{
    return *(float*)(&b);
}

// もしくは
float asfloat( int b )
{
    union {
        float _f;
	int _i;
    } temp;
    temp._i= b;
    return temp._f;
}

asfloat を使うと 1.0f を直接代入する代わりに次のように記述できます。

float b= asfloat( 0x3f800000 );

この as~系命令はかなり使えそうです。
例えば RGBA32F のテクスチャやレンダーターゲットで、R, G だけ
float とみなし、B, A を uint とみなして処理することもできます。
データのパックや圧縮など、開いているチャンネルの有効利用にも
応用できそうです。
以前作った迷路のシェーダーも、これを使っていればもっと判定が
簡略化できました。
Direct3D ShaderModel4.0 Shaderで迷路作成
Direct3D 10 ShaderModel4.0 迷路の自動探索Shader

ただ頂点データに使うなど、バーテックスシェーダーや
ジオメトリシェーダー から ピクセルシェーダー へ値がわたるときは
難しいでしょう。異なる型で補間が発生してしまうので、型を混用した
ようなデータは向いてないかもしれません。

GPU によっては浮動少数演算に特化されていて、整数専用の演算は
まだ特殊な ALU を必要としている可能性があります。演算性能を
最大限引き出すには float の方が有利なので、整数演算の多用は
パフォーマンスとの相談になってくるかもしれません。この点は
要注意です。

Direct3D 10 HLSL で再帰呼び出しの展開

HLSL では関数を再帰呼び出しすることができません。
>error X3500: ‘_Sub0’: recursive functions not yet implemented

一応 asm 命令ではシェーダープログラムのサブルーチンコールは
存在していて、call ~ ret や label 等のニモニックもあります。
今まで調べた限りでは、現在の HLSL (Shader4.0) でこれらの命令が
使われるのは、唯一 switch 文の attribute に [call] を指定した
場合だけでした。

データスタックが無いので、ローカル変数の保護など、その辺の
実装でハードルが高いのかもしれません。

とはいっても、テスト中はジオメトリシェーダー等でほんの数段で
よいから再帰的にコードを記述したくなることがあります。
シェーダー関数は基本的にすべて inline 展開されるので、指定した
数だけ勝手に inline 展開してくれれば実現可能でしょう。
将来のコンパイラで実装してほしい機能です。
([recursive(4)] _Sub0( .. ) とか、こんな感じで)

というわけで手動で再帰を展開してみます。
まず再帰関数をマクロ定義します。

#define	_DEFFUNC(V0,V1)			\
float4 _Sub##V0( uint a, float4 col )	\
{					\
    if( --a > 0 ){			\
        return _Sub##V1( a, col.yzwx );	\
    }					\
    return col;				\
}

必要な段数だけ定義します。

_DEFFUNC(4,4)
_DEFFUNC(3,4)
_DEFFUNC(2,3)
_DEFFUNC(1,2)
_DEFFUNC(0,1)

呼び出しの例

float4 PS_Main( PS_INPUT In ) : SV_Target
{
    return  _Sub0( 2, In.Color );
}

再起呼び出しの代わりに、全く同じ定義内容の別名関数を呼び出して
いるだけです。この場合 _Sub0() から呼ばれる関数は _Sub1() で
以後終了条件まで増えていきます。そのため必要な再帰の数だけ別名で
定義しておく必要が生じます。

上の例では _DEFFUNC の最後は自分自身の呼び出しをしていますが、
定数による inline 展開では、その関数が実際に呼ばれない限り HLSL
コンパイラではエラーにならないようです。
もし

    return  _Sub0( In.Level, In.Color );

のような感じで、動的なパラメータを使った呼び出しにすると最後まで
展開する必要があるためエラーになります。
このときは終端だけ専用の関数を用意します。

float4 _Sub4( uint a, float4 col )
{
    return col;
}

_DEFFUNC(3,4)
_DEFFUNC(2,3)
_DEFFUNC(1,2)
_DEFFUNC(0,1)

実際は上の例の _DEFFUNC と違って、もっと複雑な条件を持った、
もっと長い再帰関数を記述することになります。その場合いちいち行末に
「\」をつけたマクロの連続行形式で書かなければならないのが少々難点です。

Direct3D 10 Shader4.0 ジオメトリシェーダーで破壊する

GeometryShader は、Shader3.0 以前はできなかったさまざまな用途に
応用することができます。

・面(primitive)単位の処理、エッジの処理
・隣接頂点の参照
・VertexShader の代わり
・頂点(primitive)の追加
・primitive の削除
・その他いろいろ

頂点シェーダーは 1頂点単位の変換なので、そのままでは他の頂点情報の
参照ができません。また動的な追加削除は矛盾を引き起こしてしまいます。
あらかじめ面単位に分割しておいたり、隣接頂点座標を1頂点に入れて
おいたりと、さまざまな工夫と前処理が必要でした。

D3D10/DX10 で追加されたジオメトリシェーダーは、このような面(プリミ
ティブ)ごとの処理を、データ側の加工無しに行うことができます。
(topology の adjacency data は別に情報が必要)

試しにポリゴンをばらばらにするシェーダーを作ってみました。

ss06

以前 Xbox1 のゲームでも同じようなシェーダーを作成し、シェーダー
だけで任意のモデルをばらばらに破壊する表現を用いたことがあります。
当時は ShaderModel1.0 だったので、あらかじめ情報を頂点に埋め込んで
おく必要がありました。専用コンバータを用意して、共有頂点を分割したり
回転中心からの距離を求めておいたりと、専用のモデルデータに
なっています。

今回はジオメトリシェーダーのおかげで、データ自体は何もいじる必要が
ありませんでした。普通に描画するデータをそのままシェーダーに渡す
だけで簡単(?)に破壊することができます。

なお Local/World 座標での演算が必要なので、Transform 自体も
ジオメトリシェーダーで行う必要があります。
そのため VertexShader は何もせず、頂点を GeometryShader に渡して
いるだけとなっています。

データは同じですが演算量は増加します。面ごとに 3頂点分の演算が発生
するので、普通に描画するよりはかなり重くなっているはずです。

破壊については、本当ならば StreamOutput を活用すべきところですが
使っていません。単純に面法線と重力方向にローカル回転させながら
吹っ飛ばしているだけです。

wheelhandle_ss06t.zip

いつものように ss06.exe で実際に実行することができます。
DirectX10 が走る環境と DirectX SDK August2007 Runtime が必要です。
今までと違って速度調整が入っていて、約 60fps 前後で固定するように
なっています。(追記: そのままだと速すぎるからです)

Direct3D 10 Shader4.0 消えた abs と最適化

テクスチャから読み込んだピクセルの値が 0.1 付近であることを
判定しようとして、HLSL で当初こんなコードを書いていました。
テクスチャのピクセルサイズが不明なので、± 0.05 くらいの
誤差を許容しています。

int TestA( float4 color )
{
    return  color.x > 0.05f && color.x < 0.15f;
}

あまりにそのまんまなので、もう少しちゃんと書こうとして次の
ように修正してみました。

int TestB( float4 color )
{
    return  abs( color.x - 0.1f ) < 0.05f;
}

どちらが複雑度が高いか比較するためにアセンブラで確認してみます。
結果は上の TestA() 3命令で、下の TestB() が 2命令に展開されています。

// TestA
ge r1.x, r0.x, l(0.050000)
ge r1.y, l(0.150000), r0.x
and r1.x, r1.x, r1.y

// TestB
add r1.x, r0.x, l(-0.100000)
lt r1.x, |r1.x|, l(0.050000)

見てわかるとおり abs 命令がありません。lt 命令のソースオペランド
に直接絶対値指定らしき修飾子 |~| が記述されています。

Shader3.0 までは、abs は独立した命令 'abs' だったので便利に
なっています。良く考えたらもともと符号反転は出来たので、符号を
落とすだけの abs も簡単なのかもしれません。

ただし共通バイトコードでは単なる修飾子でも、ドライバレベル以降の
ネイティブコードでは独立した命令として実行される可能性があります。
NVIDIA の OpenGL 拡張命令から D3D10 Shader4.0 相当の ASM Shader
を調べてみるとしっかり ABS 命令が存在していました。

実際に比べてみます。もともと動作しているシェーダーに abs を挿入
してコンパイルし、消費する命令 slot 数が変化しないことを確認します。
比較しやすいように 300回ほどループさせて比べます。

// abs 無し
    loop 
      ige r2.x, r1.w, l(300)
      breakc_nz r2.x
      mad r1.xyz, v0.xyzx, v1.xxxx, r1.xyzx
      iadd r1.w, r1.w, l(1)
    endloop 

// abs あり
    loop 
      ige r2.x, r1.w, l(300)
      breakc_nz r2.x
      mad r1.xyz, |v0.xyzx|, |v1.xxxx|, r1.xyzx
      iadd r1.w, r1.w, l(1)
    endloop 

GeForce8800GTS で走らせたところ速度差が出ました。

10000~10200 (usec)   abs無し
14500~15400 (usec)   absあり

GeForce8800(G80) では実際には abs は個別の命令となっていて、
それぞれの絶対値演算で別の実行サイクルを消費しているように見えます。
つまり

 mad r1.xyz, |v0.xyzx|, |v1.xxxx|, r1.xyzx

は本当は 3命令相当で、さらに内部で追加の temp レジスタを消費
している可能性もあります。

このことから、Direct3D 上のバイトコードで命令 slot 数やレジスタ
数をみても、実行速度や最適化の目安に過ぎないということがわかります。
また HLSL コンパイラの段階では abs をコストフリーと考えて
最適化している可能性もあります。

将来ぎりぎりの最適化を行うようになったら、この辺は要注意ですね。

さて、最初に戻って TestA と TestB の比較ですが、ge ×2 + and の
TestA よりも、abs を使った TestB の方がずっと高速でした。
数値そのものは他の処理も含んでいるのであまり意味を持たないのですが、
差が生じていることはわかります。

26000 (usec)   TestA
15000 (usec)   TestB

もしかしたら単なる命令差よりも、文脈上 B の方が消費レジスタが1つ
少ないことが原因かもしれません。

例えば

E= A*B*C*D

という演算を行う場合、一般的な CPU や Shader1.0 世代の GPU では

(1) R1= A*B
(2) R1= R1*C
(3) R1= R1*D

よりも

(4) R1= A*B
(5) R2= C*D
(6) R1= R1*R2

の方が高速です。これは最適化のテクニックとしても良く用いられます。
その理由は (1)~(3) の演算にはすべて依存関係があり、前の演算結果が
が出るまでパイプラインがストールするからです。

(4) と (5) は依存関係が全く無いので、完全に並列演算が可能となります。
アウトオブオーダーなら C,D の準備が出来次第、(4) よりも (5) を
先に実行するかもしれません。

ところが今の GPU は逆であり、パイプラインストールは完全に他の
スレッドで埋めてしまいます。よって (4)~(6) は余計な R2 を消費する
分だけ逆にスレッド並列化を阻害し、(1)~(3) よりも低速になってしまう
わけです。

TestA と TestB の関係も同じで、TestA の方が最初の2命令に依存関係が
無いので、今までの感覚で見ているとついこちら方が良いコードに見えて
しまいます。