Archives

August 2007 の記事

同じシェーダーで、かつ同じコンパイルオプションを指定しているのに、
生成されたシェーダーのバイナリが完全に同一になりません。
こんな症状にしばらく悩んでいました。

判明した結論は下記のとおり。

「Direct3D10 は使用する API によって HLSL コンパイラが異なっている」

この違いは April2007 から生じており、当時のリリースノートを
良く読むとそれらしいことが書いてありました。
(マニュアルの「What's New in the April 2007 DirectX SDK」)

以下、HLSL Compile 系 API の違いなどを説明しながら調べた結果を
書いてみます。


DirectX10 では Effect(fx) や HLSL 関連の API も core に含まれる
ようになりました。そのため必ずしも D3DX を使う必要は無く、
エフェクトのコンパイルから生成まで core API だけで実現する
ことができます。

それでも D3DX には Shader や Effect のコンパイル関連の API が
数多く用意されています。core API とほとんど同じ名前で似たものが多く、
あまり違いが無いように見えます。
では D3DX の Compile 系関数を使う利点はどこにあるのでしょうか。

  (1) Compile → Create の手順が一度に出来る。
  (2) ファイルやリソースから直接読み込める。
  (3) 非同期実行できる。(ID3D10ThreadPump)
  (4) #include に対応している。


core 側の API は Compile と Create の API が分かれています。
例えば ID3D10Effect の場合

 1. D3D10CompileEffectFromMemory( .. )
 2. D3D10CreateEffectFromMemory( .. )

と2ステップ必要になります。API が分かれることで、
コンパイル済みバイナリをファイル保存できるし、
コンパイルされたバイナリを読み込めば Compile 無しに Create
だけ通すことも出来ます。

Compile 後のバイナリをそのままファイルに書き出せば、
fxc.exe で作った fxo ファイルと全く同じものができました。


ちなみに API は FromMemory しかなく、FromFile も FromResource
もありません。以前は D3DX に含まれていたことから、わかり
やすいように関数名だけ継承したものと思われます。


D3DX の場合は洗練された汎用性よりも便利さ優先で、下記の命令で
いきなり ID3D10Effect のインスタンスを生成することが出来ます。

 D3DX10CreateEffectFromFile( ... )
 D3DX10CreateEffectFromMemory( ... )
 D3DX10CreateEffectFromResource( ... )

メモリだけでなく直接ファイルやリソースから作ることも出来ます。

ちなみにマニュアルにはミスがあって、最後の引数

  HRESULT *pHResult

が正しく載っていないものがあります。これは ID3DX10ThreadPump で
非同期実行したときに、実行結果を受け取るためのバッファです。
非同期実行しない場合は関数の戻り値で結果がわかるので、この
パラメータは NULL で構いません。


また core API は内部で勝手にファイルアクセスされると困るので、
HLSL 内の #include ディレクティブに対応していません。
#include を意図したとおりに機能させるには、自前でインプリメント
した ID3D10Include が必要です。
#include 以外のプリプロセッサコマンドはそのまま通ります。


D3DX の関数では何もしなくても、内部で勝手に ID3D10Include を
用意してくれます。
もちろん自分で定義した ID3D10Include を渡すことも出来ます。

マニュアルを見ると FromFile の場合のみ自動で #include を処理
してくれるようなことが書いてあります。FromMemory/Resource
ではユーザー定義の ID3D10Include に頼らなければいけないように
見えます。ところが実際に試してみると、

 D3DX10CreateEffectFromMemory()
 D3DX10CompileFromMemory()
 D3DX10PreprocessShaderFromMemory()

どれも ID3D10Include 無しに #include 可能でした。実際、下記の
コードの代わりに

D3DX10CreateEffectFromFile(
	"sysdef.fx",
	NULL,	// macro
	NULL,	// include
	"fx_4_0",
	0,	// HLSL flags
	0,	// FX flags
	g_iDevice,
	NULL,	// EffectPool
	NULL,	// ThreadPump
	&iEffect,
	&blobError,
	NULL	// HResult
	)

FromMemory を使った次の書き方をしても動きました。

const char*	memory= "#include \"sysdef.fx\"\n";
D3DX10CreateEffectFromMemory(
	memory,
	strlen( memory ),
	fxFileName,
	NULL,	// macro
	NULL,	// include
	"fx_4_0",
	0,	// HLSL flags
	0,	// FX flags
	g_iDevice,
	NULL,	// EffectPool
	NULL,	// ThreadPump
	&iEffect,
	&blobError,
	NULL	// HResult
	)

FromResource() もそのままで #include 処理出来るかもしれません。

このように、一見重複に見えるけど D3DX 側には便利な関数が
それなりの理由をもって用意されているわけです。
と、最初はこのくらいに考えていました。

ところが実際は、大きく分けて下記の2種類の HLSL コンパイラが
共存していることになります。

・core API が呼び出す HLSL コンパイラ
・D3DX が呼び出す HLSL コンパイラ

ID3D10Shader の D3D10_SHADER_DESC Creator を見るとこの両者の
違いがわかります。

・D3D10CompileEffectFromMemory() の場合
     Microsoft (R) HLSL Shader Compiler

・D3DX10CompileFromMemoryD3DX()  (August2007) の場合
     Microsoft (R) HLSL Shader Compiler 9.19.949.1104

D3DX が呼び出すコンパイラは C:\Windows\System32 以下にある
dll です。それぞれ内部のバージョン番号も記してみました。

D3DCompiler_33.dll    9.18.949.0015 (April2007)
D3DCompiler_34.dll    9.19.949.0046 (June2007)
D3DCompiler_35.dll    9.19.949.1104 (August2007)

D3DX の Compile 系 API を呼び出したときだけこれらの dll が
読み込まれていることが確認できます。
core API が呼び出しているコンパイラは上記よりもさらに古い
バージョンとなるわけです。

April2007 のリリースノートを見ると bug fix や最適化など
いろいろ修正が入ったようです。

すべての API で新しいコンパイラに置き換わらないのは、
おそらく core 側に入ったことで、安易に変更出来なくなった
ためでしょう。

ちなみに fxc.exe のコマンドラインには

Microsoft (R) D3D10 Shader Compiler 9.19.949.1075

と表示されますが、生成されたバイナリは 9.19.949.1104 となり
D3DX 側のコンパイラが呼ばれていることがわかります。

実際にコンパイルされた結果を見ると、わかる範囲でですが
若干テンポラリレジスタの割り当てが変更されており、
ConstantBuffer もシェーダーで参照している分しか宣言しない
ようになっていました。


●結論

D3DX 側関数を使う理由にもう1つ

  (5) 最新の HLSL コンパイラを使うことが出来る

がありました。HLSL のコンパイルは fxc.exe か D3DX の関数を
使いましょう。core API 側の関数を使うと古いバージョンの
コンパイラを呼び出してしまいます。
(そしてSDKのリリースノートはちゃんと読んでおきましょう・・)


AMD が新しい命令セット SSE5 を発表したそうです。

AMD、新たなx86拡張命令セット「SSE5」~「Bulldozer」コアに搭載予定

こちら のページから資料を見ることができます。

3オペランド命令は扱いやすいので素直にうれしいですね。
命令セットをざっと眺めてみると、16bit fp のサポートも
あるみたいです。これはいい!

CVTPH2PS  fp16×4 → fp32×4
CVTPS2PH  fp32×4 → fp16×4

符号1、指数5、仮数10 の s10e5 で、Shader の half 型と一緒です。
相互変換命令によるサポートですが、GPU との相性もいいだろうし
HDR テクスチャの生成や変換も速くなるでしょう。

他にも shader 等ではおなじみの積和命令があります。例えば

FMADDPS  dest, src1, src2, src3

これは dest= src1*src2 + src3 の演算を行うもので、shader だと

mad  r0, r1, r2, r3

に相当します。でもこれ、3オペランドどころか 4オペランドです。
どうやら各フィールドは完全に独立しておらず、どこかの src
レジスタを dest と共有しなければいけないようにみえます。

FMADDPS  xmm1, xmm1, xmm2, xmm3/mem32
FMADDPS  xmm1, xmm1, xmm3/mem32, xmm2
FMADDPS  xmm1, xmm2, xmm3/mem32, xmm1
FMADDPS  xmm1, xmm3/mem32, xmm2, xmm1

よく読んでみると確かに、レジスタフィールドは
DREX.dest、ModRM.reg、ModRM.r/m の3箇所で、
残る1つのソースは dest と同じレジスタを使うと書いてありました。
あまり素直に喜べないかもしれません。

演算時の符号バリエーションとして次の4種類、それぞれ個別の
命令があるようです。

dest=  src1*src2 + src3
dest=  src1*src2 - src3
dest= -src1*src2 + src3
dest= -src1*src2 - src3

また整数演算用の4オペランド積和命令もあります。

これら以外にも、比較などいろいろ追加命令があります。
例えば PHADDBQ を使うと、8bit の値×8 の合計がいっぺんに求まります。
128bit レジスタは 16byte 相当なので、上位 8個と下位 8個の byte
値の合計2個になります。

8bit + 8bit → 16bit
16bit + 16bit → 32bit
32bit + 32bit → 64bit

と、加算3段階分です。


DirectX10 の PixelShader は、入力パラメータの補間方法を
選択することができます。例えば

struct PS_INPUT {
    float4 Pos                : SV_POSITION;
    float3 Normal             : NORMAL;
    noperspective float2 Tex0 : TEXCOORD0;
    float2 Tex1               : TEXCOORD1;
};

float4 PS_Main( PS_INPUT In ) : SV_Target
{
	...
}

こんな感じで In.Tex0 を受け取るとパースペクティブ補正がかかりません。
同次除算をいちいち Shader で打ち消す必要も無いので、補正無しの
リニアな値が欲しい場合は Shader4.0 に移植するだけで動作が速くなる
可能性があります。

他にも centroid, nointerpolation, linear といった宣言ができます。
使用可能な組み合わせをまとめてみました。

・nointerpolation (== constant)
・linear
・linear centroid
・linear noperspective
・linear noperspective centroid

無指定時は linear 相当なので、linear は書かなくてもかまいません。
nointerpolation は linear を打ち消すことができ、この場合補間
しない constant 相当となります。
整数値を渡す場合は補間できないので nointerpolation を使います。

linear 時は centroid と noperspective の組み合わせが可能です。
centroid は表記方法が違いますが D3D9 にもありました。

これら組み合わせを変えて試したところ、GeForce8800GTX では
noperspective の有り無しで若干速度が変化しました。
それ以外の組み合わせでは特に速度変化が表面上わかりませんでした。

1600	nointerpolation
1643	linear perspective
1643	linear perspective centroid
1600	linear noperspective
1600	linear noperspective centroid

実行時間(usec)の変化

パースペクティブ補正は演算量が多いためか、ピクセル面積が多いと
上記のように若干速度に違いがでるようです。

なお、この数値は機能の違いを調べるために差が出る状況を作り出した
ものです。負荷があがるかどうか増減を見るだけにしてください。
補間指定によってどれくらい遅くなるかなど、比率での比較は
できませんのでご注意ください。

RADEON HD2900XT ではまだ変化がでる状況が見られないので、
組み合わせによって負荷に違いがあるかどうかわかりませんでした。


DirectX10 対応 GPU は表面上機能的な差が無いので、ハードの違いを
意識することがあまりなくなりました。

ATI(AMD) の RADEON HD2900XT を最初に使った印象がまさにそうで、
対応機能をチェックしたり、それぞれ個別に専用のシェーダーを用意
したりする必要も無さそうです。

これまで GeForce8800GTS/GTX 周りの Shader 実行速度を調べて
きました。やっぱり RADEON の結果も気になるでしょう。

実は RADEON のデータも取ってあります。
でも実時間よりもどうも遅い数値が出てるようで、いまいち信頼に
欠けるのです。

テストでプログラムは Query の TIMESTAMP を使っているのですが、
2倍の値が返ってきているように見えます。または Frequency が
半分なのかもしれません。プログラムの問題だとは思うのですが
まだ原因がよくわかっていません。
Catalyst 7.8 に上げても同じでした。


もう1点、使っているテスト PC の電源が足りないようです。
RADEON HD2900XT は本来、PCI-E 用の 8pin + 6pin の電源を使います。
マニュアルには一応 6pin + 6pin でも動作すると書かれていたので、
GeForce8800GTS/GTX と同じように 6pin + 6pin で動かしていました。

ただこれだと HD2900XT 本来の性能を発揮できず、動作クロックが
抑えられてしまうようです。


・測定された実行時間はおそらく 2倍の値(半分で実時間)になっている。
・電源が足りずに動作クロックが抑えられた状態になっている。

よって上記2点の理由により、測定値は絶対的な指標として他の
GPU との比較ができず、相対的に遅くなる条件や傾向の判断用の
データとなる点ご了承ください。

まずは先日の GTX と同じ、[loop] の方が高速になる unroll/loop
展開テストです。temporary register 数の増加がどれくらい実行
速度に影響を与えているか、その判断にもなるかと思います。
使用したドライバは Catalyst 7.8 です。

loop回数
  loop回数 [unroll] [loop] temp reg
      40     1225    1014      5
      60     2550    1400      7
      80     3340    1780     12
     100     8100    2160     15
     120     9600    2550     17
     140    11000    2950     18
     160    12500    3340     18
     180    13960    3710     18
     200    15436    4092     18

・縦 時間 (縦軸は半分の値と思ってください)
・横 ループ回数

テンポラリレジスタが増えると遅くなる傾向は GeForce8800 と
同じです。80(12)~100回(15) に大きな隔たりができています。
このあたりでおそらくレジスタ数と動作スレッド数のバランスが
崩壊しています。18固定後の負荷上昇率が意外に小さいので、
ループ処理自体の動作効率は良いようです。

100回以上は一定の上昇率なので、このケースではテンポラリ
レジスタ数 15以上はほぼ同じ負荷となっているようです。
ネイティブコード変換でぜんぜん違うプログラムに置き換わっている
のかもしれません。

テンポラリレジスタ数増加に対する耐性が弱く、比較的低い位置
で負荷が上昇するものの、シェーダー自体の実行効率は良いので
ループ回数が極端に増えれば良い結果がでそうです。
本来比較できない今のデータのままでも、場合によっては
GeForce の結果を追い越すかもしれません。
本来高速な条件では速度が伸びないけど、遅い条件でも割と耐える
感じです。


次に出力レジスタ(ラスタライザの補間パラメータ数)数と動作速度の
関係は下記のとおりです。内部の最適化 (VLIW化?) の影響なのか、
別のところに要因があるせいなのかわかりません。

出力レジスタ数
・縦 時間 (縦軸は半分の値と思ってください)
・横 出力レジスタ数

9~10まで一気に上昇するものの11でなぜか下がります。
11~15の数値はかなり速くなっており、出力レジスタ数の影響が
もともと無いのか、それとも意味が無いと思って最適化されたのか
もしれません。

Catalyst 7.8 にしたら今まで動いていたプログラムで動かなく
なったものがあるので 7.7 に戻しました。
安定したデータが取れるのはもうちょっと先になるかもしれません。
まずは電源を・・


Shader.jp さんからの情報ですが
ゲーム開発者のための技術説明会 「Gamefest Japan 2007」 開催

CEDEC から Meltdown が消えたなと思っていたら、独自に
Gamefest Japan を行うみたいですね。
二日間にわたって開催されて結構内容も幅広いようです。
9月は忙しくなりそうです。

ゲーム開発のための技術説明会 Gamefest Japan 2007 開催 ゲーム制作技術情報を日本で初めて広く一般へ公開
こちらにも書いてあるように Gamefest はもともと開発者限定の
クローズドなイベントだったので、それが広く一般に公開された形です。
ターゲットはやはり開発者で、マルチプラットフォーム化等の推進が
狙いとのことです。

そういえばゲームは、Wii/DS 等ユーザー層の拡大がここ最近のテーマでした。
もしかしたら MS は XNA 関連を中心にして、ユーザーだけでなく
開発の一般への浸透化や、開発者の層の拡大もある程度視野に入れて
いるのかもしれません。


前々回
Direct3D 10 Shader4.0 ループと最適化
で行ったテストを、GeForce8800GTX でも試してみました。

テストしたのは、逆転して [loop] の方が高速になる 3番目の
shader です。GeForce8800GTX の傾向は GTS とまったく同じで、
loop 120 回以上で速度低下が顕著になります。

Unrollと速度

・縦軸は実行にかかった時間(usec)。位置が低い方が高速。
・横軸はループ回数

GeForce8800GTX
loop回数  time    GTS比
    40     371     1.52
    60     570     1.52
    80     962     1.55
   100    1752     1.51
   120    2120     1.57
   140    3870     1.50
   160    5620     1.50
   180    7200     1.49
   200    8780     1.45

このデータを見る限り、temporary register の割り当てに関しては
特に GTS と GTX で差がないように見えます。つまりシェーダー
ユニットに対する、割り当て可能な register pool の割合はおそらく
同率になっているのでしょう。
GTS はシェーダーユニット数が少ないけれど、その分潤沢にレジスタを
使えるわけではありませんでした。


数値を取っていて気になったのは、思った以上に GTX が速いという
ことです。上のグラフには前々回の GTS の結果も重ねており、また
表には GTS 比でどれくらい速いのかも書き込んでみました。

だいたい 1.5倍程度 GTX の方が高速に動作している計算です。
こんなに差があったかな・・と思ってスペックを確認してみました。

DirectX10 GPU メモ

              stream processor  shader clock  memory clock  mem-bus
GeForce8800Ultra    128sp         1500MHz       1080MHz      384bit
GeForce8800GTX      128sp         1350MHz        900MHz      384bit
GeForce8800GTS       96sp         1200MHz        800MHz      320bit

Stream Processor の数で 約1.33 倍
Shader Clock の差で 1.125 倍

1.33 × 1.125 ≒ 1.50

ぴったり計算どおりでした。

シェーダーの実行速度に対して、メモリ速度は clock で 1.125倍、
bus 幅で 1.2倍、あわせて 1.35 倍です。

シェーダーの演算ではなくメモリが足を引っ張る状況では、GTS と
GTX の速度差はもっと小さくなります。
普段体感している速度差は、おそらくこちらの方が近いのではない
でしょうか。


例えば前回(昨日)
Direct3D 10 Shader4.0 補間レジスタ数と速度の関係
のグラフをもう一度じっくり見てみます。

出力レジスタの個数と速度

出力数が 11以上の右側では、GTS と GTX の差を計算してみると
ちょうど 1.5倍前後になっていることがわかります。
ここは上と同じで計算どおりなので、純粋にシェーダーの演算能力
で頭打ちになっているといえるでしょう。


それに比べて左側、11未満の結果では、GTS と GTX の差がほとんど
ありません。GTX の速度は GTS 比でわずか 1.06~1.1 倍程度です。

ほぼこれは Core や Shader、Memory 等の Clock 比 1.125倍に相当
すると考えられます。

つまりここでのボトルネックは、core か Shader Unit 内部に存在
する固定ユニットの実行速度なのでしょう。GTS でも GTX でも GPU
内にたぶん同一個数実装されていると推測できます。

もし仮にこれが 1.35倍に開いていたら、それはおそらくメモリが
足を引っ張っている部分です。


これらの結果から GTX は GTS と比較して、状況によって
1.125~1.5 倍高速に実行できます。メモリ速度を考えると、本当に
1.5倍の速度差がでるようなケースはそれほど多くないと思います。


また同じように GeForce8800Ultra で計算すると、GTS 比で次のよう
になります。

Shader 1.66倍
Memory 1.62倍
Clock 1.224~1.35

これは速いですね。メモリ速度も Shader 並みの比率を保っているので、
比較的1.6倍に近いスループットが期待できそうです。


コードを書いていて気になったので少々実験してみました。
VertexShader または GeometryShader が出力する値はラスタライザ
(RasterizerStage) に渡ります。
PixelShader は各ピクセルごとに補間された値を受け取ります。

この VertexShader の出力数値の数と描画速度の関係を調べてみました。
ここでは GeometryShader は NULL にしています。

横軸がシェーダーで使用した出力レジスタの総数、
縦がかかった時間(usec)です。

出力レジスタ数と速度の関係

出力数 命令数  88GTS   88GTX
   2     12     145     136 usec
   3     13     150     130
   4     15     147     138
   5     16     148     140
   6     17     148     134
   7     18     148     140
   8     19     157     135
   9     20     164     135
   10    21     173     135
   11    22     335     221
   12    23     347     229
   13    24     360     237
   14    25     376     248
   15    26     398     263
   16    27     410     271

・88GTS = GeForce8800GTS 640 の結果
・88GTX = GeForce8800GTX の結果
・GTS固定 = GeForce8800GTS で、出力レジスタ数を2個固定にし、
 命令数だけ増やしていったもの。


88GTS で出力レジスタ数が2~7個の間特に変化がないのは、動作時間へ
与える影響が他の実行ボトルネックに隠れてしまったためと考えられます。
7~10 までの緩やかな上昇がそれ以前も継続していた可能性が高いです。

その緩やかな上昇の要因として、出力レジスタ数の増加だけではなく
実行する命令数そのものが増えたことも考慮しなければなりません。

そのため出力レジスタ数を2個固定にして、プログラムの長さ(slot数)
を 12~27 まで変化させて同じように速度を量ってみました。
それがグラフ中の「GTS固定」です。
これを見るとほぼ一定なので、やはり10個以前でも出力レジスタ数が
動作に何らかの影響を与えている可能性があります。


注目すべきところはやっぱり出力数 10を超えたところで生じる急激な
変化です。ハード的なバッファの限界、同時に実行可能な補間ユニット
の数、内部バス転送能力の限界、等が考えられます。

VertexShader そのものの命令数が増加して実行時間が長くなれば、
それだけ Rasterizer にも余裕ができます。当初は、その拮抗点が
ちょうど10個目の位置にあったのではないかと仮定しました。
だけど「GTS固定」の結果を見ると、この程度の命令数ではほとんど
実行に影響を与えていないことがわかります。
よって 10 はハード的なマジックナンバーである可能性が高いです。

なお、あとから GeForce8800GTX でも同等のテストを行ってみましたが、
10個を越えた時点で発生する大きな変化は一緒でした。
(グラフに加えました)


ShaderModel 毎に対応している補間レジスタ数は次のとおりです。

ShaderModel2.0
・clmap された COLOR0~1 (2個)
・TEXCOORD0~7 (8個)
・POSITION、FOG

ShaderModel3.0
・自由に割り振れる 12個 (o0~11)

ShaderModel4.0
・自由に割り振れる 16個 (o0~o15)


ShaderModel 2.0~3.0 では、たぶんほとんどのケースで出力レジスタ
数は 10個以内に収まってしまうでしょう。
GeForce68/78 など 3.0 当時の設計でも、仕様上の上限よりは若干低い
位置にハードウエア的な制限があったと考えられます。
GeForce8800 で従来のシェーダーも非常に高速なのは、もしかしたら
こんなところにも原因があるのかもしれません。


今回のテストの結論は、もし速度を追求するなら出力レジスタ数は
10個以内に収めましょう、ということです。

ただ、これはあくまでボトルネックになる得る条件の1つに過ぎないので、
通常はピクセル面積ももっと大きいし、VertexShader も長くなるしと
他の実行サイクルの影に隠れてあまり表に出てこない可能性があります。

相殺されている分には実質的な負荷ではないので、高速化を考える場合
はこのあたりのさじ加減が難しいですね。


Direct3D 10 の Shader4.0 は、実行可能な命令スロットも多いし
整数型も整数演算も扱えるし、HLSL を使うと Shader であることを
忘れそうになります。
条件分岐やループ命令等もそのまま記述し、当たり前のように実行
できるようになりました。

例えばまったく意味のない内容ですが、VertexShader の最後に
わざとこんなコードを書いてみます。

Out.Normal= 0;
for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= In.Normal;
	Out.Normal+= wpos;
}

