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
@@ -37,19 +37,20 @@ public class MainActivity extends SDLActivity {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
applyImmersiveMode();
chosenRefreshRate = applyRefreshRate(requestedRefreshRate(launchArgs()));
// --refresh-rate N pins the display mode for a test run; otherwise native_render switches it with
// the frame-rate setting (setDisplayRefreshRate).
final float pinned = requestedRefreshRate(launchArgs());
if (pinned > 0)
applyRefreshRate(pinned);
}
// The display rate picked in onCreate (0: left to the system); handed to native as --refresh-rate.
private int chosenRefreshRate = 0;
private String launchArgs() {
Intent intent = getIntent();
String args = intent != null ? intent.getStringExtra("args") : null;
return args == null || args.trim().isEmpty() ? DEFAULT_ARGS : args;
}
// --refresh-rate N (0 = system default); absent = the display's highest rate.
// --refresh-rate N (0 = system default); -1 when absent.
private static float requestedRefreshRate(String args) {
final String[] tokens = args.trim().split("\\s+");
for (int i = 0; i + 1 < tokens.length; ++i) {
@@ -141,6 +142,52 @@ public class MainActivity extends SDLActivity {
return tenths / 10f;
}
// Called from native code (android_perf.h): the display mode whose refresh rate is closest to `hz`
// (the highest one when hz < 0), for the frame-rate setting. The panel then need not scan out at 120 Hz
// for a 60 fps game.
public void setDisplayRefreshRate(float hz) {
runOnUiThread(() -> applyRefreshRate(hz));
}
// Called from native code (android_perf.h): the highest refresh rate of the current resolution.
public float maxRefreshRate() {
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return -1f;
float best = display.getRefreshRate();
final Display.Mode current = display.getMode();
for (Display.Mode mode : display.getSupportedModes())
if (mode.getPhysicalWidth() == current.getPhysicalWidth() &&
mode.getPhysicalHeight() == current.getPhysicalHeight())
best = Math.max(best, mode.getRefreshRate());
return best;
}
// Called from native code (android_perf.h): BATTERY_PROPERTY_CURRENT_NOW as reported -- uA per the
// docs, mA on some vendors; the sign convention varies too (android_perf::battery_power sorts both
// out). 0 if unknown.
public float batteryCurrentRaw() {
final BatteryManager manager = (BatteryManager) getSystemService(BATTERY_SERVICE);
if (manager == null)
return 0f;
final int raw = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW);
return raw == Integer.MIN_VALUE ? 0f : raw;
}
// Called from native code (android_perf.h): battery voltage in mV, -1 if unknown.
public float batteryVoltage() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return battery != null ? battery.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) : -1f;
}
// Called from native code (android_perf.h): 1 while a charger is connected (the current then no
// longer measures the game's draw), 0 otherwise.
public float batteryPlugged() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return battery != null && battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0 ? 1f : 0f;
}
// Called from native code (android_perf.h): the rate the display runs at now.
public float currentRefreshRate() {
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
@@ -204,8 +251,6 @@ public class MainActivity extends SDLActivity {
// Test phase: FPS readout in the top-left corner. Drop this line for release builds.
if (!args.contains("--show-fps"))
args = args.trim() + " --show-fps";
if (!args.contains("--refresh-rate") && chosenRefreshRate > 0)
args = args.trim() + " --refresh-rate " + chosenRefreshRate;
return args.trim().split("\\s+");
}