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
@@ -10,11 +10,18 @@
|
||||
// - PerformanceHint: ADPF APerformanceHint (API 33). The loop reports each frame's CPU work against the
|
||||
// frame budget, so the governor raises the clocks before a frame misses instead of idling the CPU at
|
||||
// its lowest step between short bursts (the 556-748 MHz seen in perf-20260929-153205.csv).
|
||||
// prefer_power_efficiency(): APerformanceHint_setPreferPowerEfficiency (API 35), set by the frame-rate
|
||||
// policy (main.cpp FrameRatePolicy) except in the highest mode.
|
||||
// - set_display_refresh_rate() / max_refresh_rate(): MainActivity switches the display mode at run time
|
||||
// for the frame-rate setting (60 Hz unless the setting wants more).
|
||||
// - battery_power(): BatteryManager current + voltage for the perf log's power columns; the power is only
|
||||
// meaningful off the charger (USB adb counts as a charger: test over wireless adb).
|
||||
//
|
||||
// Every NDK entry point is looked up at run time: minSdk is 24. On other platforms all of this is a no-op.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -50,11 +57,48 @@ inline double call_activity_float(const char* name) {
|
||||
return hz;
|
||||
}
|
||||
|
||||
inline void call_activity_set_float(const char* name, float value) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (!env || !activity) return;
|
||||
jclass cls = env->GetObjectClass(activity);
|
||||
if (jmethodID method = env->GetMethodID(cls, name, "(F)V"))
|
||||
env->CallVoidMethod(activity, method, jfloat(value));
|
||||
if (env->ExceptionCheck()) env->ExceptionClear();
|
||||
env->DeleteLocalRef(cls);
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
|
||||
inline double display_refresh_rate() { return call_activity_float("currentRefreshRate"); }
|
||||
// The highest refresh rate the display offers at its current resolution; -1 if unknown.
|
||||
inline double max_refresh_rate() { return call_activity_float("maxRefreshRate"); }
|
||||
// Switches the display mode to the refresh rate closest to `hz` (MainActivity.preferredDisplayModeId).
|
||||
inline void set_display_refresh_rate(float hz) { call_activity_set_float("setDisplayRefreshRate", hz); }
|
||||
// The app-vsync rate (Choreographer); -1 until the first measurement.
|
||||
inline double render_rate() { return call_activity_float("currentRenderRate"); }
|
||||
inline double battery_celsius() { return call_activity_float("batteryTemperature"); }
|
||||
|
||||
struct BatteryPower {
|
||||
double current_ma = 0; // drawn from the battery, positive while discharging
|
||||
double voltage_mv = -1;
|
||||
double power_mw = -1; // current x voltage; -1 while plugged in or unknown
|
||||
bool plugged = false;
|
||||
};
|
||||
|
||||
// BatteryManager's CURRENT_NOW is uA by the docs but mA on some vendors, and its sign convention varies:
|
||||
// a game draws well over 20 mA, so a magnitude above 20000 can only be uA; unplugged, the whole current is
|
||||
// the draw. Plugged in, the charger supplies part of it and the reading says nothing about the game.
|
||||
inline BatteryPower battery_power() {
|
||||
BatteryPower out;
|
||||
const double raw = call_activity_float("batteryCurrentRaw");
|
||||
out.current_ma = std::fabs(raw) > 20000.0 ? std::fabs(raw) / 1000.0 : std::fabs(raw);
|
||||
out.voltage_mv = call_activity_float("batteryVoltage");
|
||||
out.plugged = call_activity_float("batteryPlugged") > 0.5;
|
||||
if (!out.plugged && out.current_ma > 0 && out.voltage_mv > 0)
|
||||
out.power_mw = out.current_ma * out.voltage_mv / 1000.0;
|
||||
return out;
|
||||
}
|
||||
|
||||
// false when the call is unavailable (API < 30) or rejected.
|
||||
inline bool request_frame_rate(SDL_Window* window, float fps) {
|
||||
using SetFrameRate = int32_t (*)(ANativeWindow*, float, int8_t);
|
||||
@@ -79,6 +123,8 @@ public:
|
||||
update_target_ = reinterpret_cast<UpdateTarget>(dlsym(lib, "APerformanceHint_updateTargetWorkDuration"));
|
||||
report_ = reinterpret_cast<Report>(dlsym(lib, "APerformanceHint_reportActualWorkDuration"));
|
||||
close_ = reinterpret_cast<Close>(dlsym(lib, "APerformanceHint_closeSession"));
|
||||
prefer_efficiency_ = reinterpret_cast<PreferEfficiency>(
|
||||
dlsym(lib, "APerformanceHint_setPreferPowerEfficiency"));
|
||||
if (!get_manager_ || !create_ || !report_ || !close_) return false;
|
||||
void* manager = get_manager_();
|
||||
if (!manager) return false;
|
||||
@@ -103,17 +149,29 @@ public:
|
||||
if (session_ && actual_ns > 0) report_(session_, actual_ns);
|
||||
}
|
||||
|
||||
// API 35: the scheduler may favour efficiency cores and lower clocks for the session's threads.
|
||||
// false when unavailable.
|
||||
bool prefer_power_efficiency(bool enabled) {
|
||||
if (!session_ || !prefer_efficiency_) return false;
|
||||
if (enabled == prefer_efficiency_on_) return true;
|
||||
prefer_efficiency_on_ = enabled;
|
||||
return prefer_efficiency_(session_, enabled) == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
using GetManager = void* (*)();
|
||||
using CreateSession = void* (*)(void*, const int32_t*, size_t, int64_t);
|
||||
using UpdateTarget = int (*)(void*, int64_t);
|
||||
using Report = int (*)(void*, int64_t);
|
||||
using Close = void (*)(void*);
|
||||
using PreferEfficiency = int (*)(void*, bool);
|
||||
GetManager get_manager_ = nullptr;
|
||||
CreateSession create_ = nullptr;
|
||||
UpdateTarget update_target_ = nullptr;
|
||||
Report report_ = nullptr;
|
||||
Close close_ = nullptr;
|
||||
PreferEfficiency prefer_efficiency_ = nullptr;
|
||||
bool prefer_efficiency_on_ = false;
|
||||
void* session_ = nullptr;
|
||||
std::int64_t target_ns_ = 0;
|
||||
};
|
||||
@@ -125,7 +183,14 @@ inline double display_refresh_rate() {
|
||||
return mode ? double(mode->refresh_rate) : -1.0;
|
||||
}
|
||||
inline double render_rate() { return display_refresh_rate(); }
|
||||
inline double max_refresh_rate() { return display_refresh_rate(); }
|
||||
inline void set_display_refresh_rate(float) {}
|
||||
inline double battery_celsius() { return -1.0; }
|
||||
struct BatteryPower {
|
||||
double current_ma = 0, voltage_mv = -1, power_mw = -1;
|
||||
bool plugged = false;
|
||||
};
|
||||
inline BatteryPower battery_power() { return {}; }
|
||||
inline bool request_frame_rate(SDL_Window*, float) { return false; }
|
||||
class PerformanceHint {
|
||||
public:
|
||||
@@ -133,6 +198,7 @@ public:
|
||||
bool active() const { return false; }
|
||||
void set_target(std::int64_t) {}
|
||||
void report(std::int64_t) {}
|
||||
bool prefer_power_efficiency(bool) { return false; }
|
||||
};
|
||||
inline int32_t current_thread_id() { return 0; }
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user