描画するデータは次のとおり。

・Teapot (Maxのプリミティブ)
・ポリゴン数 57600
・頂点数 29646

Pixel の影響をできるだけ受けないように、PixelShader は固定の
定数を return するだけとし、描画面積も点に近い状態でテストします。

実行するとさすがに時間がかかって、GPU 時間で 1410 usec ほど
消費しました。(GeForce8800GTS 640)
コンパイルされたコードはこんな感じです。命令コードに loop 命令が
あって、本当に Shader 内でループ実行されていることがわかります。

    vs_4_0
    dcl_input v0.xyz
    dcl_input v1.xyz
    dcl_output_siv o0.xyzw , position
    dcl_output o1.xyz
    dcl_constantbuffer cb0[8], immediateIndexed
    dcl_constantbuffer cb1[3], immediateIndexed
    dcl_temps 3
    mov r0.xyz, v0.xyzx
    mov r0.w, l(1.000000)
    dp4 r1.x, cb1[0].xyzw, r0.xyzw
    dp4 r1.y, cb1[1].xyzw, r0.xyzw
    dp4 r1.z, cb1[2].xyzw, r0.xyzw
    mul r0.xyzw, r1.yyyy, cb0[5].xyzw
    mad r0.xyzw, cb0[4].xyzw, r1.xxxx, r0.xyzw
    mad r0.xyzw, cb0[6].xyzw, r1.zzzz, r0.xyzw
    add o0.xyzw, r0.xyzw, cb0[7].xyzw
    mov r0.xyzw, l(0,0,0,0)
    loop 
      ige r1.w, r0.w, l(200)
      breakc_nz r1.w
      add r2.xyz, r0.xyzx, v1.xyzx
      add r0.xyz, r1.xyzx, r2.xyzx
      iadd r0.w, r0.w, l(1)
    endloop
    mov o1.xyz, r0.xyzx
    ret 
    // Approximately 19 instruction slots used

