仮眠プログラマーのつぶやき

自分がプログラムやっていて、思いついたことをつぶやいていきます。 2025年からzennに移行

自作ゲームやツール、ソースなどを公開しております。
①ポンコツ自動車シュライシュラー
DOWNLOAD
②流体力学ソース付き
汚いほうDOWNLOAD
綺麗なほうDOWNLOAD
③ミニスタヲズ
DOWNLOAD
④地下鉄でGO
DOWNLOAD
⑤ババドン
DOWNLOAD
⑥圧縮拳(ツール)
DOWNLOAD
⑦複写拳
DOWNLOAD
⑧布シミュレーション
DOWNLOAD
⑨minecraft巨大電卓地形データ
DOWNLOAD
⑩フリュードランダー
デジゲー博頒布α版
DOWNLOAD
⑪パズドラルート解析GPGPU版
DOWNLOAD
⑫ゲーム「流体de月面着陸」
DOWNLOAD

プラグイン作成

HSPSHAD ver1.7リリースしようか、でも・・・

パズドラルート解析PC版ではGPGPUの技術を使うため、現在製作中の自作プラグインHSPSHADを使っての作成であった。
当初は、HSPSHADのバグがあったら直したり、HSPSHADにこんな機能があったらプログラムしやすいなという課題を、解析ツールの開発をしながらピックアップしてプラグインにフィードバックしていくつもりであった。


が、その開発は困難を極めた。


ツイッターでもつぶやき発狂していたが、 バグには3種類のバグが有る。

コンパイルエラー
ランタイムエラー
理論エラー




コンパイルエラーはF5でエラーが出るタイプのバグで、ランタイムエラーは例えば0で除算しました等というやつだ。
そして理論エラーは一見正常に動いているが、プログラミングしている側がみて予想外の挙動を示してしまっているバグだ。
この理論エラーが非常に厄介。
このタイプのバグを発見するには挙動の違っている箇所を見つけ、その原因となるプログラム記述を見つけ出さなくてはならない。
そのためには、プログラム動作中に変数の中身をチェックしながら、どのタイミングで意図しない値が生まれているかはかることも必要になってくる。
最悪、同じプログラムをコピペしないで1から作り直すという究極のデバッグ方法を取らざるを得なくなる。


今回開発したルート解析GPGPU版でも、GPUの計算結果があっているのかイマイチ確信が持てず、CPUのみで動くプログラムも1から作ったのだ。
そして動かしてみたところ、計算結果は全く異なるものであった。

ここで問題なのが、計算結果が違う原因バグがどちらにあるか、または両方にあるかどうかということだ。
もしCPUのみで動く方のプログラムが完璧に合っていれば、GPGPU版の方のプログラムに問題があると言えデバッグのしがいがあるというものだ。

ではもしCPUのみで動く方のプログラムが間違っていたら?
それではGPGPU版のソースといくらにらめっこしても解決にはたどり着けない。
最初はGPGPU版にバグがあると仮定してバグを探すだろうが、なかなか原因が見当たらないとなった時、CPU動作のプログラムが絶対に合っているか確信が持てなくなってくる。
そのためにまたあってるか確かめるプログラムを作るといったイタチごっこだ。
イタチごっこといったが、たいていは、確かめプログラムは簡略化して作るので、確かめプログラムの確かめプログラム・・といつかは単純なソースになってバグが発見できるかもしれないけど。

だがそれでもやっぱり、非常に骨の折れる作業であることに間違いない!


理論エラーとはプログラマーが一番恐れるバグである。
今回の開発でも理論エラーを起こしているのを確かめるプログラムが理論エラーを起こしてしまい、嵌って多くの時間が徒労に終わった。
今回の開発でその理論エラー多発の巨悪の根源はというと「GPUでのfloat型、int型数値演算の変な仕様」のせいだと言っても過言ではない。
恐ろしいことに、同じプログラムで書いてもGPUによって計算結果が異なってしまうのだ!!

