C++ AMPでGPGPU！

この記事は[C++ Advent Calendar 2015](http://www.adventar.org/calendars/926)の20日目の記事です．
昨日は[ろっさむ(4_mio_11)](https://twitter.com/4_mio_11)さんの「[つまづいたっていいじゃないか、C++だもの](http://rossamu.hatenablog.com/entry/2015/12/19/225717)」でした．

---

I([@wx257osn2](https://twitter.com/wx257osn2))です．また20日です[^0]．

ところで[今年はやたらと](http://qiita.com/Riyaaaa_a/items/40054b893e70b54f5a26)[GPUを燃やそうとする人が](http://qiita.com/aokomoriuta/items/7ee1dc8499c40fbee84f)[多いんですが](http://okdshin.hatenablog.com/entry/2015/12/13/)一体どうしたんですか？[^1]
ネタが被るんじゃないかとびくびくしながら1ヶ月過ごすのは中々つらかったです．

[^0]: 3年連続20日目担当
[^1]: 私は自室が北側で寒いので燃やしてます．

# **おことわり**
当記事ではVS2015を使ってC++ AMPで遊びます．当記事でC++ AMPと言ったらMSVC上でのそれです．
というわけでWindows7[^2]以降とVisual Studio 2015[^3]，及びDirectX11.0以降に対応したGPUが必要です．
持ってない人は諦めてください．
ちなみに私の環境はWin8とVS2015，GTX780です．

[^2]: Win7はPlatform Update適用済みの場合のみ可…このご時世に未適用の環境なんてそんなに残ってるとは思いませんが．
[^3]: 「VC++はC++ではない」的な話をしたい方，最近のMSさんは以前に比べればメチャクチャ頑張ってるので，そろそろVC++のことも許してあげてください．まだTwo Phase Lookup実装されてませんが，実装するつもりではいるらしいです．

# **C++ AMPについて**
3日目に[りやさん(Riyaaaaa)](https://twitter.com/riyaaaa_a)さんが[入門記事](http://qiita.com/Riyaaaa_a/items/40054b893e70b54f5a26)を書いてらっしゃるのでそちらを読んでください(丸投げ)

# **気になる記事が**
C++ AMPを学ぶ際，やはり1次資料的な位置付けとなるのがMSDNとかMSDN Blogsですが，そんなMSDNの中に面白い記事が．

>[グラフィックス (C++ AMP)
https://msdn.microsoft.com/ja-jp/library/hh913015.aspx](https://msdn.microsoft.com/ja-jp/library/hh913015.aspx)

要約すると，

1. 計算バッファとして空間局所性の高い「テクスチャ」を利用できる
1. `norm`/`unorm`[^4]，short vectorクラス[^5]が提供されている
1. **`get_texture`と`make_texture`を使うことでD3Dの`texture1d`/`texture2d`/`texture3d`と相互運用できる[^6]**

の3つです．
つまり，**GPGPUで2次元テクスチャの各ピクセルに対して何らかの処理をして値を書き込む**みたいなことも出来るというわけ．
**…あれ？それなんてピクセルシェーダー[^13]？[^7]**

というわけで，C++ AMPによるGeneral-Purpose computing on Graphics Processing UnitsをGraphical-purpose computing[^8]に使ってみましょう．

[^4]: 値の範囲が[`-1.f`,`1.f`]/[`0.f`,`1.f`]にクランプされる`float`
[^5]: `int`,`uint`,`float`,`double`,`norm`,`unorm`の1～4次元のベクトルクラス，HLSLやったことある人にはお馴染みのアレ(機能は制限されてるとかなんとか，私HLSL知らないので…)
[^6]: 流石はMS製という感じ．実際MSVCにおけるC++ AMPはDirectComputeをバックエンドとして実装されているので，こういった芸当が可能なのだと思われます．
[^13]: フラグメントシェーダーとも言う．ぶっちゃけ何処理すんだかちっともわからない名前なので「ピクセルシェーダー」の方が好きです．
[^7]: 先述の通りバックエンドはDirectComputeなので，C++ AMPはコンピュートシェーダー相当のことが出来ます．テクスチャに対してピクセル単位で処理を行えば，ある程度ピクセルシェーダーのように振る舞うことも可能ということです．
[^8]: 「C++ AMPでGraphical-Purpose computing on Graphics Processing Units！」

# **C++ AMPグラフィックスの概要 : short vectorクラスとテクスチャクラス**
というわけでまずは件の記事の内容をざざっと．
先におことわりですが，今回はシェーダープログラミングに必要な最低限の内容のみを取り扱っていきます．
簡単のために，多数のオーバーロードやオプションの引数，使わなかった要素については大胆に解説を削っていきますのでご了承ください．
まず，グラフィックス関連の諸々はみな`amp_graphics.h`内で`concurrency::graphics`名前空間に入ってます[^11]．
`concurrency::graphics::norm`と`concurrency::graphics::unorm`は「[^4]」の補足の通りです．それ以上の詳細はMSDN行ってください．

##**short vectorクラス**
short vectorクラスはC++ AMPで使える「[^5]」です．
1次元についてはそれぞれの型そのものであり，実際に所謂ベクトルクラスっぽくなるのは2次元以上です．
命名は，`T_N`の形になります．`int`型2次元なら`int_2`，`unorm`型4次元なら`unorm_4`…という感じです．
これらが`concurrency::graphics`名前空間に属するので，`using`等をしない場合には`concurrency::graphics::int_2`等といった形になります[^9]．
基本的にそこらのベクトルクラスみたいなものですが，特徴的な機能としてスウィズリング式(swizzling expression)があります．
例えば，`norm_4`の第0要素と第2要素を取り出した`norm_2`が欲しい…と言ったとき，以下のように記述することが出来ます．

```cpp
const graphics::concurrency::norm_4 vector = {0.f, .1f, .2f, .3f};
auto vec = vector.xz; //decltype(vec) : graphics::concurrency::norm_2
vector.xz = graphics::concurrency::norm_2{.4f, .5f};
// vector == {.4f, .1f, .5f, .3f}
```

このように，特定の識別子をメンバのごとく連ねることでアクセスが簡単にできるようになっています．
これら識別子は以下のように対応しています．

| アクセス先 | 識別子1 | 識別子2 |
|:----------|:------:|:------:|
| 0         |   `x`  |   `r`  |
| 1         |   `y`  |   `g`  |
| 2         |   `z`  |   `b`  |
| 3         |   `w`  |   `a`  |

識別子は`xyzw`と`rgba`でグループとなっており，これらを混ぜて使うことは出来ません．

```cpp
vec.xy //OK
vec.xg //NG
```

また，`vec.xx`等のように同じ識別子を連ねることも出来ません[^10]．

そして，`concurrency::graphics::short_vector_traits<T>`はshort vectorクラス`T_N`に対して(擬似コードですが)大体以下のような定義がなされています．

```cpp
struct short_vector_traits<T_N>{
  using type = T;
  static cosnt int value = N;
};
```

これを用いることで`template`を用いつつ処理を分岐したりまとめたり，といったことが可能です．

[^11]: 厳密にはshort vectorクラスは`amp_short_vectors.h`に入ってますが`amp_graphics.h`でincludeされているのでとりあえずそっちincludeしとけばいいと思います
[^9]: 一応typedefとして`concurrency::graphics::direct3d::int2`という形もあるのですが，今回は扱いません．
[^10]: 正直これ欠陥だと思う…ちなみに内部実装は`__declspec(property(get=getter, put=setter))`って感じ．こう見てみると，いざsetter定義しようとするとどうすんだこれってなる(`std::tie(x, x) = std::make_pair(1, 2)`みたいなもんなので)のも事実なので難しいところではあります(まぁsetter定義しなきゃいいんじゃ…とも思うんですが)

##**`concurrency::graphics::texture<T, N>`と`concurrency::graphics::texture_view<T, N>`**
続いてテクスチャクラスについてです．
`texture<T, N>`の`T`には`double_4`を除く1・2・4次元のshort vectorクラスが，`N`には1～3が入り，これによってテクセル形式`T`，次元`N`のテクスチャが生成されます．
まぁ大体`concurrency::array<T, N>`と大差無いです．しかし，唯一大幅に違うのは，`texture`クラスには実質的に[^12]writeアクセスが出来ないということです．
`texture`のメンバは，`operator[]`も`operator()`も`get()`も全て**値**を返します．**参照**は返してくれません．
あと`std::vector`への変換とかは出来ません．基本的には，テクスチャはアクセラレーター固有のバッファとして扱うことになります．

さて，書き込めない`texture`をどう使えばいいのかというと，`concurrency::array_view<T, N>`の如く，`concurrency::graphics::texture_view<T, N>`というクラスが存在し，こちらから`set()`メンバ関数を通して書き込むことが出来ます[^16]．
`texture_view`も概ね`array_view`みたいなものですが，CPUや異なるアクセラレーターへのデータの自動的な移動などは行ってくれません．
テクスチャはあくまでアクセラレーター固有の(ry

[^12]: 実際には`T`が`int`とか`float`とかだったら書き込めるらしいですが，そんな状況私が考えうる限りZバッファぐらいしか無い気がしますし，後述の方法でいくらでも出来るので…
[^16]: 決して書き込み専用のクラスというわけではなく，特に`texture_view<const T, N>`とすることで読み取り専用の`texture_view`が作れたりするのですが，今回は使ってないのでパスします

##**D3Dとの相互運用**
さて，C++ AMP単体だと完全に「アクセラレーターに強く紐付けられた速い一時バッファ」でしかないテクスチャですが，D3Dとの相互運用によってただのバッファからGUIやらゲームやらで使える「テクスチャ」に変貌します．
`concurrency::direct3d`名前空間には一般的なD3Dとの相互運用性のためのユーティリティ群が，`concurrency::graphics::direct3d`にはグラフィックス関連のD3Dとの相互運用性のためのユーティリティ群が入っています．
`concurrency::direct3d::create_accelerator_view`関数[^14]にD3DのDeviceインターフェースを投げると`concurrency::accelerator_view`が返ってきます．
この`acclerator_view`を用いて以後のC++ AMPプログラミングを進めることが可能です[^15]．
そして，D3D側で作成したテクスチャと上述の`create_accelerator_view`にて作成した`accelerator_view`オブジェクトを`concurrency::graphics::direct3d::make_texture`関数[^14]に渡すことで，C++ AMPの`concurrency::graphics::texture`を得ることが出来ます．
これを使えば，C++ AMPでの画像処理に入力画像を渡すことも容易です．
また，C++ AMPで処理した`concurrency::graphics::texture`を渡すことで元になるD3Dのテクスチャインターフェースを返す関数が`concurrency::graphics::direct3d::get_texture`[^14]です．
一応補足しておくと，`create_accelerator_view`でD3D Deviceから生成した`accelerator_view`を用いて作成した`concurrency::graphics::texture`であれば，`make_texture`を使っていなくとも`get_texture`可能です．

[^14]: 恐らくD3Dのバージョンが上がった際にも対応できるように，或いは次元ごとに関数を分けたくないという考えからか，D3DのインターフェースはIUnknown*になっています．
[^15]: 同時にD3D DeviceとC++ AMP Accelerator Viewを操作する場合には`concurrency::direct3d::d3d_access_lock`などを用いてロックを取る必要があるようですが，今回は使ってないのでパス

#**C++でシェーダーを書こう**
それではようやく本題です．C++のみでシェーダーアートを楽しんでみましょう．
## **大量の関門**
と言いたいところですが，実はそのままシェーダーを書くには問題がメチャクチャたくさんあります．

0. short vector用の関数が無い  
無いです．`float`用の数学関数などは`amp_math.h`の`concurrency::fast_math`名前空間に用意されているので，諦めて自分で各次元用に自作しましょう．  
ちなみに，`min`や`max`，`floor`や`abs`なども当然無いですが，そもそもvectorとして大変重要な`dot`と`cross`も存在しません．自作しましょう．
0. [modが負値を返しうる](http://fimbul.hateblo.jp/entry/2014/02/27/204826)  
メチャクチャ落とし穴ですが，C++の剰余は負値を返すことがあります．  
いや，それ自体は別に落とし穴でもなんでもないんですが，問題なのはGLSLの`mod`は負値を返しません．  
というわけで，実は`concurrency::fast_math::fmod`を直接使うと，剰余として常に正の値が返ってくることを想定したプログラムが上手く動作しないことがあります．
0. C++ AMPはインデックスの順番が逆  
縦横のサイズを指定する`concurrency::graphics::texture`のコンストラクタをよく見てみると，**コメントに**引数の順番が`height`, `width`の順であるとの記述があります．  
同様に，`concurrency::graphics::texture<int, 2> texture`に対する`texture.extent`によって得られる`concurrency::extent<2>`から取り出した`concurrency::index<2>`は`[0]`が`y`, `[1]`が`x`の順に並びます．  
よって，`x = idx[0], y = idx[1]`のようなコードをうっかり書いてしまうと，解像度が縦横同一でない限りほぼ確実にうまく動かなくなります．
0. short vectorのコンストラクタが貧弱  
特にGLSLのコードを移植する際に弊害となりえます．1次元，2次元，1次元の3つのshort vectorをコンストラクタに渡して4次元のshort vectorを作る…といったことがそのままでは出来ません．
0. 行列型が存在しない  
やる気あんのかって感じですが，行列型もありません．
0. テクセル形式  
本来は問題では無いのですが，D2Dと組み合わせようとするとここで問題が発生します．  
C++ AMPの`concurrency::graphics::texture`から最終的にD2Dの`ID2D1Bitmap1`インターフェースに落としこむことを考えた時，D2D側のピクセル形式は実質的に[`DXGI_FORMAT_B8G8R8A8_UNORM`](https://msdn.microsoft.com/ja-jp/library/windows/desktop/dd756766%28v=vs.85%29.aspx#section_7)一択となります．  
一方，`get_texture`で得られるD3D Textureインターフェースのテクセル形式は`DXGI_FORMAT_R8G8B8A8_UNORM`([Creating textures with specific DXGI_FORMAT](http://blogs.msdn.com/b/nativeconcurrency/archive/2012/07/02/interop-with-direct3d-textures-in-c-amp.aspx)参照)です．  
つまり，一度何らかの方法でD3D Textureのテクセル形式をRGBAからBGRAに変更する必要があります．  

と．このように問題が山積しています．

##**めんどうくさいので**
テンプレ作りました．

> Shampoo
> https://github.com/wx257osn2/shampoo

git submoduleを使っているのでよしなにしてください．
`shader.hpp`内にいい感じにシェーダーを書くとなんかそれっぽく動きます．
サンプルとしてネット上に公開されているGLSLシェーダーを幾つか(一部除き作者の皆様には無断で)C++コードに移植して入れておいたので参考にしてください．
そこそこ素直に移植できます．さらに，C++なのでautoとか使い放題です．

###**TODO**
0. 関数が足りない  
サンプルで必要な分は最低限作りましたが，その程度です．足りません．
0. 行列型がない  
サンプルの一つに行列を使ったものがありましたが，定数だった上に面倒だったので関数として直書きしてしまいました．そのうち行列型も作ります．
0. スウィズリング式がアレ  
上述の通り，`vec.xx`などのように同じ識別子を連ねることが出来ないため問題となる部分がいくつか出ています．そのうち実装します．

#**まとめ**
0. C++ AMPにはグラフィックスに特化した機能がある
0. C++ AMPでシェーダーアートを楽しむことが出来る
0. 標準のみで書きやすいとは言っていない

#**謝罪**
がむさん，tomohiroさん，i-saintさん，勝手にシェーダ使ってごめんなさい…
なにか問題がございましたらおっしゃってください，リポジトリごと作りなおして消します…

***

というわけで，Advent Calendarも残すところあと5日間となりました．
明日は21日目，[みやたけ ゆき](https://www.facebook.com/chihaya.takatuki)さんです．

***