WWDC Quick Look 💓 By SwiftGGTeam
Optimize the Core Image pipeline for your video app

Optimize the Core Image pipeline for your video app

观看原视频

Highlight

Apple 建议视频 App 为每个视图复用一个 CIContext,关闭中间结果缓存,并在与 Metal 混用时把 CIContext 绑定到同一 MTLCommandQueue,以减少内存占用和 GPU 队列等待。

核心内容

视频滤镜和静态图片滤镜的压力不同。视频每一帧都在变化,App 如果频繁创建 CIContext,或者让 Core Image 和自己的 Metal 渲染各走一条命令队列,就会把时间花在初始化、缓存和等待同步上。画面越接近实时播放,这些开销越容易变成掉帧。

这场 session 把优化拆成三段。先把 CIContext 做成每个 view 一个长期对象,并关闭中间结果缓存;再把滤镜尽量放到 Metal 实现中,优先使用内建 Core Image filter,必要时编写 .ci.metal kernel;最后选择适合视频播放的 view,使用 AVMutableVideoComposition 交给 AVFoundation 播放,或用 MTKView 自己控制渲染和显示。

这些建议都围绕同一个目标:让 Core Image 的工作贴近视频帧的生命周期。每帧只保留必要状态,GPU 工作放在正确的队列里,最终输出直接进入视频播放或 Metal drawable。

详细内容