それでは、ルート解析ツールの開発途中で私が遭遇したとんでもないバグたちを紹介していこう。





・同じプログラムなのにパソコンによってレンダリングターゲットに描画できたりできなかったりする

原因1=GPUによってテクスチャの大きさが2の累乗サイズでないと使えない場合があるため。
原因2=GPUによって使えないレンダーターゲットフォーマットがあるため。
2はもっともなバグであるが、1が意外とキツい。本当は1024*1025でいいところを1024*2048で作らなくてはいけない状況があるからだ。これでは無駄なテクスチャメモリが消費されるだけでなく、そのテクスチャ上でシェーダーをかける際にも無駄な領域での演算が増えてしまう。
これを解決するにはシェーダーを掛けられる範囲を指定できるような命令を新たに追加しなくてはいけなかった。
だがこれだけですんだのでまだいい方。


・R8G8B8A8はrgbaの4要素に0~255までの値を格納できるフォーマット。だが代入した値が0~128までは合うが129~255の値は1ずれている。
原因=シェーダでは整数型テクスチャへの出力の値は全て0.0~1.0で管理するため。
0.0~0.99609375(255.0/256.0)ではない。つまりシェーダ記述内で出力(代入したいint型変数)を「float(int型)/256.0」ではなく「float(int型)/255.0」としないといけない。同じようにR16G16B16A16では「65535」で割らないといけない。


・ピクセルシェーダ内で操作している画素X座標を取得するため、テクスチャーカラー(これも座標によって0.0~1.0の値が入っている)に画面横ピクセルサイズをかけたのに、どうも(0,1,2,3,・・)というようになっていない。それもパソコンによって合ったり合ってなかったり。

原因=float型の誤差 + float型→int型に変換するときの切り捨て。
テクスチャーカラーの値は、例えば3*1の1次元テクスチャでは(0.0  , 0.33333・・ , 0.66666・・)という値が入っている。
なので3をかければ(0,1,2)となるはずなのだが、ここでfloat型の精度の問題がでてくる。
0.33333333・・・はfloat精度だとせいぜい0.333299994468689にごまかされてしまう。これでは3倍しても1.0にはならず0.9998・・といった具合になり、それをint型に変換すると0になってしまうのだ。

さてここで問題なのは、2進法で割り切れない数で乗算したときにそれが起こるということだ。例えばテクスチャの大きさが8192×8192だと、テクスチャーカラーの値は(0/8192,1/8192,2/8192・・)で、これらは全て2進法では割り切れる数字のみだ。ということは、横サイズの8192をかけたらしっかりと(0,1,2,3・・)となる。これがこのバグの発見を遅らせる罠であった。
さらに大きい罠で、GPUによってはご丁寧にfloat乗算後の四捨五入(丸め込みと言うやつ?)をしてくれるものがある。0.333299994468689×3.0が0.9998でなく1.0になってしまうのだ(いい事なんだけど)。どうりでパソコンによって演算結果が違うわけだ。
これも、このバグの発見を大きく遅らせた非常にたちの悪いバグ仕様である。

余談だがなぜテクスチャカラーの方は値のとりうる範囲が0.0~1.0ではなく0.0~(1.0-1.0/WIDTH) なのだろう。(ここでWIDTHはテクスチャ横サイズ)
先ほどの整数フォーマットのテクスチャに出力するときはしっかりと0.0~1.0にしなくてはダメなのに。本当にHLSLは謎仕様である。


・16777216+1が16777217にならない

原因=sm_30ではint型変数の四則演算は全てfloat型の回路で代用されるため。(ニセint型)
ここでfloatの内部構造を説明しよう。32bit floatは先頭1bitが正負、次の8bitが指数部、最後の23bitが仮数部である。ここで16777216 (2^24) は非常に大きい値であるためfloat型にとっては仮数部に入りきらない細かい1の位の誤差など気にしていらないのだ。16777216の次の値は仮数部の最小bitに1を加えても16777218になる。だからどうしてもfloatで16777217は表せない。仮数部が23bitしかないfloatの宿命、精度の限界というやつだ。

