2.3 缓冲区:CPU 写入 vs GPU 常驻


文档摘要

2.3 缓冲区:CPU 写入 vs GPU 常驻 本节摘要:WebGL 缓冲区是 GPU 可消费的一块类型化内存。CPU 用 或 写入,之后顶点着色器通过 Attribute 指针读取,索引绘制通过元素缓冲读取。写入频率决定你该给驱动的使用提示:几乎只写一次,还是每帧流式更新。把动态数据塞进“当静态用”的习惯路径,或反过来每帧重传整网静态网格,是两端账单同时恶化的典型写法。 本节地图 阅读完本节,你应当能够: 说明 与 分别服务谁 对照 整块替换与 局部更新 解释 STATIC、DYNAMIC、STREAM 提示对驱动的含义,以及它们不是硬协议 在 WebGL1 与 WebGL2 之间对照 VAO 如何打包缓冲绑定 判断粒子位置、骨骼矩阵、静态建筑网格各自该用哪种写入策略 缓冲是仓库,不是

2.3 缓冲区:CPU 写入 vs GPU 常驻

本节摘要:WebGL 缓冲区是 GPU 可消费的一块类型化内存。CPU 用 bufferDatabufferSubData 写入,之后顶点着色器通过 Attribute 指针读取,索引绘制通过元素缓冲读取。写入频率决定你该给驱动的使用提示:几乎只写一次,还是每帧流式更新。把动态数据塞进“当静态用”的习惯路径,或反过来每帧重传整网静态网格,是两端账单同时恶化的典型写法。

本节地图

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

  1. 说明 ARRAY_BUFFERELEMENT_ARRAY_BUFFER 分别服务谁
  2. 对照 bufferData 整块替换与 bufferSubData 局部更新
  3. 解释 STATIC、DYNAMIC、STREAM 提示对驱动的含义,以及它们不是硬协议
  4. 在 WebGL1 与 WebGL2 之间对照 VAO 如何打包缓冲绑定
  5. 判断粒子位置、骨骼矩阵、静态建筑网格各自该用哪种写入策略

缓冲是仓库,不是 JavaScript 数组的别名

Float32Array 活在 CPU 堆上。createBufferbufferData 才在 GPU 侧(或驱动管理的共享区)建仓库。拷贝发生在写入那一刻。之后你改 JS 数组,画面不会变,除非再写一次。这是初学者最常见的错觉:以为 GPU 还指着那份数组。没有。合同是拷贝语义,不是引用语义。

仓库有绑定点。ARRAY_BUFFER 是当前用于顶点属性的仓库入口;ELEMENT_ARRAY_BUFFER 是索引入口。很多调用没有把缓冲当参数传来传去,而是“先 bind,再操作当前绑定”。状态机风格让漏 bind 的后果是:数据写进了上一份对象。排查“网格串味”,先打印你以为的绑定,再怀疑着色器。

CPU: Float32Array [x y z x y z ...] │ bufferData 拷贝 GPU: Buffer 对象 常驻,直到 delete 或上下文丢失 │ vertexAttribPointer 解释 VS: aPosition 每个顶点取一截

上下文丢失(切后台、GPU 重置)会让仓库蒸发。引擎通常有恢复流程;裸写必须监听丢失与恢复,重新 bufferData。这再次证明:CPU 数组才是可重建的源,GPU 缓冲是缓存。源被你丢掉、只留 GPU 对象,丢失后便无法恢复。

写入方式与频率对照

操作 行为 适合 不适合
bufferData 整块 可分配或替换整仓 首次上传、尺寸变化 每帧只改几个顶点却重传整网
bufferSubData 改仓内一段 局部变形、部分粒子 每次改的范围其实是整仓
映射缓冲 WebGL2 更完整 流式顶点 WebGL1 几乎别指望同一套代码
每帧 bufferData 新数组 简单粗暴 原型 静态场景、大网格

使用提示 STATIC_DRAWDYNAMIC_DRAWSTREAM_DRAW 是给驱动的频率暗示:数据多久改一次、是否作为绘制源。它们不是权限位,乱填通常仍能画,但驱动可能把仓库放在不合适的堆。我的经验规则:网格一辈子不变 → STATIC;每几帧改一块 → DYNAMIC;每帧由 CPU gener 全新内容 → STREAM。粒子若改为 GPU 反馈更新,提示应跟着策略改,而不是永远 STREAM。

gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // 若干帧后只改前 12 个 float gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(head));

