diff --git a/app/build.gradle b/app/build.gradle index 62637d031..5049e6d52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -285,6 +285,8 @@ dependencies { implementation libs.playServicesGamesV2 implementation libs.workRuntimeKtx + + implementation "com.github.topjohnwu.libsu:core:6.0.0" } spotless { diff --git a/app/src/main/app/shell/UnifiedActivity.kt b/app/src/main/app/shell/UnifiedActivity.kt index ab661f60d..cb1339e3b 100644 --- a/app/src/main/app/shell/UnifiedActivity.kt +++ b/app/src/main/app/shell/UnifiedActivity.kt @@ -215,6 +215,7 @@ import com.winlator.cmod.shared.ui.CarouselView import com.winlator.cmod.shared.ui.dialog.PopupDialog import com.winlator.cmod.shared.ui.dialog.PopupTextAction import androidx.compose.foundation.focusGroup +import com.winlator.cmod.feature.power.PerformanceManager import com.winlator.cmod.shared.ui.focus.controllerFocusGlow import com.winlator.cmod.shared.ui.focus.controllerMenuInput import com.winlator.cmod.shared.ui.focus.controllerTextFieldEscape @@ -931,6 +932,7 @@ class UnifiedActivity : com.winlator.cmod.runtime.display.GlassesManager.init(this) bootstrapStartupState() maybeAutoSignInGoogleOnLaunch() + PerformanceManager.checkForPossibleCrash() // Surface store-session events as toasts. lifecycleScope.launch { diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index c79f8eca3..0d76293a7 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -142,6 +142,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import androidx.compose.foundation.focusable +import androidx.compose.material.icons.outlined.Power import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key @@ -150,6 +151,8 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import com.winlator.cmod.shared.ui.focus.controllerFocusBorder import com.winlator.cmod.shared.ui.focus.controllerFocusGlow +import com.winlator.cmod.feature.power.CpuPolicy +import com.winlator.cmod.feature.power.PerformanceManager import com.winlator.cmod.shared.ui.focus.controllerSliderEscape import com.winlator.cmod.shared.ui.focus.controllerTextFieldEscape import com.winlator.cmod.shared.ui.outlinedSwitchColors @@ -575,6 +578,16 @@ class GameSettingsStateHolder { val drives = mutableStateOf("") val isLoaded = mutableStateOf(false) + val cpuFanMode = mutableStateOf("") + val cpuGovernor = mutableStateOf("") + val cpuEditingMode = mutableStateOf(false) + val cpuBoostState = mutableStateOf(true) + val cpuPolicies = mutableStateOf>(emptyList()) + val cpuTargetFPS = mutableStateOf(0) + val cpuAutoTargetFPS = mutableStateOf(false) + val cpuAvgFrameCount = mutableStateOf(3) + val gpuFrequency = mutableStateOf(0) + val performanceMode = mutableStateOf("") } interface GameSettingsCallbacks { @@ -654,6 +667,7 @@ private const val SEC_INPUT = 7 private const val SEC_ADVANCED = 8 private const val SEC_DRIVES = 9 private const val SEC_SAVES = 10 +private const val SEC_POWER = 11 private fun buildSections(isSteam: Boolean, isContainer: Boolean): List> { val list = mutableListOf>() @@ -672,6 +686,9 @@ private fun buildSections(isSteam: Boolean, isContainer: Boolean): List AdvancedSection(state, callbacks) SEC_DRIVES -> DrivesSection(state, callbacks) SEC_SAVES -> SavesSection(state, callbacks) + SEC_POWER -> PerformanceControlSection(state, callbacks) } Spacer(Modifier.height(SettingSectionGap)) } @@ -1052,6 +1070,232 @@ private fun SidebarItem( } } +@Composable +private fun PerformanceControlSection( + state: GameSettingsStateHolder, + callbacks: GameSettingsCallbacks +) { + val governors = PerformanceManager.getCpuGovernors() + val fanModes = PerformanceManager.getSupportedFanModes() + val performanceModes = PerformanceManager.getSupportedPerformanceModes() + val gpuFrequencies = PerformanceManager.gpuFrequencies + var fanModeIndex by remember { mutableStateOf(fanModes?.indexOf(state.cpuFanMode.value) ?: 0) } + var governorIndex by remember { mutableStateOf(governors?.indexOf(state.cpuGovernor.value) ?: 0) } + + if (state.cpuPolicies.value.isEmpty()) { + val policies = PerformanceManager.getCpuPolicies() + if (!policies.isNullOrEmpty()) + state.cpuPolicies.value = policies + } + SettingGroup { + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + if (!governors.isNullOrEmpty()) { + Box(Modifier.weight(1f)) { + SettingDropdown( + label = stringResource(R.string.performance_cpu_governor_label), + entries = governors, + selectedIndex = governors.indexOf(state.cpuGovernor.value), + onSelected = { + state.cpuGovernor.value = governors[it] + } + ) + } + } + + if (!performanceModes.isNullOrEmpty()) { + Box(Modifier.weight(1f)) { + SettingDropdown( + label = stringResource(R.string.performance_system_performance_label), + entries = performanceModes, + selectedIndex = performanceModes.indexOf(state.performanceMode.value), + onSelected = { + state.performanceMode.value = performanceModes[it] + } + ) + } + } + } + } + Spacer(Modifier.height(SettingSectionGap)) + + SettingGroup { + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + if (PerformanceManager.isFanSupported && !fanModes.isNullOrEmpty()) { + Box(Modifier.weight(1f)) { + SettingDropdown( + label = stringResource(R.string.performance_cpu_fan_label), + entries = fanModes, + selectedIndex = fanModes.indexOf(state.cpuFanMode.value), + onSelected = { + state.cpuFanMode.value = fanModes[it] + } + ) + } + } + } + } + Spacer(Modifier.height(SettingSectionGap)) + + SettingGroup { + Column { + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + if (!state.cpuAutoTargetFPS.value) { + Box(Modifier.weight(1f)) { + SettingCheckbox( + stringResource(R.string.performance_cpu_editing_enabled), + state.cpuEditingMode.value, + onCheckedChange = { state.cpuEditingMode.value = it }) + } + Box(Modifier.weight(1f)) { + SettingCheckbox( + stringResource(R.string.performance_cpu_enable_boost), + state.cpuBoostState.value, + onCheckedChange = { + state.cpuBoostState.value = it + if (state.cpuPolicies.value.isNotEmpty()) + state.cpuPolicies.value.forEach { policy -> + policy.boostState = it + } + } + ) + } + } + Box(Modifier.weight(1f)) { + SettingCheckbox( + "AutoFPS", + state.cpuAutoTargetFPS.value, + onCheckedChange = { + if (state.cpuTargetFPS.value == 0) + state.cpuTargetFPS.value = 60 + state.cpuAutoTargetFPS.value = it + }) + } + } + if (state.cpuAutoTargetFPS.value) { + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + Box(Modifier.weight(1f)) { + val fpsMin = 30 + val supportedMax = state.refreshRateEntries.value + .mapNotNull { it.trim().substringBefore(" ").toIntOrNull() } + .maxOrNull() ?: 60 + val maxFps = supportedMax.coerceAtLeast(fpsMin) + var lastFps by remember(maxFps) { + mutableStateOf( + (if (state.cpuTargetFPS.value > 0) state.cpuTargetFPS.value else 60) + .coerceIn(fpsMin, maxFps) + ) + } + SettingSlider( + label = stringResource(R.string.performance_fps_target_label), + value = lastFps, + range = fpsMin..maxFps, + valueText = "$lastFps FPS", + steps = (maxFps - fpsMin - 1).coerceAtLeast(0), + onValueChange = { + val v = it.coerceIn(fpsMin, maxFps) + lastFps = v + state.cpuTargetFPS.value = v + } + ) + } + + Box(Modifier.weight(1f)) { + SettingSlider( + label = stringResource(R.string.performance_frames_per_check_label), + value = state.cpuAvgFrameCount.value, + range = 1..10, + valueText = stringResource(R.string.performance_frames_label, state.cpuAvgFrameCount.value), + steps = (0 - 10 - 1).coerceAtLeast(0), + onValueChange = { + state.cpuAvgFrameCount.value = it + } + ) + } + } + } else if (state.cpuPolicies.value.isNotEmpty()) { + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + for (index in state.cpuPolicies.value.indices) { + var checked by remember { mutableStateOf(state.cpuPolicies.value[index].enabled) } + Box(Modifier.weight(1f)) { + SettingCheckbox( + stringResource(R.string.performance_cpu_enabled_cluster, state.cpuPolicies.value[index].policyId.toString()), + checked, + onCheckedChange = { it -> + val disabledClusterCount = state.cpuPolicies.value.count { !it.enabled } + if (!it && disabledClusterCount >= state.cpuPolicies.value.size - 1) + return@SettingCheckbox + checked = it + state.cpuPolicies.value[index].enabled = it + } + ) + } + } + } + + for (index in state.cpuPolicies.value.indices) { + if (!state.cpuPolicies.value[index].enabled) continue + val frequencies = state.cpuPolicies.value[index].availableFrequencies ?: return@SettingGroup + var minIndex by remember { mutableStateOf(frequencies.indexOf(state.cpuPolicies.value[index].minFrequency)) } + var maxIndex by remember { mutableStateOf(frequencies.indexOf(state.cpuPolicies.value[index].maxFrequency)) } + + + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + Box(Modifier.weight(1f)) { + SettingSlider( + label = stringResource(R.string.performance_cpu_cluster_min_label, state.cpuPolicies.value[index].policyId.toString()), + value = minIndex, + range = frequencies.indices, + valueText = PerformanceManager.formatFrequency(frequencies[minIndex].toInt()), + steps = (frequencies.size-2).coerceAtLeast(0), + enabled = true, + onValueChange = { + if (minIndex >= maxIndex) { + maxIndex = if ((minIndex + 1) >= frequencies.size) frequencies.size - 1 else minIndex + 1 + } + minIndex = it + } + ) + } + Box(Modifier.weight(1f)) { + SettingSlider( + label = stringResource(R.string.performance_cpu_cluster_max_label, state.cpuPolicies.value[index].policyId.toString()), + value = maxIndex, + range = frequencies.indices, + valueText = PerformanceManager.formatFrequency(frequencies[maxIndex].toInt(), state.cpuPolicies.value[index].boostState), + steps = (frequencies.size-2).coerceAtLeast(0), + enabled = true, + onValueChange = { + if (maxIndex <= minIndex) { + minIndex = if ((maxIndex - 1) < 0) 0 else maxIndex - 1 + } + maxIndex = it + } + ) + } + } + state.cpuPolicies.value[index].minFrequency = frequencies[minIndex] + state.cpuPolicies.value[index].maxFrequency = frequencies[maxIndex] + } + if (!PerformanceManager.isGpuSupported || gpuFrequencies.isEmpty()) return@SettingGroup + Row(horizontalArrangement = Arrangement.spacedBy(SettingItemGap)) { + Box(Modifier.weight(1f)) { + SettingSlider( + label = stringResource(R.string.performance_gpu_frequencies_label), + value = gpuFrequencies.indexOf(state.gpuFrequency.value), + range = gpuFrequencies.indices, + valueText = "${state.gpuFrequency.value}MHz", + steps = (0 - gpuFrequencies.size - 1).coerceAtLeast(0), + onValueChange = { + state.gpuFrequency.value = gpuFrequencies[it] + } + ) + } + } + } + } + } +} + @Composable private fun GeneralSection( state: GameSettingsStateHolder, diff --git a/app/src/main/feature/power/PerformanceManager.kt b/app/src/main/feature/power/PerformanceManager.kt new file mode 100644 index 000000000..5ce92f38f --- /dev/null +++ b/app/src/main/feature/power/PerformanceManager.kt @@ -0,0 +1,544 @@ +package com.winlator.cmod.feature.power + +import java.io.File +import com.winlator.cmod.shared.util.RootManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.let + +data class CpuPolicy( + var enabled: Boolean, + val policyId: Int, + val policyName: String, + var boostState: Boolean, + var minFrequency: Long, + var maxFrequency: Long, + val boostFrequency: Long?, + val cpuCores: List, + val availableFrequencies: MutableList? +) + +data class SystemInformation( + var fanMode: Int?, + var cpuGovernor: String?, + var gpuFrequency: Int?, + var policies: List?, + var performanceMode: Int? +) + +object PerformanceManager { + private const val IDLE_TIMEOUT_NS = 1500000000L + private const val CPU_PATH = "/sys/devices/system/cpu" + private const val GPU_PATH = "/sys/class/kgsl/kgsl-3d0" + private const val POLICY_PATH = "/sys/devices/system/cpu/cpufreq" + private const val CPU_BOOST_PATH = "/sys/devices/system/cpu/cpufreq/boost" + private const val CPU_POLICY_BASE_PATH = "/sys/devices/system/cpu/cpufreq/" + private val FAN_MODES = mapOf("QUIET" to 1, "SMART" to 4, "SPORT" to 5, "CUSTOM" to 6) + private val PERFORMANCE_MODES = mapOf("Standard" to 0, "Performance" to 1, "High Performance" to 2) + + val isDeviceSupported + get() = RootManager.isRooted + + var targetFPS: Float = 0f + var fpsAvgFrameCount = 0 + var gpuFrequencyIndex: Int = 0 + var gpuFrequencies: MutableList = mutableListOf() + var useAutoTargeting: Boolean = false + + private var ticks: Long = 0L + private var fpsLastFrameNano = 0L + private var fpsPastFrames: MutableList = mutableListOf() + private var autoTargetPolicies: List? = null + + val disabledCores = mutableListOf() + val isFanSupported = checkForFanSupport() + val isGpuSupported = checkForGpuSupport() + private val defaultSystemInfo: SystemInformation? = getSystemInformation() + + fun checkForGpuSupport(): Boolean { + if (!isDeviceSupported) return false + val gpuDirectorySymlink = File(GPU_PATH) + if (!gpuDirectorySymlink.isDirectory) return false + val frequencies = RootManager.readSysfsFile("${gpuDirectorySymlink.canonicalPath}/gpu_available_frequencies") + if (frequencies.isNullOrEmpty()) return false + frequencies.split(" ").reversed().forEach { it -> + runCatching { + val freq = (it.toLong() / 1_000_000).toInt() + gpuFrequencies.add(freq) + }.getOrDefault(0) + } + return gpuFrequencies.size >= 2 + } + + fun checkForFanSupport(): Boolean { + if (!isDeviceSupported) return false + val response = RootManager.executeAsRoot("settings get system fan_mode") ?: return false + return response.toIntOrNull() != null + } + + fun checkForPossibleCrash() { + if (!isDeviceSupported) return + val numberOfCpuCores = getNumberOfCpuCores() ?: return + var resetDefaultSystemPolicies = false + getCpuBoostState()?.let { + if (!it) + setCpuBoostState(true) + } + for (cpu in 0..? { + if (!isDeviceSupported || !isGpuSupported) return null + return PERFORMANCE_MODES.map { it.key } + } + + fun getSupportedFanModes(): List? { + if (!isDeviceSupported || !isFanSupported) return null + return FAN_MODES.map { it.key } + } + + fun getDefaultSystemPolicies(): List? { + if (!isDeviceSupported) return null + return defaultSystemInfo?.policies + } + + fun getCpuBoostState(): Boolean? { + if (!isDeviceSupported) return null + return RootManager.readSysfsFile(CPU_BOOST_PATH) == "1" + } + + fun getCpuGovernors(): List? { + if (!isDeviceSupported) return null + val cpuPath = "${CPU_PATH}/cpu0/cpufreq/scaling_available_governors" + return runCatching { RootManager.readSysfsFile(cpuPath)?.split(" ")?.map{it} }.getOrNull() + } + + fun getCpuGovernor(cpu: Int=0): String? { + if (!isDeviceSupported) return null + return RootManager.readSysfsFile("${CPU_PATH}/cpu${cpu}/cpufreq/scaling_governor") + } + + fun getNumberOfCpuCores(): Int? { + if (!isDeviceSupported) return null + val content = RootManager.readSysfsFile("${CPU_PATH}/present") ?: return null + val parts = content.split("-") + if (parts.size == 2) + return runCatching { parts[1].toInt()+1 }.getOrNull() + return null + } + + fun getCpuPolicies(): List? { + if (!isDeviceSupported) return null + val numberOfCpuCores = getNumberOfCpuCores() ?: return null + val policies = mutableMapOf>() + for (cpu in 0.. + val frequencies: MutableList? = RootManager.readSysfsFile("${policyDirectory}/scaling_available_frequencies")?.split(" ")?.map { it -> runCatching { it.toLong()}.getOrDefault(0) } as MutableList? + val minFrequency = runCatching { frequencies!!.first() }.getOrDefault(0) + val maxFrequency = runCatching { frequencies!!.last() }.getOrDefault(0) + val boostFrequency = runCatching { RootManager.readSysfsFile("${policyDirectory}/cpuinfo_max_freq")!!.toLong() }.getOrDefault(0) + //if (boostState && frequencies != null && maxFrequency > frequencies.last()) + // frequencies.add(maxFrequency) + //frequencies = frequencies + listOf(maxFrequency) + CpuPolicy( + enabled = true, + policyId = index, + policyName = File(policyDirectory).name, + boostState = boostState, + minFrequency = minFrequency, + maxFrequency = maxFrequency, + cpuCores = cpuCoreList, + availableFrequencies = frequencies, + boostFrequency = boostFrequency + ) + } + } + + private fun calculateFPS(): Float { + val nowNano = System.nanoTime() + if (fpsLastFrameNano == 0L || nowNano - fpsLastFrameNano > IDLE_TIMEOUT_NS) { + fpsLastFrameNano = nowNano + return 0f + } + val fps = 1000000000.0f / (nowNano - fpsLastFrameNano) + fpsLastFrameNano = nowNano + return fps + } + + fun onTick() { + if (!isDeviceSupported || targetFPS == 0f || !useAutoTargeting) return + ticks++ + if (ticks.toFloat() == targetFPS*60f) { + val policies = getCpuPolicies() ?: return + for (policy in policies) { + policy.enabled = true + policy.boostState = true + if (policy.availableFrequencies.isNullOrEmpty()) continue + policy.maxFrequency = policy.availableFrequencies.first() + policy.minFrequency = policy.availableFrequencies.first() + } + if (isGpuSupported && gpuFrequencies.isNotEmpty() && gpuFrequencies.size > 2) + setGpuFrequency(gpuFrequencies.first(), gpuFrequencies.first()) + autoTargetPolicies = policies + setCpuCorePolicies(policies) + return + } + + var fps = calculateFPS() + if (fps == 0f) return + + if (ticks%targetFPS==0f && !autoTargetPolicies.isNullOrEmpty()) { + fpsPastFrames.add(fps) + if (fpsPastFrames.size < fpsAvgFrameCount) return + fps = fpsPastFrames.sum() / fpsPastFrames.size + var shouldUpdate = false + if (fps > targetFPS*1.1) { + for (policy in autoTargetPolicies) { + if (policy.availableFrequencies.isNullOrEmpty()) continue + val coreHasBoost = policy.availableFrequencies.last() != policy.boostFrequency + if (coreHasBoost && policy.maxFrequency == policy.boostFrequency) { + policy.maxFrequency = policy.availableFrequencies.last() + shouldUpdate = true + } + else if (policy.maxFrequency == policy.boostFrequency){ + policy.maxFrequency = policy.availableFrequencies.takeLast(2).first() + shouldUpdate = true + } else { + val maxIndex = policy.availableFrequencies.indexOf(policy.maxFrequency) + if (maxIndex > 1) { + policy.maxFrequency = policy.availableFrequencies[maxIndex - 1] + shouldUpdate = true + } + } + if (coreHasBoost && policy.maxFrequency == policy.boostFrequency) { + policy.minFrequency = policy.availableFrequencies.last() + shouldUpdate = true + } else { + val maxIndex = policy.availableFrequencies.indexOf(policy.maxFrequency) + if (maxIndex-1 <= 0) continue + policy.minFrequency = policy.availableFrequencies[maxIndex-1] + shouldUpdate = true + } + } + if (isGpuSupported && gpuFrequencyIndex > 1) + gpuFrequencyIndex -= 1 + + }else if (fps < targetFPS*0.98) { + for (policy in autoTargetPolicies) { + if (policy.availableFrequencies.isNullOrEmpty()) continue + val maxIndex = policy.availableFrequencies.indexOf(policy.maxFrequency) + if (maxIndex == -1) { + policy.minFrequency = policy.availableFrequencies.last() + shouldUpdate = true + } else { + if (maxIndex + 1 >= policy.availableFrequencies.size && policy.boostFrequency != null) + policy.maxFrequency = policy.boostFrequency + else + policy.maxFrequency = policy.availableFrequencies[maxIndex + 1] + policy.minFrequency = policy.availableFrequencies[maxIndex] + shouldUpdate = true + } + } + if (isGpuSupported && gpuFrequencyIndex+1 < gpuFrequencies.size) + gpuFrequencyIndex+=1 + } + fpsPastFrames.clear() + if (shouldUpdate) { + CoroutineScope(Dispatchers.IO).launch { + if (isGpuSupported && gpuFrequencies.isNotEmpty()) + setGpuFrequency(gpuFrequencies[gpuFrequencyIndex-1], gpuFrequencies[gpuFrequencyIndex]) + autoTargetPolicies?.let { + setCpuCorePolicies(it) + } + } + } + } + } + + fun getCpuCoreAvailableFrequencies(cpu: Int): List? { + if (!isDeviceSupported) return null + return RootManager.readSysfsFile("${CPU_PATH}/cpu${cpu}/scaling_available_frequencies")?.split(" ")?.map { it -> runCatching { it.toLong()}.getOrDefault(0) } + } + + fun getCpuPolicyAvailableFrequencies(policyName: String): List? { + if (!isDeviceSupported) return null + return RootManager.readSysfsFile("${CPU_POLICY_BASE_PATH}/${policyName}/scaling_available_frequencies")?.split(" ")?.map { it -> runCatching { it.toLong()}.getOrDefault(0) } + } + + fun getCpuCoreOnlineState(cpuCore: Int): Boolean? { + if (!isDeviceSupported) return null + val onlineState = RootManager.readSysfsFile("${CPU_PATH}/cpu${cpuCore}/online") + return onlineState == "1" + } + + fun setGpuFrequency(gpuMin: Int, gpuMax: Int, lockfile: Boolean=true): Boolean? { + if (!isDeviceSupported || !isGpuSupported) return null + if (gpuFrequencies.isEmpty() || gpuFrequencies.size < 2) return null + if (!gpuFrequencies.contains(gpuMin) || !gpuFrequencies.contains(gpuMax)) return false + if (lockfile) gpuFrequencyIndex = gpuFrequencies.indexOf(gpuMax) + RootManager.writeSysfsFile("${GPU_PATH}/max_pwrlevel", "0", lockfile) + RootManager.writeSysfsFile("${GPU_PATH}/max_clock_mhz", gpuMax.toString(), lockfile) + RootManager.writeSysfsFile("${GPU_PATH}/min_clock_mhz", gpuMin.toString(), lockfile) + return true + } + + fun setCpuCoreGovernor(cpuCore: Int, governor: String): Boolean? { + if (!isDeviceSupported) return null + val governorPath = "${CPU_PATH}/cpu${cpuCore}/cpufreq/scaling_governor" + return RootManager.writeSysfsFile(governorPath, governor) + } + + fun setAllCpuCoreGovernor(governor: String): Boolean? { + if (!isDeviceSupported) return null + val numberOfCpuCores = getNumberOfCpuCores() ?: return null + for (cpu in 0..numberOfCpuCores) + setCpuCoreGovernor(cpu, governor) + return true + } + + fun setCpuBoostState(state: Boolean): Boolean? { + if (!isDeviceSupported) return null + return RootManager.writeSysfsFile(CPU_BOOST_PATH, if(state) "1" else "0") + } + + fun setCpuCoreOnlineState(cpuCore: Int, state: Boolean): Boolean? { + if (!isDeviceSupported) return null + val response = RootManager.writeSysfsFile("${CPU_PATH}/cpu${cpuCore}/online", if(state) "1" else "0") + if (response) { + if (state) + disabledCores.remove(cpuCore) + else + disabledCores.add(cpuCore) + } + return true + } + + fun setCpuCoresOnlineState(cpuCores: List, state: Boolean): Boolean? { + if (!isDeviceSupported) return null + if (cpuCores.isEmpty()) return false + for (cpuCore in cpuCores) { + val response = RootManager.writeSysfsFile( + "${CPU_PATH}/cpu${cpuCore}/online", + if (state) "1" else "0" + ) + if (response) { + if (state) + disabledCores.remove(cpuCore) + else + disabledCores.add(cpuCore) + } + } + return true + } + + fun setPolicyOnlineState(policy: CpuPolicy, state: Boolean): Boolean? { + if (!isDeviceSupported) return null + if (!policy.cpuCores.isNotEmpty()) return null + policy.cpuCores.forEach { setCpuCoreOnlineState(it, state) } + return true + } + + fun setCpuCoreFrequency(cpuCore: Int, minFrequency: Long, maxFrequency: Long): Boolean? { + if (!isDeviceSupported) return null + val cpuFrequencyPath = "${CPU_PATH}/cpu${cpuCore}/cpufreq" + if (!RootManager.writeSysfsFile("${cpuFrequencyPath}/scaling_min_freq", minFrequency.toString())) return false + if (!RootManager.writeSysfsFile("${cpuFrequencyPath}/scaling_max_freq", maxFrequency.toString())) return false + return true + } + + fun setCpuPolicyFrequency(policyPath: String, minFrequency: Long, maxFrequency: Long): Boolean? { + if (!isDeviceSupported) return null + if (!RootManager.writeSysfsFile("${policyPath}/scaling_min_freq", minFrequency.toString())) return false + if (!RootManager.writeSysfsFile("${policyPath}/scaling_max_freq", maxFrequency.toString())) return false + return true + } + + fun setCpuCorePolicies(cpuCorePolicies: List, setBoostFrequency: Boolean=false): Boolean? { + if (!isDeviceSupported) return null + for (cpuPolicy in cpuCorePolicies) { + setCpuBoostState(cpuPolicy.boostState) + if (cpuPolicy.enabled) { + setCpuCoresOnlineState(cpuPolicy.cpuCores, true) + if (setBoostFrequency && cpuPolicy.boostState && cpuPolicy.boostFrequency != null && cpuPolicy.boostFrequency.toInt() != 0) + setCpuPolicyFrequency("${POLICY_PATH}/${cpuPolicy.policyName}", cpuPolicy.minFrequency, cpuPolicy.boostFrequency) + else + setCpuPolicyFrequency("${POLICY_PATH}/${cpuPolicy.policyName}", cpuPolicy.minFrequency, cpuPolicy.maxFrequency) + } else + setCpuCoresOnlineState(cpuPolicy.cpuCores, false) + } + return true + } + + + fun setCpuCorePolicies(rawCpuPolicy: String, setBoostFrequency: Boolean=false): Boolean? { + if (!isDeviceSupported) return null + val cpuCorePolicies = parseRawPoliciesFromString(rawCpuPolicy) + if (cpuCorePolicies.isNullOrEmpty()) return null + setCpuCorePolicies(cpuCorePolicies, setBoostFrequency) + return true + } + + fun setFanMode(fanMode: String): Boolean? { + if (!isDeviceSupported || !isFanSupported) return null + val mode = FAN_MODES[fanMode] ?: return null + return RootManager.executeAsRoot("settings put system fan_mode ${mode}") != null + } + + fun setPerformanceMode(performanceMode: Int): Boolean? { + if (!isDeviceSupported || !isGpuSupported) return null + return RootManager.executeAsRoot("settings put system performance_mode ${performanceMode}") != null + } + + fun setPerformanceMode(performanceMode: String): Boolean? { + if (!isDeviceSupported || !isGpuSupported) return null + val mode = PERFORMANCE_MODES[performanceMode] ?: return null + return RootManager.executeAsRoot("settings put system performance_mode ${mode}") != null + } + + fun setFanMode(fanMode: Int): Boolean? { + if (!isDeviceSupported || !isFanSupported) return null + if (!FAN_MODES.containsValue(fanMode)) return null + return RootManager.executeAsRoot("settings put system fan_mode ${fanMode}") != null + } + + fun parseRawPoliciesFromString(rawPolicies: String): List? { + // Example policies string: "policyID-policyName:minFrequency-maxFrequency!start_core-end_core?policyOnlineState-boostState-boostfrequency," "0-policy0:1000-1111!3-7?1-1-9999," + if (!isDeviceSupported) return null + val policies = mutableListOf() + for (policy in rawPolicies.split(",")) { + try { + val policyId = policy.split(":")[0].split("-")[0].toInt() + val policyName = policy.split(":")[0].split("-")[1] + val minFrequency = policy.split(":")[1].split("!")[0].split("-")[0].toLong() + val maxFrequency = policy.split(":")[1].split("!")[0].split("-")[1].toLong() + val cpuCoresRange = policy.split(":")[1].split("!")[1].split("?")[0].split("-") + val cpuCoreState = policy.split(":")[1].split("!")[1].split("?")[1].split('-')[0].toBoolean() + val boostState = policy.split(":")[1].split("!")[1].split("?")[1].split('-')[1].toBoolean() + val boostFrequency = policy.split(":")[1].split("!")[1].split("?")[1].split('-')[2] + + val cpuCores = mutableListOf() + (cpuCoresRange[0].toInt()..cpuCoresRange[1].toInt()) + .forEach { cpu -> + cpuCores.add(cpu) + } + + policies.add(CpuPolicy( + enabled = cpuCoreState, + policyId = policyId, + policyName = policyName, + boostState = boostState, + minFrequency = minFrequency, + maxFrequency = maxFrequency, + boostFrequency = if (boostFrequency == "0") null else boostFrequency.toLong(), + cpuCores = cpuCores, + availableFrequencies = getCpuPolicyAvailableFrequencies(policyName) as MutableList? + )) + } catch (e: Exception) {} + } + return policies + } + + fun policiesToString(policies: List): String? { + if (!isDeviceSupported) return null + var stringPolicies = "" + for (policy in policies) { + stringPolicies += "${policy.policyId}-${policy.policyName}:${policy.minFrequency}-${policy.maxFrequency}!${policy.cpuCores.first()}-${policy.cpuCores.last()}?${policy.enabled}-${policy.boostState}-${policy.boostFrequency}," + } + return stringPolicies + } + + fun setSystemInformation(systemInfo: SystemInformation, lockfile: Boolean=true, reset: Boolean=false) { + if (!isDeviceSupported) return + systemInfo.fanMode?.let { setFanMode(it) } + systemInfo.cpuGovernor?.let { setAllCpuCoreGovernor(it) } + systemInfo.gpuFrequency?.let { setGpuFrequency(gpuFrequencies.first(), it, lockfile) } + systemInfo.policies?.let { setCpuCorePolicies(it, reset) } + systemInfo.performanceMode?.let { setPerformanceMode(it) } + } + + fun formatFrequency(valueKhz: Int, boosted: Boolean = false): String { + val base = when { + valueKhz >= 1_000_000 -> String.format("%.2f GHz", valueKhz / 1_000_000f) + valueKhz >= 1_000 -> String.format("%.0f MHz", valueKhz / 1_000f) + else -> "$valueKhz kHz" + } + return if (boosted) "$base+" else base + } + + fun resetSystemToDefault(): Boolean? { + if (!isDeviceSupported) return null + ticks = 0 + gpuFrequencyIndex = 0 + autoTargetPolicies = null + defaultSystemInfo?.let { setSystemInformation(it, false, true) } + return true + } +} \ No newline at end of file diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index 8704af95c..b95b49ace 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -75,6 +75,7 @@ import com.winlator.cmod.shared.ui.dialog.ContainerProgressPopup import com.winlator.cmod.shared.ui.dialog.PopupDialog import com.winlator.cmod.shared.ui.dialog.WinNativeComposeDialogs import android.os.Environment +import com.winlator.cmod.feature.power.PerformanceManager /** * Compose replacement for the legacy `ContainerDetailFragment`. Reuses @@ -455,6 +456,25 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( state.directXComponents.value = directX state.generalComponents.value = general + if (PerformanceManager.isDeviceSupported && c != null) { + state.cpuGovernor.value = c.getExtra("cpuGovernor") + state.cpuBoostState.value = c.getExtra("cpuBoostState").toBoolean() + state.cpuEditingMode.value = c.getExtra("cpuEditingMode").toBoolean() + state.cpuFanMode.value = c.getExtra("cpuFanMode") + state.cpuTargetFPS.value = c.getExtra("cpuTargetFPS").toIntOrNull() ?: 0 + state.cpuAvgFrameCount.value = c.getExtra("cpuAvgFrameCount").toIntOrNull() ?: 3 + state.cpuAutoTargetFPS.value = c.getExtra("cpuAutoTargetFPS").toBoolean() + state.gpuFrequency.value = c.getExtra("gpuFrequency").toIntOrNull() ?: 0 + state.performanceMode.value = c.getExtra("performanceMode") + + val cpuPolicies = c.getExtra("cpuPolicies") + if (cpuPolicies.isNotEmpty()) { + val policies = PerformanceManager.parseRawPoliciesFromString(cpuPolicies) + if (policies != null) + state.cpuPolicies.value = policies + } + } + val envVarsStr = c?.getEnvVars() ?: Container.DEFAULT_ENV_VARS val items = parseEnvVarItems(envVarsStr) state.sdl2Compatibility.value = EnvVars(envVarsStr).get("SDL_XINPUT_ENABLED") == "1" @@ -774,6 +794,20 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( state.emulator64Entries.value, state.selectedEmulator64.intValue ) + if (PerformanceManager.isDeviceSupported && c != null) { + c.putExtra("cpuBoostState", state.cpuBoostState.value.toString()) + if (state.cpuGovernor.value.isNotEmpty()) + c.putExtra("cpuGovernor", state.cpuGovernor.value) + c.putExtra("cpuEditingMode", state.cpuEditingMode.value.toString()) + c.putExtra("cpuPolicies", PerformanceManager.policiesToString(state.cpuPolicies.value)) + c.putExtra("cpuFanMode", state.cpuFanMode.value) + c.putExtra("cpuTargetFPS", state.cpuTargetFPS.value.toString()) + c.putExtra("cpuAvgFrameCount", state.cpuAvgFrameCount.value.toString()) + c.putExtra("cpuAutoTargetFPS", state.cpuAutoTargetFPS.value.toString()) + c.putExtra("gpuFrequency", state.gpuFrequency.value.toString()) + c.putExtra("performanceMode", state.performanceMode.value) + } + val midiSoundFontEntries = state.midiSoundFontEntries.value val midiIdx = state.selectedMidiSoundFont.intValue val midiSoundFont = diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index 75fc5471b..b0cdaa8ef 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -83,6 +83,7 @@ import com.winlator.cmod.runtime.input.controls.InputControlsManager import com.winlator.cmod.runtime.audio.midi.MidiManager import com.winlator.cmod.runtime.display.winhandler.WinHandler import com.winlator.cmod.feature.artwork.SteamArtworkScraper +import com.winlator.cmod.feature.power.PerformanceManager import java.io.File import java.lang.reflect.Field import java.util.Arrays @@ -455,6 +456,69 @@ class ShortcutSettingsComposeDialog private constructor( } state.cpuCheckedWoW64.value = checkedWoW64 + CoroutineScope(Dispatchers.IO).launch { + if (PerformanceManager.isDeviceSupported) { + state.cpuGovernor.value = shortcut.getExtra("cpuGovernor") + state.cpuBoostState.value = shortcut.getExtra("cpuBoostState").toBoolean() + state.cpuEditingMode.value = shortcut.getExtra("cpuEditingMode").toBoolean() + state.cpuFanMode.value = shortcut.getExtra("cpuFanMode") + state.cpuTargetFPS.value = shortcut.getExtra("cpuTargetFPS").toIntOrNull() ?: 0 + state.cpuAvgFrameCount.value = shortcut.getExtra("cpuAvgFrameCount").toIntOrNull() ?: 3 + state.cpuAutoTargetFPS.value = shortcut.getExtra("cpuAutoTargetFPS").toBoolean() + state.gpuFrequency.value = shortcut.getExtra("gpuFrequency").toIntOrNull() ?: 0 + state.gpuFrequency.value = shortcut.getExtra("gpuFrequency").toIntOrNull() ?: 0 + state.performanceMode.value = shortcut.getExtra("performanceMode") + + val cpuPolicies = shortcut.getExtra("cpuPolicies") + if (cpuPolicies.isNotEmpty()) { + var policies = PerformanceManager.parseRawPoliciesFromString(cpuPolicies) + if (!policies.isNullOrEmpty()) + state.cpuPolicies.value = policies + else { + policies = PerformanceManager.getDefaultSystemPolicies() + if (policies != null) + state.cpuPolicies.value = policies + } + } else { + val containerPolicies = shortcut.container.getExtra("cpuPolicies") + state.cpuGovernor.value = shortcut.container.getExtra("cpuGovernor") + state.cpuBoostState.value = shortcut.container.getExtra("cpuBoostState").toBoolean() + state.cpuEditingMode.value = shortcut.container.getExtra("cpuEditingMode").toBoolean() + state.cpuFanMode.value = shortcut.container.getExtra("cpuFanMode") + state.cpuTargetFPS.value = shortcut.container.getExtra("cpuTargetFPS").toIntOrNull() ?: 0 + state.cpuAvgFrameCount.value = shortcut.container.getExtra("cpuAvgFrameCount").toIntOrNull() ?: 3 + state.cpuAutoTargetFPS.value = shortcut.container.getExtra("cpuAutoTargetFPS").toBoolean() + state.gpuFrequency.value = shortcut.container.getExtra("gpuFrequency").toIntOrNull() ?: 0 + state.performanceMode.value = shortcut.container.getExtra("performanceMode") + + if (containerPolicies.isNotEmpty()) { + var policies = PerformanceManager.parseRawPoliciesFromString(containerPolicies) + if (!policies.isNullOrEmpty()) { + state.cpuPolicies.value = policies + shortcut.putExtra("cpuPolicies", containerPolicies) + shortcut.putExtra("cpuGovernor", state.cpuGovernor.value) + shortcut.putExtra("cpuBoostState", state.cpuBoostState.value.toString()) + shortcut.putExtra("cpuEditingMode", state.cpuEditingMode.value.toString()) + shortcut.putExtra("cpuFanMode", state.cpuFanMode.value) + shortcut.putExtra("cpuTargetFPS", state.cpuTargetFPS.toString()) + shortcut.putExtra("cpuAvgFrameCount", state.cpuAvgFrameCount.toString()) + shortcut.putExtra("cpuAutoTargetFPS", state.cpuAutoTargetFPS.toString()) + shortcut.putExtra("gpuFrequency", state.gpuFrequency.toString()) + shortcut.putExtra("performanceMode", state.performanceMode.value) + } + else { + policies = PerformanceManager.getDefaultSystemPolicies() + if (!policies.isNullOrEmpty()) + state.cpuPolicies.value = policies + } + } else { + val policies = PerformanceManager.getDefaultSystemPolicies() + if (policies != null) + state.cpuPolicies.value = policies + } + } + } + } // Win Components loadWinComponents() @@ -1364,6 +1428,20 @@ class ShortcutSettingsComposeDialog private constructor( ) } + if (PerformanceManager.isDeviceSupported) { + shortcut.putExtra("cpuBoostState", state.cpuBoostState.value.toString()) + if (state.cpuGovernor.value.isNotEmpty()) + shortcut.putExtra("cpuGovernor", state.cpuGovernor.value) + shortcut.putExtra("cpuEditingMode", state.cpuEditingMode.value.toString()) + shortcut.putExtra("cpuPolicies", PerformanceManager.policiesToString(state.cpuPolicies.value)) + shortcut.putExtra("cpuFanMode", state.cpuFanMode.value) + shortcut.putExtra("cpuTargetFPS", state.cpuTargetFPS.value.toString()) + shortcut.putExtra("cpuAvgFrameCount", state.cpuAvgFrameCount.value.toString()) + shortcut.putExtra("cpuAutoTargetFPS", state.cpuAutoTargetFPS.value.toString()) + shortcut.putExtra("gpuFrequency", state.gpuFrequency.value.toString()) + shortcut.putExtra("performanceMode", state.performanceMode.value) + } + // Container defaults flag shortcut.putExtra( EXTRA_USE_CONTAINER_DEFAULTS, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 364f9c208..89368dd2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -319,6 +319,22 @@ Re-added existing shortcut Failed to add shortcut. + + Enabled CPU Frequencies + Enabled CPU Boost + Enabled Cluster %s + Cluster %s Min + Cluster %s Max + CPU Governor + Performance + Fan Control + Power Controls + FPS Target + Frames per update. + %s Frames + GPU Frequency + + Exec Arguments Target Path diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index b5429ae71..64e09d5f0 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -50,6 +50,7 @@ import androidx.core.view.WindowInsetsCompat; import com.winlator.cmod.BuildConfig; import com.winlator.cmod.feature.leaderboard.SessionRecordingController; +import com.winlator.cmod.feature.power.PerformanceManager; import com.winlator.cmod.feature.stores.steam.enums.Marker; import com.winlator.cmod.feature.stores.steam.utils.MarkerUtils; import com.winlator.cmod.feature.stores.steam.utils.PrefManager; @@ -1835,6 +1836,7 @@ public void onModifyWindowProperty(Window window, Property property) { @Override public void onFramePresented(Window window, WindowManager.FrameSource source, int serial) { if (window != null && window.id == rendererWindowId) rendererWindowPresented = true; + PerformanceManager.INSTANCE.onTick(); if (shouldRecordFpsFrame(window, source)) { frameRating.recordGameFrame(source == WindowManager.FrameSource.PRESENT, serial); if (mangoHud != null) mangoHud.recordGameFrame(source == WindowManager.FrameSource.PRESENT); @@ -1877,6 +1879,7 @@ public void onFailed(Exception e) { String controlsProfile = shortcut != null ? shortcut.getExtra("controlsProfile", "") : ""; Runnable runnable = () -> { + startPerformanceManager(); setupUI(); if (controlsProfile.isEmpty()) { simulateConfirmInputControlsDialog(); @@ -1984,6 +1987,82 @@ this, shortcut, isCloudSyncEnabledForShortcut(), } } + private void startPerformanceManager() { + //All wintoast are just for debug will remove later + var performanceManager = PerformanceManager.INSTANCE; + if (performanceManager.isDeviceSupported()) { + if (shortcut.getExtra("cpuPolicies").isEmpty() && !container.getExtra("cpuPolicies").isEmpty()) { + WinToast.show(this, "settings empty using container default"); + String[] fields = {"cpuPolicies", "cpuEditingMode", "cpuBoostState", "cpuFanMode", "cpuGovernor", "cpuAutoTargetFPS", "cpuTargetFPS", "cpuAvgFrameCount"}; + for (String field : fields) shortcut.putExtra(field, container.getExtra(field)); + } + var cpuPolicies = shortcut.getExtra("cpuPolicies"); + var cpuEditingMode = Boolean.parseBoolean(shortcut.getExtra("cpuEditingMode")); + var cpuBoostState = Boolean.parseBoolean(shortcut.getExtra("cpuBoostState")); + var cpuFanMode = shortcut.getExtra("cpuFanMode"); + var gpuFrequency = shortcut.getExtra("gpuFrequency"); + var performanceMode = shortcut.getExtra("performanceMode"); + var cpuGovernor = shortcut.getExtra("cpuGovernor"); + if (!cpuFanMode.isEmpty()) { + WinToast.show(this, String.format("CPU fan mode set: %s", cpuFanMode)); + performanceManager.setFanMode(cpuFanMode); + } + if (!cpuGovernor.isEmpty()) { + WinToast.show(this, String.format("CPU governor set: %s", cpuGovernor)); + performanceManager.setAllCpuCoreGovernor(cpuGovernor); + } + if (!performanceMode.isEmpty()) { + WinToast.show(this, String.format("Performance mode set: %s", performanceMode)); + performanceManager.setPerformanceMode(performanceMode); + } + if (Boolean.parseBoolean(shortcut.getExtra("cpuAutoTargetFPS"))) { + var cpuTargetFPS = shortcut.getExtra("cpuTargetFPS"); + var cpuAvgFrameCount = shortcut.getExtra("cpuAvgFrameCount"); + if (!cpuTargetFPS.isEmpty() && !cpuAvgFrameCount.isEmpty()) { + try { + var targetFPS = Integer.parseInt(cpuTargetFPS); + var frameCountPerCheck = Integer.parseInt(cpuAvgFrameCount); + + if (targetFPS > 0) { + performanceManager.setUseAutoTargeting(true); + performanceManager.setFpsAvgFrameCount(frameCountPerCheck); + performanceManager.setTargetFPS(targetFPS); + } + }catch (Exception ignored) {} + WinToast.show(this, "Auto FPS enabled"); + return; + } + } + if (cpuEditingMode && !cpuPolicies.isEmpty()) { + WinToast.show(this, "CPU policies set"); + performanceManager.setCpuCorePolicies(cpuPolicies, false); + } else if (!cpuBoostState) { + performanceManager.setCpuBoostState(false); + } + if (!cpuEditingMode && !cpuPolicies.isEmpty()) { + var policies = performanceManager.parseRawPoliciesFromString(cpuPolicies); + if (policies != null && !policies.isEmpty()) { + policies.forEach(it -> { + if (!it.getEnabled()) + performanceManager.setPolicyOnlineState(it, false); + }); + } + } + WinToast.show(this, String.format("disabled cores %s: ", performanceManager.getDisabledCores())); + if (!gpuFrequency.isEmpty()) { + try { + var gpuFreq = Integer.parseInt(gpuFrequency); + performanceManager.setGpuFrequency(gpuFreq, gpuFreq,true); + var freqs = performanceManager.getGpuFrequencies(); + if (freqs != null) { + WinToast.show(this, String.format("GPU SET: %s", freqs.get(gpuFreq))); + } + + } catch (Exception ignored) {} + } + } + } + private String resolveDesktopPathFromUri(android.net.Uri uri) { if (uri == null) return null; try { @@ -3292,6 +3371,10 @@ private void exit() { }, "XServerExitCleanup").start(); }, 1000); }); + var performanceManager = PerformanceManager.INSTANCE; + if (performanceManager.isDeviceSupported()) { + performanceManager.resetSystemToDefault(); + } } private void closeAfterSessionExit() { diff --git a/app/src/main/runtime/display/ui/MangoHudView.java b/app/src/main/runtime/display/ui/MangoHudView.java index 5cff40e1d..33cb8e3e7 100644 --- a/app/src/main/runtime/display/ui/MangoHudView.java +++ b/app/src/main/runtime/display/ui/MangoHudView.java @@ -19,6 +19,8 @@ import android.view.View; import android.view.ViewGroup; import androidx.preference.PreferenceManager; + +import com.winlator.cmod.feature.power.PerformanceManager; import com.winlator.cmod.runtime.system.CPUStatus; import java.io.BufferedReader; import java.io.File; @@ -1192,7 +1194,9 @@ protected void onDraw(Canvas canvas) { y += rowH; } if (elements[EL_CORES]) { + var disabledCores = PerformanceManager.INSTANCE.getDisabledCores(); for (int i = 0; i < coreCount; i++) { + if (disabledCores.contains(i)) continue; float x = drawLabel(canvas, coreLabels[i], C_CPU, y); x = drawStatCell(canvas, sbCorePct[i], "%", x, y, 3); drawStatCell(canvas, sbCoreMhz[i], "MHz", x, y, 4); diff --git a/app/src/main/shared/util/RootManager.kt b/app/src/main/shared/util/RootManager.kt new file mode 100644 index 000000000..70335a2b0 --- /dev/null +++ b/app/src/main/shared/util/RootManager.kt @@ -0,0 +1,71 @@ +package com.winlator.cmod.shared.util + +import android.os.IBinder +import android.os.Parcel +import com.topjohnwu.superuser.Shell +import java.nio.charset.Charset + + +object RootManager { + var isRooted: Boolean = false + get() = isDeviceRooted() + private set + + private val binder: IBinder? = runCatching { + val serviceManager = Class.forName("android.os.ServiceManager") + val getService = serviceManager.getDeclaredMethod("getService", String::class.java) + val rawBinder = getService.invoke(serviceManager, "PServerBinder") as IBinder + rawBinder + }.getOrNull() + + fun readSysfsFile(path: String): String? { + if (!isRooted) return null + val result = executeAsRoot("cat '$path'") ?: return null + return result + } + + fun writeSysfsFile(path: String, value: String, lockFile: Boolean=true): Boolean { + return try { + var command = "chmod 644 '$path'; echo '$value' > '$path';" + if (lockFile) + command+=" chmod 444 '$path'" + val result = executeAsRoot(command) + return result != null + } catch (e: Exception) { + false + } + } + + private fun decodeReply(reply: Parcel): String? { + return reply.createByteArray() + ?.toString(Charset.defaultCharset()) + ?.trim() + ?.let { value -> if (value == "null") null else value } + } + + fun executeAsRoot(cmd: String): String? { + if (binder != null) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + return try { + data.writeStringArray(arrayOf(cmd, "1")) + binder.transact(0, data, reply, 0) + decodeReply(reply) + } catch (throwable: Throwable) { + return null + } finally { + data.recycle() + reply.recycle() + } + } else if (Shell.isAppGrantedRoot() == true) { + val result: Shell.Result = Shell.cmd(cmd).exec() + if (!result.isSuccess) return null + return result.out.toString() + } + return null + } + + fun isDeviceRooted(): Boolean { + return binder != null || Shell.isAppGrantedRoot() == true + } +} \ No newline at end of file