ではなぜシェーダーでint型の宣言はできても計算はfloatに従うのか。それはsm_30の仕様だからというもっともな回答はおいておいて、恐らくsm_30に対応した頃の古いGPUでは、まだint型の計算そのものが行なえなかったからなのではないかと推測する。
ともあれ、HSPSHADがなるべく多くの環境で実行できるようにと対応シェーダモデルを4.0でなく3.0にしてしまったのが、ここに来てとんでもない悪さをした。
この時点で、int型は-16777216~16777216までしか正しい計算ができないという縛りプレイをプログラマーに要求することになったわけだ。


・29を6で割った余りが4になる

原因=ニセint型と余剰を求める%演算子の仕様
とんでもないバグである。先ほど申したfloatの丸め込みを行なうGPUでは29%6の値はちゃんと5になる。だが丸め込みをしないGPUでは29%6が4になる。まぁint型同士の「+」演算子による加算も無理やりfloatでやられてしまうことは分かっていたから「%」演算子がfloatでやられてしまうのにはなんら不思議ではない。丸め込み機能付きGPUでしか正しい結果が得られないことを見ると、この罠は2つの合わせ技、応用トラップだということが分かる。それを示すかのように29%8の値は、どちらも同じ値を出力してくれた。8は2の累乗だからだ。

これでsm_30のシェーダ内での縛りが更に増え、int型は上限下限がたったの1600万程度なのに加え、除算と余剰演算子で右辺値に2の累乗以外を指定できないこととなる!
ここまでくるととんだ縛りプレイである。数値計算なんてできたのもではない。


・シェーダをかける範囲を指定したのに、パソコンによってかけられたシェーダ範囲が1ドットズレている

原因=範囲指定はfloat型で0.0~1.0の値で指定するため
さっきまでのバグありきの話だが、ここまでくればfloatの丸め込みか何かが悪さをしていると考えるだろう。
つまりこのバグを直すのは不可能、という事だけはよーく分かる。


・レンダリングターゲットテクスチャ(VRAM)を作成し、シェーダなどを通した後、そのテクスチャデータを取ってこようとすると2回目移行失敗する。

原因=不明
同じテクスチャから二回以上メインメモリにデータを転送できないようである。当然これも非常に困ったバグである。だがパズドラルート解析GPGPU版を作るまでこんな目立つバグに気が付かなかった・・





と、枚挙にいとまがないが大体こんな感じだ。

ボロクソ書いているけれど、そもそも大前提としてHLSLでsm_30でGPGPUをやろうなんていうのが間違い・・・HLSLは3Dゲームなどの画像処理に使うものであり数値計算用ではない。
それは分かっていたのだが、今回の開発で改めてHLSLでGPGPUをやるには限界があるということを痛感した。

とてもプラグインとして他の人に使ってもらうレベルではない。
いろんな環境でGPGPUができることようにとsm_30(シェーダーモデル3.0)を採用したのも大きな間違いであった。
3.0ではギリギリ、シェーダーでのint型変数を完全にサポートしていない。

実は4.0以降ではサポートしている上に、数値計算用に対応したDirect computeという技術がシェーダーモデル5.0からできるのだが、つい最近のグラボでかつOSはwindows 7以上でしか動かないというキツイ縛りがある。
以下にシェーダーモデルと対応OSを書いていくが、今時XPに対応していないプラグインというのはちょっときついかなと思ったのも事実だ。それにDirect X 11に対応しているグラボでないとダメというのも厳しい条件である。

シェーダーモデル2.0 : Direct X 8 : Windows XP
シェーダーモデル3.0 : Direct X 9 : Windows XP
シェーダーモデル4.0 : Direct X 10 : Windows VISTA
シェーダーモデル5.0 : Direct X 11 : Windows 7