WebGL1 里属性布局散落全局:绑 VBO、vertexAttribPointerenableVertexAttribArray,换网格就要重做一遍。VAO 把“哪些缓冲、如何切分”打包。WebGL2 核心拥有 VAO;WebGL1 靠扩展。对照很直接:没有 VAO 时,CPU 每 draw 前重复解释仓库;有 VAO 时,解释发生在建立时,绘制只切对象。第 1.1 节提过别把 2.0 当 1.0 用,这里是具体落点。

交错存储与分缓冲是另一组对照。把位置、法线、UV 塞进一个 VBO,用 stride 切开,缓存更友好,一次绑定。分成三个 VBO,更新位置时不必动 UV,动态网格更省。静态角色网格我倾向交错;CPU 每帧改位置的粒子,位置单独一个 STREAM 仓,颜色 STATIC。没有万能布局,只有“谁在写、写多频”的布局。

图:缓冲生命周期两端

图:缓冲生命周期两端

策略:谁写、写多频、写多大

建筑场景的墙体:CPU 加载时写一次 STATIC,整局只读。角色世界矩阵:那不是顶点缓冲,是 Uniform,别误塞 VBO。CPU 蒙皮若每帧把变形后的位置写入 VBO,仓库变成 STREAM,CPU 与总线一起忙——第 4.4 节对照 GPU 蒙皮,就是为了把这笔写入从 CPU 拿掉。

多缓冲(double/triple buffering)用于流式:CPU 写 A 仓时 GPU 读 B 仓,避免读写冲突造成同步。WebGL 里这套要自己管,引擎对动态几何常有类似池。原型阶段可以不管;性能面板出现 bufferSubData 与绘制争用时再上。

WebGL2 的 copyBufferSubData、Uniform Buffer,让“CPU 少写、GPU 内部搬”成为选项。UBO 适合多着色器共享的同一块相机矩阵。对照 WebGL1:同一矩阵要对每个 program uniformMatrix4fv 一次,CPU 重复提交。迁 2.0 却仍对每个 program 逐个传 4x4,等于放弃这条减 CPU 的路。

⚠️ 常见坑:每帧 new Float32ArraybufferData 静态网格。GC 与拷贝会在低端机上比着色器更明显。网格不变就让仓库活着。

💡 关键直觉:缓冲优化问三句——还改吗、改哪一段、能不能改成 Uniform 或 VS 公式从而不再写仓。

删除与泄漏:页面里反复创建几何却不 deleteBuffer,显存会涨。引擎的 dispose 必须真正走到删除。单页应用切路由时最常见。CPU 侧数组可以丢掉以省堆;GPU 侧必须显式删。两边的“丢掉”不是同一个 API。

问题:TYPED_ARRAY 已经很快了,为什么还要在意上传?

类型化数组避免了普通 JS 数组到驱动的二次转换,这只解决了 CPU 内部格式。上传仍可能是跨进程、跨芯片拷贝。一万顶点一次无所谓;每帧一百万顶点,带宽会先于 ALU 爆掉。所以程序化城市若拓扑不变,应在加载帧上传,不要在逻辑帧生成。生成可以在 Worker,上传仍要回到拥有上下文的线程——上下文不同步共享,这是 WebGL 的硬边界。

交错布局的 vertexAttribPointer 参数:size、type、normalized、stride、offset。CPU 必须与 GPU 解释一致。stride 为 0 表示紧密排列。写错 offset,法线会吃到位置的字节,光照会像发霉。这是组装合同,不是运行时随机。把布局写成一张表贴在创建 VAO 的代码旁,比事后猜更省命。

丢失、共享、以及“谁持有源数据”

浏览器会在切后台、GPU 进程崩溃时丢上下文。监听丢失事件后,所有 Buffer、Texture、Program 句柄作废。恢复时按 CPU 源重建。若你加载模型后把 JS 数组丢掉,只剩 GPU 句柄,恢复只能重新网络请求。长应用要决定:源数据常驻内存以便秒恢复,还是接受恢复时重新加载。这是内存与体验的对照,不是 API 细节。引擎通常有自己的恢复,裸写必须登记一份“重建清单”:哪些仓、哪些纹理、哪些程序。清单就是 CPU 作为真相源的证明。

Worker 里不能直接碰属于页面的 WebGL 上下文。OffscreenCanvas 可以把上下文搬到 Worker,但那是另一份上下文,对象不能共享。粒子模拟在 Worker 算位置,仍要回传类型化数组到拥有上下文的线程再上传。回传本身是 CPU 拷贝。对照:模拟与上传是否值得分线程,要看拷贝是否比主线程计算更便宜。很多时候主线程算 1 万粒子比来回传更快。分线程不是免费的端。

