パズドラルート解析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」のために