GPU Gems 2 · Chapter 25. GPU Image Processing in Apple's Motion

从数据依赖、fragment 资源预算、采样精度、alpha 表示和调试路径五个层面,复盘 Apple Motion 如何把高质量图像处理迁移到早期 GPU。

学习目标

  • 能解释 GPU Image Processing in Apple's Motion 为什么先按数据依赖筛选算法,再把逐像素和独立邻域操作映射到 GPU
  • 能实现受资源预算约束的 fragment pipeline,处理 texture indirection、除零、重复采样和 8-bit intermediate 的精度风险
  • 能回答:当画面出现 softening、透明暗边或跨 GPU 白色异常时,应该切换哪项实验开关,并用什么 reference 与探针定位根因

先问“能否并行”,再问“能快多少”

Motion 面向实时合成与动态图形:设计者希望拖动参数后立即看到高质量结果,而 CPU pipeline 很难在当时的机器上持续满足这种反馈。像素处理搬到 GPU 后可获得数量级的加速,但前提不是“算法里有循环就改成 shader”,而是每个输出像素能够独立求值。

没有这层筛选,直方图、顺序递推和复杂分支会被强行塞进不匹配的执行模型;即使程序勉强运行,也可能在资源上限、纹理写回或硬件差异处失败。先预测:选择“亮度直方图”后把 pass 数拉到 4,mapping verdict 会改善吗?

Apple Motion GPU image-processing 实验

先预测:把 histogram 或 sequential filter 直接塞进 fragment shader,再增加 pass,真的能解决数据依赖吗?

Motion image pipeline lab · 邻域卷积mapping verdictmultipass可以 gather 邻域;核过大时拆成多个 pass。texture reads / output:9render passespass 18-bitpass 28-bitedge retention after repeated samplingquality recordinstructions / pass24softening risk11%alpha fringe0%quantization1.44可执行:继续与 CPU reference 做像素级误差比较

记录每个 pass 的格式、采样位置、资源预算,并与 CPU reference 比较。

本章主题 GPU Image Processing in Apple's Motion 的可复用经验是:GPU 不是更快的通用 CPU。它提供大量并行像素执行与高带宽读取,同时把输出位置、执行顺序和某些控制流限制得很紧;架构必须围绕这些边界组织。

先看数据依赖,再决定是否搬到 GPU算法数据流结论逐像素颜色变换1 input → 1 output直接映射独立邻域卷积many reads → 1 outputgather 友好直方图统计1 input → chosen binscatter 困难顺序滑动累加depends on prior output并行不成立GPU 擅长从任意位置读取,但旧式管线难以把每个结果写向任意位置或共享顺序状态

1. 算法适配:固定输出、自由读取最自然

逐像素 color transformation 是最直接的映射:一个 fragment 读取同位置颜色,执行 gamma、brightness 或 LUT lookup,再写回同位置输出。独立 neighborhood operation 也适合 GPU,因为每个输出像素可以读取周围若干输入,彼此不需要通信。

以卷积为例,输出位置 pp 的值是

O(p)=kKwkI(p+Δk).O(p)=\sum_{k\in K} w_k I(p+\Delta_k).

这个式子在说:每个 fragment 只 gather 固定邻域 KK 的输入并加权,所有输出位置可以并行执行。Gaussian blur 或 edge detection 的核太大时,还可拆成 separable passes 或多次小核处理。

直方图正好相反:每个像素读取自己的 luminance,却要选择一个共享 bin 并递增。旧式 fragment pipeline 的输出位置由 rasterization 固定,也没有今天常见的原子累加,因此 scatter 比 gather 困难得多。顺序 box filter 也不适配,因为当前像素要复用前一像素的 running total,而 fragment 执行顺序不可见。

2. 资源预算:最先耗尽的未必是指令数

Motion 选择低级 ARB_fragment_program,代价是编写和调试更难,收益是能直接看见底层指令与硬件限制。原章记录的 ATI R3xx 上限是 64 条 arithmetic、32 次 texture access,NV34 的总指令预算更宽;但真正频繁卡住 Motion 的,是 dependent texture chain 的层级。