元のソースコードに loop 展開のアトリビュートを次のように追加すると

Out.Normal= 0;
[unroll] // attribute
for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= In.Normal;
	Out.Normal+= wpos;
}

145 usec と実行時間が一気に 1/10 になりました。
出力コードを見てみると・・

   vs_4_0
    dcl_input v0.xyz
    dcl_input v1.xyz
    dcl_output_siv o0.xyzw , position
    dcl_output o1.xyz
    dcl_constantbuffer cb0[8], immediateIndexed
    dcl_constantbuffer cb1[3], immediateIndexed
    dcl_temps 3
    mov r0.xyz, v0.xyzx
    mov r0.w, l(1.000000)
    dp4 r1.y, cb1[1].xyzw, r0.xyzw
    mul r2.xyzw, r1.yyyy, cb0[5].xyzw
    dp4 r1.x, cb1[0].xyzw, r0.xyzw
    dp4 r1.z, cb1[2].xyzw, r0.xyzw
    mad r0.xyzw, cb0[4].xyzw, r1.xxxx, r2.xyzw
    mad r0.xyzw, cb0[6].xyzw, r1.zzzz, r0.xyzw
    add o0.xyzw, r0.xyzw, cb0[7].xyzw
    mul r0.xyz, v1.xyzx, l(200.000000, 200.000000, 200.000000, 0.000000)
    mad o1.xyz, r1.xyzx, l(200.000000, 200.000000, 200.000000, 0.000000), r0.xyzx
    ret 
    // Approximately 12 instruction slots used

