Apple silicon 搭載 Mac で、ビットマップ CGContext を介して共有 MTLBuffer に描画し、同じバッファを基盤とする Metal テクスチャを作成します。重要な条件は、RGBA バイト列の一致、行ストライドのアラインメント、ストレージの保持、CPU/GPU アクセスの順序制御です。

このガイドでは、赤い長方形と青い楕円を生成して描画し、Metal がテクスチャを介してその描画を読み取れることを確認します。デスクトップアプリの Metal レンダラー向けに生成画像を準備する Mac 開発者を対象としています。この共有方法は、Apple の makeTexture(descriptor:offset:bytesPerRow:) API で文書化されています。

どの共有方法を選ぶべきか?

まず Metal でストレージを割り当てます。buffer.contents() で CPU アドレスを取得し、そのアドレスをビットマップコンテキストの描画先として使ってから、そのバッファからテクスチャを作成します。Apple の説明によると、ビットマップ描画は渡されたメモリを更新し、バッファを基盤とするテクスチャはそのバッファの割り当てを共有します。これらの API を組み合わせれば、中間画像のアップロードなしに連携できます。Graphics Contexts、MTLBuffer.contents()、MTLTexture。

この例では Apple ファミリーの GPU を明示的に必要とします。Apple の最新のストレージに関する説明では、Apple ファミリー以外の GPU では共有テクスチャを利用できないとされています。Metal に対応するすべての Mac でこの割り当て方法が使えると想定せず、GPU の対応を前提条件として扱ってください。MTLStorageMode.shared。

要件 適切な方法
Core Graphics で描画し、Apple silicon 上で Metal が同じ割り当てを読み取る 以下のように、共有バッファとバッファを基盤とするテクスチャを使う
独立して割り当てたテクスチャに描画内容を渡す 対応する CPU アクセス可能なテクスチャに replace(region:mipmapLevel:withBytes:bytesPerRow:) でアップロードする
GPU で繰り返し使うため、独立したプライベートテクスチャが有効 ブリットエンコーダーでステージングリソースからコピーする
選択した GPU が共有テクスチャを作成できない この共有テクスチャの例ではなく、対応するアップロードまたはステージングコピーの方法を使う

これらの方法では、所有権とコピーの動作が異なります。Apple の説明では、replace は即時の CPU コピーを行い、ブリットエンコーダーは CPU アクセス可能なストレージとプライベートストレージを含むリソース間でデータを移動します。直接共有する方法ではアップロード時のコピーを避けられますが、バッファを基盤とするテクスチャではテクスチャ最適化が制限される場合があると Apple は注意を促しています。テクスチャピクセルの置き換え、MTLBlitCommandEncoder、バッファを基盤とするテクスチャの制約。

ピクセル、サイズ、所有権の条件を定める

明示的なバイト配置を使います。アドレスが増える順に、1 ピクセルあたり R, G, B, A の 4 バイトです。Core Graphics では premultipliedLast と byteOrder32Big を組み合わせ、Metal では rgba8Unorm を使います。バイトオーダーの設定により、ネイティブ整数の表現方法に依存せず、メモリの解釈を明示できます。Apple は、アルファの位置、バイトオーダーの制御、Metal の正規化 RGBA 4 成分をそれぞれ説明しています。CGImageAlphaInfo.premultipliedLast、CGBitmapInfo、MTLPixelFormat.rgba8Unorm。

アルファが乗算済みの場合、保存された色チャンネルにはすでにアルファが反映されています。計算例として、透明なストレージに半透明の真っ赤を描くと、量子化誤差を考慮して、およそ 128, 0, 0, 128 になります。後段のシェーダーでもこの解釈を保つ必要があります。サンプリングした RGB にアルファをもう一度乗算すると、暗くなります。この例ではバイト確認を簡単にするため、不透明な図形を使います。Bitmap Images and Image Masks。

この例では CGColorSpace.linearSRGB を選びます。Apple の定義では、これは線形伝達関数を持つ sRGB 測色特性です。これにより描画の色解釈を明示できます。メモリを共有するだけでは色空間変換は行われません。テクスチャをレンダラーに組み込む際も、同じ解釈を保ってください。CGColorSpace.linearSRGB。

割り当て前に行ストライドを計算する

表示される 1 行の長さは width × 4 ですが、割り当てるストライドはそれより大きくなる場合があります。選択したデバイスとフォーマットに対して minimumLinearTextureAlignment(for:) を呼び出し、表示行の長さをそのアラインメントの倍数になるよう切り上げます。Core Graphics と Metal の両方に、同じ bytesPerRow を渡してください。minimumLinearTextureAlignment(for:)。

説明用にデバイスのアラインメントを 256 バイトと仮定すると、257 × 128 の画像では、表示行あたり 1,028 バイトが必要です。アラインメント後のストライドは 1,280 バイト、割り当てる総バイト数は 163,840 です。行あたりの余分な 252 バイトはパディングであり、追加のピクセルではありません。このプログラムは、この説明用の値を前提にせず、実際のアラインメントを問い合わせます。