资源不是一个总数:instructions、reads 与 indirection 分开计dependent texture chainsourcelevel 13D LUTlevel 2palettelevel 3next readlevel 4坐标依赖前一次 texture result,就增加一层 indirection优先:vertex/constants 预计算 → arithmetic → 最后才拆 passmulti-pass fallbackpass Apbufferpass B拆 pass 能跨过单 shader 限制,却会付出中间写回、读回与量化成本

读取 source color、用颜色索引 3D LUT、再用 LUT 结果索引 palette,已经形成三层 dependent reads。若硬件只允许四层,下一次相关读取就会把一个看似不长的程序推到边缘。相反,来自 interpolated vertex components 或 constants 的坐标不增加这一层级,因此可以把稳定计算提前到 vertex stage 或 CPU。

vec4 source = texture(sourceImage, uv);
vec3 graded = texture(colorLut, source.rgb).rgb;
vec3 mapped = texture(palette, vec2(graded.r, 0.5)).rgb;
outColor = vec4(mapped, source.a);

这段代码只有三次采样,却有连续坐标依赖;不能只用 texture() 的数量判断可运行性。工程记录应分别列出 arithmetic instructions、texture accesses、indirection depth 和 temporary registers。

超过单 shader 限制时,可以把算法拆成多个 pass,并把中间结果写入 pbuffer 供下一 pass 读取。这样能降低单 pass 复杂度,但会增加 full-frame write/read、同步和格式量化。优化顺序应是先消除不必要依赖,再用 arithmetic 替代 table read,最后才接受 multi-pass 成本。

3. 数值语义:除零不能靠“后来乘回零”

不同 GPU 对 reciprocal zero 的行为可能不同:一种实现给出极大的有限值,另一种按 IEEE 语义产生 NaN 或 infinity。若 pipeline 把 premultiplied color 除以 alpha 转成 straight color,处理后再乘回 alpha,那么透明像素的 rgb / 0 就会把厂商差异暴露出来。

相同 shader,不同除零语义会走向不同输出inputpremultiplied pixelrgb / αα = 0vendor behaviorlarge finite valueNaN propagationlater × 0 differsportable fixmax(α, ε)known domainCPU referencefinite outputepsilon 不是装饰:它把未定义边界变成跨 GPU 可测试的数值契约

NaN 的危险在于传播:后续乘零并不会可靠地恢复零,最终可能在 framebuffer 里表现为纯白异常。对已知非负 alpha,可在 reciprocal 前把分母推离零:

const float EPSILON = 1e-5;
float safeAlpha = max(source.a, EPSILON);
vec3 straightRgb = source.rgb / safeAlpha;
vec3 processed = applyColorEffect(straightRgb);
outColor = vec4(processed * source.a, source.a);

这个保护必须和输入域一起定义。如果分母允许负值,简单 max 会改变符号,需要按绝对值和符号构造 safe denominator。发布测试应包含 alpha 为零、接近零、正常值以及非有限值探针,并跨目标 GPU 比较。

4. 过滤质量:免费采样仍有表示成本

Bilinear filtering 在 GPU 上几乎没有额外指令成本,但“免费”不等于“无误差”。一对一 copy 本应让输入 texel 落到同位置输出 pixel;若 rasterizer 插值造成亚像素偏离,filter 会混入邻居,图像经过多个 pass 后逐步 softening。确定不做缩放、旋转时,可用 nearest sampling 阻断小于一像素的偏差;大型 quad 也可分网格,在 CPU 端更精确地给定顶点坐标。

bilinear filtering 的两类陷阱:采样位置与 alpha 表示one-to-one copy softeningcenter: exact texelfractional offset: neighbor bleedidentity pass 可改 nearesttransparent edge filteringstraight αtransparent RGB leakspremultipliedRGB already × αfilter associated color + alphaOver composite: no dark fringe“硬件过滤免费”只指指令成本;质量仍由采样对齐与颜色表示契约决定

考虑白色不透明像素 (1,1,1,1)(1,1,1,1) 与透明黑像素 (0,0,0,0)(0,0,0,0) 的中点。straight-alpha 纹理分通道插值得到 RGB 0.5、alpha 0.5,之后 Over 到黑色时 RGB 还会乘 alpha,显示贡献只有 0.25,于是产生暗边。premultiplied 表示直接插值“颜色贡献”和 alpha,中点贡献为 0.5,符合覆盖面积。

