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
@@ -0,0 +1,31 @@
#pragma once
// PORT: the player's frame-rate setting (system option dialog, app.Get/SetFrameRateMode), shared between
// the script thread and the host's render loop (native_render/main.cpp), which paces frames by it and
// saves it. 40250 has no counterpart: the PC client ran at the monitor's rate.
#include <atomic>
namespace MtFrameRate {
enum EMode {
MODE_SAVER = 0, // 30 fps
MODE_STANDARD = 1, // 60 fps, 30 after a while without input
MODE_HIGH = 2, // the display's highest rate (120 on current phones), 30 after a while without input
MODE_COUNT
};
inline std::atomic<int>& Mode()
{
static std::atomic<int> mode{MODE_STANDARD};
return mode;
}
inline int Get() { return Mode().load(std::memory_order_relaxed); }
inline void Set(int mode)
{
if (mode >= 0 && mode < MODE_COUNT)
Mode().store(mode, std::memory_order_relaxed);
}
} // namespace MtFrameRate
@@ -0,0 +1,75 @@
// The frame-rate row of the system option dialog (PORT; 40250 has no counterpart, the PC client ran at
// the monitor's rate), run by PythonBoot::RunMainScript as module mt_framerate. It wraps
// uiSystemOption.OptionDialog's __Load and fills the row the original layout leaves empty at y 210 (its
// shadow slider is commented out), in the style of the fog buttons above it. The value is
// app.Get/SetFrameRateMode (platform/EterBase/FrameRateMode.h); the host paces frames by it and saves it.
// Strings are UTF-8 here and go through player.AutoHuntLocalize into the locale's code page.
static const char c_szFrameRateOptionScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import player
import ui
import uiSystemOption
L = player.AutoHuntLocalize
SMALL_BUTTON = "d:/ymir work/ui/public/small_button_%02d.sub"
ROW_Y = 210
MODE_TEXTS = ("30帧", "60帧", "最高")
def _Refresh(buttons):
mode = app.GetFrameRateMode()
for i in xrange(len(buttons)):
if i == mode:
buttons[i].Down()
else:
buttons[i].SetUp()
def _Select(buttons, mode):
app.SetFrameRateMode(mode)
_Refresh(buttons)
def _Attach(dialog):
board = dialog.GetChild("board")
label = ui.TextLine()
label.SetParent(board)
label.SetPosition(30, ROW_Y + 2)
label.SetText(L("画面帧率"))
label.Show()
buttons = []
for i in xrange(len(MODE_TEXTS)):
button = ui.RadioButton()
button.SetParent(board)
button.SetUpVisual(SMALL_BUTTON % 1)
button.SetOverVisual(SMALL_BUTTON % 2)
button.SetDownVisual(SMALL_BUTTON % 3)
button.SetPosition(110 + 50 * i, ROW_Y)
button.SetText(L(MODE_TEXTS[i]))
button.SetEvent(lambda mode=i: _Select(buttons, mode))
button.Show()
buttons.append(button)
_Refresh(buttons)
dialog.mtFrameRateLabel = label
dialog.mtFrameRateButtons = buttons
def __Install():
load = uiSystemOption.OptionDialog._OptionDialog__Load
def Load(self):
load(self)
try:
_Attach(self)
except:
import dbg
dbg.TraceError("mt_framerate: cannot add the frame-rate row")
uiSystemOption.OptionDialog._OptionDialog__Load = Load
__Install()
)PY";
@@ -876,6 +876,7 @@ namespace PythonBoot
// have to run once the root scripts are importable; a failure leaves the game without them, not broken.
#include "AutoHuntScript.inc"
#include "RegisterScript.inc"
#include "FrameRateOptionScript.inc"
static void install_script_module(const char* c_szModule, const char* c_szScript)
{
@@ -931,6 +932,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
{
install_script_module("mt_autohunt", c_szAutoHuntScript);
install_script_module("mt_register", c_szRegisterScript);
install_script_module("mt_framerate", c_szFrameRateOptionScript);
}
return true;
}
@@ -3,6 +3,7 @@
#include "resource.h"
#include "PythonApplication.h"
#include "../EterLib/Camera.h"
#include "../../platform/EterBase/FrameRateMode.h" // PORT: frame-rate setting
extern bool PERF_CHECKER_RENDER_GAME;
extern D3DXCOLOR g_fSpecularColor;
@@ -1191,6 +1192,21 @@ PyObject* appLogoClose(PyObject* poSelf, PyObject* poArgs)
return Py_BuildNone();
}
// PORT: the frame-rate setting of the system option dialog (platform/EterBase/FrameRateMode.h).
PyObject* appGetFrameRateMode(PyObject* poSelf, PyObject* poArgs)
{
return Py_BuildValue("i", MtFrameRate::Get());
}
PyObject* appSetFrameRateMode(PyObject* poSelf, PyObject* poArgs)
{
int iMode;
if (!PyTuple_GetInteger(poArgs, 0, &iMode))
return Py_BuildException();
MtFrameRate::Set(iMode);
return Py_BuildNone();
}
void initapp()
{
static PyMethodDef s_methods[] =
@@ -1257,6 +1273,8 @@ void initapp()
{ "GetCursorPosition", appGetCursorPosition, METH_VARARGS },
{ "GetRandom", appGetRandom, METH_VARARGS },
{ "GetFrameRateMode", appGetFrameRateMode, METH_VARARGS }, // PORT
{ "SetFrameRateMode", appSetFrameRateMode, METH_VARARGS }, // PORT
{ "RunPythonFile", appRunPythonFile, METH_VARARGS },
{ "IsWebPageMode", appIsWebPageMode, METH_VARARGS },
{ "ShowWebPage", appShowWebPage, METH_VARARGS },