当たり前です。これは元の例題が悪かったです。
ただの積和なので畳み込まれてしまいました。
ここまできちんとオプティマイズかかるんですね。
逆に attribute が [loop] (または無指定) だと意味のない演算でも
そのままループに展開されてしまうわけです。

ほんのわずかに複雑なコードにしてみます。

for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= Temp[i&3];
	Out.Normal+= wpos;
}

これでもまだ法則性があるので最適化の余地があります。
[loop] で 20slot、[unroll] で 409slot の命令になります。
速度は2倍差ほど。

[loop]    20 slot  1652 usec
[unroll] 409 slot   806 usec

[unroll] だとこんな感じに展開されています。

    add r0.xyz, r0.xyzx, cb1[0].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[1].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[2].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[3].xyzx
    :

さらに法則性を取り除きます。

for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= Temp[(int)(Pos.x*i)&3];
	Out.Normal+= wpos;
}

これだとおそらく [unroll] でも極端な差が出ないと予想できます。
逆転しました。

[loop]     23 slot   3296 usec
[unroll]  564 slot  12786 usec

[unroll] 側の時間増加が極端なので何か他に原因がありそうです。
使用する命令スロット数の増加もペナルティがあるのかもしれません。
ループ回数を変えて計測してみました。

横loop回数/縦usec

 loop  u-slot [unroll]  l-slot  [loop]
   40    124      565      23     651 usec
   60    179      866      23     967
   80    234     1488      23    1284
  100    289     2653      23    1600
  120    344     3320      23    1915
  140    399     5810      23    2232
  160    454     8430      23    2558
  180    509    10730      23    2882
  200    564    12786      23    3296

