feat: Add RAM usage overlay and improve thermal display

- Add new RAM usage overlay showing current/max memory usage with color gradient
- Simplify thermal overlay to show temperature in C/F with color indication
- Add SHOW_RAM_OVERLAY boolean setting (disabled by default in UI)
- Set default values for thermal overlay and black backgrounds to true
- Update copyright headers to include Citron Emulator Project

The RAM overlay displays native heap usage with color gradient from green
(low usage) to red (high usage). The thermal display was simplified to show
just temperatures while maintaining the color indication based on system
thermal status.
This commit is contained in:
Zephyron 2025-02-01 19:18:35 +10:00
parent 137034ca2c
commit 0216eaa071
No known key found for this signature in database
GPG key ID: 2177ADED8AC966AF
6 changed files with 95 additions and 26 deletions

View file

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.citron.citron_emu.features.settings.model package org.citron.citron_emu.features.settings.model
@ -27,6 +28,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SHOW_INPUT_OVERLAY("show_input_overlay"), SHOW_INPUT_OVERLAY("show_input_overlay"),
TOUCHSCREEN("touchscreen"), TOUCHSCREEN("touchscreen"),
SHOW_THERMAL_OVERLAY("show_thermal_overlay"), SHOW_THERMAL_OVERLAY("show_thermal_overlay"),
SHOW_RAM_OVERLAY("show_ram_overlay"),
USE_AUTO_STUB("use_auto_stub"); USE_AUTO_STUB("use_auto_stub");
override fun getBoolean(needsGlobal: Boolean): Boolean = override fun getBoolean(needsGlobal: Boolean): Boolean =

View file

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.citron.citron_emu.fragments package org.citron.citron_emu.fragments
@ -11,9 +12,11 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.BatteryManager import android.os.BatteryManager
import android.os.Bundle import android.os.Bundle
import android.os.Debug
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
@ -68,6 +71,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var emulationActivity: EmulationActivity? = null private var emulationActivity: EmulationActivity? = null
private var perfStatsUpdater: (() -> Unit)? = null private var perfStatsUpdater: (() -> Unit)? = null
private var thermalStatsUpdater: (() -> Unit)? = null private var thermalStatsUpdater: (() -> Unit)? = null
private var ramStatsUpdater: (() -> Unit)? = null
private var _binding: FragmentEmulationBinding? = null private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -83,6 +87,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var powerManager: PowerManager private lateinit var powerManager: PowerManager
private val ramStatsUpdateHandler = Handler(Looper.myLooper()!!)
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
if (context is EmulationActivity) { if (context is EmulationActivity) {
@ -376,6 +382,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Setup overlays // Setup overlays
updateShowFpsOverlay() updateShowFpsOverlay()
updateThermalOverlay() updateThermalOverlay()
updateRamOverlay()
} }
} }
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
@ -470,6 +477,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
if (ramStatsUpdater != null) {
ramStatsUpdateHandler.removeCallbacks(ramStatsUpdater!!)
}
_binding = null _binding = null
} }
@ -552,8 +562,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
emulationViewModel.emulationStarted.value && emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value !emulationViewModel.isEmulationStopping.value
) { ) {
// Get thermal status // Get thermal status for color
val thermalStatus = when (powerManager.currentThermalStatus) { val thermalStatus = when (powerManager.currentThermalStatus) {
PowerManager.THERMAL_STATUS_NONE -> 0f
PowerManager.THERMAL_STATUS_LIGHT -> 0.25f PowerManager.THERMAL_STATUS_LIGHT -> 0.25f
PowerManager.THERMAL_STATUS_MODERATE -> 0.5f PowerManager.THERMAL_STATUS_MODERATE -> 0.5f
PowerManager.THERMAL_STATUS_SEVERE -> 0.75f PowerManager.THERMAL_STATUS_SEVERE -> 0.75f
@ -563,34 +574,57 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
else -> 0f else -> 0f
} }
// Convert to Fahrenheit for additional info // Convert to Fahrenheit
val fahrenheit = (temperature * 9f / 5f) + 32f val fahrenheit = (temperature * 9f / 5f) + 32f
// Create progress bar using block elements // Color based on thermal status (green to red)
val progressBarLength = 12 val red = (thermalStatus * 255).toInt()
val filledBars = (thermalStatus * progressBarLength).toInt() val green = ((1f - thermalStatus) * 255).toInt()
val progressBar = buildString {
append("") // Left border
repeat(filledBars) { append("") }
repeat(progressBarLength - filledBars) { append("") }
append("") // Right border
append(" ")
append(String.format("%3d%%", (thermalStatus * 100).toInt()))
}
// Color interpolation based on temperature (green at 30°C, red at 45°C)
val normalizedTemp = ((temperature - 30f) / 15f).coerceIn(0f, 1f)
val red = (normalizedTemp * 255).toInt()
val green = ((1f - normalizedTemp) * 255).toInt()
val color = android.graphics.Color.rgb(red, green, 0) val color = android.graphics.Color.rgb(red, green, 0)
binding.showThermalsText.setTextColor(color) binding.showThermalsText.setTextColor(color)
binding.showThermalsText.text = String.format( binding.showThermalsText.text = String.format("%.1f°C • %.1f°F", temperature, fahrenheit)
"%s\n%.1f°C • %.1f°F", }
progressBar, }
temperature,
fahrenheit private fun updateRamOverlay() {
val showOverlay = BooleanSetting.SHOW_RAM_OVERLAY.getBoolean()
binding.showRamText.setVisible(showOverlay)
if (showOverlay) {
ramStatsUpdater = {
if (emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value
) {
val runtime = Runtime.getRuntime()
val nativeHeapSize = Debug.getNativeHeapSize()
val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
val nativeHeapUsed = nativeHeapSize - nativeHeapFreeSize
val usedMemInMB = nativeHeapUsed / 1048576L
val maxMemInMB = nativeHeapSize / 1048576L
val percentUsed = (nativeHeapUsed.toFloat() / nativeHeapSize.toFloat() * 100f)
// Color interpolation from green to red based on usage percentage
val normalizedUsage = (percentUsed / 100f).coerceIn(0f, 1f)
val red = (normalizedUsage * 255).toInt()
val green = ((1f - normalizedUsage) * 255).toInt()
val color = Color.rgb(red, green, 0)
binding.showRamText.setTextColor(color)
binding.showRamText.text = String.format(
"\nRAM: %d/%d MB (%.1f%%)",
usedMemInMB,
maxMemInMB,
percentUsed
) )
ramStatsUpdateHandler.postDelayed(ramStatsUpdater!!, 1000)
}
}
ramStatsUpdateHandler.post(ramStatsUpdater!!)
} else {
if (ramStatsUpdater != null) {
ramStatsUpdateHandler.removeCallbacks(ramStatsUpdater!!)
}
} }
} }
@ -723,6 +757,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()
findItem(R.id.thermal_indicator).isChecked = findItem(R.id.thermal_indicator).isChecked =
BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()
findItem(R.id.ram_meter).apply {
isChecked = BooleanSetting.SHOW_RAM_OVERLAY.getBoolean()
isEnabled = false // This grays out the option
}
findItem(R.id.menu_rel_stick_center).isChecked = findItem(R.id.menu_rel_stick_center).isChecked =
BooleanSetting.JOYSTICK_REL_CENTER.getBoolean() BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()
findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean() findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
@ -749,6 +787,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true true
} }
R.id.ram_meter -> {
// Do nothing since it's disabled
true
}
R.id.menu_edit_overlay -> { R.id.menu_edit_overlay -> {
binding.drawerLayout.close() binding.drawerLayout.close()
binding.surfaceInputOverlay.requestFocus() binding.surfaceInputOverlay.requestFocus()

View file

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -51,7 +52,7 @@ struct Values {
Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android}; Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android};
Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android}; Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android};
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds", Settings::Setting<bool> black_backgrounds{linkage, true, "black_backgrounds",
Settings::Category::Android}; Settings::Category::Android};
// Input/performance overlay settings // Input/performance overlay settings
@ -67,7 +68,9 @@ struct Values {
Settings::Category::Overlay}; Settings::Category::Overlay};
Settings::Setting<bool> show_performance_overlay{linkage, true, "show_performance_overlay", Settings::Setting<bool> show_performance_overlay{linkage, true, "show_performance_overlay",
Settings::Category::Overlay}; Settings::Category::Overlay};
Settings::Setting<bool> show_thermal_overlay{linkage, false, "show_thermal_overlay", Settings::Setting<bool> show_thermal_overlay{linkage, true, "show_thermal_overlay",
Settings::Category::Overlay};
Settings::Setting<bool> show_ram_overlay{linkage, true, "show_ram_overlay",
Settings::Category::Overlay}; Settings::Category::Overlay};
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay", Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
Settings::Category::Overlay}; Settings::Category::Overlay};