1. 为视频视图创建 CIContext(00:52

Apple 的第一条建议是每个 view 只创建一个 CIContextCIContext 容易创建,但初始化会消耗时间和内存。视频帧每次都不同,中间结果缓存的收益很低,关闭它能降低内存占用。

let context = CIContext(options: [
    .cacheIntermediates: false,
    .name: "MyAppView"
])

关键点:

  • .cacheIntermediates: false 对应 transcript 中的视频场景:每帧内容不同,缓存中间图像会增加内存压力。
  • .name 给 context 一个可读名称,配合 Core Image 调试工具时更容易定位是哪一个 view 的管线。
  • 这个 context 应该跟 view 生命周期绑定,不要在每一帧重新创建。

如果 App 已经有自己的 Metal 渲染步骤,CIContext 应该使用同一条 Metal command queue。session 中的时间线示例说明,分开的队列会让 App 发出 wait 命令,GPU 管线中会出现空泡。

let context = CIContext(mtlCommandQueue: queue, options: [
    .cacheIntermediates: false
])

关键点:

  • mtlCommandQueue 让 Core Image 和其他 Metal render 共用同一条队列。
  • 输入或输出是 Metal texture 时,这种写法能减少跨队列同步。
  • transcript 明确指出,共用队列可以移除等待,让工作更好地流水化。

2. 优先使用内建 Core Image filter(02:59

session 建议优先使用内建 filter,因为这些 filter 已经由 Metal 实现。Xcode 文档也补充了参数说明、示例图片和示例代码。

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.CIFilterBuiltins 提供类型化的 filter 入口,示例中直接创建 CIFilter.motionBlur()
  • inputImageangleradius 是这个滤镜的输入属性。
  • 返回 outputImage 时还只是 Core Image recipe,真正渲染会在后面的播放或 view 输出阶段发生。

3. 把自定义 CIKernel 写进 .ci.metal03:56

当内建 filter 不够用时,session 建议使用 Metal 编写自定义 CIKernel。这样可以把编译工作移动到 App 构建阶段,并获得 gather reads、group writes、half-float math、语法高亮和构建期语法检查。

// 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;
}

关键点:

  • #include <CoreImage/CoreImage.h> 引入普通 Metal 类和 Core Image 提供的附加类型。
  • extern "C" 是 kernel 函数声明要求。
  • coreimage::sample_t 表示输入图像中的像素,transcript 说明它是线性、预乘 alpha 的 RGBA float4,适合 SDR 和 HDR。
  • coreimage::destination 提供目标像素坐标,示例用 dest.coord() 生成斑马纹。
  • 条件 s.r > 1 || s.g > 1 || s.b > 1 对应 HDR 像素高于 SDR 白点的判断。

4. 用 AVMutableVideoComposition 接入 AVPlayerView(05:50

如果 App 主要目标是播放带滤镜的视频,AVPlayerView 是最简单的路径。关键对象是 AVMutableVideoComposition,它用 asset 和每帧回调创建。回调里设置 Core Image filter,然后把输出交回 request。

let videoComposition = AVMutableVideoComposition(
    asset: asset,
    applyingCIFiltersWithHandler: { request in
        let filter = HDRZebraFilter()
        filter.inputImage = request.sourceImage

        if let output = filter.outputImage {
            request.finish(with: output, context: myCtx)
        } else {
            request.finish(with: err)
        }
    }
)

关键点:

  • applyingCIFiltersWithHandler 的 handler 会按视频帧调用。
  • request.sourceImage 是当前帧输入图像。
  • request.finish(with:context:) 把 Core Image 输出交回 AVFoundation。
  • transcript 提到 Xcode 调试时可以点开 CIImage 的 eye 图标,查看组成该图像的 recipe;示例里还能看到 10-bit HDR surface 从 HLG 自动色彩管理到 Core Image working space。

5. 用 MTKView 控制渲染和显示(07:01

需要更多控制权时,可以用 MTKView。初始化阶段创建 CIContext,允许 Core Image 使用 Metal Compute,并在 HDR view 上设置浮点像素格式和扩展动态范围。

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

    override init(frame frameRect: CGRect, device: MTLDevice?) {
        let dev = device ?? MTLCreateSystemDefaultDevice()!
        context = CIContext(mtlDevice: dev, options: [.cacheIntermediates: false])
        commandQueue = dev.makeCommandQueue()!

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

        framebufferOnly = false
        colorPixelFormat = MTLPixelFormat.rgba16Float
        if let caml = layer as? CAMetalLayer {
            caml.wantsExtendedDynamicRangeContent = true
        }
    }
}

关键点:

  • framebufferOnly = false 让 Core Image 能使用 Metal Compute。
  • rgba16Float 对应 macOS HDR view 的设置建议。
  • wantsExtendedDynamicRangeContent = trueCAMetalLayer 支持扩展动态范围内容。

绘制时,session 使用 CIRenderDestination,传入返回 drawable texture 的 block。这样 CIContext 可以先开始排队 Metal work,再等待上一帧完成。

func draw(in view: MTKView) {
    let size = self.convertToBacking(self.bounds.size)
    let destination = CIRenderDestination(
        width: Int(size.width),
        height: Int(size.height),
        pixelFormat: colorPixelFormat,
        commandBuffer: nil
    ) {
        return view.currentDrawable!.texture
    }

    context.startTask(toRender: image, from: rect, to: destination, at: point)

    let commandBuffer = commandQueue.makeCommandBuffer()!
    commandBuffer.present(view.currentDrawable!)
    commandBuffer.commit()
}

关键点:

  • CIRenderDestination 记录目标尺寸、像素格式和延迟获取 texture 的 block。
  • startTask(toRender:from:to:at:) 启动 Core Image 渲染任务。
  • 最后用 command buffer present 当前 drawable,完成本帧显示。

核心启发

  • 实时 HDR 过曝提示:做一个视频预览叠加层,把高于 SDR 白点的区域标红。这个 session 的 HDRZebra kernel 已经给出判断方式;可以先用 .ci.metal 文件实现 kernel,再把它接入 AVMutableVideoComposition
  • 低内存视频滤镜预览器:给剪辑 App 做一组实时滤镜预览。入口是每个 preview view 复用一个 CIContext,并设置 .cacheIntermediates: false,让滑动时间线时不会积累无用中间帧。
  • Core Image 与 Metal 混合渲染播放器:在已有 Metal overlay 的播放器里加入 Core Image 滤镜。关键是用同一个 MTLCommandQueue 创建 CIContext,让 Metal 输入、Core Image 处理和最终叠加保持在同一条队列上。
  • 可视化滤镜调试模式:给内部调试版加一个按钮,打开当前帧的 CIImage recipe 检查。session 展示了 Xcode 的 eye 图标调试入口,可以把这个流程写进性能排查手册。
  • HDR MetalKit 预览窗口:为 macOS 视频工具做一个 MTKView 预览。先设置 framebufferOnly = falsergba16FloatwantsExtendedDynamicRangeContent,再用 CIRenderDestination 把 Core Image 输出送到当前 drawable。

关联 Session

评论

GitHub Issues · utterances