動画AppのためのCore Imageパイプラインの最適化

https://developer.apple.com/videos/play/wwdc2020/10008/

概要

Core Imageの処理能力を活用し、App内の動画パフォーマンスを最適化する方法について
Appで動画にエフェクトを適用するためのCore Imageパイプラインを構築する方法について
CIContextを使用するときに、Appのメモリフットプリントを減らす方法、及び
Core Imageフィルタを使用した動画再生のためのAVPlayerViewまたはMTKView classの使用に関するベストプラクティスについて
さらに、独自のカスタムカーネルをMetal Shading Languageで書くべき理由について説明し、 Core ImageパイプラインでのMetalコマンドキューを最適に使用するためのパフォーマンスに関するヒントを紹介

CIContextの生成方法

  • CIContextの生成は1つのViewに対して1つのCIContextのみ生成する
    • 初期化に時間とメモリ容量を要するため、頻繁には生成を行わないようにする
  • 最も重要なのは中間生成物をキャッシュに格納しないこと
    • 動画は各フレームが前のフレームとは異なるため、キャッシュの無効化によりメモリ使用量を抑制します
  • Core Imageのデバッグ技術の使用時に有効であるため、contextに名前をつけること
let context =  CIContext(options: [
    .cacheIntermediates : false,
    .name : ”MyAppView”
])
  • Core Imageと他のMetal API群を混在して使用する場合(MetalTextureをCore Imageの入力値 or 出力値で使用)は、Core ImageをMetalCommandQueueで生成する
let context =  CIContext(MTLCommandQueue : queue, options: […])
  • 全ての処理が異なるキューで行われるため、正しい結果を得るにはwaitコマンドが必要
  • 非効率なパイプライン処理による無駄な時間を省くため、他のMetalレンダリング処理で使うキューと同じキューでCIContextを生成します
    • これにより待ちを減らし最適なパフォーマンスを発揮するパイプライン処理が可能

カスタムCIKernelの書き方と適用方法

  • 可能な限りMetalで最適化されたビルドインのCIFilterを使用する
  • サンプルコード
  • CIKernelの通常機能である自動タイリングや自動連結だけでなく、Metalで書くことによりコンパイル処理時間を減らすことが可能
    • ギャザー読み取り、グループ書き込み、半精度浮動小数点数などの機能も利用可能
  • Writing Custom Kernels
import CoreImage.CIFilterBuiltins

func motionBlur(inputImage: CIImage) -> CIImage? {
    let motionBlurFilter = CIFilter.motionBlur()
    motionBlurFilter.inputImage = inputImage
    motionBlurFilter.angle = 0
    motionBlurFilter.radius = 20
    return motionBlurFilter.outputImage
}

手順及びサンプル

  • CoreImage.hのヘッダーをinclude
  • kernerlのための関数 extern C を宣言
下記kernel実装では、destinationのXY座標の値を使用して、どの斜め線にいるかを決定する
// MyKernels.ci.metal
#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h
using namespace metal;

extern "C" float4 HDRZebra (coreimage::sample_t s, float time, coreimage::destination dest) 
{
    float diagLine = dest.coord().x + dest.coord().y;
    float zebra = fract(diagLine/20.0 + time*2.0);
    if ((zebra > 0.5) && (s.r > 1 || s.g > 1 || s.b > 1))
        return float4(2.0, 0.0, 0.0, 1.0);
    return s;
}

動画アプリの開発におけるViewへの最適なレンダリング方法について

  • アプリに動画エフェクト処理がある場合、UIImageView, NSImageViewの使用は避けること
    • 静的コンテンツ表示のために設計されているから
  • AVPlayerViewを使用してViewにfilter処理した動画を自動的に映す
    • パフォーマンス向上を目的として、MetalKitView(MTKView)も選択可能

AVPlayerViewを使用したfilterの実装

  • AVMutableVideoComposition ブロックは毎フレーム毎にコールされ AVAsynchronousCIImageFilteringRequest に渡される
let videoComposition = AVMutableVideoComposition(
    asset: asset, 
    applyingCIFiltersWithHandler:
    { (request: AVAsynchronousCIImageFilteringRequest) -> Void in

        // CIFilterの生成と入力値の設定、及び出力画像の取得
        let filter = HDRZebraFilter()
        filter.inputImage = request.sourceImage
        // Xcodeではデバッグ中にCIImageにマウスオーバーするとオブジェクトのアドレスが表示され、
        // 「目」のアイコンをクリックすると画像を構成するレシピが表示される
        let output = filter.outputImage

        if (output != nil) {
            // リクエストオブジェクトに出力した画像を設定
            request.finish(with: output, context: myCtx)
        }
        else { request.finish(with: err) }
    }
)

MTKViewを使用したfilterの実装

class MyView : MTKView {
    var context: CIContext
    var commandQueue : MTLCommandQueue

    override init(frame frameRect: CGRect, device: MTLDevice?) {
        let dev = device ?? MTLCreateSystemDefaultDevice()!
        // initメソッド内でCIContextを生成する
        context = CIContext(mtlDevice: dev, options: [.cacheIntermediates : false] )
        commandQueue = dev.makeCommandQueue()!

        super.init(frame: frameRect, device: dev)

        // Core ImageがMetal演算を使用出来るように frameBufferOnlyはfalseにする
        framebufferOnly = false  // allow Core Image to use Metal Compute
        colorPixelFormat = MTLPixelFormat.rgba16Float
        if let caml = layer as? CAMetalLayer {
            caml.wantsExtendedDynamicRangeContent = true
        }
    }
func draw(in view: MTKView) {

        let size = self.convertToBacking(self.bounds.size)

        // Metal textureを渡す代わりにtextureを返すブロックを渡している
        // CIContextは前のフレーム処理が終わる前に、Metal処理のキュー追加が可能となる
        let rd = CIRenderDestination(width: Int(size.width),
                                     height: Int(size.height),
                                     pixelFormat: colorPixelFormat,
                                     commandBuffer: nil)
                  { () -> MTLTexture in return view.currentDrawable!.texture }

        // CIContextにdestinationへの画像レンダリングタスクを指示
        context.startTask(toRender:image, from:rect, to:rd, at:point)

        // Present the current drawable
        // 現在描画可能なViewを示すためのコマンドバッファを生成する
        let cmdBuf = commandQueue.makeCommandBuffer()!
        cmdBuf.present(view.currentDrawable!)
        cmdBuf.commit()
   }
最新情報をチェックしよう!