MetalでUIImageViewライクに画像を表示する - MTKViewの最小実装編

やりたいこと：

```swift
@IBOutlet weak var imageView: MetalImageView!
```    

```swift
imageView.image = uiImage // UIImage
```

```swift
imageView.image = mpsImage // MPSImage
```

```swift
imageView.image = mtlTexture // MTLTexture
```

```swift
imageView.image = ciTexture // CIImage
```

こんな感じで**UIImageViewライクに手軽に、かつなるべくCPUとGPUを行ったり来たりせずに（余計なコンバートをせずに）高速描画**したい。



##MTKView

Metalで描画できるUIViewサブクラスの `MTKView` というものがあるのですが、これが・・・**ただ単にテクスチャを描画したいだけだとしてもかなりの手順が必要**になります。

コマンドキュー、コマンドバッファ、コマンドエンコーダ、パイプライン・・・UIKitやCore Graphicsでは意識することのなかった概念がたくさん登場します。OpenGLに慣れ親しんだ人からするとわかりやすいらしいですが、GPUプログラミング初心者である自分からすると、[Appleのサンプル](https://developer.apple.com/library/content/samplecode/MetalTexturedQuad/Introduction/Intro.html#//apple_ref/doc/uid/TP40014604)等はやりたいことに対して複雑すぎるように感じてしまいます。

というわけでここをラップするMTKViewのサブクラスをつくりつつ、Metalでテクスチャを描画するための**最小実装**を探っていきたいと思います。

```swift
class MetalImageView: MTKView, MTKViewDelegate
```

以下でその実装の要点を書きます。

### ※ 2018.1.27追記

本記事を書いたころはまだMetalの理解が浅く（今でもまだまだ勉強中ですが）、以下の実装や解説は、結構見当違いなことをやってたり言ってたりします。

たとえば単に`MTKView`に画像を描画したいのであれば**ブリットコマンドエンコーダを利用する方がシンプル**です。[こちらの動画](https://www.youtube.com/watch?v=USCCR-n0_1E)あるいは[スライド](https://www.slideshare.net/t26v0748/metal-iosdc)あるいは[書籍](https://qiita.com/shu223/items/19c7d98fc186562b4f57)をご参照ください。

- [Metalの基礎を日本語で学べる書籍 - Qiita](https://qiita.com/shu223/items/19c7d98fc186562b4f57)

##UIImage を MTLTexture に変換する

Metalでは`MTLTexture`というプロトコルに適合したオブジェクトをテクスチャとして扱います。

UIImageからMTLTextureを生成する、もしくはアセットから直接生成する実装については以下の記事に書いたので、適宜ご参照ください。

- [Metal逆引きレシピ - Qiita](http://qiita.com/shu223/items/7ed4efd54d03f0babbe0)



##シェーダを読み込みパイプラインステートを作成する

Metalシェーダのファイル（xxxx.metal）を読み込んで `MTLLibrary` を生成し、そこから使用する関数を取り出して `MTLFunction` を生成し、

```swift
guard let library = device.makeDefaultLibrary() else {fatalError()}
guard let function = library.makeFunction(name: "kernel_passthrough") else {fatalError()}
```

その関数を引数に渡して、パイプラインステート（ここではコンピュートシェーダを使うので`MTLComputePipelineState`）を生成します。

```swift
pipeline = try! device.makeComputePipelineState(function: function)
```

「パイプラインステート」というのがGPUプログラミング初心者としてはあまりピンと来ないのですが、同クラスの[リファレンス](https://developer.apple.com/reference/metal/mtlcomputepipelinestate)を見ると、

> The MTLComputePipelineState protocol defines the interface for a lightweight object used to encode a reference to a compiled compute program. 

とあり、コンパイル済みのコンピュートシェーダ関数をコマンドエンコーダで取り扱うためのもののようです。


ちなみにシェーダはこんな感じで、入力テクスチャをそのまま出力に渡しています。

```cpp:Shader.metal
#include <metal_stdlib>
using namespace metal;

kernel void kernel_passthrough(texture2d<float, access::read> inTexture [[texture(0)]],
                               texture2d<float, access::write> outTexture [[texture(1)]],
                               uint2 gid [[thread_position_in_grid]])
{
    float4 inColor   = inTexture.read(gid);
    outTexture.write(inColor, gid);
}
```

参考： http://stackoverflow.com/questions/33605241/display-jpeg-image-using-


##その他初期化時にやること


###コマンドキューを生成

MTLCommandQueueについては[こちらの記事](http://dsas.blog.klab.org/archives/52168969.html)の説明が完結でわかりやすいので引用させていただきます。

> **レンダリング（またはコンピューティング）コマンドをGPUに流すためのキュー。**
GPUのタスクが空き次第プッシュされた順にコマンドが実行されます。

ここにコマンドバッファをプッシュしていくので、最初につくっておきます。

```swift
commandQueue = device.makeCommandQueue()
```

###MTKViewのセットアップ

ちゃんと実装しようとするともっと色々必要そうですが、とりあえず今回の方法で最小限セットしないといけないのは以下の2つ。

```swift
framebufferOnly = false
delegate = self
```

`framebufferOnly` はデフォルトでは `true` で、そのままだとコンピュートシェーダが使えない的な実行時エラーになります。

今回の要件では描画を同じクラス内で処理したいので `delegate` には自身をセットします。

##描画するテクスチャとMTKViewのピクセルフォーマットを合わせる

描画する`MTLTexture`がセットされたら（MTKTextureLoaderを用いてロードしたら）、自身（MTKView）の`colorPixelFormat`を合わせます。

```swift
colorPixelFormat = texture.pixelFormat
```

これをやらないと、描画されても**変な色に**なります。


##MTKViewDelegateの実装

2つの`MTKViewDelegate`プロトコルのメソッドを実装する必要があります。

```swift
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)
```

```swift 
func draw(in view: MTKView)
```

###`drawInMTKView:`でやること

ビューに描画を行う際に呼ばれるメソッドです。

> Called on the delegate when it is asked to render into the view

ここでやるべきことをシンプルに言うと、

「**コマンドを生成してコマンドキューにプッシュする**」

となります。

コマンドキューについては上述した通りですが、「コマンドの生成」についてはコマンドバッファ（`MTLCommandBuffer`）, コマンドエンコーダ（`MTLRenderCommandEncoder`, `MTLComputeCommandEncoder`）が絡んできます。これらについて、また下記サイトの説明をお借りします。

- [DSAS開発者の部屋:Metalのコードを読む前に知っておくと楽なこと](http://dsas.blog.klab.org/archives/52168969.html)

[MTLCommandBuffer]

> MTLCommandQueueにプッシュするコマンド本体。
コマンドは主にGPUで実行されるシェーダプログラムやテクスチャ・頂点データのポインタ、その他のパラメータのポインタなどの情報で構成されます。
レンダリング用のMTLCommandBufferのオブジェクトを作成するには次のMTLRenderCommandEncoderを使用します。


[MTLRenderCommandEncoder] [^1]

> レンダリングコマンドを作成するための役割を持つクラスです。
MTLBufferやMTLTextureのオブジェクトのポインタや、以下で紹介するMTLRenderPipelineStateのオブジェクトを元にコマンドを生成します。

[^1]: ここではコンピュートシェーダを使ってるので、コマンドエンコーダは`MTLComputeEncoder`になります


以下のように実装しました。

```swift
func draw(in view: MTKView) {
    guard let drawable = view.currentDrawable else { return }
    guard let texture = texture else { return }
    
    let commandBuffer = commandQueue.makeCommandBuffer()
    
    let encoder: MTLComputeCommandEncoder = commandBuffer.makeComputeCommandEncoder()
    encoder.setComputePipelineState(pipeline)
    encoder.setTexture(texture, at: 0)
    encoder.setTexture(drawable.texture, at: 1)
    encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
    encoder.endEncoding()
    
    commandBuffer.present(drawable)
    commandBuffer.commit()
}
```

###Drawable

上記実装で、

```swift
guard let drawable = view.currentDrawable else {
    return
}
```

と、MTKView オブジェクトから `currentDrawable` なるものを取得し、

```
encoder.setTexture(drawable.texture, at: 1)
```

その `texture` をシェーダの第2引数（つまり出力側）に渡しています。

で、最後に

```swift
commandBuffer.present(drawable)
commandBuffer.commit()
```

コマンドバッファに渡してコミット。（→コマンドキューにプッシュされる）


この `currentDrawable` の型は `CAMetalDrawable` で、ヘッダにはこう説明されています。

> CAMetalDrawable **represents a displayable buffer** that vends an object that conforms to the MTLTexture protocol that may be used to create a render target for Metal.


###スレッドグループ

もうひとつ、上記実装で

```swift
encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
```

ということをやっています。引数に渡している2つの値はどちらも`MTLSize`型で、GPUで並列に実行するスレッドの数を指定しています。

それぞれの引数について、[リファレンス](https://developer.apple.com/reference/metal/mtlcomputecommandencoder/1443138-dispatchthreadgroups?language=objc)では以下のように書かれています。

- `threadgroupsPerGrid`

> The number of threadgroups for the grid, in each dimension.

- `threadsPerThreadgroup`

> The number of threads in one threadgroup, in each dimension.

前者が（グリッドあたりの）スレッドグループの数、後者が1つのスレッドグループにおけるスレッド数、になります。

正直どういう値をセットしてよいかわからないので、ここはAppleの[MetalImageProcessing](https://developer.apple.com/library/prerelease/content/samplecode/MetalImageProcessing/Introduction/Intro.html#//apple_ref/doc/uid/TP40014598)サンプルを参考に設定しました。（コンピュートシェーダで画像を取り扱っているという点で、状況は似通っているのかなと）

```swift
private let threadsPerThreadgroup = MTLSize(width: 16, height: 16, depth: 1)
```

```swift
threadgroupsPerGrid = MTLSize(width: (texture.width + threadsPerThreadgroup.width - 1) / threadsPerThreadgroup.width,
                              height: (texture.height + threadsPerThreadgroup.height - 1) / threadsPerThreadgroup.height,
                              depth: 1)
```

###余談：処理を書かないとどうなる？

何が必須で、何が必須じゃないかを探るため、まずはエンコーダをコメントアウトしてみます。

```swift
let commandBuffer = commandQueue.makeCommandBuffer()

// let encoder = commandBuffer.makeComputeCommandEncoder()
// encoder.setComputePipelineState(pipeline)
// encoder.setTexture(texture, at: 0)
// encoder.setTexture(drawable.texture, at: 1)
// encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
// encoder.endEncoding()

commandBuffer.present(drawable)
commandBuffer.commit()
```

→ 実行時エラーにはならないが、drawableに何も書き込まれていないので、**真っ黒に描画される**


コマンドバッファもコメントアウトしてみます。

```swift
// let commandBuffer = commandQueue.makeCommandBuffer()

// let encoder = commandBuffer.makeComputeCommandEncoder()
// encoder.setComputePipelineState(pipeline)
// encoder.setTexture(texture, at: 0)
// encoder.setTexture(drawable.texture, at: 1)
// encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
// encoder.endEncoding()

// commandBuffer.present(drawable)
// commandBuffer.commit()
```

→ **真っ白に描画される**。描画命令がないので、そもそも描画が行われず、下のビューが見えている、と解釈しました。

##出来上がり

以上で、

```swift
imageView.image = UIImage(named: "filename")
```

とやるだけでMetalで画像を描画するクラスが出来ました。

が、`contentMode`に対応してないし、2x/3xといったRetinaスケールも対応してないので、実際のところ**UIImageViewの代替としてはまだまだ完成度が程遠い**超原始的パージョンです。

最低限の対応（contentMode/Retina）ができたらGitHubにアップしたいと思います。