少なくとも bytesPerRow × height バイトを割り当てます。メモリ上の列 x、行 y にあるピクセルのバイトオフセットは、y × bytesPerRow + x × 4 です。この固定サイズの例を可変サイズに対応させる場合は、正でないサイズを拒否し、割り当て前に乗算と切り上げで整数オーバーフローが起きないことを確認してください。Core Graphics の説明では、ストレージの必要サイズはストライドと高さの積です。ビットマップグラフィックスコンテキストの作成。

すべての利用が終わるまで割り当てを保持する

この設計では Metal がメモリを割り当てます。ビットマップコンテキストはそのアドレスを借用するため、コンテキストが存続する間はバッファを保持してください。非同期の GPU 処理では、その処理が完了するまでリソースも保持します。自分で buffer.contents() を解放したり、一時的な Swift 配列アクセスから取得したポインターに置き換えたりしないでください。

このプログラムでは GPU 作業を送信する前に描画コンテキストを終了して解放し、完了するまでバッファとテクスチャを保持します。これは、呼び出し側が用意するビットマップメモリのモデルと、Metal が定める CPU/GPU の順序制御要件に従っています。Graphics Contexts、CPU と GPU の作業の同期。

合成画像を描画する完全な例を作る

macOS のコマンドラインプロジェクトを作成するか、以下のプログラム全体を main.swift として保存します。現行の Xcode macOS SDK を使用してください。このコードは、bitmapInfo 引数に CGBitmapInfo を取る現在の CGContext イニシャライザーを使います。古い SDK では、代わりに bitmapInfo.rawValue を要求する旧イニシャライザーが公開されている場合があります。CGContext ビットマップイニシャライザー。

この描画では、Core Graphics の直接呼び出しと AppKit の楕円描画を組み合わせます。NSGraphicsContext(cgContext:flipped:) は既存のコンテキストをラップするため、どちらも同じ描画先に描画されます。AppKit のグラフィックス状態を保存して復元すると、描画後に以前のカレントコンテキストが復元されます。NSGraphicsContext イニシャライザー、saveGraphicsState()、restoreGraphicsState()。

import AppKit
import CoreGraphics
import Metal

// Fixed, small dimensions keep all size arithmetic bounded.
let width = 257
let height = 128
let format: MTLPixelFormat = .rgba8Unorm

guard let device = MTLCreateSystemDefaultDevice(),
      device.supportsFamily(.apple1) else {
    fatalError("This example requires an Apple-family GPU.")
}
let alignment = device.minimumLinearTextureAlignment(for: format)
guard alignment > 0 else { fatalError("Invalid alignment.") }
let rowBytes = ((width * 4 + alignment - 1) / alignment) * alignment
let byteCount = rowBytes * height

guard let buffer = device.makeBuffer(
    length: byteCount, options: .storageModeShared
) else { fatalError("Drawing buffer allocation failed.") }

let descriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: format, width: width, height: height, mipmapped: false
)
descriptor.storageMode = .shared
descriptor.usage = .shaderRead

guard let texture = buffer.makeTexture(
    descriptor: descriptor, offset: 0, bytesPerRow: rowBytes
) else { fatalError("Buffer-backed texture creation failed.") }
precondition(texture.buffer === buffer)

// This scope finishes CPU drawing before GPU submission.
do {
    guard let space = CGColorSpace(name: CGColorSpace.linearSRGB) else {
        fatalError("Color space creation failed.")
    }
    let info = CGBitmapInfo(
        rawValue: CGImageAlphaInfo.premultipliedLast.rawValue
    ).union(.byteOrder32Big)
    guard let cg = CGContext(
        data: buffer.contents(), width: width, height: height,
        bitsPerComponent: 8, bytesPerRow: rowBytes,
        space: space, bitmapInfo: info
    ) else { fatalError("Bitmap context creation failed.") }

    cg.setFillColor(red: 1, green: 0, blue: 0, alpha: 1)
    cg.fill(CGRect(x: 0, y: 0,
                   width: CGFloat(width), height: CGFloat(height)))

    NSGraphicsContext.saveGraphicsState()
    defer { NSGraphicsContext.restoreGraphicsState() }
    NSGraphicsContext.current = NSGraphicsContext(
        cgContext: cg, flipped: false
    )
    NSColor.blue.setFill()
    NSBezierPath(ovalIn: NSRect(x: 32, y: 24, width: 48, height: 48)).fill()
}