80 と 120 前後で大きな変化があるようです。slot 数でいえば
234~399のあたりです。
この数値を目安にして別の演算でも同じ傾向が出るか確認してみます。
これも unroll でリニアなコードに展開されます。

Out.Normal= 0;
for( int i= 0 ; i< 100 ; i++ ){
	Out.Normal+= pow( i, In.Pos.x );
}

横loop回数/縦usec

 loop  u-slot  [unroll]  [loop]
  100    159      252      843 usec
  200    309      532     1656
  300    458      824     2482
  400    609     1130     3323
  500    759     1400     4263
  600    909     1686     5120
  700   1059     1988     5970
  800   1209     2290     6678

きれいなリニアです。命令は 1000slot 超えても問題ないし、
しかも unroll の方が速いし、命令スロット数はまったく影響を
与えていないように見えます。

[unroll] で遅くなる先ほどの shader が、unroll すると意外に
temporary register を消費していることがわかりました。

横loop回数/縦tempreg数

          loop  slot   time  temp
[unroll]   40   124     565     5
[unroll]   60   179     866     7
[unroll]   80   234    1488    12
[unroll]  100   289    2653    15
[unroll]  120   344    3320    17
[unroll]  140   399    5810    18
[unroll]  160   454    8430    18
[unroll]  180   509   10730    18
[unroll]  200   564   12786    18

80~120 前後での急激な負荷上昇と一致します。どうやら速度低下の
原因は、セオリー通り temporary register 数だったようです。
単なるループ展開だと思って見落としていました。


結論は、[unroll] の方が高速。だけど最適化によって temp register
が増えてしまうくらいなら素直に [loop] した方がまし。


unroll するけど積極的な最適化をしない attribute もあると
もう少し違ってくる可能性があります。
最適化レベルの /O0~/O3 はとくに変化が見られませんでした。
[unroll] かつ /Od が一番近い結果になりますが、実行すると差が
でません。内部で最適化かかってしまっていたのか、切り替えがうまく
機能していなかったのか、本当に同じ速度だったのか、
まだまだ調べる余地がありそうです。


昔のものですが必要になったのでメモ。D3D9 です。
レンダリングした結果を保存するには
・BackBuffer を Lock して読み込んで保存
ですが、そのままでは RenderTarget は Lock できないので、
同じフォーマットかつ Lock 可能なサーフェースを作ってコピー。

IDirect3DSurface9*	iBackBuffer= NULL;
iDevice->GetBackBuffer( 0, 0, &iBackBuffer );

// copy先作成
D3DFORMAT	sformat= D3DFMT_A8R8G8B8;
//D3DFORMAT	sformat= D3DFMT_A16B16G16R16F;
IDirect3DSurface9*	iSurface= NULL;
iDevice->CreateRenderTarget(
		width,
		height,
		sformat,
		D3DMULTISAMPLE_NONE, 0,
		TRUE,	// lockable
		&iSurface
	);

// copy
iDevice->StretchRect( iBackBuffer, NULL, iSurface, NULL, D3DTEXF_NONE );

// 読み込み
D3DLOCKED_RECT	rect;
iSurface->LockRect( &rect, NULL, D3DLOCK_READONLY );

void*	ptr= rect.pBits;

  // ... 読み込み

iSurface->UnlockRect();

iSurface->Release();
iBackBuffer->Release();

16F は扱いづらいので、変換が必要なときは 32F にコンバート。


Direct3D 10 では Window のリサイズが比較的簡単にできます。
もちろん何もしない状態でも、DirectX9 と同じようにサイズの異なる
FrontBuffer に対して BackBuffer を拡縮 Copy します。
よってそのままでもウィンドウのリサイズが可能です。
Direct3D 10 の場合、さらに BackBuffer のサイズも容易に追従
させることができます。

サイズの変更は IDXGISwapChain の ResizeBuffer() です。
SwapChain が作成した Buffer のリサイズは行いますが、Buffer を
参照しているオブジェクトがあると同時には解決できません。
それらのオブジェクトはあらかじめ Release() しておく必要があります。

通常は BackBuffer から ID3D10RenderTargetView を作りますので、
RenderTargetView は先に開放しておきます。

また OMSetRenderTarget() で同時に DepthStencilView を登録する
場合、RenderTargetView とサイズが異なると受け付けてくれません。
結局 DepthStencilView も作り直す必要があります。

まずは Release()

ID3D10RenderTargetView*	iRTV= NULL;
iDevice->OMSetRenderTarget( 1, &iRTV, NULL );	// NULL, NULL の設定
iRenderTargetView->Release();
iDepthStencilView->Release();

次にリサイズ。サイズに 0, 0 を渡すと、Window の ClientRect
にあわせてくれます。(ここでは WindowMode のみ対象にしています)

iSwapChain->ResizeBuffers( 2,
	0, 0,	// ClientRect を参照する
	SwapChainDesc.BufferDesc.Format,	// Format は DESC から
	0 );

必要なバッファを作り直します。この手順は最初の初期化時と全く
同じなので、共通化しておいたほうが良いです。
BackBuffer と DepthStencil の作成

// back buffer
ID3D10Texture2D*	iBackBuffer= NULL;
iSwapChain->GetBuffer( 0, __uuidof( ID3D10Texture2D ),
		reinterpret_cast<void**>( &iBackBuffer ) );
iDevice->CreateRenderTargetView(
			iBackBuffer, NULL, &iRenderTargetView );
iBackBuffer->Release();

// depth stencil buffer
ID3D10Texture2D*	iTexture= NULL;
D3D10_TEXTURE2D_DESC	tex2ddesc= {
	width, height, 1, 1,	// width, height, mip, array
	DXGI_FORMAT_D24_UNORM_S8_UINT,	// format
	{	1,	0,	},	// sample
	D3D10_USAGE_DEFAULT,
	D3D10_BIND_DEPTH_STENCIL,
	0,	// cpu flags
	0	// misc flags
};
iDevice->CreateTexture2D( &tex2ddesc, NULL, &iTexture );

D3D10_DEPTH_STENCIL_VIEW_DESC	vdesc= {
	DXGI_FORMAT_D24_UNORM_S8_UINT,
	D3D10_DSV_DIMENSION_TEXTURE2D
};
iDevice->CreateDepthStencilView(
		iTexture, &vdesc, &iDepthStencilView );
iTexture->Release();

g_iDevice->OMSetRenderTargets( 1,
	&g_iRenderTargetView, g_iDepthStencilView );

忘れてはならないのが Viewport の設定です。
またここでは書いていませんが、ProjectionMatrix も再計算が必要です。

// viewport
D3D10_VIEWPORT	viewport= {
	0, 0, width, height, 0.0f, 1.0f
};
iDevice->RSSetViewports( 1, &viewport );


WM_SIZE でサイズが変わったことを検知したら、これらの処理を
呼び出します。アイコン化最小化などもきちんと区別しておかないと、
サイズ (0, 0) に対してリサイズが発生してしまうので注意。

またリサイズによって VRAM が足りなくなり、速度が極端に低下する
可能性も考えられます。場合によっては、極端な速度低下を検出したら
元のサイズに戻す処理も入れた方が良いのかもしれません。


昔に比べたら扱えるツールもかなり増えました。
DDS 形式のデータの取り扱いも、もうさほど困ることがないとは
思いますが、一応手に入りやすい DDS 関連ツールのまとめを
作ってみました。

