Optimize fixed-function Vulkan shaders for Adreno 512 and recover Android surfaces
This commit is contained in:
@@ -30,3 +30,21 @@ adb shell am start -n org.metin2port.client/.MainActivity \
|
||||
从桌面图标启动(不传参数)时连接 `MainActivity.DEFAULT_ARGS` 里的真服登录界面;显式传入不含 `--live-server` 的参数时运行进程内假服。测试真服前确认模拟器/设备能访问相应主机与端口。用 `adb logcat` 查看崩溃和启动错误。`adb devices -l` 没有设备时,先连接模拟器或开启真机 USB 调试。
|
||||
|
||||
更详细的构建依赖与命令见 [src/host/README.md](../src/host/README.md)。
|
||||
|
||||
## Mi 8 Lite / Adreno 512
|
||||
|
||||
2026-09-30 真机适配:绘制状态使用动态 uniform buffer,片元着色器按纹理混合和透明测试状态编译专用管线,
|
||||
减少 Adreno 512 每个像素的存储读取和动态分支。Android 的默认场景分辨率仍为 75%、MSAA 2×,UI 保持屏幕分辨率。
|
||||
此 GPU 首次启动默认使用省电模式(30 FPS);已保存的帧率设置和显式命令行参数优先,其他 GPU 不改变默认模式。
|
||||
|
||||
`--generic-shaders`(或 `MT_GENERIC_SHADERS=1`)保留 uniform buffer、关闭片元专用化,供诊断对比。
|
||||
完整测量和画面对比见 [Mi 8 Lite 适配记录](XIAOMI8LITE-ADAPTATION-20260930.md)。
|
||||
|
||||
可重复的分段性能测试(需已安装 APK 并同步资源):
|
||||
|
||||
```sh
|
||||
ANDROID_SERIAL=<serial> python3 tools/perf/run_render_isolation.py build/qa/device-test \
|
||||
--sections baseline,terrain,baseline_repeat --render-scale 0.75 --msaa 2 --fps-cap 30
|
||||
```
|
||||
|
||||
每段排除首个加载后的采样窗口,记录 6 个 2 秒窗口;输出 CSV、截图、内存快照和汇总 JSON,结束后恢复正常登录启动。
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Mi 8 Lite 真机适配(2026-09-30)
|
||||
|
||||
已在连接的 Mi 8 Lite 上安装优化版 Android 客户端。相同村庄、24 只测试怪物(总计 28 个 actor)、
|
||||
60% 场景分辨率、MSAA 1×、30 FPS 上限下,完整场景从约 4.9 FPS 提高到约 30 FPS;
|
||||
默认 75% 场景分辨率、MSAA 2× 也达到约 30 FPS,UI 保持 2280×1080 屏幕分辨率。
|
||||
|
||||
## 设备与方法
|
||||
|
||||
- Xiaomi Mi 8 Lite(platina),Snapdragon 660 / Adreno 512,4 GB RAM,2280×1080 / 60 Hz。
|
||||
- LineageOS 22.2-20260103-UNOFFICIAL-platina,Android 15 / API 35;本轮没有更换系统或 GPU 驱动。
|
||||
- APK 内的原生代码以 Release 构建;使用同一份中文资源,版本替换通过 `adb install -r` 保留资源。
|
||||
- 本地测试服 `metin2_map_a1`,`--fake-mobs 24`;完整场景包含地形、角色、特效、触控与 UI。
|
||||
- 每次排除加载后第一个 2 秒窗口,再取 6 个 2 秒窗口平均;完整场景做重复测试。
|
||||
- 手机经 USB 充电,电池温度约 28–32°C;电流/功率读数不可用,本轮不能评价续航或长期热降频。
|
||||
|
||||
## 实现
|
||||
|
||||
1. 每次绘制的固定管线状态由动态 storage buffer 改为 std140 动态 uniform buffer,
|
||||
使 GPU 使用适合常量广播的访问路径;1264 字节布局保持一致,骨骼调色板仍用 storage buffer。
|
||||
2. 片元管线按两个纹理阶段的操作、参数及透明测试开关/函数进行 specialization,
|
||||
编译时排除不使用的混合与 discard 分支;透明参考值、雾、材质因子仍是动态参数。
|
||||
管线缓存包含全部 specialization 值,避免不同材质误用同一管线。
|
||||
3. Android Adreno 512 在没有保存帧率偏好时默认省电模式(30 FPS);保存设置和显式测试参数优先。
|
||||
场景分辨率、抗锯齿及其他 GPU 的初始帧率设置保持原有默认值。
|
||||
4. 修复 Android 切后台时显示 Surface 丢失导致的退出:获取图像和提交显示均处理
|
||||
`VK_ERROR_SURFACE_LOST_KHR`,下一次 SDL 生命周期处理后重建 Surface、交换链与显示信号量。
|
||||
5. 性能工具支持选择场景分辨率、MSAA 和帧率上限,并允许新安装设备的性能日志目录为空。
|
||||
|
||||
`--generic-shaders` / `MT_GENERIC_SHADERS=1` 可关闭片元专用化用于诊断,仍保留 uniform buffer 优化。
|
||||
|
||||
## 测量
|
||||
|
||||
| 构建 / 完整场景 | 场景比例 / MSAA | 平均 FPS | GPU ms |
|
||||
| --- | --- | ---: | ---: |
|
||||
| 优化前 | 0.60 / 1× | 4.88 | 205.51 |
|
||||
| 优化前重复 | 0.60 / 1× | 4.87 | 205.54 |
|
||||
| 仅 uniform buffer | 0.60 / 1× | 11.80 | 84.91 |
|
||||
| 仅 uniform buffer 重复 | 0.60 / 1× | 11.73 | 85.16 |
|
||||
| 加片元 specialization | 0.60 / 1× | 29.97 | 26.94 |
|
||||
| 加片元 specialization 重复 | 0.60 / 1× | 30.00 | 26.37 |
|
||||
| 优化后较高画质 | 0.75 / 2× | 30.02 | 27.33 |
|
||||
| 优化后全分辨率测试 | 1.00 / 2× | 30.03 | 29.54 |
|
||||
|
||||
仅跳过地形的分段测试:优化前 15.45 FPS / GPU 64.72 ms,uniform buffer 后 29.98 FPS / 26.87 ms,
|
||||
再专用化后 30.05 FPS / 21.06 ms。正常版本保留地形;分段测试只用于定位开销。
|
||||
|
||||
60% 同参数对比约提升 6.1 倍,GPU 时间减少约 87%。FPS 已触及测试上限,不能据此推算 60 FPS 能力。
|
||||
实时动画导致重复启动时的绘制数略有变化;录制帧的对比另用完全一致的绘制数据。
|
||||
|
||||
这些结果说明此前低帧率有很大的客户端渲染优化空间;它们不能单独证明驱动没有其他问题。
|
||||
|
||||
## 画面与功能验证
|
||||
|
||||
录制村庄的完整绘制数据,对优化前、uniform buffer 版及专用化版分别回放;
|
||||
另回放已有的刺客技能和目标透明度混合捕获。屏幕像素比较排除动态 FPS 显示矩形
|
||||
`x=[140,340), y=[0,50)` 后,三种场景均为 **0 个不同像素**;目标透明度捕获连 FPS 区域也完全相同。
|
||||
这是对选定捕获的回归检查,未覆盖所有地图和技能。
|
||||
|
||||
Android Release 和 macOS Release 构建通过;真机默认配置、移动、背包、切前后台与正常登录启动的结果
|
||||
见下方最终测试结果及截图。测试结束恢复正常服务器选择界面,没有自动登录测试账号。
|
||||
|
||||
## 原始证据
|
||||
|
||||
所有文件在 `build/qa/xiaomi8lite-adaptation-20260930/`(构建产物,不入 Git):
|
||||
|
||||
- `baseline/`、`ubo/`、`specialized/`、`quality75/`、`quality100/`:CSV、截图、内存快照、汇总 JSON。
|
||||
- `default-profile.csv` / `default-profile.json`:最终安装版的默认参数测试。
|
||||
- `adaptation.mtdr`、`*-replay.bmp`、`baseline-assassin-skill.bmp` / `final-assassin-skill.bmp`、
|
||||
`baseline-destalpha.bmp` / `final-destalpha.bmp`、`pixel-comparison.json`:相同绘制数据回放及像素差异。
|
||||
- `movement.png`、`inventory.png`、`resume.png`、`normal-login.png`:功能截图。
|
||||
- `final.apk`:最终已安装版本;`resume-build.log` / `macos-build.log`:最终构建记录。
|
||||
|
||||
本轮没有进行真实服务器的账号登录、多人战斗、长时间挂机和副本压力测试;30 FPS 结论适用于上述测试场景。
|
||||
|
||||
## 最终安装版结果
|
||||
|
||||
未传分辨率、MSAA、帧率参数时,日志确认 `msaa=2 render_scale=0.750000 fps_mode=0 fps_cap=30`。
|
||||
6 个有效窗口平均 29.98 FPS、GPU 27.20 ms,帧时间 p95 平均 33.80 ms,RSS 约 565 MB。
|
||||
触控移动和背包打开通过截图检查。切后台测试中 Surface 在 present 阶段丢失,恢复前台后重建成功,
|
||||
进程 PID 10734 保持不变,原游戏场景和背包继续显示;随后在服务器选择界面重复两次切前后台,
|
||||
PID 11014 同样保持不变(`resume-cycles.json`)。最终恢复服务器选择界面,崩溃缓冲区为空。
|
||||
|
||||
最终 APK SHA256:`c414589acf29c1fe71dc6efc8ccf6526847a90116bbf4f0b399f4caee4ed15cf`。
|
||||
@@ -177,3 +177,13 @@ python3 tools/perf/run_render_isolation.py build/qa/render-isolation
|
||||
The runner uses the same village/24-monster fixture, 60% scene scale, no MSAA and a 30 FPS cap. It saves
|
||||
six two-second windows after discarding the first interval for each section, screenshots, memory snapshots,
|
||||
and `summary.json`. The final repeated baseline checks drift. It restores the normal login launch when finished.
|
||||
|
||||
|
||||
### Fixed-function shader execution
|
||||
|
||||
Draw constants use a std140 dynamic uniform buffer; bone palettes remain storage buffers.
|
||||
Fragment pipelines specialize the two texture-stage programs and alpha-test enable/function.
|
||||
The cache includes every specialization value, while alpha reference, fog and texture-factor values remain dynamic.
|
||||
`--generic-shaders` / `MT_GENERIC_SHADERS=1` disables specialization for rendering comparisons.
|
||||
Android Adreno 512 starts in 30 FPS saver mode when no frame-rate preference exists; saved preferences and test overrides take priority.
|
||||
See [Mi 8 Lite measurements](../../docs/XIAOMI8LITE-ADAPTATION-20260930.md).
|
||||
|
||||
@@ -23,9 +23,9 @@ int g_fps_mode = -1;
|
||||
int g_idle_fps = 30;
|
||||
double g_idle_after_s = 15.0;
|
||||
|
||||
void FrameRatePolicy::load() {
|
||||
void FrameRatePolicy::load(bool prefer_saver) {
|
||||
path_ = settings_path();
|
||||
int mode = MtFrameRate::MODE_STANDARD;
|
||||
int mode = prefer_saver ? MtFrameRate::MODE_SAVER : MtFrameRate::MODE_STANDARD;
|
||||
if (std::ifstream in{path_}; in) in >> mode;
|
||||
if (g_fps_mode >= 0) mode = g_fps_mode;
|
||||
MtFrameRate::Set(mode);
|
||||
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
|
||||
bool pinned() const { return g_fps_cap > 0 || g_refresh_rate > 0; }
|
||||
|
||||
void load();
|
||||
void load(bool prefer_saver = false);
|
||||
|
||||
Decision decide(double idle_seconds, bool auto_hunt);
|
||||
|
||||
|
||||
@@ -407,9 +407,9 @@ int run_live_client(
|
||||
// test phone: 54 fps against the period, 60 against 80% of it).
|
||||
const auto budget_for = [](int fps) { return std::chrono::nanoseconds(800'000'000LL / std::max(1, fps)); };
|
||||
FrameRatePolicy policy;
|
||||
policy.load();
|
||||
policy.load(renderer.prefer_saver_default());
|
||||
const auto frame_budget = budget_for(
|
||||
g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60));
|
||||
g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : policy.decide(0.0, false).fps));
|
||||
android_perf::PerformanceHint hint;
|
||||
{
|
||||
std::vector<int32_t> tids{android_perf::current_thread_id()};
|
||||
|
||||
+2
-1
@@ -147,6 +147,7 @@ int main(int argc, char** argv) {
|
||||
else if (arg == "--fake-idle-ms" && i+1 < argc) setenv("MT_FAKE_IDLE_MS", argv[++i], 1);
|
||||
else if (arg == "--frames-in-flight" && i+1 < argc) frames_in_flight = std::stoi(argv[++i]);
|
||||
else if (arg == "--msaa" && i+1 < argc) setenv("MT_MSAA", argv[++i], 1);
|
||||
else if (arg == "--generic-shaders") setenv("MT_GENERIC_SHADERS", "1", 1);
|
||||
else if (arg == "--render-scale" && i+1 < argc) setenv("MT_RENDER_SCALE", argv[++i], 1);
|
||||
else if (arg == "--diagnostic-skip-render" && i+1 < argc) {
|
||||
const std::string section = argv[++i];
|
||||
@@ -163,7 +164,7 @@ int main(int argc, char** argv) {
|
||||
"[--draws N] [--triangles-per-draw N] [--capture FILE] [--animate-first-draw] "
|
||||
"[--animate-bones] [--no-vsync] [--live-client DIR] [--fake-mobs N] "
|
||||
"[--gpu-skinning|--no-gpu-skinning] [--no-terrain] [--mobile] [--capture-out FILE] [--screenshot-out FILE.bmp] [--show-fps] [--perf-log] "
|
||||
"[--fps-cap N] [--refresh-rate HZ] [--fps-mode 0|1|2] [--idle-fps N] [--idle-after S] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--render-scale 0.25..1] [--fake-idle-ms MS] [--cpu-shadow] [--diagnostic-skip-render terrain|characters|effects|touch|scene]");
|
||||
"[--fps-cap N] [--refresh-rate HZ] [--fps-mode 0|1|2] [--idle-fps N] [--idle-after S] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--generic-shaders] [--render-scale 0.25..1] [--fake-idle-ms MS] [--cpu-shadow] [--diagnostic-skip-render terrain|characters|effects|touch|scene]");
|
||||
}
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (native_perf::PerfLog::requested_by_env()) g_perf_log = true;
|
||||
|
||||
@@ -27,7 +27,7 @@ struct PushConstants {
|
||||
};
|
||||
static_assert(sizeof(PushConstants) <= 128, "PushConstants must fit within Vulkan's 128-byte minimum guarantee");
|
||||
|
||||
// std430 payload selected with a dynamic storage-buffer offset for every draw: the D3D8
|
||||
// std140 payload selected with a dynamic uniform-buffer offset for every draw: the D3D8
|
||||
// fixed-function state in effect for the draw (texture stages, texture coordinate processing,
|
||||
// lighting, material, fog, alpha test). A zero flags.x marks a 2D UI batch.
|
||||
struct alignas(16) FixedFunctionState {
|
||||
|
||||
@@ -8,12 +8,32 @@ layout(location = 0) out vec4 out_color;
|
||||
layout(set = 0, binding = 0) uniform sampler2D tex_sampler;
|
||||
layout(set = 0, binding = 1) uniform sampler2D mask_sampler;
|
||||
|
||||
// Resolve the fixed stage program when the pipeline is compiled. This removes
|
||||
// unused texture operations and discard paths, especially on older mobile GPUs.
|
||||
// The default retains the generic program for an explicit diagnostic comparison.
|
||||
layout(constant_id = 0) const uint specialized = 0u;
|
||||
layout(constant_id = 1) const uint fixed_function = 0u;
|
||||
layout(constant_id = 2) const uint color0_op = 0u;
|
||||
layout(constant_id = 3) const uint color0_arg1 = 0u;
|
||||
layout(constant_id = 4) const uint color0_arg2 = 0u;
|
||||
layout(constant_id = 5) const uint alpha0_op = 0u;
|
||||
layout(constant_id = 6) const uint alpha0_arg1 = 0u;
|
||||
layout(constant_id = 7) const uint alpha0_arg2 = 0u;
|
||||
layout(constant_id = 8) const uint color1_op = 0u;
|
||||
layout(constant_id = 9) const uint color1_arg1 = 0u;
|
||||
layout(constant_id = 10) const uint color1_arg2 = 0u;
|
||||
layout(constant_id = 11) const uint alpha1_op = 0u;
|
||||
layout(constant_id = 12) const uint alpha1_arg1 = 0u;
|
||||
layout(constant_id = 13) const uint alpha1_arg2 = 0u;
|
||||
layout(constant_id = 14) const uint alpha_test_enabled = 0u;
|
||||
layout(constant_id = 15) const uint alpha_test_func = 0u;
|
||||
|
||||
layout(push_constant) uniform DrawConstants {
|
||||
mat4 mvp;
|
||||
vec4 params; // bone base + 1, pretransformed, UI mask, unused
|
||||
} draw;
|
||||
|
||||
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
|
||||
layout(set = 1, binding = 1, std140) uniform FixedFunctionState {
|
||||
uvec4 stage_color[2];
|
||||
uvec4 stage_alpha[2];
|
||||
vec4 fog_color;
|
||||
@@ -78,19 +98,23 @@ vec4 apply_stage(uvec4 color_state, uvec4 alpha_state,
|
||||
}
|
||||
|
||||
void main() {
|
||||
if (fixed_state.flags.x != 0u) {
|
||||
if ((specialized != 0u ? fixed_function : fixed_state.flags.x) != 0u) {
|
||||
// D3D8 texture stage cascade: stage 1 runs unless its COLOROP is D3DTOP_DISABLE.
|
||||
vec4 diffuse = in_color;
|
||||
vec4 color = apply_stage(fixed_state.stage_color[0], fixed_state.stage_alpha[0],
|
||||
uvec4 c0 = specialized != 0u ? uvec4(color0_op, color0_arg1, color0_arg2, 0u) : fixed_state.stage_color[0];
|
||||
uvec4 a0 = specialized != 0u ? uvec4(alpha0_op, alpha0_arg1, alpha0_arg2, 0u) : fixed_state.stage_alpha[0];
|
||||
uvec4 c1 = specialized != 0u ? uvec4(color1_op, color1_arg1, color1_arg2, 0u) : fixed_state.stage_color[1];
|
||||
uvec4 a1 = specialized != 0u ? uvec4(alpha1_op, alpha1_arg1, alpha1_arg2, 0u) : fixed_state.stage_alpha[1];
|
||||
vec4 color = apply_stage(c0, a0,
|
||||
diffuse, diffuse, texture(tex_sampler, in_uv));
|
||||
if (fixed_state.stage_color[0].x > 1u && fixed_state.stage_color[1].x > 1u)
|
||||
color = apply_stage(fixed_state.stage_color[1], fixed_state.stage_alpha[1],
|
||||
if (c0.x > 1u && c1.x > 1u)
|
||||
color = apply_stage(c1, a1,
|
||||
diffuse, color, texture(mask_sampler, in_mask_uv));
|
||||
if (fixed_state.alpha_test.x != 0u) {
|
||||
if ((specialized != 0u ? alpha_test_enabled : fixed_state.alpha_test.x) != 0u) {
|
||||
// D3DRS_ALPHAREF is an 8-bit reference compared against the 8-bit pixel alpha.
|
||||
uint a = uint(clamp(color.a, 0.0, 1.0) * 255.0 + 0.5);
|
||||
uint ref = fixed_state.alpha_test.z & 255u;
|
||||
uint func = fixed_state.alpha_test.y;
|
||||
uint func = specialized != 0u ? alpha_test_func : fixed_state.alpha_test.y;
|
||||
bool passes = func == 1u ? false :
|
||||
func == 2u ? a < ref :
|
||||
func == 3u ? a == ref :
|
||||
|
||||
@@ -24,7 +24,7 @@ layout(push_constant) uniform DrawConstants {
|
||||
} draw;
|
||||
|
||||
// D3D8 fixed-function vertex state; see FixedFunctionState in render_state.h. Lights are in camera space.
|
||||
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
|
||||
layout(set = 1, binding = 1, std140) uniform FixedFunctionState {
|
||||
uvec4 stage_color[2]; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX
|
||||
uvec4 stage_alpha[2]; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS
|
||||
vec4 fog_color;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "host_util.h"
|
||||
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -43,6 +45,11 @@ void VulkanWindow::select_device() {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(physical_, &properties);
|
||||
device_name_ = properties.deviceName;
|
||||
#ifdef __ANDROID__
|
||||
// Measured on Mi 8 Lite: 30 FPS leaves GPU headroom at the normal 75% / 2x quality.
|
||||
// Only the initial setting changes; saved player preferences and CLI overrides win.
|
||||
prefer_saver_default_ = device_name_ == "Adreno (TM) 512" || device_name_ == "Adreno 512";
|
||||
#endif
|
||||
timestamp_period_ns_ = properties.limits.timestampPeriod;
|
||||
VkPhysicalDeviceFeatures supported_features{};
|
||||
vkGetPhysicalDeviceFeatures(physical_, &supported_features);
|
||||
@@ -191,6 +198,18 @@ void VulkanWindow::recreate_swapchain() {
|
||||
for (auto view : image_views_) vkDestroyImageView(device_, view, nullptr);
|
||||
image_views_.clear();
|
||||
if (swapchain_) { vkDestroySwapchainKHR(device_, swapchain_, nullptr); swapchain_ = VK_NULL_HANDLE; }
|
||||
if (surface_lost_) {
|
||||
// Present may have failed before consuming its semaphore. Recreate those
|
||||
// alongside the surface after all submitted work has finished.
|
||||
for (auto semaphore : rendered_) vkDestroySemaphore(device_, semaphore, nullptr);
|
||||
rendered_.clear();
|
||||
vkDestroySurfaceKHR(instance_, surface_, nullptr);
|
||||
surface_ = VK_NULL_HANDLE;
|
||||
if (!SDL_Vulkan_CreateSurface(window_, instance_, nullptr, &surface_))
|
||||
throw std::runtime_error(SDL_GetError());
|
||||
surface_lost_ = false;
|
||||
SDL_Log("Vulkan surface restored");
|
||||
}
|
||||
create_swapchain();
|
||||
create_framebuffers();
|
||||
get_pipeline(0, 2, 0);
|
||||
|
||||
+14
-3
@@ -32,7 +32,10 @@ void VulkanWindow::render(
|
||||
std::uint32_t ui_width,
|
||||
std::uint32_t ui_height,
|
||||
const std::vector<UIRenderCommand>& ui_commands) {
|
||||
if (extent_.width == 0 || extent_.height == 0) return;
|
||||
// SDL pumps Android pause/resume before this call. Retry a lost surface only
|
||||
// on the next iteration, after SDL can supply the resumed native window.
|
||||
if (surface_lost_) recreate_swapchain();
|
||||
if (surface_lost_ || extent_.width == 0 || extent_.height == 0) return;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
f_ = &frames_[frame_slot_ % frames_in_flight_];
|
||||
check(vkWaitForFences(device_, 1, &f_->fence, VK_TRUE, UINT64_MAX), "vkWaitForFences");
|
||||
@@ -42,6 +45,11 @@ void VulkanWindow::render(
|
||||
prune_textures();
|
||||
std::uint32_t image_index = 0;
|
||||
const auto acquired = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, f_->acquire, VK_NULL_HANDLE, &image_index);
|
||||
if (acquired == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
surface_lost_ = true;
|
||||
SDL_Log("Vulkan surface lost during acquire; waiting for resume");
|
||||
return;
|
||||
}
|
||||
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
recreate_swapchain();
|
||||
return;
|
||||
@@ -98,7 +106,10 @@ void VulkanWindow::render(
|
||||
// An Android surface can continuously report SUBOPTIMAL while the app deliberately
|
||||
// uses the identity transform. The image is still presentable; rebuilding every frame
|
||||
// stalls rendering without changing that status.
|
||||
if (presented == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
if (presented == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
surface_lost_ = true;
|
||||
SDL_Log("Vulkan surface lost during present; waiting for resume");
|
||||
} else if (presented == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
recreate_swapchain();
|
||||
} else if (presented != VK_SUCCESS && presented != VK_SUBOPTIMAL_KHR) {
|
||||
check(presented, "vkQueuePresentKHR");
|
||||
@@ -246,7 +257,7 @@ void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const T
|
||||
}
|
||||
}
|
||||
const auto pipeline = get_pipeline(cull, depth, blend, draw.lines,
|
||||
static_cast<std::uint8_t>(draw.z_func), target != nullptr);
|
||||
static_cast<std::uint8_t>(draw.z_func), target != nullptr, &draw);
|
||||
// D3DTOP_DISABLE (1) on stage 0 or 1 ends the cascade before stage 1 samples.
|
||||
const bool bind_tex1 = !draw.texture1.empty() && draw.color_op[0] > 1 && draw.color_op[1] > 1;
|
||||
const auto descriptor = get_texture_descriptor(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "host_util.h"
|
||||
#include "blend_state.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -149,16 +150,42 @@ void VulkanWindow::create_offscreen_pass() {
|
||||
}
|
||||
|
||||
VkPipeline VulkanWindow::get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
|
||||
bool lines, std::uint8_t z_func, bool offscreen) {
|
||||
const std::uint32_t key = std::uint32_t(cull) | (std::uint32_t(depth) << 8) |
|
||||
bool lines, std::uint8_t z_func, bool offscreen, const Render3DDraw* draw) {
|
||||
std::array<std::uint32_t, 17> key{};
|
||||
key[0] = std::uint32_t(cull) | (std::uint32_t(depth) << 8) |
|
||||
(std::uint32_t(blend) << 16) | (lines ? (1u << 24) : 0u) |
|
||||
(std::uint32_t(z_func & 0xfu) << 25) | (offscreen ? (1u << 29) : 0u);
|
||||
static const bool specialize = std::getenv("MT_GENERIC_SHADERS") == nullptr;
|
||||
key[1] = specialize ? 1u : 0u;
|
||||
if (specialize && draw) {
|
||||
key[2] = 1u;
|
||||
for (int stage = 0; stage < 2; ++stage) {
|
||||
const int base = 3 + stage * 6;
|
||||
key[base] = draw->color_op[stage];
|
||||
key[base + 1] = draw->color_arg1[stage];
|
||||
key[base + 2] = draw->color_arg2[stage];
|
||||
key[base + 3] = draw->alpha_op[stage];
|
||||
key[base + 4] = draw->alpha_arg1[stage];
|
||||
key[base + 5] = draw->alpha_arg2[stage];
|
||||
}
|
||||
key[15] = draw->alpha_test;
|
||||
key[16] = draw->alpha_test ? draw->alpha_func : 0u;
|
||||
}
|
||||
if (auto it = pipelines_.find(key); it != pipelines_.end()) return it->second;
|
||||
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
stages[0].sType = stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex_module_; stages[0].pName = "main";
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment_module_; stages[1].pName = "main";
|
||||
std::array<VkSpecializationMapEntry, 16> entries{};
|
||||
for (std::uint32_t i = 0; i < entries.size(); ++i)
|
||||
entries[i] = {i, i * std::uint32_t(sizeof(std::uint32_t)), sizeof(std::uint32_t)};
|
||||
VkSpecializationInfo specialization{};
|
||||
specialization.mapEntryCount = entries.size();
|
||||
specialization.pMapEntries = entries.data();
|
||||
specialization.dataSize = 16 * sizeof(std::uint32_t);
|
||||
specialization.pData = key.data() + 1;
|
||||
stages[1].pSpecializationInfo = &specialization;
|
||||
|
||||
VkVertexInputBindingDescription binding{0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX};
|
||||
VkVertexInputAttributeDescription attributes[9] = {
|
||||
|
||||
@@ -84,7 +84,9 @@ void VulkanWindow::create_descriptors_and_buffers() {
|
||||
bone_bindings[0].descriptorCount = 1;
|
||||
bone_bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
bone_bindings[1].binding = 1;
|
||||
bone_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
|
||||
// Draw state is constant across a draw. Uniform memory lets mobile GPUs broadcast
|
||||
// these values instead of fetching them as general storage data for each fragment.
|
||||
bone_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||
bone_bindings[1].descriptorCount = 1;
|
||||
bone_bindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
VkDescriptorSetLayoutCreateInfo bone_layout_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
@@ -95,7 +97,7 @@ void VulkanWindow::create_descriptors_and_buffers() {
|
||||
VkDescriptorPoolSize pool_sizes[3] = {
|
||||
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 16384},
|
||||
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4 * kMaxFramesInFlight},
|
||||
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 4 * kMaxFramesInFlight}};
|
||||
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 4 * kMaxFramesInFlight}};
|
||||
VkDescriptorPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
pool_info.maxSets = 8192;
|
||||
@@ -111,7 +113,7 @@ void VulkanWindow::create_descriptors_and_buffers() {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(physical_, &properties);
|
||||
const auto alignment = std::max<VkDeviceSize>(
|
||||
1, properties.limits.minStorageBufferOffsetAlignment);
|
||||
1, properties.limits.minUniformBufferOffsetAlignment);
|
||||
state_stride_ = ((sizeof(FixedFunctionState) + alignment - 1) / alignment) * alignment;
|
||||
staging_alignment_ = std::max<VkDeviceSize>(16, properties.limits.optimalBufferCopyOffsetAlignment);
|
||||
|
||||
@@ -124,7 +126,7 @@ void VulkanWindow::create_descriptors_and_buffers() {
|
||||
|
||||
slot.state_capacity = 256;
|
||||
void* state_raw = nullptr;
|
||||
create_host_buffer(slot.state_capacity * state_stride_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
|
||||
create_host_buffer(slot.state_capacity * state_stride_, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||
slot.state_buffer, slot.state_memory, &state_raw);
|
||||
slot.state_mapped = static_cast<std::uint8_t*>(state_raw);
|
||||
|
||||
@@ -144,7 +146,7 @@ void VulkanWindow::create_descriptors_and_buffers() {
|
||||
writes[binding].dstBinding = static_cast<std::uint32_t>(binding);
|
||||
writes[binding].descriptorCount = 1;
|
||||
writes[binding].descriptorType = binding == 0
|
||||
? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
|
||||
? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||
writes[binding].pBufferInfo = &buffer_infos[binding];
|
||||
}
|
||||
vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr);
|
||||
@@ -224,7 +226,7 @@ void VulkanWindow::ensure_state_capacity(std::size_t required) {
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
void* mapped = nullptr;
|
||||
create_host_buffer(next * state_stride_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
|
||||
create_host_buffer(next * state_stride_, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||
buffer, memory, &mapped);
|
||||
vkUnmapMemory(device_, f_->state_memory);
|
||||
vkDestroyBuffer(device_, f_->state_buffer, nullptr);
|
||||
@@ -238,7 +240,7 @@ void VulkanWindow::ensure_state_capacity(std::size_t required) {
|
||||
write.dstSet = f_->bone_descriptor_set;
|
||||
write.dstBinding = 1;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||
write.pBufferInfo = &info;
|
||||
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ VulkanWindow::PreparedDraw VulkanWindow::scene_composite_draw() {
|
||||
for (int i = 0; i < 16; ++i) m[i] = i % 5 == 0 ? 1.0f : 0.0f;
|
||||
PreparedDraw composite{};
|
||||
composite.geometry = &scene_quad_;
|
||||
composite.pipeline = get_pipeline(0, 0, 0);
|
||||
composite.pipeline = get_pipeline(0, 0, 0, false, 4, false, &quad);
|
||||
composite.descriptor = scene_texture_.descriptor;
|
||||
composite.constants = make_push_constants(quad, 0.0f);
|
||||
// Unit square (y down) to D3D NDC (y up), as the pretransformed draws map screen pixels.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -201,6 +202,7 @@ public:
|
||||
std::size_t texture_upload_count() const { return texture_upload_count_; }
|
||||
std::size_t texture_uploaded_bytes() const { return texture_uploaded_bytes_; }
|
||||
const std::string& device_name() const { return device_name_; }
|
||||
bool prefer_saver_default() const { return prefer_saver_default_; }
|
||||
const char* present_mode_name() const;
|
||||
Timings timings() const { return timings_; }
|
||||
native_perf::RendererTotals perf_totals() const;
|
||||
@@ -322,7 +324,8 @@ private:
|
||||
|
||||
// offscreen: for offscreen_pass_ (single-sampled render-target texture) instead of pass_.
|
||||
VkPipeline get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
|
||||
bool lines = false, std::uint8_t z_func = 4, bool offscreen = false);
|
||||
bool lines = false, std::uint8_t z_func = 4, bool offscreen = false,
|
||||
const Render3DDraw* draw = nullptr);
|
||||
|
||||
VkDescriptorSet allocate_texture_descriptor_set(VkImageView view0, VkImageView view1,
|
||||
VkSampler sampler0 = VK_NULL_HANDLE,
|
||||
@@ -395,7 +398,9 @@ private:
|
||||
VkDevice device_ = VK_NULL_HANDLE;
|
||||
VkQueue queue_ = VK_NULL_HANDLE;
|
||||
std::uint32_t queue_family_ = 0;
|
||||
bool surface_lost_ = false;
|
||||
std::string device_name_;
|
||||
bool prefer_saver_default_ = false;
|
||||
VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
|
||||
VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D extent_{};
|
||||
@@ -491,7 +496,9 @@ private:
|
||||
VkShaderModule vertex_module_ = VK_NULL_HANDLE;
|
||||
VkShaderModule fragment_module_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout_ = VK_NULL_HANDLE;
|
||||
std::unordered_map<std::uint32_t, VkPipeline> pipelines_;
|
||||
// Raster state followed by the exact fragment specialization values; no hash-only
|
||||
// identity, so different stage programs can never alias the same pipeline.
|
||||
std::map<std::array<std::uint32_t, 17>, VkPipeline> pipelines_;
|
||||
std::unordered_map<GeometryId, Geometry, GeometryIdHash> geometries_;
|
||||
std::uint64_t frame_number_ = 0;
|
||||
std::uint64_t timed_frames_ = 0;
|
||||
|
||||
@@ -22,7 +22,7 @@ def shell(command):
|
||||
return adb('shell', command).decode(errors='replace').strip()
|
||||
|
||||
def files():
|
||||
return set(shell('ls ' + PERF + 'perf-*.csv 2>/dev/null').splitlines())
|
||||
return set(shell('ls ' + PERF + 'perf-*.csv 2>/dev/null || true').splitlines())
|
||||
|
||||
def parse(text):
|
||||
return list(csv.DictReader(line for line in text.splitlines() if line and not line.startswith('#')))
|
||||
@@ -32,13 +32,17 @@ def main():
|
||||
parser.add_argument('output', type=Path)
|
||||
parser.add_argument('--sections', default='baseline,terrain,characters,effects,touch,scene,baseline_repeat')
|
||||
parser.add_argument('--windows', type=int, default=6)
|
||||
parser.add_argument('--render-scale', type=float, default=0.6)
|
||||
parser.add_argument('--msaa', type=int, choices=[1,2,4,8], default=1)
|
||||
parser.add_argument('--fps-cap', type=int, default=30)
|
||||
args = parser.parse_args()
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
results = []
|
||||
try:
|
||||
for section in args.sections.split(','):
|
||||
before = files()
|
||||
launch = '--interactive --fake-mobs 24 --perf-log --show-fps --fps-cap 30 --msaa 1 --render-scale 0.6'
|
||||
launch = ('--interactive --fake-mobs 24 --perf-log --show-fps --fps-cap %d --msaa %d --render-scale %g'
|
||||
% (args.fps_cap, args.msaa, args.render_scale))
|
||||
if not section.startswith('baseline'):
|
||||
launch += ' --diagnostic-skip-render ' + section
|
||||
shell('am force-stop ' + PKG)
|
||||
|
||||
Reference in New Issue
Block a user