// Diagnostic copy: prove the GPU can read through the texture.
guard let readback = device.makeBuffer(
    length: byteCount, options: .storageModeShared
), let queue = device.makeCommandQueue(),
   let command = queue.makeCommandBuffer(),
   let blit = command.makeBlitCommandEncoder() else {
    fatalError("Readback setup failed.")
}
blit.copy(
    from: texture, sourceSlice: 0, sourceLevel: 0,
    sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
    sourceSize: MTLSize(width: width, height: height, depth: 1),
    to: readback, destinationOffset: 0,
    destinationBytesPerRow: rowBytes, destinationBytesPerImage: 0
)
blit.endEncoding()
command.commit()
command.waitUntilCompleted()
guard command.status == .completed else {
    fatalError("GPU copy failed: \(String(describing: command.error))")
}
let pixel = readback.contents().assumingMemoryBound(to: UInt8.self)
precondition(pixel[0] == 255 && pixel[1] == 0 &&
             pixel[2] == 0 && pixel[3] == 255)
withExtendedLifetime(buffer) {
    print("Shared RGBA texture verified; row stride: \(rowBytes) bytes")
}

対象の Mac でコンパイルして実行します。

xcrun swiftc main.swift -o SharedRGBA -framework AppKit -framework CoreGraphics -framework Metal
./SharedRGBA

テクスチャの buffer プロパティで、基盤となるリソースを確認します。その後、診断処理でテクスチャのピクセルを別の共有バッファにコピーし、GPU の処理完了後に未変更の赤い隅を確認します。この読み戻しは意図的なテスト用コピーで、共有描画領域とは別です。Apple はテクスチャからバッファへのコピー操作を説明し、2D コピーでは destinationBytesPerImage: 0 を指定するとしています。MTLTexture.buffer、テクスチャからバッファへのコピー。

ストライド計算は、決定的なサンプル入力で確認しました。取得した API の説明に照らして Swift プログラムを確認しましたが、調査環境は Swift と Apple フレームワークのない Linux のため、コンパイルも実行もできませんでした。

CPU 描画と GPU 利用の順序を制御する

共有ストレージでも、アクセス順序の管理は必要です。テクスチャを読み取るコマンドをコミットする前に、CPU の描画を完了してください。送信後は、GPU が使い終わるまでその割り当てを変更しないでください。Apple は共有リソースに対するこの調整を明示的に求めています。MTLStorageMode.shared。

1 回だけ実行するこの例では、waitUntilCompleted() によって受け渡しの流れを確認しやすくなります。コマンドバッファと完了ハンドラーが終了するまでブロックするためです。インタラクティブなレンダラーでは、毎フレームこのブロッキング待機を行わないでください。代わりに複数のバッファとテクスチャのペアを用意し、GPU の完了通知を受け取ってから各ペアを利用可能なプールに戻します。Apple は同期サンプルでこのリソースプールのパターンを示しています。waitUntilCompleted()、CPU と GPU の作業の同期。

後から GPU が一方の別名を通じて書き込み、もう一方から読み取るようにした場合は、同期を改めて確認してください。Apple によると、バッファを基盤とするテクスチャのデータはレンダーパス間で一貫性を保てますが、同一パス内では別名を通じた変更がすぐには反映されない場合があります。この例では GPU による読み取りを 1 回行う前に CPU 描画を完了するため、その問題は生じません。バッファを基盤とするテクスチャの一貫性。

レイアウトの問題を切り分けてからレンダラーを変更する

問題が起きた条件を特定するため、次の手順で確認します。ここに示す確認事項は、上記で説明したピクセル配置、基盤リソース、ストライド、同期のルールに基づいています。

  1. テクスチャの作成に失敗する: GPU が共有テクスチャに対応していること、両方のリソースが .shared を使うこと、オフセットとストライドが問い合わせたアラインメントを満たすことを確認します。テクスチャは 2D、シングルサンプル、ミップレベル 1、配列要素 1 にします。
  2. 赤が青に見える: 個々のバイトを調べます。この例では、RGBA のバイト順、premultipliedLast、byteOrder32Big、.rgba8Unorm の組み合わせが必要です。
  3. 行が斜めにずれる: コンテキストとテクスチャのストライドを比較します。パディングも含め、両方に rowBytes を使う必要があります。
  4. 画像が上下逆に見える: 描画変換とレンダラーのテクスチャ座標の対応を確認します。この例では上下反転しない描画コンテキストを使います。AppKit の flipped 引数は初期状態で上下反転しているかどうかを表し、保存された行の順序を入れ替えるものではありません。
  5. 更新が不安定に見える: CPU 描画と GPU アクセスが同じ割り当てに対して重なっていないか確認します。再利用する前に処理完了を追跡してください。

作成時の制約は Apple のリニアテクスチャの説明に基づき、上下反転状態の違いは AppKit イニシャライザーの説明に基づいています。独自の表示シェーダーを追加する際は、向きの扱いを明示してください。リニアテクスチャの要件、AppKit の上下反転状態のパラメーター。

基盤となるバッファ、バイト配置、パディングを含むストライド、アクセス順序が一致すれば、共有テクスチャを介して合成画像を Metal から利用できます。画像のサイズ変更や描画処理の追加後もこれらの条件を維持してください。対象 GPU や後段のテクスチャ要件に応じて独立したストレージが必要な場合は、ステージングコピーを選んでください。

出典