DDSテクスチャの表示&作成ツールの解説

ただ、Maya とか CGツール系も DDS 自体には対応したものの、扱える
フォーマットがそれぞれ微妙に食い違っていたり制限があったりします。
この辺はなかなか厄介です。

さらに今後 Direct3D10 向けの画像データをどう扱うのかが、
問題になってきそうです。

例えばこの blog でも過去に書いてますが、テクスチャ画素の RGB の
並びが Direct3D 10 では逆順になりました。
ベクトル系データの x y z w にあわせて統一するためです。
従来 A R G B だった色の並びが A B G R になります。

Direct3D もうひとつのユニファイド

ちなみにメモリ上ではリトルエンディアンなので、従来 B G R A と
並んでいたものが R G B A になります。
ある意味自然な配列になりました。
DXGI_FORMAT での表記も R G B A 順となっています。

D3D10 専用フォーマットが登場したとしても、ツール側での対応はまた
しばらく時間がかかりそうですね。


前回の続きで、サンプル ss00 の簡単なソース解説を続けます。
Direct3D 10 ss00 サンプルの解説 (1)

ソースはこちらからどうぞ
HYPERでんち


●ウィンドウ作成 WinMain()

Windows のアプリケーションは Window を作成する必要があります。
簡単にするために、WinMain() 内部でウィンドウの作成も
メッセージループも記述してしまっています。
フレームレートの調整やウエイト等も一切せずにループしているので
ご注意ください。(一応 Sleep が入っています)


●描画 Render()

最初にフレームバッファをクリアします。

float color[4]= {
 0.0f, 0.0f, 1.0f, 0.0f
};
g_iDevice->ClearRenderTargetView( g_iRenderTargetView, color );
g_iDevice->ClearDepthStencilView( g_iDepthStencilView,
  D3D10_CLEAR_DEPTH|D3D10_CLEAR_STENCIL, 1.0f, 0 );

Direct3D9 以前までは Clear() 命令ひとつで行われていた部分です。
D3D10 では、RenderTarget と Depth Buffer それぞれ個別にクリアします。
またカラー値が R8G8B8A8_UINT ではなく R32G32B32A32_FLOAT による
指定が可能となりました。
Direct3D9 では HDR 値の初期化には Shader を使う等の別の手段が必要でした。


サンプルではデモのためにカメラを回転しているのでその計算が若干入ります。
固定ならば Effect に対して Matrix を書き込んで終わりです。

g_iEffect->GetVariableByName( "WorldToView" )->AsMatrix()->SetMatrix(
 (float*)&view );


ID3D10Effect のパラメータ類を反映させるために

g_iEffect->GetTechniqueByIndex( 0 )->GetPassByIndex( 0 )->Apply( 0 );

を実行します。Effect 内に Technique が1つしかないこと、Pass も 1つ
しかないことがわかっているなら、これだけでも十分動きます。


Direct3D10 では初期値が未登録で、描画前に必ず設定しなければならないのが
この IASetPrimitiveTopology() でした。
オーソドックスな TRIANGLELIST にします。
g_iDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );


このシェーダーは頂点バッファもインデックスバッファも不要なので、
いきなり描画です。

g_iDevice->DrawInstanced( 36, 12, 0, 0 );

Cube なので Triangle ×2× 6面分=36頂点、さらに 12個のインスタンスを
描画しています。

最後は g_iSwapChain->Present( 0, 0 ); です。
Direct3D9 以前の BeginScene()~EndScene() 系が必要ないので、
D3D10 では非常にすっきりしています。

Matrix 計算、ID3DEffect への変数アクセス以外には、描画のために 5つの API
しか呼び出していません。


以前掲載した D3D10 のサンプルプログラム ss00 を少々解説します。
ss00.cpp 以外にライブラリやら DXUT も使わず、できるだけ
Direct3D 10 の API だけで済むようコードを減らしていったものです。
それでも初期化は思ったより長い関数になりました。
Direct3D 10 シェーダー4.0サンプルプログラム

あらためてゼロから書いてみると結構手間がかかりますね。
それでも DirectX3~6 あたりまでと比べると、シェーダーが必須になった
だけで、初期化自体は簡単になっているはずです。

ソースはこちらからどうぞ
HYPERでんち

動作には Vista + DirectX10 対応 GPU が必要です。


ss00.cpp

●初期化 InitDevice()

InitDevice() は初期化を行います。

Direct3D10 で最初に必要なインターフェースは次の2つです。

ID3D10Device
IDXGISwapChain

この2つは名称が異なるものの、D3D10CreateDeviceAndSwapChain()
関数一発で簡単に作ることができます。
D3D10CreateDeviceAndSwapChain()

DirectX9 以前のように、Device の前に IDirect3D を作る必要がなくなりました。
IDirect3D が行っていた Display 周りの選択や制御が DXGI に移行した形と
なっています。


D3DX10CreateDeviceAndSwapChain() 作成時に与えるパラメータは、
Direct3D9 の D3DPRESENT_PARAMETERS とほとんど同じです。
D3DPRESENT_PARAMETERS
ここは Direct3D9 のコードを基にしていても比較的容易に対応できる
部分でしょう。

フレームバッファのサイズ、リフレッシュレート、フレームバッファのフォーマット、
フロントバッファへ反映させるときのエフェクトやタイミング、フルスクリーン
かどうか、などを与えています。


DXGI_SWAP_CHAIN_DESC swapdesc= {
 {
  width, height, // フレームバッファサイズ
  { 60, 1, }, // リフレッシュレート
  DXGI_FORMAT_R8G8B8A8_UNORM, // フォーマット
  DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
  DXGI_MODE_SCALING_UNSPECIFIED,
 },
 { 1, 0, }, // sample
 DXGI_USAGE_RENDER_TARGET_OUTPUT,
 1,
 hwnd, // Window ハンドル
 winmode, // WindowMode か FullScreen か
 DXGI_SWAP_EFFECT_DISCARD,
 0
};

D3D10CreateDeviceAndSwapChain(
 NULL,
 D3D10_DRIVER_TYPE_HARDWARE,
 NULL,
 D3D10_CREATE_DEVICE_DEBUG,
 D3D10_SDK_VERSION,
 &swapdesc,
 &g_iSwapChain, // IDXGISwapChain
 &g_iDevice // ID3D10Device
);


ID3D10Device と IDXGISwapChain ができたら View を作ります。
この View は Direct3D10 になって登場した新しい概念です。
メモリ上のテクスチャ空間に対して、実際にどのような手段で Shader が
アクセスするのか決定します。
リソースの自由度が上がった分、具体的にどのような使い方をするのか
ひと手間必要になったわけですね。


IDXGISwapChain から BackBuffer を取り出し、Direct3D からアクセスするための
ID3D10RenderTargetView を作成します。

ID3D10Texture2D* iBackBuffer= NULL;
g_iSwapChain->GetBuffer( 0, __uuidof( ID3D10Texture2D ),
  reinterpret_cast<void**>( &iBackBuffer ) );
g_iDevice->CreateRenderTargetView( iBackBuffer, NULL, &g_iRenderTargetView );
ZRELEASE( iBackBuffer );


次に Depth Buffer を作成しています。
ここでは何も考えずに Stencil のない D3D10 新フォーマットと思われる
D32_FLOAT を使っています。

