把 CGImage 转成 Metal 可读取的纹理,常见实现会先创建 CGContext,再把图片绘制进去。对于普通 8-bit 不透明图,这一步很容易被理解成“画进去就好了”。但当目标是尚未初始化的 RGBA16Float 上传缓冲,而且源图带有透明度时,绘制到底是复制,还是合成,必须成为明确的合同。
本文讨论 CPU 侧图片上传到高精度纹理的边界,不讨论滤镜算法、HDR 显示观感、相机输入、视频导出或性能。
先给结论
- 新建上传缓冲应建立确定的像素事实,不能把半透明源像素与未知目标内容混合。
- 像素拷贝适用于没有定义背景的上传目标;混合合成只适用于背景已经初始化且语义明确的画布。
- 回归测试应读取高精度输出,直接检查 alpha、数值误差与有限性,而不是只看一次预览。
- 媒体生命周期与帧调度仍属于媒体层;纹理加载器只负责可靠地交付逐帧像素。
为什么问题常被误判为滤镜错误
若 CGContext 以混合语义绘制半透明源图,结果并非单纯覆盖目标,而会把源颜色、源 alpha 和目标已有值组合起来。对于已经有明确背景的图层画布,这是正确行为;但新建上传缓冲并没有可依赖的背景。
这时参与计算的目标值可能是旧内存残留,也可能是难以解释的半精度浮点数据。最终表现会是透明图片偶发变色、alpha 不稳定,甚至在后续计算里出现非有限值。症状看起来发生在滤镜阶段,根因却更早:图像还没以可预测的方式写入纹理。
关键在于不要把“上传”与“合成”当成同一个动作。上传要把源图转换后的像素写入目标;合成才是在已有、已定义的目标像素之上叠加图层。两者的前提不同,因此不应共用默认行为。
用复制语义建立上传边界
Harbeth 的 TextureLoader.drawCGImageToTexture 在 CGContext 绘制前设置 .copy。这里的目标是尚未初始化的上传缓冲,语义应当是复制源像素,而不是把半透明颜色叠加到随机内存。
// 简化示例:向新建上传缓冲复制像素,不执行背景混合。
context.setBlendMode(.copy)
context.draw(cgImage, in: destinationRect)
这不是某个滤镜的特殊参数,而是输入边界的约束。后续若要做贴纸、图层或蒙版组合,应另行在已初始化、颜色空间和 alpha 语义都已明确的目标上执行混合;不要让纹理上传器暗中承担组合策略。
用数值回读而不是截图验证
TextureReadbackTests.testTransparentHalfFloatFallbackDoesNotBlendWithUninitializedMemory 以 RGBA16Float 为目标,覆盖 sRGB、Display P3 与 extendedLinearDisplayP3 三个色彩空间;每个空间又覆盖 alpha 为 0、0.5、1 的输入,并重复上传和读取 16 次。
断言逐通道要求输出是有限值,并与预期值保持不超过 0.003 的误差。它检验的是上传后的数值稳定性,而不只是“某张图看起来正常”。同一测试组还覆盖半精度扩展线性空间的 round trip,以及高精度 CGImage fallback 不应降级为 RGBA8。
本轮已在 macOS arm64e 上运行 swift test --filter TextureReadbackTests:40 项通过、0 失败,其中包括上述透明半精度回读测试。这个结果证明当前测试环境中的上传与回读合同;它不等同于真机、相机、视频编码或完整 HDR 输出的端到端验收,也不构成性能结论。
Harbeth / Kakapos 如何承接
Harbeth 是 Apple 平台的 GPU 图像与逐帧处理引擎;本例属于其 CGImage 到 Metal 纹理的上传边界。它不承担相机、录制、播放器或时间线。
Kakapos 负责媒体生命周期与编排。若它接入 Harbeth 作为逐帧处理后端,媒体层仍应管理帧调度、录制、导出与取消状态,并保持后端可替换;不应把这些状态塞进纹理加载器,也不应因 .copy 的实现细节而绑定某个媒体流程。
小结
遇到透明图片偶发变色、alpha 异常或高精度路径出现 NaN 时,先检查三个问题:目标缓冲是否已初始化、绘制是否意外采用混合、fallback 是否降级了像素格式。很多问题不在滤镜公式,而在像素进入 GPU 前是否已经建立可靠的边界合同。
把上传与合成分开是高精度纹理管线的关键边界;对 Metal/HDR 开发者可直接复用其 .copy 与数值回读测试思路。