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命令に依存関係が
無いので、今までの感覚で見ているとついこちら方が良いコードに見えて
しまいます。

em1key Bluetooth keyboard RBK-2000BTII の設定 その2

Bluetooth Keyboard RBK-2000BT2 の日本語キーボード化カスタマイズを
下記エントリで紹介しました。
em1key Bluetooth keyboard RBK-2000BTII の日本語カスタマイズ例
さらにカスタマイズを行い、任意に ON/OFF できる設定ファイルを
作ってみました。

scriptcommand_RBK2K_JP.txt

上記ファイルをダウンロードしたのち、ファイル名を scriptcommand.txt
にリネームして em1key でご利用ください。

ファイルの 290行目 以降にある CSW_~ のシンボルが、各種設定の
有効・無効を切り替えるスイッチになっています。TRUE で有効、
FALSE で無効です。

このファイルは RBK-2000BT2 向けの設定のみ抽出しているので、
それ以外の余計な機能は含まれていません。機種を問わず利用しやすく
なっていると思います。またカスタマイズの参考用にどうぞ。

前回の記事からさらに、下記のカスタマイズが追加されています。
もちろん個別に ON/OFF できます。

● SHIFT+[0] で ‘_’

SHIFT+[0] キーで ‘_’ を打てるようにします。

● [/?] を本来の位置に配置する

[/?] キーが遠くに離れているので、つい ‘/’ を打とうとしてカーソルが
上に移動してしまいます。そこで次のような置き換えも行ってみました。

キーボード右下のキー群

[.>] [↑] [Sft] [/?]
[←] [↓] [→] [Del]

  ↓

[.>] [/?] [Sft] [\|]
[←] [↓] [↑] [→ ]

これで、’/’ や ‘?’ を本来のキー位置で打てるようになります。
その代わりカーソルキーの配列が横一列になってしまいます。
でも実はこれ、vi カーソルと全く同じ並びなので
個人的にはかえって都合が良かったりします。

無くなった [Delete] キーは Ctrl+[BS] で入ります。

作者が実際に使っている設定は、em1key デフォルトの
scriptcommand.txt に RBK2K 用のキー入れ替えを追加したものです。
内蔵キーボード用の設定と外部キーボード用のカスタマイズを共存
させています。こちらもアップロードしておきました。
追加部分はファイルの最後のみで、完全に独立しています。
個別に ON/OFF できるようなスイッチは特に設けていません。
scriptcommand_default_RBK2K_JP.txt

日本語キーボード化の次は 親指シフト 設定も試してみよう、
と思っていたら、すでに遠藤さんが設定を作成しており
こちらのエントリで公開されていました! たいへんありがたいです。

遠藤諭の東京カレー日記 Advanced/W-ZERO3[es]で親指シフト(追加)

Direct3D 10 HLSL のプリプロセッサ機能の限界

DX10/D3D10 では Effect(fx)/HLSL のプリプロセッサが拡張されている
ことを下記のエントリで書きました。
Direct3D 10 HLSL の関数型マクロ定義

その後もう少しだけ調べてみましたが、残念ながらまだ C/C++ と
完全互換というわけにはいかないようです。
もしかしたら BOOST_PP でも動くんじゃないか、と思って試したら
甘かったです。

●引数無しの関数型マクロが定義できない

#define NAME_A() 0x40f

上記のような引数無しの関数型マクロ名がエラーになります。
ダミーでも NAME_A(_r) のように何か引数を与えないとだめでした。

●意味のない # 行が無視されない

例えば単独で出てくる行頭の ‘#’ のみの行がエラーになります。

●#if 等で bit 演算できない

#if BITFLAG & 4

こんな形の bit 判定はエラーになります。| や ^ 等もだめ。
一般的な四則演算や論理演算はもちろん大丈夫です。

Effect(fx)/HLSL 単体でここまで凝った使い方をすることはあまり
無いかもしれません。
ただ、定数シンボルなどを共通化するために C++ ソースとヘッダを
共有する可能性はあるので、この辺の互換性には注意しておきたい
ところです。