ID3D10Texture2D* iTexture= NULL;
D3D10_TEXTURE2D_DESC tex2ddesc= {
 width, height, 1, 1, // width, height, mip, array
 DXGI_FORMAT_D32_FLOAT, // format
 { 1, 0, }, // sample
 D3D10_USAGE_DEFAULT,
 D3D10_BIND_DEPTH_STENCIL,
 0, // cpu flags
 0 // misc flags
};
g_iDevice->CreateTexture2D( &tex2ddesc, NULL, &iTexture );

D3D10_DEPTH_STENCIL_VIEW_DESC vdesc= {
 DXGI_FORMAT_D32_FLOAT,
 D3D10_DSV_DIMENSION_TEXTURE2D
};
g_iDevice->CreateDepthStencilView( iTexture, &vdesc, &g_iDepthStencilView );
ZRELEASE( iTexture );


初期化はこれでおしまいです。

ここからは実際にレンダリングに必要な設定を行います。
今回は非常に単純な描画なので、特に毎フレーム実行しなくても良い処理を
初期化の部分に書いてしまっています。
シェーダーの生成以外の部分、パラメータ設定関連は、必要であれば1フレーム
の描画中に何回か書き換えることになるでしょう。

// レンダーターゲットの設定
g_iDevice->OMSetRenderTargets( 1,
  &g_iRenderTargetView, g_iDepthStencilView );

// ビューポートの設定
D3D10_VIEWPORT viewport= {
 0, 0, width, height, 0.0f, 1.0f
};
g_iDevice->RSSetViewports( 1, &viewport );


これら関数はどちらも一度に複数個登録できます。今回は1つだけなので
定数 1 を最初に渡しています。


// shader の作成
ID3D10Blob* iblob= NULL;
if( FAILED( D3DX10CreateEffectFromFile(
  TEXT("cube.fx"),
  NULL, NULL, "fx_4_0", 0, 0, g_iDevice,
  NULL, NULL, &g_iEffect, &iblob, NULL ) ) ){

 OutputDebugStringA( reinterpret_cast<char*>(
   iblob->GetBufferPointer() ) ); // エラーが発生したら表示
 ZRELEASE( iblob );
}

ここでは fx ファイルから Shader を作成しています。
今回のサンプルでは ID3D10Effect をそのまま使っています。
HLSL のコンパイル時にはエラーが発生することがあるので、
コンパイラのエラーメッセージをそのまま表示出力しています。

最後に Effect の変数を初期化しています。

// projection の設定
D3DXMATRIX projection;
D3DXMatrixPerspectiveFovLH( &projection, 3.141592f/3.0f,
 WINDOW_SIZE_WIDTH/(float)WINDOW_SIZE_HEIGHT, 1.0f, 10000.0f );
g_iEffect->GetVariableByName( "Projection" )->AsMatrix()->SetMatrix( (float*)&projection );


次回は残りの処理と描画です。


Direct3D10 では、SetShaderResources や SetConstantBuffers や
OMSetRenderTargets や SOSetTargets や IASetVertexBuffers など、
レンダリングに必要なリソース登録 API のほとんどが
複数設定可能になっています。
一度に登録可能なスロット数も 16だったり 128だったり、
非常に多くなりました。

Set してあるインターフェースは Release() して解放されず、
Read 用に Set してあるリソースは Write 用に Set することができません。
使い終わったら、または別の用途で設定する場合にそれぞれ
NULL を設定して登録を解除します。

API も Slot も増えたので、シーン終了時など一度に初期化するのは
結構大変になったなと思ってました。
ところがこれ、ID3D10Device::ClearState() だけでいけるようです。
こんな便利な関数があったとは。
マニュアルはきちんと読んでおいた方がいいですね。


Query 系を少しだけ試してみました。やはり RADEON HD2900XT でも
同じように使えます。TIMESTAMP 取れました。よし。

ただドライバによって結果が異なるかもしれませんが、
GeForce8800GTS (163.11) では ID3D10Query の PIPELINE_STATISTICS の
CInvocations, CPrimitives が 0 のままです。
RADEON HD2900XT だとそれっぽい値が入っています。

IA 系の基礎的なパラメータは一緒ですが、CI~CP~以外にも値の
違うものがあります。例えば VSinvocations など。
おそらく描画に StreamOutput を使っているせいだと思いますが、
RADEON は IAVertices と同じ値で、GeForce はなぜか半分以下の値です。

SO_STATISTICS のPrimitivesStorageNeeded は、GeForce の場合
NumPrimitivesWritten と同じですが、RADEON の場合やたらと大きな値です。
基本的に動的な増減も無く、3 in 3 out しか使ってないのでこれは
計算方法の違いでしょうか。

まったく同じデータを表示してみた結果の例

・キャラクタの表示なので描画面積はかなり小さい
・ボーンアニメーションしているので、Pixel 数が常に変動しており誤差がある
・結果を表示するフォント描画の分も計算に含まれている

●GeForce8800GTS 163.11
 IAVertices 4092
 IAPrimitives 1504
 VSInvocations 1893
 GSInvocations 115
 GSPrimitives 230
 CInvocations 0
 CPrimitives 0
 PSInvocations 9034
 NumPrimitivesWritten 95
 PrimitivesStorageNeeded 95


●RADEON HD2900XT 07.7
 IAVertices 4100
 IAPrimitives 1512
 VSInvocations 4100
 GSInvocations 123
 GSPrimitives 246
 CInvocations 1635
 CPrimitives 1635
 PSInvocations 9656
 NumPrimitivesWritten 95
 PrimitivesStorageNeeded 1389

Primitive 数が 8 個だけ RADEON の方が多いのは、上記の計測値もポリゴン
でフォント描画しているためです。つまり、CInvocations, CPrimitives,
PrimitivesStorageNeeded の値でちょうど 8桁分、数字が多いわけです。
データもプログラムも同一です。

もう少しライブラリを整備したらきちんと調べていきます。

ちなみに Counter はどちらも全滅でした。
D3D10_COUNTER_INFO は両者とも
LastDeviceDependentCounter= 0
NumSimultaneousCounters= 0
NumDetectableParallelUnits= 1


2007/08/06
Direct3D 10 Query

CPU と GPU は基本的に非同期に動作しますが、CPU 側でも GPU から値を
受け取ったり、動作状況を見てタイミングを計ったりすることがあります。
特に CPU と GPU の「動作と描画のタイミング取り」は重要で、入力から
どれくらいの遅延を許容するのか、CPU と GPU がどれくらい並列に
動くのか、設計時に把握しておく必要があるでしょう。

たとえば CPU 側で何らかのデータを受け取る場合、GPU 処理のタイミング
を何も考えないと、お互いに同期のための Block が発生してしまいます。
やはりきちんとフレームを制御しながら Double Buffering にするなど
いろいろ工夫が必要です。

GPU 側の動作状況を調べるには、Direct3D9 なら IDirect3DQuery9 を使います。
GPU のコマンドにいわゆる Fence の挿入が可能で、普段見えない GPU の
動きを調べることができます。

Direct3D10 の場合は ID3D10Query です。API が異なりますが主要な機能は
D3D9 とほとんど変わらず同じように使えるようです。
一部の同期用 (EVENT等)コマンドは Begin が無いのも一緒。
でも API が違います。

