2.4 着色器:CPU 编译 vs GPU 运行


文档摘要

2.4 着色器:CPU 编译 vs GPU 运行 本节摘要:着色器源码是字符串,在 CPU 上交给驱动编译、链接成 program;真正的逐顶点、逐片元执行发生在 GPU。编译失败出日志,运行错误出画面。WebGL1 用 GLSL ES 1.00,WebGL2 用 3.00,限定符与内置输出都不同。把编译放进每帧循环,或把 1.00 源码丢进 2.0 上下文,属于搞混了“何时编译、在哪份合同下编译”。 上手前先明确 阅读完本节,你应当能够: 按顺序列出 createShader、compile、attach、link、use 各自发生在哪一端 对照编译失败与运行时画面错误的排查入口 说明 precision 声明为什么在片元着色器里几乎总是强制的 对比 GLSL ES 1.00 与 3.

2.4 着色器:CPU 编译 vs GPU 运行

本节摘要:着色器源码是字符串,在 CPU 上交给驱动编译、链接成 program;真正的逐顶点、逐片元执行发生在 GPU。编译失败出日志,运行错误出画面。WebGL1 用 GLSL ES 1.00,WebGL2 用 3.00,限定符与内置输出都不同。把编译放进每帧循环,或把 1.00 源码丢进 2.0 上下文,属于搞混了“何时编译、在哪份合同下编译”。

上手前先明确

阅读完本节,你应当能够:

  1. 按顺序列出 createShader、compile、attach、link、use 各自发生在哪一端
  2. 对照编译失败与运行时画面错误的排查入口
  3. 说明 precision 声明为什么在片元着色器里几乎总是强制的
  4. 对比 GLSL ES 1.00 与 3.00 的 attribute/varying 与 in/out
  5. 给出程序缓存策略:何时热替换着色器,何时绝不在动画帧里编译

字符串不是 GPU 程序,直到链接成功

写在 JavaScript 模板字符串里的 GLSL,对 GPU 而言只是文本。compileShader 让驱动前端把它变成中间表示;linkProgram 把 VS 与 FS 对上 varying、把 Uniform 槽位定下来。这两步发生在 CPU 可感知的调用里,可能阻塞数十毫秒。useProgram 之后的每一次 draw,才是 GPU 按该程序奔跑。

把编译比成把乐谱交给乐团排练,draw 比成演出。排练可以失败(音不准、声部对不上),演出失败则是观众听见错音(画面错)甚至舞台塌(GPU 超时)。查排练要用 getShaderInfoLog / getProgramInfoLog;查演出要用缩小公式、用色块代替纹理、看是否除零或错误采样。两套工具不要混。

function compile(gl, type, src) { const sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh); if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(sh); gl.deleteShader(sh); throw log; } return sh; }

日志是给CPU读的。忽略状态检查、直接 link,WebGL 可能静默使用失败对象,draw 什么都不出。这比抛异常更阴险。教学代码里“检查 COMPILE_STATUS”不是啰嗦,是分端:这里失败就不要进入 GPU 运行。

两代语言合同,以及精度这件事

主题 GLSL ES 1.00 WebGL1 GLSL ES 3.00 WebGL2
版本行 通常不写 必须 #version 300 es 且在首行
顶点输入 attribute in
阶段之间 varying VS out 对 FS in
片元输出 gl_FragColor 用户声明 out vec4
纹理采样 texture2D texture
循环 常需常量边界 宽松许多
整数与位运算 明显更完整

精度:片元着色器在 ES 里必须声明默认精度,或给每个浮点变量写精度。highp 在部分片段实现上是可选的,写了可能降到 mediump 或直接编译失败。CPU 侧编译器按设备能力说话,所以同一份源码在桌面过、在某手机不过。对照策略:颜色与 UV 用 mediump;位置相关、阴影比较再局部 highp,并准备一条 mediump 回退。不要全篇 highp 还以为是“更专业”。

precision mediump float; varying vec2 vUv; uniform sampler2D uTex; void main() { gl_FragColor = texture2D(uTex, vUv); }

VS 与 FS 之间的 varying 必须同名同类型,链接期对齐。这是 CPU 链接器的工作。运行期 GPU 只负责插值。插值默认是透视校正的;在 1.0 里你很少改它。若 VS 输出的法线没归一化、FS 又当单位向量用,那是运行错误:编译完全成功,画面却有块状高光。查这类问题不要盯日志,要盯公式假设。