环状多缓冲的实现要点是:知道 GPU 可能还在读上一帧的仓。WebGL 没有通用的 fence 那么好用,实践中用三倍缓冲加“本帧只写 index%3 的仓”来降低冲突概率。若仍出现闪烁,可能是 SubData 与绘制重叠。退回整块 bufferData 有时反而稳,虽然 theoretically 更贵。稳与快的对照要以时间线为准,不要以文档里的提示枚举为准。提示枚举是暗示,实现可以忽略。

交错布局的对齐,以及 Instanced 缓冲

某些实现对类型与偏移对齐敏感。半浮点、byte 法线 packed 时,偏移要按规范对齐。CPU 填写 packed 数据要用 DataView 或手工位移,填错则 GPU 解释成噪声法线。为了省带宽去 packed,要同时投资调试可视化。省了带宽却加了两天调试,不一定赚。先用 float 交错跑通,再在性能节按需 packed。这是 2.3 与 4.7 的交接:过早 packed 是优化错序。

实例化缓冲是另一份 ARRAY_BUFFER,divisor 为 1。它与网格 STATIC 仓分开,更新策略独立。网格永不写,实例仓每帧写。两仓绑在同一 VAO。VAO 记录两份解释。漏设 divisor,实例数据会被当成每顶点,一万实例变成网格顶点爆炸或越界。divisor 是通道粒度的硬件开关,设错没有好错误信息。画一个实例立方体验证 divisor,再扩到一万。

对照问答:提示枚举、泄漏、Worker

STATIC 数据后来要改,能改吗?

能。提示不是锁。改只是可能更慢或引发重新分配。偶尔改一次材质颜色顶点,直接 SubData 即可。若发现改的频率变成每帧,应换提示并考虑拆仓。不要因为曾经标了 STATIC 就每帧新建 Buffer 对象。新建对象比原地改更贵,也更容易漏删。

为何切路由后越来越卡?

GPU 对象没删。几何与纹理句柄活在上下文里,JS 侧数组已经丢掉,GC 救不了显存。单页应用在路由离开时走 dispose:deleteBuffer、deleteTexture、deleteProgram。引擎要调用官方 dispose 并确认没有匿名闭包抓住几何。内存曲线向上而帧时间后段才坏,是泄漏典型像,不像着色器突然变重。

Worker 算粒子再传回主线程值不值?

一万以下往往不值,拷贝与同步吃掉收益。十万以上或还要做空间哈希,可以值。传回的应是类型化数组,不要结构化克隆普通对象。主线程只负责上传与 draw。上下文仍不能在 Worker 与页面之间共享对象。OffscreenCanvas 是另一条路,等于把整条管线搬走,而不是只搬粒子。

工程备忘:对象池与统计

长应用给 Buffer 做池:按尺寸分桶,避免频繁 create/delete。池统计打到调试面板:当前仓数、字节、本帧 SubData 次数。次数突然涨,说明有人把静态网当动态写。统计比感觉早发现。删除时从池取出再 delete,不要只把 JS 引用置空。上下文恢复时池全部作废,按清单重建,不要试图复用旧句柄。句柄数字可能被重用到别的对象,复用会静默画错网格。这比泄漏更阴险。开发版给每个仓起名字,draw 前打印当前 VAO 绑定名。名字是卫生,不是幼稚。

上下文丢失演练应写进开发手册:在浏览器里强制丢 GPU 进程,看场景是否能重建。重建失败的仓就是清单漏项。漏项在用户手机上表现为白屏,日志还在你这台没丢过的机器上。演练比文档有效。动态几何的环状三缓冲在粒子上很值,在角色网格上通常不值,因为角色该走蒙皮而不是改仓。选错策略会让 2.3 与 4.4 互相打架:动画系统写仓,缓冲系统按 STATIC 提示,驱动把仓库放在不适合每帧写的堆。两边都按自己的正确性工作,合起来却慢。联调时把写入次数打到屏幕,次数跟角色顶点数成正比,就是走错路了。改回骨矩阵 Uniform,次数应掉到角色数这一级。这才是对照,而不是把提示枚举从 STATIC 改成 STREAM 当修好。

本章回顾

  • 拷贝语义:JS 数组改了画面不变,除非再次写入缓冲。
  • 绑定点:顶点走 ARRAY_BUFFER,索引走 ELEMENT_ARRAY_BUFFER,操作的是当前绑定。
  • 提示跟频率:STATIC / DYNAMIC / STREAM 是暗示,策略仍要你按改动频率选写入 API。
  • VAO 打包解释:把 CPU 每 draw 的指针劳动前移到创建期。
  • 布局跟谁写走:静态交错,动态字段拆仓。
  • 上下文丢失:GPU 仓会蒸发,CPU 源才是重建依据。

下一节对照着色器:编译链接发生在 CPU 与驱动,真正的逐顶点逐片元奔跑发生在 GPU。


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