View file

@ -171,6 +171,21 @@
</FrameLayout> </FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/show_ram_text"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="24dp"
android:clickable="false"
android:focusable="false"
android:textColor="@android:color/white"
android:shadowColor="@android:color/black"
android:shadowRadius="3"
android:layout_below="@id/show_fps_text"
tools:ignore="RtlHardcoded" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView

View file

@ -11,6 +11,11 @@
android:title="@string/emulation_thermal_indicator" android:title="@string/emulation_thermal_indicator"
android:checkable="true" /> android:checkable="true" />
<item
android:id="@+id/ram_meter"
android:title="@string/emulation_ram_meter"
android:checkable="true" />
<item <item
android:id="@+id/menu_edit_overlay" android:id="@+id/menu_edit_overlay"
android:title="@string/emulation_touch_overlay_edit" /> android:title="@string/emulation_touch_overlay_edit" />

View file

@ -480,6 +480,7 @@
<string name="emulation_done">Done</string> <string name="emulation_done">Done</string>
<string name="emulation_fps_counter">FPS counter</string> <string name="emulation_fps_counter">FPS counter</string>
<string name="emulation_thermal_indicator">Thermal indicator</string> <string name="emulation_thermal_indicator">Thermal indicator</string>
<string name="emulation_ram_meter">RAM meter</string>
<string name="emulation_toggle_controls">Toggle controls</string> <string name="emulation_toggle_controls">Toggle controls</string>
<string name="emulation_rel_stick_center">Relative stick center</string> <string name="emulation_rel_stick_center">Relative stick center</string>
<string name="emulation_dpad_slide">D-pad slide</string> <string name="emulation_dpad_slide">D-pad slide</string>