Uniform 位置在链接后由 CPU 查询。每帧 getUniformLocation 是把查询当运行时热路径,纯属 CPU 浪费。缓存位置表。Attribute 位置可以链接前 bindAttribLocation,或链接后查询。WebGL2 可在 GLSL 里写 location layout,少一次查询。这些全是编译期/链接期的 CPU 事务,与 GPU ALU 无关。

工程:编译次数、变体数量、热替换

材质变体(有没有法线贴图、有没有蒙皮)会把一份“逻辑着色器”炸成多份 program。引擎常在加载时编译常用变体,缺的再懒编译。懒编译若落在交互第一帧,会卡一下——那是 CPU 编译账单,不是 GPU 突然变慢。对照做法:预热可见材质;或接受第一帧卡并打点,不要在每帧根据灯光数量重新拼字符串编译。

做法 发生端 何时合理 何时有害
启动时编译全部变体 CPU 变体少、加载可遮罩 变体组合爆炸导致启动过长
首次使用时编译 CPU 变体多 战斗中第一次切材质卡死
每帧编译 CPU 从未合理 永远有害
热替换源码 CPU 编辑器内调试 忘了删调试路径就上生产
运行复杂循环 FS GPU 后处理必须 可下放到纹理查找却没做

我更倾向把着色器当资产:独立字符串、有版本注释、有一份 CPU 侧的编译器包装统一打日志。业务代码只拿 program 句柄。这样 1.00 与 300 es 可以并存两套资产,而不是在运行时用正则替换 attribute。正则替换属于把语言合同交给运气。

⚠️ 常见坑:片元着色器忘记 precision,桌面浏览器碰巧通过,移动端编译失败。日志写 undeclared precision。这不是设备“不支持 WebGL”,是源码没满足 ES 合同。

💡 关键直觉:着色器有两次生命。第一次生命在 CPU 上证明自己合法;第二次生命在 GPU 上对每个顶点每个片元跑。优化第一次靠减少编译次数,优化第二次靠减少代数与纹理依赖。

调试运行期:把 FS 改成输出法线颜色、UV、纯光点积,是在 GPU 路径上做二分。不要为此重新发明 CPU 日志。discard 过多会破坏早期深度优化,那是运行期 GPU 问题。分支若在 warp 内发散,移动 GPU 更痛。这些都假设程序已经链接成功。先保证第一次生命结束,再谈第二次的快慢。

问题:为什么有的错误只有某台手机上出现?

编译器前端各家不同,对未定义行为、精度、循环边界的宽松程度不同。CPU 日志在那台设备上才能代表那台设备。用桌面成功证明手机必成功,等于用一台编译器的结果替另一台发言。真机抽样编译,是 WebGL 项目相对原生 GL 更麻烦的一点:用户 GPU 就是编译器。

第一个程序的着色器应短到可以印在一页纸上:VS 做 MVP 乘法并传颜色,FS 输出颜色。确认编译链接在你的目标上下文版本下通过,再加纹理。加效果时每次只加一个 Uniform 和一个公式,方便把“编译失败”和“公式错”分开。引擎材质编辑器若能显示编译日志,打开它;关掉日志等于扔掉 CPU 端的唯一证词。

变体工厂与调试二分,都发生在第一次生命

生产材质很少只有一份 FS。有没有骨骼、有没有法线贴图、有几盏灯,会组合出变体。CPU 侧应有明确的关键字:SKIN 1NORMAL_MAP 1,用字符串拼接或预生成。关键字爆炸时,用“运行时分支 + 静态循环上限”折中:灯最多四盏,空盏颜色为零,避免 1/2/3/4 灯四套程序。分支有 GPU 代价,变体有 CPU 编译与内存代价。对照要量化:变体数量 × 编译时间 是否可放进加载条。放不进就砍关键字,而不是让第一枪卡 200 毫秒。

调试二分建议固定流程。编译失败:看日志第一行文件与列,通常是 version、precision、拼写、1.00/300 混用。链接失败:查 VS/FS 之间 varying 名字、类型、数组大小。运行画面错:先输出纯色证明 program 在跑,再输出 UV、法线、世界位置,再接公式。不要一上来改光照模型。引擎材质编辑器若能显示编译日志,把它接到你们的错误上报。没有日志的运行错误会被当成“显卡兼容性”,浪费兼容性预算。

