native: frame-rate setting, idle frame rate and battery power telemetry

- MtFrameRate setting (30 / 60 default / display max) as app.Get/SetFrameRateMode,
  with a "画面帧率" row injected into the system option dialog (mt_framerate); saved
  in the SDL pref path.
- FrameRatePolicy in the live loop: 30 fps after 15 s without input (not while auto
  hunting), display switched to 60 Hz unless the highest mode, ADPF
  setPreferPowerEfficiency, vsync pacing when the display already runs at the target.
  --fps-mode / --idle-fps / --idle-after; --fps-cap / --refresh-rate still pin a test run.
- Perf log: battery current/voltage/power, plugged, fps_mode, idle columns;
  perf_summary groups power by mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 18:14:19 +09:00
co-authored by Claude Opus 5.5
parent c2cc093ed0
commit 82503315c6
9 changed files with 453 additions and 19 deletions
+149 -7
View File
@@ -12,6 +12,7 @@
#include "platform/ScriptLib/PythonBoot.h"
#include "platform/UserInterface/ServerClock.h"
#include "platform/EterBase/TraceErrorObserver.h"
#include "platform/EterBase/FrameRateMode.h"
#include "../tests/port_login_flow_server.h"
#include <thread>
#endif
@@ -1033,6 +1034,13 @@ public:
out.write(reinterpret_cast<const char*>(file.data()), std::streamsize(file.size()));
}
// Seconds since the last touch, mouse or key event; 0 while a finger or mouse button is held (a held
// joystick or attack button sends no events). The frame-rate policy's idle test.
double input_idle_seconds() const {
if (fingers_down_ > 0 || buttons_down_ > 0) return 0.0;
return std::chrono::duration<double>(std::chrono::steady_clock::now() - last_input_).count();
}
void request_screenshot(std::string path, std::uint64_t frame) {
screenshot_path_ = std::move(path);
screenshot_frame_ = frame;
@@ -1277,6 +1285,7 @@ public:
#endif
SDL_Event event;
while (SDL_PollEvent(&event)) {
note_input(event);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// Touches arrive as SDL_EVENT_FINGER_*; the touch-synthesized mouse copy
// would register as a second finger (101) and turn every drag into a pinch.
@@ -3570,6 +3579,27 @@ private:
VkExtent2D extent_{};
std::vector<VkImageView> image_views_;
std::vector<VkImage> swapchain_images_;
void note_input(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_FINGER_DOWN: ++fingers_down_; break;
case SDL_EVENT_FINGER_UP:
case SDL_EVENT_FINGER_CANCELED: fingers_down_ = std::max(0, fingers_down_ - 1); break;
case SDL_EVENT_MOUSE_BUTTON_DOWN: ++buttons_down_; break;
case SDL_EVENT_MOUSE_BUTTON_UP: buttons_down_ = std::max(0, buttons_down_ - 1); break;
case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WILL_ENTER_BACKGROUND:
// An up event lost with the focus must not keep the client out of the idle rate for good.
fingers_down_ = buttons_down_ = 0;
break;
case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_DID_ENTER_FOREGROUND: break;
default: return;
}
last_input_ = std::chrono::steady_clock::now();
}
std::chrono::steady_clock::time_point last_input_ = std::chrono::steady_clock::now();
int fingers_down_ = 0, buttons_down_ = 0;
// --screenshot-out: read back the swapchain image of one frame into a BMP file.
std::string screenshot_path_;
std::uint64_t screenshot_frame_ = 0;
@@ -3739,6 +3769,74 @@ int g_fps_cap = 0;
// --refresh-rate N: the surface's preferred frame rate (Android ANativeWindow_setFrameRate; MainActivity
// also picks the matching display mode). 0 leaves the system default.
int g_refresh_rate = 0;
// --fps-mode N: the frame-rate setting for this run (a test override: the saved setting stays);
// --idle-fps N: the rate after --idle-after S seconds without input (0 = never lower it).
int g_fps_mode = -1;
int g_idle_fps = 30;
double g_idle_after_s = 15.0;
// The frame-rate setting (system option dialog, platform/EterBase/FrameRateMode.h) turned into a frame
// rate, a display refresh rate and the ADPF budget; --fps-cap / --refresh-rate pin those for a test run.
// Frames the player does not watch closely cost the same power as the ones they do, so after a while
// without input (and no auto hunt running) the rate drops to --idle-fps until the next touch. The display
// is asked for 60 Hz unless the setting wants more: a 120 Hz panel scanning out a 60 fps game costs power
// for nothing.
class FrameRatePolicy {
public:
struct Decision {
int mode = -1;
int fps = 0; // frames per second the loop aims at
int display_hz = 0; // the refresh rate asked of the display
bool idle = false;
bool operator==(const Decision&) const = default;
};
bool pinned() const { return g_fps_cap > 0 || g_refresh_rate > 0; }
void load() {
path_ = settings_path();
int mode = MtFrameRate::MODE_STANDARD;
if (std::ifstream in{path_}; in) in >> mode;
if (g_fps_mode >= 0) mode = g_fps_mode;
MtFrameRate::Set(mode);
saved_mode_ = MtFrameRate::Get();
max_hz_ = int(std::lround(android_perf::max_refresh_rate()));
if (max_hz_ < 30) max_hz_ = 60;
}
Decision decide(double idle_seconds, bool auto_hunt) {
Decision d;
d.mode = MtFrameRate::Get();
if (d.mode != saved_mode_) save(saved_mode_ = d.mode);
const int active = d.mode == MtFrameRate::MODE_SAVER ? 30
: d.mode == MtFrameRate::MODE_HIGH ? max_hz_ : std::min(60, max_hz_);
d.idle = g_idle_fps > 0 && g_idle_fps < active && idle_seconds >= g_idle_after_s && !auto_hunt;
d.fps = d.idle ? g_idle_fps : active;
d.display_hz = d.mode == MtFrameRate::MODE_HIGH && !d.idle ? max_hz_ : std::min(60, max_hz_);
return d;
}
int max_hz() const { return max_hz_; }
private:
static std::string settings_path() {
std::string dir;
if (char* pref = SDL_GetPrefPath("metin2port", "client")) {
dir = pref;
SDL_free(pref);
}
return dir + "frame_rate.cfg";
}
void save(int mode) {
std::ofstream out{path_, std::ios::trunc};
out << mode << '\n';
}
std::string path_;
int saved_mode_ = -1;
int max_hz_ = 60;
};
bool py_exec(const std::string& code) {
std::string err;
@@ -4083,16 +4181,21 @@ int run_live_client(
perf_log.set_refresh_rate_source([] { return android_perf::display_refresh_rate(); });
perf_log.set_render_rate_source([] { return android_perf::render_rate(); });
perf_log.set_battery_source([] { return android_perf::battery_celsius(); });
perf_log.set_power_source([] {
const auto p = android_perf::battery_power();
return native_perf::PowerSample{p.current_ma, p.voltage_mv, p.power_mw, p.plugged};
});
// ADPF: each frame's CPU work (everything but the vsync/fence waits and the cap's sleep) against the
// frame budget. Opened once the script thread exists, i.e. here.
using steady = std::chrono::steady_clock;
// The target is 80% of the frame period: the governor settles the clocks so the reported work just
// meets the target, so a target equal to the period leaves every other frame late (cap 60 on the
// test phone: 54 fps against the period, 60 against 80% of it).
const auto frame_budget = [] {
const int fps = g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60);
return std::chrono::nanoseconds(800'000'000LL / fps);
}();
const auto budget_for = [](int fps) { return std::chrono::nanoseconds(800'000'000LL / std::max(1, fps)); };
FrameRatePolicy policy;
policy.load();
const auto frame_budget = budget_for(
g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60));
android_perf::PerformanceHint hint;
{
std::vector<int32_t> tids{android_perf::current_thread_id()};
@@ -4106,10 +4209,46 @@ int run_live_client(
int completed = 0;
std::vector<double> frame_ms;
if (frames > 0) frame_ms.reserve(static_cast<std::size_t>(frames));
const auto cap_period = g_fps_cap > 0 ? std::chrono::nanoseconds(1'000'000'000LL / g_fps_cap)
: std::chrono::nanoseconds(0);
auto cap_period = g_fps_cap > 0 ? std::chrono::nanoseconds(1'000'000'000LL / g_fps_cap)
: std::chrono::nanoseconds(0);
auto next_frame_at = steady::now();
FrameRatePolicy::Decision decision;
auto display_checked_at = steady::time_point{};
// The loop sleeps to the policy's rate unless the display already runs at it (vsync then paces the
// loop, and a second clock beating against it would drop frames).
const auto apply_policy = [&] {
if (policy.pinned()) return;
const auto now = steady::now();
const auto next = policy.decide(renderer.input_idle_seconds(), PythonBoot::AutoHuntIsEnabled());
const bool changed = !(next == decision);
if (changed) {
if (next.display_hz != decision.display_hz) {
android_perf::set_display_refresh_rate(float(next.display_hz));
android_perf::request_frame_rate(renderer.sdl_window(), float(next.display_hz));
}
hint.set_target(budget_for(next.fps).count());
// Efficiency cores and lower clocks, except where the setting asks for the highest rate.
hint.prefer_power_efficiency(next.mode != MtFrameRate::MODE_HIGH || next.idle);
perf_log.set_fps_cap(next.fps);
perf_log.set_frame_policy(next.mode, next.idle);
SDL_Log("frame rate: mode %d -> %d fps, display %d Hz%s", next.mode, next.fps, next.display_hz,
next.idle ? " (idle)" : "");
decision = next;
}
if (changed || now - display_checked_at > std::chrono::seconds(1)) {
display_checked_at = now;
const double hz = android_perf::display_refresh_rate();
const bool vsync_paced = hz > 0 && std::fabs(hz - decision.fps) < 2.0;
const auto period = vsync_paced ? std::chrono::nanoseconds(0)
: std::chrono::nanoseconds(1'000'000'000LL / decision.fps);
if (period != cap_period) {
cap_period = period;
next_frame_at = now;
}
}
};
while ((frames == 0 || completed < frames) && renderer.poll(true) && PythonBoot::IsAppLooping()) {
apply_policy();
double pace_ms = 0.0;
if (cap_period.count()) {
// Fixed cadence: a late frame starts the next one at once, a frame more than one period
@@ -4255,6 +4394,9 @@ int main(int argc, char** argv) {
else if (arg == "--perf-log") g_perf_log = true;
else if (arg == "--fps-cap" && i+1 < argc) g_fps_cap = std::max(0, std::stoi(argv[++i]));
else if (arg == "--refresh-rate" && i+1 < argc) g_refresh_rate = std::max(0, std::stoi(argv[++i]));
else if (arg == "--fps-mode" && i+1 < argc) g_fps_mode = std::stoi(argv[++i]);
else if (arg == "--idle-fps" && i+1 < argc) g_idle_fps = std::max(0, std::stoi(argv[++i]));
else if (arg == "--idle-after" && i+1 < argc) g_idle_after_s = std::max(0.0, std::stod(argv[++i]));
#endif
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]);
@@ -4266,7 +4408,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] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--fake-idle-ms MS] [--cpu-shadow]");
"[--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] [--fake-idle-ms MS] [--cpu-shadow]");
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (native_perf::PerfLog::requested_by_env()) g_perf_log = true;