native: perf telemetry, 2 frames in flight, 120 Hz pacing, GPU shadow map

- --perf-log CSV + logcat telemetry (perf_log.h, EterBase/PerfCounters.h),
  incl. display refresh rate / app vsync rate, thermal, CPU clocks;
  tools/pull_perf_logs.sh + tools/perf_summary.py
- Vulkan renderer: two frames in flight, in-frame texture uploads (no
  vkQueueWaitIdle), --fps-cap / --refresh-rate / --frames-in-flight
- Android: ANativeWindow_setFrameRate, ADPF performance hint,
  appCategory=game (ColorOS no longer drops to 60 Hz when idle)
- Character shadow map rendered on the GPU into an offscreen render target
  instead of CPU rasterization + per-frame 1 MB upload; --cpu-shadow keeps
  the old path. Device (120 Hz, 32 mobs): 75 -> ~115 fps, script thread
  8.9 -> 3.4 ms. draw_capture format v10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 17:59:23 +09:00
co-authored by Claude Opus 5.5
parent 3e3708ef06
commit c2cc093ed0
16 changed files with 1883 additions and 261 deletions
@@ -7,6 +7,8 @@
<application android:label="@string/app_name"
android:allowBackup="false"
android:appCategory="game"
android:isGame="true"
android:extractNativeLibs="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true"
@@ -1,7 +1,9 @@
package org.metin2port.client;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -9,7 +11,9 @@ import android.view.Display;
import android.view.DisplayCutout;
import android.view.HapticFeedbackConstants;
import android.view.RoundedCorner;
import android.view.Choreographer;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
@@ -24,13 +28,124 @@ import org.libsdl.app.SDLActivity;
* without --live-server (e.g. --es args "--fake-mobs 64") run the in-process fake-server smoke flow.
*/
public class MainActivity extends SDLActivity {
private static final String DEFAULT_ARGS = "--live-server 192.168.21.203:11000:13000 --login-screen";
// --perf-log: native_render/perf_log.h writes perf-*.csv under files/perf (tools/pull_perf_logs.sh).
private static final String DEFAULT_ARGS =
"--live-server 192.168.21.203:11000:13000 --login-screen --perf-log --show-fps";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
applyImmersiveMode();
chosenRefreshRate = applyRefreshRate(requestedRefreshRate(launchArgs()));
}
// 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.
private static float requestedRefreshRate(String args) {
final String[] tokens = args.trim().split("\\s+");
for (int i = 0; i + 1 < tokens.length; ++i) {
if (tokens[i].equals("--refresh-rate")) {
try {
return Float.parseFloat(tokens[i + 1]);
} catch (NumberFormatException e) {
return -1;
}
}
}
return -1;
}
/**
* Asks for the display mode with the current resolution and the refresh rate closest to `wanted`
* (the highest one when wanted < 0). The vendor can still override it; currentRefreshRate() reports
* what the display actually runs at.
*/
private int applyRefreshRate(float wanted) {
if (wanted == 0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return 0;
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return 0;
final Display.Mode current = display.getMode();
Display.Mode best = null;
for (Display.Mode mode : display.getSupportedModes()) {
if (mode.getPhysicalWidth() != current.getPhysicalWidth() ||
mode.getPhysicalHeight() != current.getPhysicalHeight())
continue;
if (best == null) {
best = mode;
continue;
}
final float rate = mode.getRefreshRate();
final boolean better = wanted < 0 ? rate > best.getRefreshRate()
: Math.abs(rate - wanted) < Math.abs(best.getRefreshRate() - wanted);
if (better)
best = mode;
}
if (best == null)
return 0;
final WindowManager.LayoutParams params = getWindow().getAttributes();
params.preferredDisplayModeId = best.getModeId();
getWindow().setAttributes(params);
Log.i("Metin2Args", "display mode " + best.getModeId() + " " + best.getPhysicalWidth() + "x" +
best.getPhysicalHeight() + " @ " + best.getRefreshRate() + " Hz");
return Math.round(best.getRefreshRate());
}
// The app-vsync rate, measured with a Choreographer callback. It differs from the panel's refresh rate
// when the system renders the app at a divisor of it (ColorOS drops a non-game app to 60 fps after
// 2.5 s without a touch while the panel stays at 120 Hz). Started by the first currentRenderRate().
private volatile float renderRate = -1f;
private boolean renderRateStarted = false;
// Called from native code (android_perf.h), off the UI thread.
public float currentRenderRate() {
synchronized (this) {
if (!renderRateStarted) {
renderRateStarted = true;
runOnUiThread(() -> Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long windowStart = 0;
int frames = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (windowStart == 0) {
windowStart = frameTimeNanos;
} else if (++frames >= 30) {
renderRate = frames * 1e9f / (frameTimeNanos - windowStart);
windowStart = frameTimeNanos;
frames = 0;
}
Choreographer.getInstance().postFrameCallback(this);
}
}));
}
}
return renderRate;
}
// Called from native code (android_perf.h): battery temperature in C, -1 if unknown.
public float batteryTemperature() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
final int tenths = battery != null ? battery.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -10) : -10;
return tenths / 10f;
}
// 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()
: getWindowManager().getDefaultDisplay();
return display != null ? display.getRefreshRate() : -1f;
}
@Override
@@ -89,6 +204,8 @@ 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+");
}