Commit Graph

1 Commits

Author SHA1 Message Date
troytt 4cc49a01aa perf(render): 视口剔除 + 单批次合图 + 零分配顶点写入 (refs #16)
主循环掉到 20 tps 的原因不是"缺引擎",而是三件具体的事,这个改动把它们一次做掉。

1. 视口剔除。此前整张地图无条件提交:把 act-scene 的绘制顺序在 365 张烘焙关卡上
   静态重放,平均每帧 2 848 个四边形、最差 14 400 个(act4 迪亚波罗),而真正落在
   1280x720 视口里的只有 7%~11%。现在关卡建好时把每条绘制列表的包围盒预先摊平成
   一块 Float32Array,每帧线性扫描做四次浮点比较。实测平均 2 848 -> 204 个/帧
   (7.2%),最差 14 400 -> 437 个。

   之所以是线性扫描而不是二分或分桶:绘制列表并不按 y 单调(地面 0/365 单调,最大
   回退 9 520 px),任何空间索引都得重新合并回画家顺序。14 400 次比较约 15~30 µs,
   占 16 ms 预算的 0.2%,换来的是顺序原样保留。墙和对象那条两路深度归并里,剔除只
   跳过"画"这一步,游标照常前进——否则后面所有遮挡关系都会错位。

2. 一次 draw call。此前批次一遇到图集页切换就得断开,同一帧里页号来回横跳,实测
   平均 361 次、最差 3 077 次 draw call。现在每个在用的图集页各占一个纹理单元,
   单元号随顶点属性进着色器,片元按 switch 选采样器,只有在用页数超过
   MAX_TEXTURE_IMAGE_UNITS 时才断批。

   这里没有采用 issue 里原先设想的 TEXTURE_2D_ARRAY:页高差异极大(2048x1999 到
   2048x48),纹理数组要求各层等大,实测朴素做法显存涨 1.73 倍(最差 2.82 倍,单关
   156.8 MB),装箱后仍要 1.29 倍。而全部 365 张关卡里单关最多只有 10 个页,加角色
   图集 11 个,稳稳低于 WebGL2 保证的 16 个单元——多单元合批同样是 1 次 draw call,
   显存零增长,资源也不用重烘焙。

3. 零分配顶点写入。quad() 原本每次建 4 个临时数组再解构,实测 2 849 个四边形写一帧
   要 244.9 µs;展开成标量后 106.0 µs(2.31 倍),每帧少建 11 396 个临时数组。同时
   顶点从 6 个/四边形降到 4 个并改用索引缓冲,上传前先 orphan 一次缓冲区避免与 GPU
   抢同一块内存。

顺带修掉几个让问题一直看不见、或者纯属每帧浪费的地方:

- state.drawCalls 原本在 act-scene 和 walk 里都硬编码成 1,这个假读数正是问题长期
  没被发现的原因;现在读渲染器自己的计数,HUD 一并显示图元数、剔除数和帧耗时。
- pages.filter(...).length 每帧建一个临时数组只为数个数,改成循环。
- HUD 文本每帧重建(十次字符串拼接 + 四次 toFixed),改成 4 Hz。
- NPC 名牌每帧 innerHTML='' 再 createElement,改成复用元素只改位置。
- onTick 里的 playerMoving 用 let 遮蔽了外层同名变量,导致 onRender 永远读到 false、
  角色走路动画从不播放。改成赋值。

护栏:新增 tests/cull.test.ts,除边界条件外,用全部 365 张关卡真实数据重放一遍,
断言"开剔除后画出的序列必须是不开剔除时那个序列的子序列"(顺序一致、只少不多),
并守住剔除率。verify-renderer-lifecycle 从 26 条加到 37 条,锁住"60 个四边形在 3 个
页之间来回切只花 1 次 draw call"、超出单元预算恰好多花 1 次、计数器在 begin() 归零、
以及着色器里采样器数组的形状。

Refs #16
2026-09-15 01:30:39 +00:00