Blur 与 resampling 应在 premultiplied space 中完成;某些 color correction 则需要 straight RGB。两者切换必须使用安全除法,并在 effect contract 中明确输入/输出 alpha mode,不能让每个 pass 猜测当前表示。

5. 中间精度与可观测调试

多 pass 不只增加带宽,还会把 shader 内部高精度结果反复量化到 pbuffer。Motion 的大部分流程使用 8-bit/channel,但 motion blur accumulation 会累加许多 samples,8-bit 很快出现 banding。支持 floating-point pbuffer 的硬件可以改用 16-bit;只有 8-bit texture 时,原章把 16-bit fixed-point 值拆成两个 8-bit channels,并以额外 pass 换取范围。

若以 8.8 fixed-point 表示非负值 vv,可写成

h=v/256,l=frac(v).h=\left\lfloor v/256\right\rfloor,\qquad l=\operatorname{frac}(v).

这个式子在说:一个 channel 保存缩放后的 whole part,另一个保存 fractional part;读取时用 256h+l256h+l 重建。真实 packing 还必须定义 normalization、rounding、clamp 和最大可累加样本数。

黑盒调试:每次只回答一个问题CPU reference算法本身正确吗?syntax / native limitsshader 是否执行?framebuffer probe哪一步数值偏离?cross-GPU compare是否为 vendor 语义?无单步调试器时,把中间值当颜色输出;reference 与探针共同缩小故障范围

没有 shader 单步调试器时,诊断要建立可观测性。先让 CPU reference 输出小图和边界样本;再确认 shader 语法与 native limits;随后临时把某个 intermediate value 缩放到 0 至 1,写成 framebuffer 颜色,必要时用 readback 取精确值;最后在不同 GPU 上运行同一最小复现。

三步迁移:从算法筛选到像素级验收

每一步都保存输入样本、目标 GPU、shader resource report、intermediate format 与最大误差。性能通过但 reference 不一致不算完成;静态截图正常但透明边界、零值和多 pass 压力样本失败,也不能发布。

分步1 / 3

第一步:画清数据依赖

把 effect 标成 image-in/image-out、statistics-out 或 sequential state。逐像素颜色与独立卷积走 gather;histogram 改 reduction/compute/CPU;顺序算法先重写,再谈 shader。

先看数据依赖,再决定是否搬到 GPU算法数据流结论逐像素颜色变换1 input → 1 output直接映射独立邻域卷积many reads → 1 outputgather 友好直方图统计1 input → chosen binscatter 困难顺序滑动累加depends on prior output并行不成立GPU 擅长从任意位置读取,但旧式管线难以把每个结果写向任意位置或共享顺序状态

本章小结

  • 先判断固定输出、gather、scatter 与顺序依赖,再选择 GPU 路径。
  • texture indirection、指令、读取和寄存器是不同预算,multi-pass 还会引入精度成本。
  • 零除和 NaN 行为必须显式防护,不能依赖单一 GPU 的偶然结果。
  • 一对一采样要对齐 texel center;透明过滤应使用 premultiplied alpha。
  • CPU reference、诊断色与 framebuffer probe 让黑盒 shader 变得可定位。

练习

问题 1|筛选算法。 color correction、7×77\times7 Gaussian blur、histogram 和 sliding box filter 中,哪些可直接映射到 fragment pipeline,哪些必须重写?

问题 2|诊断透明白点。 一个 premultiplied effect 在 alpha 为零时先除 alpha,最后乘回 alpha;ATI 路径正常,另一 GPU 出现白点。最可能的根因和最小修复是什么?

问题 3|修改实验并给出交付证据。 在 MotionLab 中选择“邻域卷积”,把 pass 增到 4、sample offset 调高、alpha 切为 straight,并保持 8-bit intermediate。你会如何改造 pipeline?

名词解释

本章出现的专业名词,用大白话再讲一遍。

fragment program
gather
scatter
texture indirection
premultiplied alpha
CPU reference implementation

资料与写作方式声明

本章以GPU Gems 2 · Chapter 25. GPU Image Processing in Apple's Motion权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…