ではXPなど少し古いPCでGPGPUができないのか調べていると、OpenCLがいいということが分かってきた。
CUDAではnVidia製のグラボでしか動かないという制約があるが、OpenCLはRadeon製でもIntel 製でも動くし、何よりDirect computeより早い!


さてもうここまでくると、HSPSHADに暗雲立ち込めて・・・なんて話ではなく、HLSLのsm_30はとんでもなく扱いに困るシロモノであった事がわかり、とりあえずこのプラグイン開発はここで打ち止め。
数値計算用であわよくば画像処理も使えていいとか豪語してたが、残念ながら数値計算では使えないようなので、HSPSHADは「シェーダー」の面影を全く残さず、OpneCLとHSPをつなぐプラグインとして生まれ変わらせるべく、1から作り直そうと思う!


「HSPでGPGPU」のために

HSPSHAD途中経過。今回はマルチレンダリングターゲット

HSPSHADを今年に入って作り続けてもうver1.5になった。
このプラグインは命令数こそ少ないが(現在16種類)できること、機能はとても多く、その根源は全てエフェクトファイル=HLSLの力を借りていることによる。逆にHLSLがよく分からずエフェクトファイルが上手く書けなければ、このプラグインを使っている意味はほとんど無いのだ。

残念ながらHLSLのサンプルはなんだか多いような少ないようなよくわからない感じだし、仮にネット上に落ちているとしても大半は3Dゲームのソースとして公開されている。

なのでこのブログではこれから、数値計算用途の視点からHLSLのソースを解説していければいいかなと思っている。



さて話は本題にいくが
HSPSHADで、今まで、一つのシェーダで一つのレンダリングターゲットにしかレンダリングできなかったのが、4つまでレンダリングターゲットを指定できるようになった。

例えば、Aというテクスチャが32bit 浮動小数点の数値を格納しているテクスチャだとする。
これの仮数分と指数部を分けてBとCのテクスチャで取得したい場合、今まででは仮数分だけを取得するシェーダと、指数部だけを取得するシェーダの両方を作成し、別々にシェーダ実行しないといけなかったのが、これにより一つのシェーダーに統合できるというわけだ。

しかし、グラフィックボードが古いものだと物によってはできないこともあるらしい。
私の環境Geforce GTX550tiで、マルチレンダリングターゲット最大数は4であった。
HSPSHADにはこの最大数を取得する命令も追加し、さらに使いやすくなった(と思う)。


ということでHSPSHAD ver 1.5とマルチレンダリングターゲットのサンプルを公開
DOWNLOAD
無題

画面に出しているのはマルチレンダリングターゲットとはあまり関係なく、HLSL内でテクスチャ座標がどのように指定できるかを表している。
テクスチャ座標は3Dプログラミングっぽく言えばUV座標のこと。
ピクセル指定は(x,y)という整数座標ではなく、float型の0.0~1.0までで表される小数で行う。

サンプルの例(4×4ピクセル)では、左上は(0.000,0.000)、右下は(0.750,0.750)として座標指定しているのが分かるように、HSPスクリーン上で数値を表示させたものだ。




今回参考にさせていただいたサイト
http://marupeke296.com/DXG_No50_MultiPassRendering.html

「HSPSHAD」mrefでウィンドウのrgb分解実験

サンプルはこちら

HSP標準命令のmref でウィンドウの画面データを取得することがでる。それをpeek や pokeなどでいじるのが定番だが、今回はHSPSHADを使えばシェーダに送り込ませることができますよ、という記事。

D3DFMT_L8フォーマットはint 8bitの明度情報を1ドットとするレンダリングバッファである。
これはHSP画面データのRGB(8bit+8bit+8bit)を1ドットとするものの3分の1の容量。

なのでD3DFMT_L8で640×480の3倍の大きさのバッファ1920×480を作って、そこにmrefで取得したデータを転送。シェーダで扱えるような形式にする。
今度はD3DFMT_L8の640×480の大きさを3つ作り、それぞれをR要素、G要素、B要素を格納するバッファとする。

