视频采集、播放或滤镜处理在 macOS 上经常遇到这样的链路:AVFoundation 回调给出 CMSampleBuffer,内部包装着一个 CVPixelBuffer,里面是解码后的 YUV 或 BGRA 帧。如果直接把 CVPixelBuffer 拷贝到内存再调用 glTexImage2D,每一帧都会经历一次 CPU 到 GPU 的显存上传,分辨率一高,拷贝和纹理创建的开销就非常明显。CVOpenGLTextureCache 是 Core Video 提供的一套纹理缓存接口,它利用 IOSurface 共享内存机制,让 CVPixelBuffer 直接映射成 OpenGL 纹理对象,避免重复拷贝,很适合旧版 macOS 图形管线中的实时视频处理。

基于 IOSurface 的零拷贝设计
CVOpenGLTextureCache 的名字里虽然有 Cache,但它的核心不是简单的纹理对象池,而是把 Core Video 的像素缓冲直接绑定到 OpenGL 的纹理名上。macOS 中 CVPixelBuffer 的底层通常是一块 IOSurface,这块 IOSurface 同时能被 CPU、GPU 和视频解码器访问。传统 glTexImage2D 需要把 IOSurface 中的像素复制到一块由 OpenGL 管理的内存,再上传到显存;而 CVOpenGLTextureCache 通过内部机制让 OpenGL 直接引用 IOSurface 的地址,纹理名称指向的内存就是视频帧自己的存储,渲染时 GPU 可直接采样。
这样做有两个明显好处。第一,省掉每帧一次拷贝,降低 CPU 占用;第二,纹理在视频帧生命周期内保持有效,渲染线程不需要自己维护 glTexSubImage2D 或 PBO 上传逻辑。代价则是约束更多:纹理目标、颜色格式、尺寸都要与像素缓冲严格匹配,缓存还必须和同一个 OpenGL 上下文绑定,否则会出现黑屏或花屏。
从对象关系看,一个 CVOpenGLTextureCacheRef 会关联一个 CGLContextObj 和一个 CGLPixelFormatObj。后续从该缓存创建的纹理都作用在这个上下文里。若你的渲染使用 NSOpenGLContext,需要先取出内部的 CGLContextObj 再创建缓存;如果上下文变化,老的纹理缓存就不能继续使用。
创建纹理缓存并映射视频帧
典型流程分三步:准备 OpenGL 上下文和像素格式,创建 CVOpenGLTextureCacheRef,然后在每帧到达时用 CVOpenGLTextureCacheCreateTextureFromImage 把 CVPixelBufferRef 转成 CVOpenGLTextureRef。下面是一段基于 C 接口的创建代码,假设已经有一个可用的 CGLContextObj。
#include <OpenGL/OpenGL.h>
#include <CoreVideo/CoreVideo.h>
CGLContextObj cglContext = ...;
CVOpenGLTextureCacheRef textureCache = NULL;
CVReturn err = CVOpenGLTextureCacheCreate(
kCFAllocatorDefault,
NULL,
cglContext,
CGLGetPixelFormat(cglContext),
NULL,
&textureCache);
if (err != kCVReturnSuccess) {
// 处理错误:检查上下文和像素格式是否有效
}
第一个参数是 CFAllocator,通常传 kCFAllocatorDefault;第二个参数是缓存属性字典,早期场景很少使用,直接填 NULL;第三个参数是最关键的 CGL 上下文,第四个参数是它的像素格式。最后传入 &textureCache 获取缓存对象。该对象的生命周期需要手动管理,不再使用时调用 CVOpenGLTextureCacheRelease 或 CFRelease。
拿到缓存后,视频帧到达时可执行:
CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVOpenGLTextureRef videoTexture = NULL;
CVReturn result = CVOpenGLTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
textureCache,
pixelBuffer,
NULL,
&videoTexture);
if (result == kCVReturnSuccess) {
GLuint textureName = CVOpenGLTextureGetName(videoTexture);
GLenum textureTarget = CVOpenGLTextureGetTarget(videoTexture);
// 使用 textureName 和 textureTarget 进行渲染
}
这里 CVOpenGLTextureGetName 返回的 GLuint 就是 OpenGL 纹理名称,可以直接传给 glBindTexture。CVOpenGLTextureGetTarget 则返回具体纹理目标,通常可能是 GL_TEXTURE_RECTANGLE_ARB 或 GL_TEXTURE_2D,这一点必须动态读取,不能写死。纹理坐标的归一化方式会随目标不同而变化,矩形纹理使用像素坐标而不是 0 到 1 的归一化坐标。
需要特别留意,如果函数返回的不是 kCVReturnSuccess,不要继续访问 videoTexture。常见失败原因包括像素格式不受支持、缓存上下文与当前 OpenGL 上下文不一致、以及 CVPixelBuffer 并未使用 IOSurface 存储。
渲染时常见的错误与参数匹配
视频帧的颜色格式是使用这个接口最容易犯错的地方。CVPixelBuffer 可能是 420v、420f、2vuy、BGRA 等格式。CVOpenGLTextureCache 并非任意格式都能自动转换,创建纹理时它会根据像素缓冲的格式和 OpenGL 扩展能力决定内部颜色布局。对于 NV12 这类双平面格式,有时会创建两个纹理平面,需要通过平面接口处理;而老接口更多用于 RGB 或 2vuy 单平面帧,因此做 YUV 转换时需要先在着色器中处理。
另一个高频问题是纹理目标与采样方式不匹配。若 CVOpenGLTextureGetTarget 返回 GL_TEXTURE_RECTANGLE_ARB,着色器里要用 texture2DRect 或 sampler2DRect 采样,并且纹理坐标使用实际像素宽度和高度。若把它当作 GL_TEXTURE_2D 传入归一化坐标,画面通常只会显示左下角的一部分,或者完全错乱。
释放顺序也不能忽略。每一帧创建出来的 CVOpenGLTextureRef 会占用缓存中的引用,如果只 CFRelease 而不调用 CVOpenGLTextureCacheFlush,底层 IOSurface 可能一直被缓存抱住,长时间运行后内存增长。建议在当前帧渲染完成、不再需要该帧数据时执行:
glActiveTexture(GL_TEXTURE0); glEnable(textureTarget); glBindTexture(textureTarget, textureName); glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // 绘制四边形,绑定纹理并输出到默认帧缓冲 // ... CVOpenGLTextureCacheFlush(textureCache, 0); CFRelease(videoTexture);
注意 CVOpenGLTextureCacheFlush 的第二个参数通常传 0,表示不强制清空所有引用,只释放不再需要的资源。不要在渲染还没结束就调用 Flush,否则该帧可能被提前回收。如果渲染与解码不在同一线程,需要保证调用顺序,必要时用锁或串行队列保护。
另外,创建缓存时传入的 CGL 上下文必须在调用纹理相关函数时保持当前。若使用 NSOpenGLContext,可以先执行 [nsContext makeCurrentContext]。切换上下文后再使用旧缓存,容易出现纹理名有效但绑定失败的问题。
从 CVOpenGLTextureCache 看 CVMetalTextureCache 的迁移思路
OpenGL 在 macOS 10.14 之后已被官方标记为废弃,但大量旧项目仍然依赖它。理解 CVOpenGLTextureCache 的设计,对迁移到 Metal 有很大帮助。Metal 侧对应的是 CVMetalTextureCache,创建时需要传入 MTLDevice 而不是 CGLContextObj,然后通过 CVMetalTextureCacheCreateTextureFromImage 将 CVPixelBuffer 转成 id<MTLTexture>。两者都基于 IOSurface 零拷贝思路,只是纹理坐标、颜色附件和着色器语言不同。
例如,在 Metal 中从同一个 CVPixelBuffer 创建纹理后,可直接在 fragment shader 中采样,并用 MTLTextureUsageShaderRead 标记使用方式。虽然 API 名字从 OpenGL 换成了 Metal,但缓存创建、按帧映射、用完释放的整体流程没有变。对还在维护 OpenGL 版本的项目来说,把 CVOpenGLTextureCache 的映射和释放逻辑理清,后续迁移到 CVMetalTextureCache 时只需要替换创建参数和纹理对象类型。
最后要提醒,CVOpenGLTextureCache 只解决纹理上传问题,不负责颜色空间转换和显示同步。渲染链路上仍要处理好 CMSampleBuffer 的时间戳、颜色附件颜色空间、以及显示刷新率。把零拷贝纹理映射放进一个稳定的帧同步循环中,才能在旧版 macOS 图形栈中获得稳定、低延迟的视频渲染效果。

