diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d5d829e2..265660add 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,19 +85,28 @@ option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENAB if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL) set(vvl_version "sdk-1.3.261.1") set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip") - if (NOT EXISTS "${vvl_zip_file}") - # Download and extract validation layer release to externals directory - set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") - file(DOWNLOAD "${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version}-android.zip" - "${vvl_zip_file}" SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") - endif() - - # Copy the arm64 binary to src/android/app/main/jniLibs set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/") - file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" - DESTINATION "${vvl_lib_path}") + set(vvl_lib_file "${vvl_lib_path}/libVkLayer_khronos_validation.so") + + # Only download and extract if the final library file doesn't exist + if (NOT EXISTS "${vvl_lib_file}") + if (NOT EXISTS "${vvl_zip_file}") + # Download validation layer release to externals directory + set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") + file(DOWNLOAD "${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version}-android.zip" + "${vvl_zip_file}" SHOW_PROGRESS) + endif() + + # Extract if not already extracted + if (NOT EXISTS "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}") + execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") + endif() + + # Copy the arm64 binary to src/android/app/main/jniLibs + file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" + DESTINATION "${vvl_lib_path}") + endif() endif() if (ANDROID) diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 99efb9d6c..b381e978f 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -28,8 +28,8 @@ val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toIn android { namespace = "org.citron.citron_emu" - compileSdkVersion = "android-34" - ndkVersion = "26.1.10909125" + compileSdkVersion = "android-35" + ndkVersion = "27.2.12479018" // "26.1.10909125" buildFeatures { viewBinding = true @@ -57,7 +57,8 @@ android { // TODO If this is ever modified, change application_id in strings.xml applicationId = "org.citron.citron_emu" minSdk = 30 - targetSdk = 34 + //noinspection EditedTargetSdkVersion + targetSdk = 35 versionName = getGitVersion() versionCode = if (System.getenv("AUTO_VERSIONED") == "true") { @@ -106,12 +107,12 @@ android { resValue("string", "app_name_suffixed", "citron") isDefault = true - // isShrinkResources = true + isShrinkResources = true isMinifyEnabled = true isDebuggable = false - // isJniDebuggable = false + isJniDebuggable = false proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } @@ -124,7 +125,7 @@ android { isMinifyEnabled = true isDebuggable = true proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) versionNameSuffix = "-relWithDebInfo" @@ -161,7 +162,7 @@ android { externalNativeBuild { cmake { - version = "3.22.1" + version = "3.31.7" path = file("../../../CMakeLists.txt") } } @@ -178,10 +179,12 @@ android { "-DCITRON_USE_BUNDLED_VCPKG=ON", "-DCITRON_USE_BUNDLED_FFMPEG=ON", "-DCITRON_ENABLE_LTO=ON", - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", ) - abiFilters("arm64-v8a", "x86_64") + abiFilters("arm64-v8a") // , "x86_64") } } } @@ -239,7 +242,6 @@ dependencies { implementation("io.coil-kt:coil:2.2.2") implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.window:window:1.2.0-beta03") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") implementation("androidx.navigation:navigation-ui-ktx:2.7.4") diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt index 79d163196..a18a1a70e 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.citron.citron_emu.features.settings.model @@ -26,7 +27,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"), SHOW_INPUT_OVERLAY("show_input_overlay"), TOUCHSCREEN("touchscreen"), - SHOW_THERMAL_OVERLAY("show_thermal_overlay"); + SHOW_THERMAL_OVERLAY("show_thermal_overlay"), + SHOW_RAM_METER("show_ram_meter"); override fun getBoolean(needsGlobal: Boolean): Boolean = NativeConfig.getBoolean(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt index f1cf5df75..8608616c3 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.citron.citron_emu.features.settings.model @@ -26,7 +27,14 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { OVERLAY_OPACITY("control_opacity"), LOCK_DRAWER("lock_drawer"), VERTICAL_ALIGNMENT("vertical_alignment"), - FSR_SHARPENING_SLIDER("fsr_sharpening_slider"); + FSR_SHARPENING_SLIDER("fsr_sharpening_slider"), + + // Zep Zone settings + MEMORY_LAYOUT_MODE("memory_layout_mode"), + ASTC_DECODE_MODE("accelerate_astc"), + ASTC_RECOMPRESSION("astc_recompression"), + SHADER_BACKEND("shader_backend"), + VRAM_USAGE_MODE("vram_usage_mode"); override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt index 7ae4a7c00..279ee1aad 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/Settings.kt @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.citron.citron_emu.features.settings.model @@ -22,7 +23,8 @@ object Settings { SECTION_INPUT_PLAYER_SEVEN, SECTION_INPUT_PLAYER_EIGHT, SECTION_THEME(R.string.preferences_theme), - SECTION_DEBUG(R.string.preferences_debug); + SECTION_DEBUG(R.string.preferences_debug), + SECTION_ZEP_ZONE(R.string.preferences_zep_zone); } fun getPlayerString(player: Int): String = diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt index 26991b059..922d63dd3 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.citron.citron_emu.features.settings.model.view @@ -386,6 +387,53 @@ abstract class SettingsItem( override fun reset() = setBoolean(defaultValue) } put(SwitchSetting(fastmem, R.string.fastmem)) + + // Zep Zone Settings + put( + SingleChoiceSetting( + IntSetting.MEMORY_LAYOUT_MODE, + titleId = R.string.memory_layout_mode, + descriptionId = R.string.memory_layout_mode_description, + choicesId = R.array.memoryLayoutNames, + valuesId = R.array.memoryLayoutValues + ) + ) + put( + SingleChoiceSetting( + IntSetting.ASTC_DECODE_MODE, + titleId = R.string.astc_decode_mode, + descriptionId = R.string.astc_decode_mode_description, + choicesId = R.array.astcDecodeModeNames, + valuesId = R.array.astcDecodeModeValues + ) + ) + put( + SingleChoiceSetting( + IntSetting.ASTC_RECOMPRESSION, + titleId = R.string.astc_recompression, + descriptionId = R.string.astc_recompression_description, + choicesId = R.array.astcRecompressionNames, + valuesId = R.array.astcRecompressionValues + ) + ) + put( + SingleChoiceSetting( + IntSetting.SHADER_BACKEND, + titleId = R.string.shader_backend, + descriptionId = R.string.shader_backend_description, + choicesId = R.array.shaderBackendNames, + valuesId = R.array.shaderBackendValues + ) + ) + put( + SingleChoiceSetting( + IntSetting.VRAM_USAGE_MODE, + titleId = R.string.vram_usage_mode, + descriptionId = R.string.vram_usage_mode_description, + choicesId = R.array.vramUsageModeNames, + valuesId = R.array.vramUsageModeValues + ) + ) } } } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt index 79b563f3d..6b7919c34 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.citron.citron_emu.features.settings.ui @@ -98,6 +99,7 @@ class SettingsFragmentPresenter( MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl) + MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl) } settingsList = sl adapter.submitList(settingsList) { @@ -141,6 +143,14 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_DEBUG ) ) + add( + SubmenuSetting( + titleId = R.string.preferences_zep_zone, + descriptionId = R.string.preferences_zep_zone_description, + iconId = R.drawable.ic_settings, + menuKey = MenuTag.SECTION_ZEP_ZONE + ) + ) add( RunnableSetting( titleId = R.string.reset_to_default, @@ -972,4 +982,19 @@ class SettingsFragmentPresenter( add(SettingsItem.FASTMEM_COMBINED) } } + + private fun addZepZoneSettings(sl: ArrayList) { + sl.apply { + add(HeaderSetting(R.string.memory_layout_header)) + add(IntSetting.MEMORY_LAYOUT_MODE.key) + + add(HeaderSetting(R.string.astc_settings_header)) + add(IntSetting.ASTC_DECODE_MODE.key) + add(IntSetting.ASTC_RECOMPRESSION.key) + + add(HeaderSetting(R.string.advanced_graphics_header)) + add(IntSetting.SHADER_BACKEND.key) + add(IntSetting.VRAM_USAGE_MODE.key) + } + } } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt index 7e97ce77a..ae00816ef 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt @@ -8,9 +8,12 @@ import android.annotation.SuppressLint import android.app.AlertDialog import android.content.Context import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.res.Configuration import android.net.Uri +import android.os.BatteryManager import android.os.Bundle import android.os.Handler import android.os.Looper @@ -66,6 +69,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private var emulationActivity: EmulationActivity? = null private var perfStatsUpdater: (() -> Unit)? = null private var thermalStatsUpdater: (() -> Unit)? = null + private var ramStatsUpdater: (() -> Unit)? = null private var _binding: FragmentEmulationBinding? = null private val binding get() = _binding!! @@ -374,6 +378,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // Setup overlays updateShowFpsOverlay() updateThermalOverlay() + updateRamMeterOverlay() } } emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { @@ -381,7 +386,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.loadingText.setText(R.string.shutting_down) ViewUtils.showView(binding.loadingIndicator) ViewUtils.hideView(binding.inputContainer) - ViewUtils.hideView(binding.showFpsText) + ViewUtils.hideView(binding.fpsIndicatorView) + ViewUtils.hideView(binding.thermalIndicatorView) + ViewUtils.hideView(binding.ramMeterView) } } emulationViewModel.drawerOpen.collect(viewLifecycleOwner) { @@ -486,22 +493,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun updateShowFpsOverlay() { val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() - binding.showFpsText.setVisible(showOverlay) + binding.fpsIndicatorView.setVisible(showOverlay) if (showOverlay) { - val SYSTEM_FPS = 0 val FPS = 1 - val FRAMETIME = 2 - val SPEED = 3 perfStatsUpdater = { if (emulationViewModel.emulationStarted.value && !emulationViewModel.isEmulationStopping.value ) { val perfStats = NativeLibrary.getPerfStats() - val cpuBackend = NativeLibrary.getCpuBackend() - val gpuDriver = NativeLibrary.getGpuDriver() if (_binding != null) { - binding.showFpsText.text = - String.format("FPS: %.1f\n%s/%s", perfStats[FPS], cpuBackend, gpuDriver) + binding.fpsIndicatorView.updateFps(perfStats[FPS].toFloat()) } perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) } @@ -516,26 +517,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun updateThermalOverlay() { val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() - binding.showThermalsText.setVisible(showOverlay) + binding.thermalIndicatorView.setVisible(showOverlay) if (showOverlay) { thermalStatsUpdater = { if (emulationViewModel.emulationStarted.value && !emulationViewModel.isEmulationStopping.value ) { - val thermalStatus = when (powerManager.currentThermalStatus) { - PowerManager.THERMAL_STATUS_LIGHT -> "😥" - PowerManager.THERMAL_STATUS_MODERATE -> "🥵" - PowerManager.THERMAL_STATUS_SEVERE -> "🔥" - PowerManager.THERMAL_STATUS_CRITICAL, - PowerManager.THERMAL_STATUS_EMERGENCY, - PowerManager.THERMAL_STATUS_SHUTDOWN -> "☢️" - - else -> "🙂" - } if (_binding != null) { - binding.showThermalsText.text = thermalStatus + val temperature = getBatteryTemperature(requireContext()) + binding.thermalIndicatorView.updateTemperature(temperature) } - thermalStatsUpdateHandler.postDelayed(thermalStatsUpdater!!, 1000) + thermalStatsUpdateHandler.postDelayed(thermalStatsUpdater!!, 2000) } } thermalStatsUpdateHandler.post(thermalStatsUpdater!!) @@ -546,6 +538,42 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + private fun updateRamMeterOverlay() { + val showOverlay = BooleanSetting.SHOW_RAM_METER.getBoolean() + binding.ramMeterView.setVisible(showOverlay) + if (showOverlay) { + ramStatsUpdater = { + if (emulationViewModel.emulationStarted.value && + !emulationViewModel.isEmulationStopping.value + ) { + if (_binding != null) { + binding.ramMeterView.updateRamUsage() + } + ramStatsUpdateHandler.postDelayed(ramStatsUpdater!!, 1500) + } + } + ramStatsUpdateHandler.post(ramStatsUpdater!!) + } else { + if (ramStatsUpdater != null) { + ramStatsUpdateHandler.removeCallbacks(ramStatsUpdater!!) + } + } + } + + private fun getBatteryTemperature(context: Context): Float { + return try { + val batteryIntent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + if (batteryIntent != null) { + val temperature = batteryIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 250) + temperature / 10f // Convert from tenths of degrees to degrees + } else { + 25f // Fallback temperature + } + } catch (e: Exception) { + 25f // Fallback temperature + } + } + @SuppressLint("SourceLockedOrientationActivity") private fun updateOrientation() { emulationActivity?.let { @@ -676,6 +704,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() findItem(R.id.thermal_indicator).isChecked = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() + findItem(R.id.ram_meter).isChecked = + BooleanSetting.SHOW_RAM_METER.getBoolean() findItem(R.id.menu_rel_stick_center).isChecked = BooleanSetting.JOYSTICK_REL_CENTER.getBoolean() findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean() @@ -702,6 +732,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } + R.id.ram_meter -> { + it.isChecked = !it.isChecked + BooleanSetting.SHOW_RAM_METER.setBoolean(it.isChecked) + updateRamMeterOverlay() + true + } + R.id.menu_edit_overlay -> { binding.drawerLayout.close() binding.surfaceInputOverlay.requestFocus() @@ -1046,5 +1083,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { companion object { private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!) private val thermalStatsUpdateHandler = Handler(Looper.myLooper()!!) + private val ramStatsUpdateHandler = Handler(Looper.myLooper()!!) } } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/views/FpsIndicatorView.kt b/src/android/app/src/main/java/org/citron/citron_emu/views/FpsIndicatorView.kt new file mode 100644 index 000000000..320a9c142 --- /dev/null +++ b/src/android/app/src/main/java/org/citron/citron_emu/views/FpsIndicatorView.kt @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.citron.citron_emu.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.util.AttributeSet +import android.util.Log +import android.view.View +import kotlin.math.roundToInt + +class FpsIndicatorView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#80000000") // Semi-transparent black + style = Paint.Style.FILL + } + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.STROKE + strokeWidth = 2f + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 22f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 16f + typeface = Typeface.DEFAULT + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 24f + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private var currentFps: Float = 0f + private val fpsIcon: String = "📊" + + private val backgroundRect = RectF() + + fun updateFps(fps: Float) { + try { + currentFps = fps + Log.d("FpsIndicator", "FPS updated: $currentFps") + + // Update color based on FPS performance + val fpsColor = when { + currentFps >= 55f -> Color.parseColor("#4CAF50") // Green - Good performance + currentFps >= 45f -> Color.parseColor("#FF9800") // Orange - Moderate performance + currentFps >= 30f -> Color.parseColor("#FF5722") // Red orange - Poor performance + else -> Color.parseColor("#F44336") // Red - Very poor performance + } + + textPaint.color = fpsColor + smallTextPaint.color = fpsColor + borderPaint.color = fpsColor + + // Always invalidate to trigger a redraw + invalidate() + Log.d("FpsIndicator", "View invalidated, FPS: $currentFps") + } catch (e: Exception) { + // Fallback in case of any errors + currentFps = 0f + Log.e("FpsIndicator", "Error updating FPS", e) + invalidate() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 120 + val desiredHeight = 60 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + backgroundRect.set(4f, 4f, w - 4f, h - 4f) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw background with rounded corners + canvas.drawRoundRect(backgroundRect, 12f, 12f, backgroundPaint) + canvas.drawRoundRect(backgroundRect, 12f, 12f, borderPaint) + + val centerX = width / 2f + val centerY = height / 2f + + // Draw FPS icon on the left + canvas.drawText(fpsIcon, 18f, centerY - 8f, iconPaint) + + // Draw FPS value (main text) + val fpsText = "${currentFps.roundToInt()}" + canvas.drawText(fpsText, centerX, centerY - 8f, textPaint) + + // Draw "FPS" label (smaller text below) + canvas.drawText("FPS", centerX, centerY + 12f, smallTextPaint) + + Log.d("FpsIndicator", "onDraw called - FPS: $fpsText") + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citron/citron_emu/views/RamMeterView.kt b/src/android/app/src/main/java/org/citron/citron_emu/views/RamMeterView.kt new file mode 100644 index 000000000..c14c95b44 --- /dev/null +++ b/src/android/app/src/main/java/org/citron/citron_emu/views/RamMeterView.kt @@ -0,0 +1,195 @@ +// SPDX-FileCopyrightText: 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.citron.citron_emu.views + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.util.AttributeSet +import android.util.Log +import android.view.View +import kotlin.math.roundToInt + +class RamMeterView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#80000000") // Semi-transparent black + style = Paint.Style.FILL + } + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.STROKE + strokeWidth = 2f + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 20f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 14f + typeface = Typeface.DEFAULT + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 24f + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val meterBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#40FFFFFF") // Semi-transparent white + style = Paint.Style.FILL + } + + private val meterFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.GREEN + style = Paint.Style.FILL + } + + private var ramUsagePercent: Float = 0f + private var usedRamMB: Long = 0L + private var totalRamMB: Long = 0L + private var ramIcon: String = "🧠" + + private val backgroundRect = RectF() + private val meterBackgroundRect = RectF() + private val meterFillRect = RectF() + + fun updateRamUsage() { + try { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + + totalRamMB = memoryInfo.totalMem / (1024 * 1024) + val availableRamMB = memoryInfo.availMem / (1024 * 1024) + usedRamMB = totalRamMB - availableRamMB + ramUsagePercent = (usedRamMB.toFloat() / totalRamMB.toFloat()) * 100f + + // Update meter color based on usage + val meterColor = when { + ramUsagePercent < 50f -> Color.parseColor("#4CAF50") // Green + ramUsagePercent < 75f -> Color.parseColor("#FF9800") // Orange + ramUsagePercent < 90f -> Color.parseColor("#FF5722") // Red orange + else -> Color.parseColor("#F44336") // Red + } + + meterFillPaint.color = meterColor + textPaint.color = meterColor + smallTextPaint.color = meterColor + borderPaint.color = meterColor + + // Update icon based on usage + ramIcon = when { + ramUsagePercent < 50f -> "🧠" // Normal brain + ramUsagePercent < 75f -> "⚡" // Warning + ramUsagePercent < 90f -> "🔥" // Hot + else -> "💥" // Critical + } + + invalidate() + Log.d("RamMeter", "RAM usage updated: ${ramUsagePercent.roundToInt()}% (${usedRamMB}MB/${totalRamMB}MB)") + } catch (e: Exception) { + Log.e("RamMeter", "Error updating RAM usage", e) + ramUsagePercent = 0f + usedRamMB = 0L + totalRamMB = 0L + invalidate() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 140 + val desiredHeight = 60 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + backgroundRect.set(4f, 4f, w - 4f, h - 4f) + + // Setup meter rectangles - compact horizontal bar at the bottom + val meterLeft = 30f + val meterTop = h - 18f + val meterRight = w - 10f + val meterBottom = h - 10f + + meterBackgroundRect.set(meterLeft, meterTop, meterRight, meterBottom) + meterFillRect.set(meterLeft, meterTop, meterRight, meterBottom) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw background with rounded corners + canvas.drawRoundRect(backgroundRect, 12f, 12f, backgroundPaint) + canvas.drawRoundRect(backgroundRect, 12f, 12f, borderPaint) + + val centerX = width / 2f + val centerY = height / 2f + + // Draw RAM icon on the left + canvas.drawText(ramIcon, 18f, centerY - 8f, iconPaint) + + // Draw percentage text at the top center + val percentText = "${ramUsagePercent.roundToInt()}%" + canvas.drawText(percentText, centerX, centerY - 8f, textPaint) + + // Draw memory usage text below percentage + val usedGB = usedRamMB / 1024f + val totalGB = totalRamMB / 1024f + val memoryText = if (totalGB >= 1.0f) { + "%.1fGB/%.1fGB".format(usedGB, totalGB) + } else { + "${usedRamMB}MB/${totalRamMB}MB" + } + canvas.drawText(memoryText, centerX, centerY + 8f, smallTextPaint) + + // Draw RAM meter background at the bottom + canvas.drawRoundRect(meterBackgroundRect, 4f, 4f, meterBackgroundPaint) + + // Draw RAM meter fill + val fillWidth = meterBackgroundRect.width() * (ramUsagePercent / 100f) + meterFillRect.right = meterBackgroundRect.left + fillWidth + canvas.drawRoundRect(meterFillRect, 4f, 4f, meterFillPaint) + + Log.d("RamMeter", "onDraw called - Usage: $percentText, Memory: $memoryText") + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citron/citron_emu/views/ThermalIndicatorView.kt b/src/android/app/src/main/java/org/citron/citron_emu/views/ThermalIndicatorView.kt new file mode 100644 index 000000000..141df527f --- /dev/null +++ b/src/android/app/src/main/java/org/citron/citron_emu/views/ThermalIndicatorView.kt @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.citron.citron_emu.views + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.os.BatteryManager +import android.util.AttributeSet +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import org.citron.citron_emu.R +import kotlin.math.roundToInt + +class ThermalIndicatorView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#80000000") // Semi-transparent black + style = Paint.Style.FILL + } + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.STROKE + strokeWidth = 2f + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 22f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 16f + typeface = Typeface.DEFAULT + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 24f + textAlign = Paint.Align.CENTER + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private var batteryTemperature: Float = 0f + private var thermalStatus: String = "🌡️" + + private val backgroundRect = RectF() + + fun updateTemperature(temperature: Float) { + try { + batteryTemperature = temperature + Log.d("ThermalIndicator", "Battery temperature updated: ${batteryTemperature}°C") + + // Update thermal status icon based on temperature + thermalStatus = when { + batteryTemperature < 20f -> "❄️" // Cold + batteryTemperature < 30f -> "🌡️" // Normal + batteryTemperature < 40f -> "🔥" // Warm + batteryTemperature < 50f -> "🥵" // Hot + else -> "☢️" // Critical + } + + // Update text color based on temperature + val tempColor = when { + batteryTemperature < 20f -> Color.parseColor("#87CEEB") // Sky blue + batteryTemperature < 30f -> Color.WHITE // White + batteryTemperature < 40f -> Color.parseColor("#FFA500") // Orange + batteryTemperature < 50f -> Color.parseColor("#FF4500") // Red orange + else -> Color.parseColor("#FF0000") // Red + } + + textPaint.color = tempColor + smallTextPaint.color = tempColor + borderPaint.color = tempColor + + // Always invalidate to trigger a redraw + invalidate() + Log.d("ThermalIndicator", "View invalidated, temperature: ${batteryTemperature}°C, status: $thermalStatus") + } catch (e: Exception) { + // Fallback in case of any errors + batteryTemperature = 25f + thermalStatus = "🌡️" + Log.e("ThermalIndicator", "Error updating temperature", e) + invalidate() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 120 + val desiredHeight = 60 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + backgroundRect.set(4f, 4f, w - 4f, h - 4f) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw background with rounded corners + canvas.drawRoundRect(backgroundRect, 12f, 12f, backgroundPaint) + canvas.drawRoundRect(backgroundRect, 12f, 12f, borderPaint) + + val centerX = width / 2f + val centerY = height / 2f + + // Draw thermal icon on the left + canvas.drawText(thermalStatus, 18f, centerY - 8f, iconPaint) + + // Draw temperature in Celsius (main temperature) + val celsiusText = "${batteryTemperature.roundToInt()}°C" + canvas.drawText(celsiusText, centerX, centerY - 8f, textPaint) + + // Draw temperature in Fahrenheit (smaller, below) + val fahrenheit = (batteryTemperature * 9f / 5f + 32f).roundToInt() + val fahrenheitText = "${fahrenheit}°F" + canvas.drawText(fahrenheitText, centerX, centerY + 12f, smallTextPaint) + + Log.d("ThermalIndicator", "onDraw called - Celsius: $celsiusText, Fahrenheit: $fahrenheitText") + } +} \ No newline at end of file diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 00baf86a9..38a4f9ee9 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -69,6 +70,8 @@ struct Values { Settings::Category::Overlay}; Settings::Setting show_thermal_overlay{linkage, false, "show_thermal_overlay", Settings::Category::Overlay}; + Settings::Setting show_ram_meter{linkage, false, "show_ram_meter", + Settings::Category::Overlay}; Settings::Setting show_input_overlay{linkage, true, "show_input_overlay", Settings::Category::Overlay}; Settings::Setting touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay}; diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml index df91a08c5..04e06a30c 100644 --- a/src/android/app/src/main/res/layout/fragment_emulation.xml +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -143,31 +143,41 @@ android:layout_marginHorizontal="20dp" android:fitsSystemWindows="true"> - + android:layout_gravity="center_horizontal|top" + android:layout_marginTop="16dp" + android:orientation="horizontal" + android:gravity="center"> - + + + + + + + diff --git a/src/android/app/src/main/res/menu/menu_overlay_options.xml b/src/android/app/src/main/res/menu/menu_overlay_options.xml index a9e807427..d601421a7 100644 --- a/src/android/app/src/main/res/menu/menu_overlay_options.xml +++ b/src/android/app/src/main/res/menu/menu_overlay_options.xml @@ -11,6 +11,11 @@ android:title="@string/emulation_thermal_indicator" android:checkable="true" /> + + diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 1bd6455b4..cf9c29058 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -303,4 +303,71 @@ 2 + + + 4GB DRAM (Default) + 6GB DRAM (Unsafe) + 8GB DRAM (Unsafe) + 10GB DRAM (Unsafe) + 12GB DRAM (Unsafe) + 14GB DRAM (Unsafe) + 16GB DRAM (Unsafe) + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + + + + CPU + GPU + CPU Asynchronous + + + + 0 + 1 + 2 + + + + Uncompressed + BC1 + BC3 + + + + 0 + 1 + 2 + + + + GLSL + GLASM + SPIR-V + + + + 0 + 1 + 2 + + + + Conservative + Aggressive + + + + 0 + 1 + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index bc8f42292..38c219001 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -410,6 +410,13 @@ Theme and color Debug CPU/GPU debugging, graphics API, fastmem + Zep Zone + Advanced emulation settings + + + Memory Layout + ASTC Settings + Advanced Graphics Info @@ -472,7 +479,8 @@ Exit emulation Done FPS counter - Thermal indicator + Battery temperature + RAM usage meter Toggle controls Relative stick center D-pad slide @@ -1167,4 +1175,16 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Memory Layout + Increases the amount of emulated RAM from the stock 4GB of the retail Switch to the developer kit\'s 8/6GB. It doesn\'t improve stability or performance and is intended to let big texture mods fit in emulated RAM. Enabling it will increase memory use. It is not recommended to enable unless a specific game with a texture mod needs it. + ASTC Decoding Method + Controls how ASTC textures are decoded. GPU decoding is faster but may cause issues on some devices. + ASTC Recompression Method + Controls how ASTC textures are recompressed when GPU doesn\'t support them natively. + Shader Backend + Controls which shader backend to use for rendering. + VRAM Usage Mode + Controls how aggressively VRAM is used. Conservative mode limits VRAM usage for better stability. + diff --git a/src/core/hle/service/sockets/nsd.cpp b/src/core/hle/service/sockets/nsd.cpp index dbe042ac1..04eb351bb 100644 --- a/src/core/hle/service/sockets/nsd.cpp +++ b/src/core/hle/service/sockets/nsd.cpp @@ -9,8 +9,8 @@ namespace Service::Sockets { -constexpr Result ResultNotImplemented{ErrorModule::NSD, 1}; // Generic "not implemented" -constexpr Result ResultNsdNotInitialized{ErrorModule::NSD, 2}; // Example, if needed +[[maybe_unused]] constexpr Result ResultNotImplemented{ErrorModule::NSD, 1}; // Generic "not implemented" +[[maybe_unused]] constexpr Result ResultNsdNotInitialized{ErrorModule::NSD, 2}; // Example, if needed constexpr Result ResultPermissionDenied{ErrorModule::NSD, 3}; // For nsd:a specific calls constexpr Result ResultOverflow{ErrorModule::NSD, 6};