そして私の作ったピクセルシェーダーでRGB要素に分解する。

-------------------------------------------------------------------------------------------------------
PS_OUTPUT  PStyusyutu(VS_OUTPUT In) : COLOR
{
 PS_OUTPUT Out;
float ss1;
ss1=tex2D(gputex00,In.Tex0+float2(0.5/(WIDTH*3.0)+dot0,0.5/HEIGHT));
Out.Color=ss1;
return Out;
}
-------------------------------------------------------------------------------------------------------

dot0やWIDTH、HEIGHTはHSP側から変更できる任意の変数


これでR要素、G要素、B要素に分解したバッファができた。
サンプルでは、ここでR要素のバッファを以下のピクセルシェーダーに流す。
これはクリックした点を中心として円状に減算する処理だ。中心点からの距離で減算値が変わる。

---------------------------------------------------------------------------------------------------------

PS_OUTPUT  PScircle(VS_OUTPUT In) : COLOR
{
 PS_OUTPUT Out;
float4 ss1;
float xxx,yyy;
ss1=tex2D(gputex00,In.Tex0+float2(0.5/WIDTH,0.5/HEIGHT));
xxx=In.Tex0.x-mousexf;
yyy=(In.Tex0.y-mouseyf)/WIDTH*HEIGHT;
if (xxx*xxx+yyy*yyy<0.04){
ss1-=3.0*(xxx*xxx+yyy*yyy+0.04);
}
Out.Color=ss1;
return Out;
}
-------------------------------------------------------------------------------------------------------------------- 


同じようにGB要素も様々なピクセルシェーダに流す。


mref実験



一番気になるFPSだが、私の環境のcore i7 3820 + GTX550ti では約8~10であった。
あまり喜ばしい数値ではないが、HSPの画面データをGPUに転送して分解して、さらに3つのシェーダ使って、統合して、GPUからHSPに戻す、というあまりに重い処理をしているだけに仕方がないのだろう。


もちろんbuffer命令で作成した任意の大きさのバッファでも、mrefで取得して同じことができるはずなので小さいバッファならリアルタイムでいけるかもしれない。
さらに当然、高速化できる部分は多く残されているのでもっとFPSは上がる可能性は十分にある。


数値計算用に作ってきたプラグインであったが、mrefと組み合わせることで思わぬ副産物を生み出すことができた。
これからシェーダーがHSPで格段に扱いやすくなるだろう!


※PC環境によってはD3DFMT_L8に対応してなくて、クリックしてもうまく↑の画像のようにならないかもしれません

HSP用GPGPUプラグイン「HSPSHAD」途中経過

とりあえず途中経過

http://sourceforge.jp/downloads/users/1/1813/hspshadver12.rar/

サンプルソースを見てもあまりわからないかもしれないが、やっていることは1024*1024の大きさのD3DFMT_R32F(float要素がひとつ)のバッファを作りそれをGPU上の変数に見立てて計算している。

HSP側で1024*1024要素の整数型変数を作り、0-10までのランダムな値を代入。それをCPUでFLOAT型に変換してGPUバッファに転送。
これで1024*1024このランダムな値が格納されたGPUバッファができた。
それをもう一つ作る。
シェーダーで2つの値を足して2でわって1.5かけた値を出力させる。

出力結果の入ったGPUバッファをHSP側の整数型変数にデータ転送させて、さいごに一部を表示している。

まだ未完成な命令も多いが、HLSLを使えればすでにGPGPUができる状況にはなっている。

只今ver1.2だ。
上のリンクからダウンロードできる。

HSP用GPGPUプラグイン作成。機能が無いなら作ればいいじゃない

HSPの3Dプラグイン、EASY3Dを使って、HLSLも使ってGPGPUができることは前の記事で確認したとおりである。
おさらいとして、GPGPUとはグラフィックボードのビデオメモリを変数に見立てGPUで計算をすることであり、CPUをはるかに凌ぐ計算速度が得られる技術である。


