diff --git a/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt
index 85abf9d72..6a7cafc34 100644
--- a/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt
@@ -53,6 +53,7 @@ import org.citron.citron_emu.utils.NativeConfig
import org.citron.citron_emu.utils.NfcReader
import org.citron.citron_emu.utils.ParamPackage
import org.citron.citron_emu.utils.ThemeHelper
+import org.citron.citron_emu.features.thor.ThorPresentation
import java.text.NumberFormat
import kotlin.math.roundToInt
@@ -72,6 +73,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val actionMute = "ACTION_EMULATOR_MUTE"
private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
+ // PROJECT THOR: Dual Screen Dashboard for AYN Thor
+ private var thorPresentation: ThorPresentation? = null
+
private val emulationViewModel: EmulationViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -177,12 +181,67 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
InputHandler.updateControllerData()
buildPictureInPictureParams()
+
+ // PROJECT THOR: Detect secondary display and show performance dashboard
+ // This keeps the main screen immersive while showing stats on the bottom display
+ initThorPresentation()
+ }
+
+ /**
+ * PROJECT THOR: Initialize the dual-screen performance dashboard
+ * Automatically detects secondary displays (like AYN Thor's bottom screen)
+ * and shows a live performance dashboard with FPS, speed, and settings info.
+ */
+ private fun initThorPresentation() {
+ if (thorPresentation == null && ThorPresentation.hasSecondaryDisplay(this)) {
+ try {
+ thorPresentation = ThorPresentation.createIfAvailable(this)
+ thorPresentation?.let { presentation ->
+ presentation.show()
+ presentation.startUpdates()
+ Toast.makeText(
+ this,
+ getString(R.string.thor_secondary_display_detected),
+ Toast.LENGTH_SHORT
+ ).show()
+ Log.debug("[Thor] Secondary display detected - Dashboard enabled")
+ }
+ } catch (e: Exception) {
+ Log.error("[Thor] Failed to initialize presentation: ${e.message}")
+ thorPresentation = null
+ }
+ } else {
+ // Resume updates if presentation already exists
+ thorPresentation?.startUpdates()
+ }
+ }
+
+ /**
+ * PROJECT THOR: Clean up the dual-screen presentation
+ */
+ private fun cleanupThorPresentation() {
+ thorPresentation?.let { presentation ->
+ presentation.stopUpdates()
+ if (presentation.isShowing) {
+ presentation.dismiss()
+ }
+ }
+ thorPresentation = null
}
override fun onPause() {
super.onPause()
nfcReader.stopScanning()
stopMotionSensorListener()
+
+ // PROJECT THOR: Stop dashboard updates when paused
+ thorPresentation?.stopUpdates()
+ }
+
+ override fun onDestroy() {
+ // PROJECT THOR: Clean up dual-screen presentation on destroy
+ cleanupThorPresentation()
+ super.onDestroy()
}
override fun onUserLeaveHint() {
diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/thor/ThorPresentation.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/thor/ThorPresentation.kt
new file mode 100644
index 000000000..98725f20c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citron/citron_emu/features/thor/ThorPresentation.kt
@@ -0,0 +1,225 @@
+// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.citron.citron_emu.features.thor
+
+import android.app.Presentation
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.Display
+import android.view.View
+import android.widget.TextView
+import org.citron.citron_emu.NativeLibrary
+import org.citron.citron_emu.R
+import org.citron.citron_emu.features.settings.model.BooleanSetting
+import org.citron.citron_emu.features.settings.model.IntSetting
+
+/**
+ * PROJECT THOR: Dual Screen Implementation for AYN Thor
+ *
+ * This Presentation class renders a Live Performance Dashboard on the Thor's
+ * secondary bottom display, keeping the main 1080p screen completely immersive.
+ *
+ * Features:
+ * - Real-time FPS, Emulation Speed %, and Frame Time
+ * - Live settings display (Resolution, Docked State, CPU Accuracy)
+ * - Shader compilation indicator
+ */
+class ThorPresentation(
+ context: Context,
+ display: Display
+) : Presentation(context, display) {
+
+ private lateinit var fpsText: TextView
+ private lateinit var speedText: TextView
+ private lateinit var frameTimeText: TextView
+ private lateinit var resolutionText: TextView
+ private lateinit var dockedText: TextView
+ private lateinit var cpuAccuracyText: TextView
+ private lateinit var shaderText: TextView
+ private lateinit var gpuDriverText: TextView
+ private lateinit var astcModeText: TextView
+
+ private val handler = Handler(Looper.getMainLooper())
+ private var isRunning = false
+
+ // Poll interval for performance stats (500ms as specified in report)
+ private val pollInterval = 500L
+
+ private val updateRunnable = object : Runnable {
+ override fun run() {
+ if (isRunning) {
+ updatePerformanceStats()
+ handler.postDelayed(this, pollInterval)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.thor_presentation)
+
+ initializeViews()
+ updateStaticSettings()
+ }
+
+ private fun initializeViews() {
+ fpsText = findViewById(R.id.thor_fps)
+ speedText = findViewById(R.id.thor_speed)
+ frameTimeText = findViewById(R.id.thor_frame_time)
+ resolutionText = findViewById(R.id.thor_resolution)
+ dockedText = findViewById(R.id.thor_docked)
+ cpuAccuracyText = findViewById(R.id.thor_cpu_accuracy)
+ shaderText = findViewById(R.id.thor_shader_status)
+ gpuDriverText = findViewById(R.id.thor_gpu_driver)
+ astcModeText = findViewById(R.id.thor_astc_mode)
+ }
+
+ /**
+ * Update static settings that don't change during gameplay
+ */
+ private fun updateStaticSettings() {
+ // Resolution scaling - based on arrays.xml rendererResolutionValues
+ val resolutionIndex = IntSetting.RENDERER_RESOLUTION.getInt()
+ val resolutionString = when (resolutionIndex) {
+ -1 -> "0.25X (180p)"
+ 0 -> "0.5X (360p)"
+ 1 -> "0.75X (540p)"
+ 2 -> "1X (720p)"
+ 11 -> "1.25X (900p)"
+ 3 -> "1.5X (1080p)" // PROJECT THOR: Optimal for AYN Thor
+ 12 -> "1.75X (1260p)"
+ 4 -> "2X (1440p)"
+ 5 -> "3X (2160p)"
+ 6 -> "4X (2880p)"
+ 7 -> "5X"
+ 8 -> "6X"
+ 9 -> "7X"
+ 10 -> "8X"
+ else -> "${resolutionIndex}X"
+ }
+ resolutionText.text = resolutionString
+
+ // Docked mode
+ val isDocked = BooleanSetting.DOCKED_MODE.getBoolean()
+ dockedText.text = if (isDocked) "Docked" else "Handheld"
+
+ // CPU Accuracy
+ val cpuAccuracy = IntSetting.CPU_ACCURACY.getInt()
+ val cpuAccuracyString = when (cpuAccuracy) {
+ 0 -> "Auto"
+ 1 -> "Accurate"
+ 2 -> "Unsafe"
+ 3 -> "Paranoid"
+ else -> "Unknown"
+ }
+ cpuAccuracyText.text = cpuAccuracyString
+
+ // GPU Driver (if custom driver is loaded)
+ val gpuDriver = NativeLibrary.getGpuDriver()
+ gpuDriverText.text = if (gpuDriver.isNotEmpty()) gpuDriver else "System Driver"
+
+ // ASTC Decode Mode
+ val astcMode = IntSetting.ANDROID_ASTC_MODE.getInt()
+ val astcModeString = when (astcMode) {
+ 0 -> "Auto"
+ 1 -> "Native"
+ 2 -> "Decompress"
+ else -> "Auto"
+ }
+ astcModeText.text = astcModeString
+ }
+
+ /**
+ * Update real-time performance statistics
+ * Called every 500ms while emulation is running
+ */
+ private fun updatePerformanceStats() {
+ try {
+ val perfStats = NativeLibrary.getPerfStats()
+ if (perfStats != null && perfStats.size >= 4) {
+ // perfStats format: [fps, emulationSpeed, frameTime, fifoPercentage]
+ val fps = perfStats[0]
+ val speed = perfStats[1]
+ val frameTime = perfStats[2]
+
+ fpsText.text = String.format("%.1f FPS", fps)
+ speedText.text = String.format("%.0f%%", speed * 100)
+ frameTimeText.text = String.format("%.2f ms", frameTime * 1000)
+
+ // Color code FPS based on performance
+ val fpsColor = when {
+ fps >= 58 -> 0xFF00FF00.toInt() // Green - excellent
+ fps >= 45 -> 0xFFFFFF00.toInt() // Yellow - acceptable
+ fps >= 30 -> 0xFFFF8800.toInt() // Orange - playable
+ else -> 0xFFFF0000.toInt() // Red - poor
+ }
+ fpsText.setTextColor(fpsColor)
+ }
+
+ // Shader compilation status
+ val shadersBuilding = NativeLibrary.getShadersBuilding()
+ if (shadersBuilding > 0) {
+ shaderText.text = context.getString(R.string.thor_shaders_building) + " ($shadersBuilding)"
+ shaderText.visibility = View.VISIBLE
+ shaderText.setTextColor(0xFFFFAA00.toInt()) // Orange
+ } else {
+ shaderText.visibility = View.GONE
+ }
+ } catch (e: Exception) {
+ // Handle any native library exceptions gracefully
+ fpsText.text = "-- FPS"
+ speedText.text = "--%"
+ frameTimeText.text = "-- ms"
+ }
+ }
+
+ fun startUpdates() {
+ isRunning = true
+ handler.post(updateRunnable)
+ }
+
+ fun stopUpdates() {
+ isRunning = false
+ handler.removeCallbacks(updateRunnable)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ stopUpdates()
+ }
+
+ companion object {
+ private const val TAG = "ThorPresentation"
+
+ /**
+ * Check if the device has a secondary display (like AYN Thor)
+ */
+ fun hasSecondaryDisplay(context: Context): Boolean {
+ val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ val displays = displayManager.displays
+ return displays.size > 1
+ }
+
+ /**
+ * Get the secondary display for Thor presentation
+ */
+ fun getSecondaryDisplay(context: Context): Display? {
+ val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ val displays = displayManager.displays
+ // Find the first non-default display
+ return displays.firstOrNull { it.displayId != Display.DEFAULT_DISPLAY }
+ }
+
+ /**
+ * Create and show the Thor presentation on the secondary display
+ */
+ fun createIfAvailable(context: Context): ThorPresentation? {
+ val secondaryDisplay = getSecondaryDisplay(context) ?: return null
+ return ThorPresentation(context, secondaryDisplay)
+ }
+ }
+}
diff --git a/src/android/app/src/main/res/layout/thor_presentation.xml b/src/android/app/src/main/res/layout/thor_presentation.xml
new file mode 100644
index 000000000..5603e0fed
--- /dev/null
+++ b/src/android/app/src/main/res/layout/thor_presentation.xml
@@ -0,0 +1,301 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index f2b903bfd..5df5f6072 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -1325,4 +1325,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Aperture Grille
Shadow Mask
+
+ CITRON PERFORMANCE DASHBOARD
+ Frame Rate
+ Speed
+ Frame Time
+ Resolution
+ Mode
+ CPU
+ GPU Driver
+ ASTC
+ Compiling Shaders…
+ Powered by Citron
+ Secondary display detected - Dashboard enabled
+