Metal 入門

1 はじめの一歩

Apple の新しいグラフィックス API Metal の覚書。
たぶん、わかりやすいと思います。

 

猪股弘明 医師:精神科(精神保健指定医)
HorliX, OpenDolphin-2.7m 開発者

iOS? MacOS? いえいえ最初は Playground を使います

アプリまで作り込むつもりなら、プラットフォームはどちらかに決めないといけないが、まずは概念を掴みたいので Playground で動かしてみましょう(実際にはプログラムを走らせるとき Mac 向け iOS 向けのどちらかを選んでいることになりますが、下記コードはどちらでも動きます)。

使用言語は Swift? Objective-C?

「近い将来 Apple は Objective-C のサポートを打ち切る」なんて噂もあるわりに、Apple 公式の Metal サンプルコードは Objective-C で書かれていたりします。
個人的には C/C++ の既存リソースが使いやすい Objective-C を簡単には終わらせられないと思うのですが・・・。
だから使用言語はどちらでもいいんじゃないかと思うのですが、ここでは使用者の多い(と思われる) Swift をメインに使っていきます。

1-1 最初のメタルプログラミング

これ ↓ です。
3点の頂点データから2次元のビュー上に三角形を描く、というよくあるアレですが、1ファイルに完結させているのはあまりないと思います。

import PlaygroundSupport
import MetalKit

typealias float3 = SIMD3<Float>
typealias float4 = SIMD4<Float>
var Color: float4 = [1, 0, 0, 0.5]

//device, frame, view, commandBuffer を準備する
guard let device = MTLCreateSystemDefaultDevice() else {
  fatalError("GPU is not supported")
}
let frame = CGRect(x: 0, y: 0, width: 500, height: 500)
let view = MTKView(frame: frame, device: device)
view.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)

guard let drawable = view.currentDrawable else {
  fatalError("Could not create a drawable")
}
guard let commandQueue = device.makeCommandQueue() else {
  fatalError("Could not create a command queue")
}
guard let commandBuffer = commandQueue.makeCommandBuffer(),
  let renderPassDescriptor = view.currentRenderPassDescriptor,
  let renderEncoder =
  commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
else {
    fatalError("Could not create a command buffer")
}

//shader とその library を設定する
let shader = """
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
    float4 position [[position]];
    float point_size [[point_size]];
};

vertex VertexOut vertex_main(constant float3 *vertices [[buffer(0)]],
                                 uint id [[vertex_id]]){
    VertexOut vertex_out {
        .position = float4(vertices[id], 1),
        .point_size = 20.0
    };
    return vertex_out;
}

fragment float4 fragment_main(constant float4 &color [[buffer(0)]]){
      return color;
}
"""
let library = try device.makeLibrary(source: shader, options: nil)
let vertexFunction = library.makeFunction(name: "vertex_main")
let fragmentFunction = library.makeFunction(name: "fragment_main")

//pipeline を準備
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
let pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)

//ようやくデータです
var vertices: [float3] = [
  [-0.7,  0.8,   1],
  [-0.7, -0.4,   1],
  [ -0.2,  -0.1,   1]
]
let Buffer = device.makeBuffer(bytes: &vertices,
                                       length: MemoryLayout<float3>.stride * vertices.count,
                                       options: [])
renderEncoder.setVertexBuffer(Buffer, offset: 0, index: 0)
renderEncoder.setFragmentBytes(&Color,
                               length: MemoryLayout<float4>.stride, index: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0,
                             vertexCount: vertices.count)
renderEncoder.endEncoding()

//commandBuffer を経由して描画したいデータをGPUに渡します
commandBuffer.present(drawable)
commandBuffer.commit()

PlaygroundPage.current.liveView = view

Xcode から Playground のプロジェクトを作成して、上のコードをコピペして実行で動くと思います。

こんな風に赤い三角形が描画できたら、成功です。

Xcode 自体の操作方法がわからんという方は、動画で。

1-1-1 コード解説

コード解説の前に、軽く Metal の仕組みを考えてみましょう。
ポンチ絵みたいなもんですが。
M1 Mac の発売前だと「メモリが共有で 8G と 16G ・・・、えー」みたいな感想が多かったように思いますが、実際に使ってみるとそのパフォーマンスにみなさん愕然としたわけです。
M1(arm) + Metal だとこの仕組みがどうやら効率的に働くようです。
(最近ではその秘密も徐々にわかってきました。この記事とか)
その代わり、画像を描画させるとき、この点を意識してコードを書く必要があります。

実際の「コマンド」の構成は以下のようになっています。

図はこちらのアップル公式ドキュメントから取ってきました。
CPU と GPU を協調して動かす必要があるためでしょうか、曖昧さがほとんどないかなりかっちりとした構成になっています。

サンプルでも(Depth Stencil を除けば)、すべて顔を出してますね。

(書くの疲れた・・・。詳細な解説動画はこちらに上げておきます)

 

>> Rasterize 処理