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:
co-authored by
Claude Opus 5.5
parent
c2cc093ed0
commit
82503315c6
+149
-7
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user