diff --git a/docs/ANDROID-TESTING.md b/docs/ANDROID-TESTING.md index eec1125a..5be041be 100644 --- a/docs/ANDROID-TESTING.md +++ b/docs/ANDROID-TESTING.md @@ -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= 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,结束后恢复正常登录启动。 diff --git a/docs/XIAOMI8LITE-ADAPTATION-20260930.md b/docs/XIAOMI8LITE-ADAPTATION-20260930.md new file mode 100644 index 00000000..1c78c7a8 --- /dev/null +++ b/docs/XIAOMI8LITE-ADAPTATION-20260930.md @@ -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`。 diff --git a/src/host/README.md b/src/host/README.md index b9f19248..af8f4b5c 100644 --- a/src/host/README.md +++ b/src/host/README.md @@ -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). diff --git a/src/host/frame_policy.cpp b/src/host/frame_policy.cpp index 99886de6..dbae71d1 100644 --- a/src/host/frame_policy.cpp +++ b/src/host/frame_policy.cpp @@ -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); diff --git a/src/host/frame_policy.h b/src/host/frame_policy.h index fbb8fe9a..09b045e5 100644 --- a/src/host/frame_policy.h +++ b/src/host/frame_policy.h @@ -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); diff --git a/src/host/live_client.cpp b/src/host/live_client.cpp index b2fd9bb6..e2a271ea 100644 --- a/src/host/live_client.cpp +++ b/src/host/live_client.cpp @@ -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 tids{android_perf::current_thread_id()}; diff --git a/src/host/main.cpp b/src/host/main.cpp index a854d9de..51c6cb10 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -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; diff --git a/src/host/render_state.h b/src/host/render_state.h index 51a585a0..05488a75 100644 --- a/src/host/render_state.h +++ b/src/host/render_state.h @@ -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 { diff --git a/src/host/shaders/native.frag b/src/host/shaders/native.frag index 6984eade..746bf887 100644 --- a/src/host/shaders/native.frag +++ b/src/host/shaders/native.frag @@ -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 : diff --git a/src/host/shaders/native.vert b/src/host/shaders/native.vert index 8ee4186f..d1a284e5 100644 --- a/src/host/shaders/native.vert +++ b/src/host/shaders/native.vert @@ -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; diff --git a/src/host/vk_device.cpp b/src/host/vk_device.cpp index 25008ba3..589ded2c 100644 --- a/src/host/vk_device.cpp +++ b/src/host/vk_device.cpp @@ -3,6 +3,8 @@ #include "host_util.h" +#include + #include #include #include @@ -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); diff --git a/src/host/vk_frame.cpp b/src/host/vk_frame.cpp index cdd9ada7..3fc7de55 100644 --- a/src/host/vk_frame.cpp +++ b/src/host/vk_frame.cpp @@ -32,7 +32,10 @@ void VulkanWindow::render( std::uint32_t ui_width, std::uint32_t ui_height, const std::vector& 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& draws, const T } } const auto pipeline = get_pipeline(cull, depth, blend, draw.lines, - static_cast(draw.z_func), target != nullptr); + static_cast(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( diff --git a/src/host/vk_pipelines.cpp b/src/host/vk_pipelines.cpp index 7c73264e..926224f7 100644 --- a/src/host/vk_pipelines.cpp +++ b/src/host/vk_pipelines.cpp @@ -4,6 +4,7 @@ #include "host_util.h" #include "blend_state.h" +#include #include #include @@ -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 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 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] = { diff --git a/src/host/vk_resources.cpp b/src/host/vk_resources.cpp index 35b144f7..416109c8 100644 --- a/src/host/vk_resources.cpp +++ b/src/host/vk_resources.cpp @@ -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( - 1, properties.limits.minStorageBufferOffsetAlignment); + 1, properties.limits.minUniformBufferOffsetAlignment); state_stride_ = ((sizeof(FixedFunctionState) + alignment - 1) / alignment) * alignment; staging_alignment_ = std::max(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(state_raw); @@ -144,7 +146,7 @@ void VulkanWindow::create_descriptors_and_buffers() { writes[binding].dstBinding = static_cast(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); } diff --git a/src/host/vk_scene.cpp b/src/host/vk_scene.cpp index b61d213c..d88e0adb 100644 --- a/src/host/vk_scene.cpp +++ b/src/host/vk_scene.cpp @@ -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. diff --git a/src/host/vulkan_window.h b/src/host/vulkan_window.h index 00e6d718..38e788f2 100644 --- a/src/host/vulkan_window.h +++ b/src/host/vulkan_window.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -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 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, VkPipeline> pipelines_; std::unordered_map geometries_; std::uint64_t frame_number_ = 0; std::uint64_t timed_frames_ = 0; diff --git a/tools/perf/run_render_isolation.py b/tools/perf/run_render_isolation.py index ddd39e8c..8224ef0a 100644 --- a/tools/perf/run_render_isolation.py +++ b/tools/perf/run_render_isolation.py @@ -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)