From eb8f6513cfef74fc48d83e5384835ba3d8362d29 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Wed, 4 Feb 2026 19:45:45 +1000 Subject: [PATCH] feat(android): implement Project Thor dual-screen dashboard - Add ThorPresentation.kt for secondary display performance dashboard - Create thor_presentation.xml with real-time stats layout - Modify EmulationActivity to auto-detect secondary displays - Display FPS, speed, frame time, resolution, and shader status - Color-coded FPS indicator (green/yellow/orange/red) - Add 12 new string resources for Thor dashboard UI - Keep main screen immersive while bottom screen shows stats Signed-off-by: Zephyron --- .../activities/EmulationActivity.kt | 59 ++++ .../features/thor/ThorPresentation.kt | 225 +++++++++++++ .../src/main/res/layout/thor_presentation.xml | 301 ++++++++++++++++++ .../app/src/main/res/values/strings.xml | 14 + 4 files changed, 599 insertions(+) create mode 100644 src/android/app/src/main/java/org/citron/citron_emu/features/thor/ThorPresentation.kt create mode 100644 src/android/app/src/main/res/layout/thor_presentation.xml 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 +