内蔵 Preprocessor は動的コンパイルでは必要になります。
でも、もし完全に事前コンパイルしてもいいのならば、普通に C++ の
プリプロセッサを通した方が良いかもしれません。
従来 DirectX8(vsa/psa)~DirectX9 の時はいろいろ機能的に
物足りなかったので、例えば下記のような感じで Makefile を
作っていました。

# Makefile
# nmake 用

PROFILE	= fx_4_0
FXINCLUDE = -I.

.fx.fxo:
	cl /E $< $(FXINCLUDE) > $*.tmp
	fxc /T$(PROFILE) /Fo$@ $*.tmp

.SUFFIXES:	.fx .fxo

sysdef.fx というファイルが存在する状態で sysdef.fxo を作りたければ
コマンドラインから

nmake sysdef.fxo

とキータイプするだけです。Makefile はカレントに必要です。
nmake.exe や cl.exe、fxc.exe 等にパスを通しておく必要があります。

VisualStudio は Common~\IDE と VC\bin の2箇所にパスが通って
いれば OK です。例えば 32bit(x86) + VS2005 でパスを通しておく
フォルダは下記の通り。

%DXSDK_DIR%Utilities\Bin\x86
C:\Program Files\Microsoft Visual Studio 8\Common7\IDE
C:\Program Files\Microsoft Visual Studio 8\VC\bin

VisualStudio にこだわる必要は無く、gcc など他の C/C++
プリプロセッサやコマンドでも全く同じように使えます。
Makefile の書式は make コマンドによって若干異なります。
ちょっと混用して gmake + cl だとこんな感じでしょうか。

# gmake の場合
PROFILE	= fx_4_0
FXINCLUDE = -I.

%.fxo: %.fx
	cl /E $< $(FXINCLUDE) > $*.tmp
	fxc /T$(PROFILE) /Fo$@ $*.tmp

Direct3D 10 HLSL の関数型マクロ定義

SDK マニュアルを見ていて HLSL Grammar の Operators に
‘##’ や ‘#@’ という演算子を発見しました。
これは本来 #define マクロの引数に適用される演算子です。

これまで Effect fx/HLSL のプリプロセッサは非常に制限された
ものだと思っていました。例えば

・#define も基本的に定数や #if 用。
・引数を伴う関数型のマクロ定義は出来ない。

などなど。
もしやと思って実際に試してみたら、なんと関数型の引数を持ったマクロも
普通に定義して使うことが出来ました。

例えば上記演算子を使ってみるとこんな感じです。

#define	VARNAME(_n,_m)     _n##_m
#define	SEMANTIC_TEX(_id)  TEXCOORD##_id

float VARNAME(light,ambient)= 1.0f;

void VS_Main( uint vid : SV_VertexID,
			out float4 opos : SV_POSITION,
			out float2 ouv : SEMANTIC_TEX(0) )
{
  :

この場合 VARNAME(light,ambient) は「lightambient」と
連結された変数名になるし、SEMANTIC_TEX(0) は TEXCOORD0 に
置き換わります。

#@ は文字定数 ‘~’ への変換に相当します。例として次のように
記述してみます。

#define LCHAR(_n)	#@_n
int id= LCHAR(Z);

LCHAR(Z) は ‘Z’ に置き換わりました。つまり

int id= 'Z';

と等価です。

引数の文字列定数化を行う、単一の ‘#’ 演算子はありませんでした。

使えないと思っていたのでかなり驚きました。思い込んでいただけで
しょうか。
SDK Sample fx から “define” を検索しても、やはり引数を持った
関数型の define は全く使われていないようです。

試しに DirectX SDK 2006 October の fxc.exe (DX9向け) で実行したら
エラーになりました。
同じ 2006 October の fxc10.exe ではコンパイルが通ります。
D3D10 で HLSL 用プリプロセッサの機能拡張が行われたことは間違い
なさそうです。

Preprocessor は HLSL コンパイラ DLL 呼び出しとは別なので、/LD
オプションは使えませんでした。
D3D10 core API 側の Preprocessor が呼ばれると拡張タイプになり、
D3DX9 側の Preprocessor だと従来どおりの簡易型になるのではない
かと考えられます。

結論
・Direct3D10 の HLSL/Effect(fx) では、従来使えなかった関数型の
 マクロ定義が使える
・さらにマクロ引数の演算子 ## と #@ も使える