added fps counter

This commit is contained in:
izzy2lost
2026-02-28 22:20:43 -05:00
parent 4485767722
commit f53eb54a29
5 changed files with 209 additions and 0 deletions
@@ -387,8 +387,44 @@ static struct {
.cond = PTHREAD_COND_INITIALIZER,
};
static struct {
pthread_mutex_t lock;
uint64_t window_start_ms;
uint64_t last_frame_ms;
uint32_t frame_count;
float fps;
} g_fps_state = {
.lock = PTHREAD_MUTEX_INITIALIZER,
};
static void update_android_fps_counter(void)
{
const uint64_t now_ms = (uint64_t)SDL_GetTicks64();
pthread_mutex_lock(&g_fps_state.lock);
if (g_fps_state.window_start_ms == 0) {
g_fps_state.window_start_ms = now_ms;
}
g_fps_state.last_frame_ms = now_ms;
g_fps_state.frame_count++;
const uint64_t elapsed_ms = now_ms - g_fps_state.window_start_ms;
if (elapsed_ms >= 500) {
g_fps_state.fps = ((float)g_fps_state.frame_count * 1000.0f) /
(float)elapsed_ms;
g_fps_state.frame_count = 0;
g_fps_state.window_start_ms = now_ms;
}
pthread_mutex_unlock(&g_fps_state.lock);
}
void xemu_android_process_snapshot_request(void)
{
update_android_fps_counter();
if (pthread_mutex_trylock(&g_snap_req.lock) != 0) {
return;
}
@@ -460,3 +496,27 @@ Java_com_izzy2lost_x1box_MainActivity_nativeLoadSnapshot(
(void)obj;
return dispatch_snapshot(env, name, SNAP_LOAD);
}
JNIEXPORT jfloat JNICALL
Java_com_izzy2lost_x1box_MainActivity_nativeGetFps(
JNIEnv *env, jobject obj)
{
float fps = 0.0f;
const uint64_t now_ms = (uint64_t)SDL_GetTicks64();
(void)env;
(void)obj;
pthread_mutex_lock(&g_fps_state.lock);
fps = g_fps_state.fps;
if (g_fps_state.last_frame_ms == 0 ||
(now_ms - g_fps_state.last_frame_ms) > 1500) {
fps = 0.0f;
}
pthread_mutex_unlock(&g_fps_state.lock);
if (fps < 0.0f) {
fps = 0.0f;
}
return (jfloat)fps;
}
@@ -3,10 +3,16 @@ package com.izzy2lost.x1box
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Typeface
import android.hardware.input.InputManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.Gravity
import android.view.InputDevice
import android.view.KeyEvent
import android.view.View
@@ -17,6 +23,7 @@ import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ListView
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@@ -25,12 +32,14 @@ import org.libsdl.app.SDLActivity
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Locale
class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
companion object {
const val EXTRA_AUTO_LOAD_SNAPSHOT_SLOT = "com.izzy2lost.x1box.extra.AUTO_LOAD_SNAPSHOT_SLOT"
private const val SNAPSHOT_PREVIEW_HEADER_SIZE = 12
private const val TOTAL_SNAPSHOT_SLOTS = 10
private const val PREF_SHOW_FPS_COUNTER = "setting_show_fps_counter"
}
private data class SnapshotSlotPreview(
@@ -51,6 +60,17 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private var comboTriggered = false
private var startupSnapshotSlot: Int? = null
private var startupSnapshotLoadScheduled = false
private var showFpsCounter = false
private var fpsCounterView: TextView? = null
private val fpsUpdateHandler = Handler(Looper.getMainLooper())
private val fpsUpdateRunnable = object : Runnable {
override fun run() {
val view = fpsCounterView ?: return
val fps = nativeGetFps().coerceAtLeast(0f)
view.text = String.format(Locale.US, "FPS %.1f", fps)
fpsUpdateHandler.postDelayed(this, 500L)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -58,7 +78,11 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
if (requestedSlot in 1..TOTAL_SNAPSHOT_SLOTS) {
startupSnapshotSlot = requestedSlot
}
showFpsCounter = isFpsCounterEnabled()
setupOnScreenController()
if (showFpsCounter) {
setupFpsCounter()
}
setupControllerDetection()
hideSystemUI()
}
@@ -141,6 +165,107 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
updateControllerVisibility()
}
private fun dpToPx(dp: Int): Int = (dp * resources.displayMetrics.density).toInt()
private fun isFpsCounterEnabled(): Boolean {
return getSharedPreferences("x1box_prefs", MODE_PRIVATE)
.getBoolean(PREF_SHOW_FPS_COUNTER, false)
}
private fun applyFpsCounterPreference() {
val shouldShow = isFpsCounterEnabled()
if (shouldShow == showFpsCounter) {
if (showFpsCounter && fpsCounterView == null) {
setupFpsCounter()
}
return
}
showFpsCounter = shouldShow
if (showFpsCounter) {
setupFpsCounter()
startFpsUpdates()
} else {
stopFpsUpdates()
fpsCounterView?.let { view ->
(view.parent as? ViewGroup)?.removeView(view)
}
fpsCounterView = null
}
}
private fun setupFpsCounter() {
if (fpsCounterView != null) {
return
}
val parent = mLayout ?: return
val baseTopMargin = dpToPx(12)
val counter = TextView(this).apply {
text = "FPS 0.0"
setTextColor(Color.WHITE)
setTypeface(Typeface.MONOSPACE, Typeface.BOLD)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setPadding(dpToPx(10), dpToPx(4), dpToPx(10), dpToPx(4))
setBackgroundColor(Color.argb(150, 0, 0, 0))
isClickable = false
isFocusable = false
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
counter.layoutParams = if (parent is RelativeLayout) {
RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.ALIGN_PARENT_TOP)
addRule(RelativeLayout.CENTER_HORIZONTAL)
topMargin = baseTopMargin
}
} else {
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
topMargin = baseTopMargin
}
}
counter.setOnApplyWindowInsetsListener { view, insets ->
val topInset = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
insets.getInsets(WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()).top
} else {
@Suppress("DEPRECATION")
insets.systemWindowInsetTop
}
val layoutParams = view.layoutParams
if (layoutParams is ViewGroup.MarginLayoutParams) {
val wantedTopMargin = baseTopMargin + topInset
if (layoutParams.topMargin != wantedTopMargin) {
layoutParams.topMargin = wantedTopMargin
view.layoutParams = layoutParams
}
}
insets
}
fpsCounterView = counter
parent.addView(counter)
counter.bringToFront()
counter.requestApplyInsets()
}
private fun startFpsUpdates() {
fpsUpdateHandler.removeCallbacks(fpsUpdateRunnable)
fpsUpdateRunnable.run()
}
private fun stopFpsUpdates() {
fpsUpdateHandler.removeCallbacks(fpsUpdateRunnable)
}
override fun onResume() {
super.onResume()
@@ -150,7 +275,16 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
registerVirtualController()
}, 1000)
applyFpsCounterPreference()
scheduleStartupSnapshotLoadIfRequested()
if (showFpsCounter) {
startFpsUpdates()
}
}
override fun onPause() {
stopFpsUpdates()
super.onPause()
}
private fun scheduleStartupSnapshotLoadIfRequested() {
@@ -260,6 +394,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
override fun onDestroy() {
stopFpsUpdates()
inGameMenuDialog?.dismiss()
inGameMenuDialog = null
@@ -271,6 +406,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
inputManager?.unregisterInputDeviceListener(this)
fpsCounterView = null
super.onDestroy()
}
@@ -346,6 +482,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private external fun nativeSaveSnapshot(name: String): Boolean
private external fun nativeLoadSnapshot(name: String): Boolean
private external fun nativeGetFps(): Float
private fun slotName(slot: Int) = "android_slot_$slot"
@@ -47,6 +47,7 @@ class SettingsActivity : AppCompatActivity() {
val switchHrtf = findViewById<MaterialSwitch>(R.id.switch_hrtf)
val switchShaders = findViewById<MaterialSwitch>(R.id.switch_cache_shaders)
val switchFpu = findViewById<MaterialSwitch>(R.id.switch_hard_fpu)
val switchShowFps = findViewById<MaterialSwitch>(R.id.switch_show_fps_counter)
val toggleAudioDriver = findViewById<MaterialButtonToggleGroup>(R.id.toggle_audio_driver)
val btnSave = findViewById<MaterialButton>(R.id.btn_settings_save)
tvVulkanDriverName = findViewById(R.id.tv_vulkan_driver_name)
@@ -83,6 +84,7 @@ class SettingsActivity : AppCompatActivity() {
switchHrtf.isChecked = prefs.getBoolean("setting_hrtf", true)
switchShaders.isChecked = prefs.getBoolean("setting_cache_shaders", true)
switchFpu.isChecked = prefs.getBoolean("setting_hard_fpu", true)
switchShowFps.isChecked = prefs.getBoolean("setting_show_fps_counter", false)
val audioDriver = prefs.getString("setting_audio_driver", "openslES") ?: "openslES"
when (audioDriver) {
@@ -131,6 +133,7 @@ class SettingsActivity : AppCompatActivity() {
.putBoolean("setting_hrtf", switchHrtf.isChecked)
.putBoolean("setting_cache_shaders", switchShaders.isChecked)
.putBoolean("setting_hard_fpu", switchFpu.isChecked)
.putBoolean("setting_show_fps_counter", switchShowFps.isChecked)
.putString("setting_audio_driver", selectedAudioDriver)
when {
@@ -140,6 +140,14 @@
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_show_fps_counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/settings_show_fps_counter"
android:textColor="@color/xemu_text_muted" />
<!-- ── VULKAN DRIVER ── -->
<TextView
android:layout_width="match_parent"
@@ -89,6 +89,7 @@
<string name="settings_display_mode_stretch">Stretch</string>
<string name="settings_display_mode_4_3">4:3</string>
<string name="settings_display_mode_16_9">16:9</string>
<string name="settings_show_fps_counter">Show FPS Counter</string>
<string name="settings_resolution_scale">Internal Resolution Scale</string>
<string name="settings_resolution_scale_1x">1x (Native 480p)</string>