删除着色器对象:链接成功后,shader 对象可以 detach/delete 以省驱动内存,program 仍能跑。上下文丢失后 program 同样作废。热替换时先建新 program,成功再切,失败保留旧的。编辑器里热替换失败导致全黑,是因为切到了链接失败的句柄。CPU 侧应保留 lastGoodProgram。这是程序缓存的最低卫生。

精度风暴、未定义行为、以及驱动差异

normalize 零向量、除零、超出数组,GLSL 未定义。桌面可能给黑,移动可能给闪。CPU 编译器不一定拒绝。运行期防护:方向光向量在 CPU 归一化再上传;FS 里 max(dot,0) 已经防负,但仍要防零长法线。资产若有零面积三角,法线是零。生成器要丢掉退化三角。把未定义交给 GPU 碰运气,兼容性列表会无限长。

循环上限用常量。1.0 对动态循环很严。2.0 松一些,移动仍可能差。灯光循环用常量 4,用颜色为零表示空。比可变长度循环稳。稳比聪明更适合着色器第一次生命。第二次生命再谈展开与否。驱动可能自己展开。你在源码里写的循环不是最终机器码。用调试着色器扩展看转译,仅开发机。不要把转译当生产依赖。

对照问答:日志、变体、热替换

日志说 unexpected #version 怎么办?

300 es 的版本行必须在文件第一行,前面不能有空格或注释之外的东西。有人用字符串拼接时在前面加了精度或 ifdef,桌面碰巧过、移动不过。CPU 侧拼接模板要把 version 钉死在 [0]。1.00 不要写 version 300。两套模板分开,比一份源码打补丁干净。

变体编译把加载条拖到十秒?

砍关键字。灯数量用常量上限加零颜色,不要 1 到 8 各一套。骨骼有无可以留两套。法线贴图有无两套。相乘之后仍要人工封顶。加载条只预热本关可见材质。其余懒编译,并接受第一次出现时的一卡。把这一卡打点,不要假装没有。

热替换后全黑但日志成功?

可能用了链接成功却 varying 对不上的程序,或切错 program 句柄。保留 lastGood。新程序 draw 一帧调试纯色,确认再切业务。热替换失败应回滚而不是留下空句柄。编辑器路径不要流进生产包。

工程备忘:模板与禁止事项

GLSL 模板只允许替换关键字和常量上限,不允许替换函数体中段。中段替换会让日志行号对不上源码。行号对不上,第一次生命的失败就变成猜谜。禁止在运行时从网络拉一段字符串直接 compile,除非编辑器明确隔离。生产包内着色器应是构建期资产,可审查。精度:移动 FS 默认 mediump,世界位置相关计算在 VS 做完再传。传大世界坐标到 mediump FS 会抖。抖看起来像 3.1 矩阵错,其实是精度。对照输出 VS 后的值与 FS 收到的值。差了就是精度,不是乘法顺序。

着色器代码审查看三件事:版本行、精度、循环边界。不看公式漂不漂亮。漂亮公式可以后补,错误版本行会让一档机全灭。审查还要看是否在 FS 里做了本该 VS 做的世界坐标减法。mediump 下大数相减会抖,抖被当成相机 bug 转给 3.1,来回踢。规定:世界减相机只允许 CPU 或 highp VS。FS 只接已经相对化的量。变体关键字必须有上限表,表由程序维护,美术不能无限组合。组合爆炸的编译时间会出现在第一次打开新皮肤,被当成闪卡。打点到关键字名,才能知道该砍哪一个。热替换只存在编辑器开关下,生产构建把开关编译掉。开关留在生产,等于留了一扇能执行字符串的门。

要点串联

  • 编译在 CPU:字符串 → 编译 → 链接;失败只查 info log,不要猜 GPU。
  • 运行在 GPU:draw 之后逐顶点逐片元;失败看画面,用改输出做二分。
  • 两代语法:1.00 与 300 es 是不同合同,不要一份源码硬扛。
  • 精度是移动端现实:FS 必须声明;highp 不是勋章。
  • 位置查询要缓存:getUniformLocation 不是每帧手续。
  • 变体编译有账单:预热或懒编译都要承认卡顿发生在 CPU。

下一节对照两条把数据送进着色器的通道:按顶点变的 Attribute,和按绘制调用变的 Uniform。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U