IDirect3DQuery9
・Issue( D3DISSUE_BEGIN )
・Issue( D3DISSUE_END )
・GetData()

ID3D10Query
・Begin()
・End()
・GetData()

D3D9 の場合 RADEON で TIMESTAMP が取れなかったりと GPU によって
機能の違いがありましたが、D3D10 ではこの辺大丈夫そうですね。


Vista + Direct3D 10 になって API 回りも一新、便利な機能が増えています。
特に OS との親和性が高まったおかげか、管理周りの負担が減りました。
これは非常に歓迎すべきことです。
例えば D3D9 までは常に頭を悩ませていた DEVICELOST 対策が、D3D10 では
ほぼいらなくなりました。

D3D10 の新機能も別に使わないし、能力的にも DirectX9 までで満足しているし、
といった場合でも、これはきっと気になるポイントでしょう。

Windows 上では 1つのアプリケーションが GPU や VRAM を占有するわけでは
ないので、常にリソースの競合が発生します。
ほぼ占有可能なフルスクリーンモードでも、いわゆる ALT+TAB で
Task を切り替えるとリソースを明け渡さなければなりません。

このとき DirectX9 以前の API は D3DERR_DEVICELOST を返し、リソースが
他の Task に取られたため実行できないことを訴えます。
こうなったら描画動作の続行をあきらめるしかありません。
TestCooperativeLevel() を呼び出して再び利用可能になるのを待ち続けます。
利用可能な状態に戻ったことを確認したら、VRAM 内容や各種 GPU ステートを
戻してプログラムが継続できるよう復帰させる必要があるわけです。

もちろん DEVICELOST はフルスクリーンモードに限らず Window Mode でも
起こります。例えばスクリーンセーバーなど、突然他のアプリが全画面を
占拠して描画を始めることも十分ありえるからです。


D3D9 の場合でも、D3DPOOL_MANAGED を使えば VRAM 内容の復帰までは自動で
行ってくれるようになっています。これはこれでかなり手間が減りました。
それ以外のリソースは、明示的にリセットしたり作り直しです。
例えば RenderTaget 用の Surface とか、ID3DXEffect とか、
IDirect3DQuery9 など。

Direct3D 10 になると Vista がより深く Direct3D を活用するおかげか
この辺の GPU リソースもしっかり管理してくれます。
DEVICELOST 時の後始末をアプリケーションが行わなくても、ALT-TAB の
切り替えからでも、スクリーンセーバーからでも、きちんと復帰して動作を
続けてくれます。(ちょっと感動)


ただ、アプリケーションが完全にバックグラウンドに切り替わってしまったのに、
何もしないで CPU を消費し続けるのはあまり好ましいとはいえません。
画面描画可能な状態かどうかは、やっぱりアプリケーション側でも
責任を持ってきちんと判別しておく必要があります。

Direct3D 10 では SwapChain の Present() が D3DERR_DEVICELOST
の代わりに DXGI_STATUS_OCCLUDED を返します。
フルスクリーンモードからの切り替わり、スクリーンセーバー、ユーザーの
切り替えなど、バックグラウンドに回る可能性はいくらでもあるわけです。


GPU の状態を調べる ID3D10Query も、DEVIELOST 等によってインターフェース
を作成しなおす必要がなくなりました。
ただその代わり、途中で DEVICELOST 相当の状態が発生すると計測値の正当性が
失われてしまいます。この状況は TIMESTAMP_DISJOINT で調べれば判定可能
なのですが、従来は無かった動作だけに新たな処理が必要になりそうです。


Direct3D 10 の ShaderModel4.0 は非常に自由度が高く、かなりいろいろな
ことができます。3.0 以前の制約の中で結構がんばってコードを書いた経験が
あるなら、この柔軟性にはちょっとした感動を覚えるかもしれません。

特にマニュアルを読んでいて衝撃を受けたのがこれ

Using the Input-Assembler Stage without Buffers (Direct3D 10)

マニュアルの下記の場所にあります。
+ DirectX Graphics
 + Direct3D 10
  + Programming Guide
   + Pipeline Stages
    + Input-Assembler Stage
     + Getting Started with the Input-Assembler Stage
      + Using the Input-Assembler Stage without Buffers (Direct3D 10)

SV_VertexID を元に VertexShader の中で頂点を選択して描画しています。
つまり、VertexBuffer も IndexBuffer も無く、InputLayout も設定せずに
ポリゴンが描画できてしまうのです。

このシンプルさに惚れました。

自分でも同じように、頂点バッファ無しにポリゴン描画できるシェーダーを
作ってみました。fx を読み込んで描画するだけで、一切リソースを作らなくても
こんな感じの Cube を表示することができます。



形状を生成しているのは下記の部分です。
ついにシェーダーでこんなトリッキーなコードが走るようになりました。


float3 VS_Main( uint id : SV_VertexID, uint sid : SV_InstanceID ) : POSITION
{
 uint _map[]= {
  101733320,
  104889305,
  56280874,
  125360034,
 };
 id= _map[id&3]>>(((id&60)>>2)*3);
 float3 pos;
 pos.x= id & 1 ? 1 : -1;
 pos.y= id & 2 ? -1 : 1;
 pos.z= id & 4 ? 1 : -1;
 float2 ss;
 sincos( sid * 0.5236f, ss.x, ss.y );
 pos.xy+= ss * 5;
 return mul( float4( pos, 1 ), WorldToView ).xyz;
}

シェーダー全部でもこれだけです。
 ・cube.fx

描画している C++ 側はこんな感じです。

g_iEffect->GetTechniqueByIndex( 0 )->GetPassByIndex( 0 )->Apply( 0 );
g_iDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
g_iDevice->DrawInstanced( 36, 12, 0, 0 );

g_iSwapChain->Present( 0, 0 );


ソースリストと実行ファイルは
 ・「HYPERでんち」 オリジナルサンプルプログラム
に掲載しましたので、興味ある方がいましたらどうぞお試しください。


ConstantBuffer は D3D8~9 同様 32bit×4 が Element の基本サイズに
なります。ただし D3D10 は float, int の混在が可能です。
マニュアルには Packing Rule として、変数がどのように Constant Buffer
へ格納されるか説明が載っています。

配列変数の場合は、インデックス参照を行うとアドレッシングの影響を強く
受けてしまうので大胆なパッキングができません。
例えば次のように宣言して、A を変数インデックスで動的に参照する場合、

cbuffer _Array {
 uint A[256];
};

cb[] の参照は必ず Element 単位となるため 128bit のアライメント整合
が発生します。よって上記の cbuffer は
256 × 4 × 4 = 4096byte になります。(厳密には 4096-12=4084byte)
さらに次のように宣言すると

cbuffer _Array {
 uint A[256];
 float B[256];
};

本当は A を .x に、B を .y にでもパックしてしまえばデータ構造的には
要素をまとめることができるはずですが、
この場合もリニアに 512 × 4 × 4 = 8192byte とられてしまいます。
(厳密には 8192-12 = 8180byte)

struct _pack {
 uint a;
 float b;
};

cbuffer _Array {
 _pack A[256];
};

上のように自分で構造体にまとめると半分の 4096 (4096-8=4088) byte です。
ld 命令が 4要素単位となる tbuffer (Buffer) でも全く同じでした。

この「32bit×4」の制限を回避するには、Sampler を使って 1D Texture
としてアクセスする方法が考えられます。