/* * Copyright (c) Souldbminer and Horizon OC Contributors * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "governor.hpp" #include "process_management.hpp" namespace governor { #define POLL_NS 5'000'000 // 5 ms – governor poll rate #define DOWN_HOLD_TICKS 10 // 50 ms – how long to in POLL_NS to hold while ramping down #define STEP_UTIL 900 // multiplier for step calculations bool isGpuGovernorEnabled = false; bool isCpuGovernorEnabled = false; bool lastGpuGovernorState = false; bool lastCpuGovernorState = false; bool lastVrrGovernorState = false; bool hasChanged = true; bool isCpuGovernorInBoostMode = false; bool isVRREnabled = false; // thread handles Thread cpuGovernorTHREAD; Thread gpuGovernorTHREAD; Thread vrrTHREAD; void HandleGovernor(uint32_t targetHz) { u32 tempTargetHz = clockManager::gContext.overrideFreqs[HocClkModule_Governor]; if (!tempTargetHz) { tempTargetHz = config::GetAutoClockHz(clockManager::gContext.applicationId, HocClkModule_Governor, clockManager::gContext.profile, true); if (!tempTargetHz) tempTargetHz = config::GetAutoClockHz(GLOBAL_PROFILE_ID, HocClkModule_Governor, clockManager::gContext.profile, true); } auto resolve = [](u8 app, u8 temp) -> u8 { if (temp == ComponentGovernor_Disabled) return ComponentGovernor_Disabled; if (temp != ComponentGovernor_DoNotOverride) return temp; return app; }; u8 effectiveCpu = resolve(GovernorStateCpu(targetHz), GovernorStateCpu(tempTargetHz)); u8 effectiveGpu = resolve(GovernorStateGpu(targetHz), GovernorStateGpu(tempTargetHz)); u8 effectiveVrr = resolve(GovernorStateVrr(targetHz), GovernorStateVrr(tempTargetHz)); bool newCpuGovernorState = (effectiveCpu == ComponentGovernor_Enabled); bool newGpuGovernorState = (effectiveGpu == ComponentGovernor_Enabled); bool newVrrGovernorState = (effectiveVrr == ComponentGovernor_Enabled); isCpuGovernorEnabled = newCpuGovernorState; isGpuGovernorEnabled = newGpuGovernorState; isVRREnabled = newVrrGovernorState; if (newCpuGovernorState == false && lastCpuGovernorState == true) { svcSleepThread(100'000'000); // thread syncing. probably a cleaner way to do this but hey, it works! board::ResetToStockCpu(); } if (newGpuGovernorState == false && lastGpuGovernorState == true) { svcSleepThread(100'000'000); board::ResetToStockGpu(); } if (newVrrGovernorState == false && lastVrrGovernorState == true) { svcSleepThread(100'000'000); board::ResetToStockDisplay(); } if (newCpuGovernorState != lastCpuGovernorState || newGpuGovernorState != lastGpuGovernorState || newVrrGovernorState != lastVrrGovernorState) { fileUtils::LogLine("[mgr] Governor state changed: CPU %s, GPU %s, VRR %s", newCpuGovernorState ? "enabled" : "disabled", newGpuGovernorState ? "enabled" : "disabled", newVrrGovernorState ? "enabled" : "disabled"); lastCpuGovernorState = newCpuGovernorState; lastGpuGovernorState = newGpuGovernorState; lastVrrGovernorState = newVrrGovernorState; } } u32 SchedutilTargetHz(u32 util, u32 tableMaxHz) { u64 hz = (u64)tableMaxHz * util / STEP_UTIL; return (u32)(std::min(hz, static_cast(tableMaxHz))); } u32 TableIndexForHz(const clockManager::FreqTable& table, u32 targetHz) { for (u32 i = 0; i < table.count; i++) if (table.list[i] >= targetHz) return i; return table.count - 1; } u32 ResolveTargetHz(HocClkModule module) { u32 hz = clockManager::gContext.overrideFreqs[module]; if (!hz) hz = config::GetAutoClockHz( clockManager::gContext.applicationId, module, clockManager::gContext.profile, false); if (!hz) hz = config::GetAutoClockHz( GLOBAL_PROFILE_ID, module, clockManager::gContext.profile, false); return hz; } void CpuGovernorThread(void* arg) { (void)arg; u32 downHoldRemaining = 0; u32 lastHz = 0; u32 minHz = 612; u32 tick = 0; for (;;) { if (!clockManager::gRunning || !isCpuGovernorEnabled) { downHoldRemaining = 0; lastHz = 0; svcSleepThread(POLL_NS); continue; } u32 mode = 0; Result rc = apmExtGetCurrentPerformanceConfiguration(&mode); if (R_SUCCEEDED(rc) && apmExtIsBoostMode(mode)) { isCpuGovernorInBoostMode = true; downHoldRemaining = 0; lastHz = 0; continue; // TODO: figure out a way to get boost clock easily and set it instead of just skipping the governor } else if (!apmExtIsBoostMode(mode)) { isCpuGovernorInBoostMode = false; } auto& table = clockManager::gFreqTable[HocClkModule_CPU]; if (table.count == 0) continue; std::scoped_lock lock{clockManager::gContextMutex}; u32 cpuLoad = board::GetPartLoad(HocClkPartLoad_CPUMax); u32 tableMaxHz = table.list[table.count - 1]; u32 desiredHz = SchedutilTargetHz(cpuLoad, tableMaxHz); u32 targetHz = ResolveTargetHz(HocClkModule_CPU); u32 maxHz = clockManager::GetMaxAllowedHz(HocClkModule_CPU, clockManager::gContext.profile); if (targetHz && desiredHz > targetHz) desiredHz = targetHz; if (maxHz && desiredHz > maxHz) desiredHz = maxHz; u32 newHz = table.list[TableIndexForHz(table, desiredHz)]; // ramp up fast, go down slow bool goingDown = (lastHz != 0) && (newHz < lastHz); if (!goingDown) downHoldRemaining = 0; else if (downHoldRemaining == 0) downHoldRemaining = DOWN_HOLD_TICKS; if (downHoldRemaining > 0) downHoldRemaining--; if (++tick > 50) { minHz = config::GetConfigValue(HocClkConfigValue_CpuGovernorMinimumFreq); tick = 0; } if (newHz < minHz) newHz = minHz; if ((!goingDown || (downHoldRemaining == 0)) && clockManager::IsAssignableHz(HocClkModule_CPU, newHz)) { board::SetHz(HocClkModule_CPU, newHz); clockManager::gContext.freqs[HocClkModule_CPU] = newHz; lastHz = newHz; } svcSleepThread(POLL_NS); } } void GovernorThread(void* arg) { (void)arg; u32 downHoldRemaining = 0; u32 lastHz = 0; for (;;) { if (!clockManager::gRunning || !isGpuGovernorEnabled) { downHoldRemaining = 0; lastHz = 0; svcSleepThread(POLL_NS); continue; } auto& table = clockManager::gFreqTable[HocClkModule_GPU]; if (table.count == 0) continue; std::scoped_lock lock{clockManager::gContextMutex}; u32 gpuLoad = board::GetPartLoad(HocClkPartLoad_GPU); u32 tableMaxHz = table.list[table.count - 1]; u32 desiredHz = SchedutilTargetHz(gpuLoad, tableMaxHz); u32 targetHz = ResolveTargetHz(HocClkModule_GPU); u32 maxHz = clockManager::GetMaxAllowedHz(HocClkModule_GPU, clockManager::gContext.profile); if (targetHz && desiredHz > targetHz) desiredHz = targetHz; if (maxHz && desiredHz > maxHz) desiredHz = maxHz; u32 newHz = table.list[TableIndexForHz(table, desiredHz)]; bool goingDown = (lastHz != 0) && (newHz < lastHz); if (!goingDown) downHoldRemaining = 0; else if (downHoldRemaining == 0) downHoldRemaining = DOWN_HOLD_TICKS; if (downHoldRemaining > 0) downHoldRemaining--; if ((!goingDown || (downHoldRemaining == 0)) && clockManager::IsAssignableHz(HocClkModule_GPU, newHz)) { board::SetHz(HocClkModule_GPU, newHz); clockManager::gContext.freqs[HocClkModule_GPU] = newHz; lastHz = newHz; } svcSleepThread(POLL_NS); } } void VRRThread(void* arg) { (void)arg; u8 tick = 0, tick2 = 0; for (;;) { if (!clockManager::gRunning || clockManager::gContext.profile == HocClkProfile_Docked || !isVRREnabled) { svcSleepThread(POLL_NS); continue; } std::scoped_lock lock{clockManager::gContextMutex}; if(++tick2 > 100) { bool isApplicationOutOfFocus = false; Result rc = processManagement::isApplicationOutOfFocus(&isApplicationOutOfFocus); if(R_FAILED(rc)) { svcSleepThread(POLL_NS); continue; } if(isApplicationOutOfFocus) { board::ResetToStockDisplay(); svcSleepThread(POLL_NS); continue; } } u8 fps; if (clockManager::gContext.isSaltyNXInstalled) { fps = integrations::GetSaltyNXFPS(); } else { svcSleepThread(~0ULL); // effectively disable the thread if SaltyNX isn't installed, as there's no point in it running continue; } if (fps == 254) { svcSleepThread(POLL_NS); continue; } // if(appletGetFocusState() != AppletFocusState_InFocus) { // board::ResetToStockDisplay(); // continue; // } u32 targetHz = clockManager::gContext.overrideFreqs[HocClkModule_Display]; if (!targetHz) { targetHz = config::GetAutoClockHz(clockManager::gContext.applicationId, HocClkModule_Display, clockManager::gContext.profile, false); if (!targetHz) targetHz = config::GetAutoClockHz(GLOBAL_PROFILE_ID, HocClkModule_Display, clockManager::gContext.profile, false); } u8 maxDisplay; if (targetHz) { maxDisplay = targetHz; } else { maxDisplay = 60; // don't assume display stuff! } u8 minDisplay = board::GetConsoleType() == HocClkConsoleType_Aula ? 45 : 40; if (maxDisplay == minDisplay) continue; if (fps >= minDisplay && fps <= maxDisplay) { board::SetHz(HocClkModule_Display, fps); clockManager::gContext.freqs[HocClkModule_Display] = fps; clockManager::gContext.realFreqs[HocClkModule_Display] = fps; } else { for (u32 i = 0; i < 10; i++) { u32 compareHz = fps * i; if (compareHz >= minDisplay && compareHz <= maxDisplay) { board::SetHz(HocClkModule_Display, compareHz); clockManager::gContext.freqs[HocClkModule_Display] = compareHz; clockManager::gContext.realFreqs[HocClkModule_Display] = compareHz; break; } } } if (++tick > 50) { board::SetHz(HocClkModule_Display, maxDisplay); tick = 0; svcSleepThread(50'000'000); } svcSleepThread(POLL_NS); } } void startThreads() { threadCreate( &cpuGovernorTHREAD, CpuGovernorThread, nullptr, NULL, 0x2000, 0x3F, -2 ); threadCreate( &gpuGovernorTHREAD, GovernorThread, nullptr, NULL, 0x2000, 0x3F, -2 ); threadCreate( &vrrTHREAD, VRRThread, nullptr, NULL, 0x2000, 0x3F, -2 ); threadStart(&cpuGovernorTHREAD); threadStart(&gpuGovernorTHREAD); threadStart(&vrrTHREAD); } void exitThreads() { threadClose(&cpuGovernorTHREAD); threadClose(&gpuGovernorTHREAD); threadClose(&vrrTHREAD); } }