[RFC]一些关于性能优化的想法和实现/Ideas and Implementations for Performance Optimization #800
axycri7
started this conversation in
Show and tell
Replies: 2 comments
-
|
补充一下,开头提到的不到50FPS是由于华为设备上对全屏应用存在锁帧/降频的情况,即使是优化后的构建也被锁到60FPS,后面的测试实际上是使得应用窗口大小略小于全屏(长宽各大约小了几十px)跑的xwx |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
判定机制有问题 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
一切源于朋友在 MatePad 11.5S 上打一些谱面不到 50 FPS 看起来很坐牢xwx......加入如下所述改动的构建(见文末)在同条件跑满了 120 FPS
(写作打磨和 PoC 的实现使用了 Gemini 、 Codex 等 LLM 工具辅助)
现状
目前 Phira 存在很多每帧重复开销的叠加:
这些问题在移动端、低性能设备,在带特效/高物量的谱面中,带来了像素填充率和内存带宽的瓶颈。主要思路是:改变重复计算与临时分配的现状,加入按帧和内容的缓存和更多按可见性的裁剪。基于 v0.7.1,以下是一些可行的想法……
渲染缓存
NoteBuffer使用std::mem::take()提取BTreeMap,每帧为(order, texture)分组重新创建Vec<Vertex>和Vec<u16>,频繁堆分配和内存碎片化会导致 Allocator 抖动和 Frame Spike。如果将其重构为持久化的持久化 Batch 结构:Map(order, texture) -> [MeshBuffer]每帧调用
begin_frame()时仅清空长度(clear())而不释放底层 Capacity。稳定状态下不再需要很多次内存分配。以及将单 Batch 容量由 64 quad 提升至 256 quad,可以减少提交给 GPU 的 Draw Call 总数。静态背景层缓存
背景在多数帧中是静态的,但每帧仍进行绘制背景图片,绘制谱面遮罩,再绘制谱面内容。这主要会浪费 fill rate。若将背景图与谱面区域的 dim 遮罩在未变时合并绘制到独立的显存
RenderTarget,后续帧只需单次 Blit,省去了每帧重复全屏 Alpha Blend 的像素填充率和带宽浪费。以及 UI 上的分数、combo数、准度 每帧渲染,但数值并非每帧都变化。可以缓存格式化后的字符串,仅在值变化时重新 format。这个优化收益较小,但也能顺便加上(
判定线变换缓存
判定线的变换矩阵依赖其自身的动画、父 line 的位置/旋转。本来在
update、judge、render三个独立阶段分别调用递归查询。在父子链较深且线数量较多时,有大量的重复遍历。最坏复杂度近似为(K_u, K_j, K_r分别为各阶段变换被查询的频次,L为线数,D为依赖树深度):O((K_u + K_j + K_r) * L * D)可以引入
ChartFrameCache,每帧开始时根据绑定的res.time仅执行一次遍历,一次性将所有线的position、rotation、transform matrix和line_height写入缓存,后续各阶段复用flowchart LR A["res.time"] --> B["prepare_frame_cache"] B --> C["positions"] B --> D["rotations"] B --> E["transforms"] B --> F["line_heights"] C --> G["Judge Phase"] D --> G E --> G E --> H["Render Phase"] F --> I["Note update/render"]这样就退化为普通的 森林/DAG 遍历,整体复杂度降为
O(L + E),其中E为父子依赖边数。当然,当线数量变化或时间轴跳变时需要刷新缓存(判定扫描游标
由于每次触摸/键盘事件到来时,都需要寻找最近的候选音符,若每次都从当前 line 的初始指针扫描,在密集谱面下会产生很多重复计算,最坏情况近似为(
T为判定事件数,N_l为单线音符数):O(T * sum(l = 1..L, N_l))所以可以为 Click / Hold 判定维护单调游标(
st_l是通用的已判定起点,cursor_l为本次扫描起点):p_l = max(st_l, cursor_l)每次扫描只从
p_l开始;当发现 note 已判定、已错过或不可能成为候选时单调推进游标,且键盘输入在循环外预构建每条线的全局候选。游标单调推进后,重复扫描被摊还(C是当前判定窗口附近候选数量,Delta N是本帧实际推进的 note 数):O(T * C + Delta N)可见音符窗口与分组裁剪
许多 note 在当前帧不可见,但仍可能被遍历。高密度谱面中,渲染循环如果每帧从 line 的 note 列表头部扫描,会把大量已判定或屏幕外音符带入热路径。可以将音符按以下情况预排序和分组:
对每个 side/speed group 维护可见窗口:
group = [start, end)每帧将
start推过已判定的 note;渲染时按高度阈值提前停止。如果 note 与判定线的相对高度满足:h_note - h_line + y_anim > h_viewport / speed则同速度、按高度排序的后续 note 均可跳过
PBC 缓存和优化
这里主要是同一谱面在多次游玩时会不必要地多次解析?可以引入首次解析后的
.phira-pbc-cache二进制缓存。缓存 Key 采用哈希校验:key = H(version, format, parse_options, extra, chart_bytes)flowchart TD A["source chart bytes"] --> B["format inference"] B --> C{"PBC?"} C -->|"yes"| D["read directly"] C -->|"no"| E["compute cache key"] E --> F{"cache hit?"} F -->|"yes"| G["read cached PBC"] F -->|"no"| H["parse source format"] H --> I["write PBC cache"] D --> J["Chart"] G --> J I --> Jmultiple_hint的查找算法也可以进行一些修改,因为若为了找重复 note time 而对每条 line 排序,再汇总排序全局 times,加载时的复杂度高达O(sum_i(n_i * log n_i) + N * log N)。用 HashMap 统计 note time 出现次数,计数大于 1 则标记,复杂度可以降到O(N)测试结果
测试设备:
测试谱面ID:19683
开启自动游玩和夜店模式,使用“显示平均帧率”的结果作为指标:
此外还随机选了十几个谱面并测试了各Mods功能,没有观察到画面和修改前有可见差异或运行异常
PoC
在这里的 Fork 实现了上面的改进,可以算是一个 PoC :axycri7/phira 仓库中根据 Phira 官方文档 写了使用 Docker 的 Android 构建脚本 build_android.sh 和 Dockerfile 等,可以自行构建或者从 Pre-Release 下载 APK 验证
构建脚本涉及到修改包名和权限名称,只是为了临时共存测试(
更进一步:ANGLE
ANGLE 是 Chromium 项目中的图形抽象层组件,用于在 WebGL 等处将 OpenGL ES 调用转译为 Vulkan、DirectX 等高效原生 API,自 Android 10 被引入 AOSP
在 Pixel 8 的开发者选项中为原版 Phira 启用 ANGLE 作为 GLES 驱动程序(使用Vulkan作为后端)后,即使没有加上述优化,同谱面、关闭低画质模式的平均帧率也从原来的 48 FPS 到了 120 FPS ,但这不意味着上述优化是没有意义的。大部分厂商没有给设备引入系统 ANGLE ,甚至部分低端/老旧设备 GPU 不支持 Vulkan
在上述 Fork 新增了 android-angle 分支,使用了允许加载其他EGL库的 prpr-miniquad ,和由 GitHub Actions 编译的 ANGLE 库。这个分支和 ANGLE 来源目前仅写了构建 arm64-v8a 目标,毕竟 32 位 Android 设备估计GPU也不支持 Vulkan(
在 Pixel 8上,正常加载并使用 Vulkan 1.4;在 MatePad 11.5S上,正常加载并使用 Vulkan 1.2;在红米 4A 上,因 GPU 不支持 Vulkan 回落到系统 libEGL;平均帧都不低于不含 ANGLE 的构建
不过在 Maleoon GPU 的 MatePad 11.5S 上 ANGLE 对打击特效的渲染有一些视觉问题,可能是因为
prpr/src/particles.glsl的particle_transform_vertex()存在一些不妙的 shadowing 写法导致在 Vulkan 栈出现差异?结束
由于条件所限,目前的视角和测试目标仅有 android-arm64-v8a,需要考虑其他平台的情况;测试手段也限于平均帧率,若有Low帧、帧时间、内存/CPU/GPU占用率的连续测试,可能会更全面
关于提出的这些优化希望得到更多讨论和建议awa
Beta Was this translation helpful? Give feedback.
All reactions