が、EASY3Dでやるには、そこには幾つかの欠点があった。

まず一番の問題点は、E3DCreateRenderTargetTexture で作成したレンダリング用バッファのデータをメインメモリへ転送できなかったことだ。
これではHSP標準命令などで初期値を作っても、それをVRAMに転送し、計算結果をまたHSPで使える形(ddimで初期化した変数など)に戻してくることができない。

流体力学やレイトレーシングのように、数値を色として画面に表示するのが目的なものは、意外と少ない。

そもそもEASY3Dは数値計算の用途で使うものではない・・


そこで今回、数値計算に特化した、それもGPGPUで処理高速化が簡単にできるようなプラグインを作ってみようと思い立った。


HSPSDKのサンプルとにらめっこしながらとりあえず即席で作ったプラグインが下の「エイチエスピーシェーダ HSPSHAD(仮)」だ。

http://sourceforge.jp/users/toropippi/pf/tyoukihokanko/files/?id=1570


実行すると
3つの倍精度浮動小数点の配列変数が作られる。
「cpua」
「cpub」
「gpua」
要素数は1024*1024*4。

まずHSP標準命令にてcpuaの各々の配列に適当な初期値が入る。
その初期値を元に
cpubとgpuaには、それぞれCPU、GPUで同じ処理をさせた結果が入る。

処理内容は
上下左右の配列から引っ張ってきた値の平均に1234.5678をかけて40で割るというもの。

1つの要素を計算して求めるのに参照箇所が4つもあるため、これはかなりメモリアクセスの速さが要求される処理だ。


ではGPUで何が起きているかみていこう。
GPUで計算するには、まずVRAMにcpuaのデータを転送する必要がある。
その命令が「SHADCPUtoGPU」である。

E3DCreateRenderTargetTexture で変数が作られ、そこにcpuaが代入されたような状態だ。

つぎに、GPU内で演算が始まる。ここでHLSLが読み出される。
HSPからはSHADCallUserShaderで命令をしているのみ。具体的な処理は全てHLSLのエフェクトファイルに記述する必要があるということだ。
こうして、上下左右の配列・・・1234.5678をかけて40で割る工程がここで行なわれた。


最後に演算結果の格納されているVRAMから、ddim で初期化されたメインメモリ上にある変数へと数値を転送する必要がある(ここが一番重要!!!)
その命令が「SHADGPUtoCPU」
こうして、CPUでは全然頭を使わないうちに、計算結果がメインメモリに格納できたわけだ。

このようにしてCPUとGPUそれぞれ処理した結果が2つともメモリに入ったわけだから、数値エラーがないか色分けして表示してみよう。
まぁ当然結果は同じ値!
ではなく、CPU→GPUにデータを渡す際に倍精度が単精度に直されてしまったので、よく見るとほんの僅かな誤差がある。
gpgpu02

でもどうでだろう!?GPGPUがお手軽になってきた気はしないか!


そして、いつもの癖で速度比較をしてみた。
倍精度と単精度ではまず4倍近く時間が変わってくるのでCPUの時間を4分の1にして見て欲しいが、やはりGPU処理が高速。
というかHSP標準命令が遅すぎるだけ・・
HSPの処理の部分をc++で処理させたら10倍以上早かった・・

そう考えると同じくらいの速度かと思うかもしれない、がGPGPUの本領発揮となるは、たくさんの変数、算術命令があり、そして膨大な量のメモリアクセスが発生する状況下である。


2012/11/26日現在、サンプルで公開しているのは、処理が1回しか行えないひどい有様であるが、これから様々な機能を付け加えていこうと思う。

誰でも、HSPでお手軽にGPGPUプログラミングのできる時代が来ると思うと、なんだかワクワクするね!



追記:
XP以前では、サンプル が動かない可能性があります。
プロフィール

toropippi

記事検索
アクセスカウンター

    QRコード
    QRコード
    • ライブドアブログ