1853 lines
90 KiB
C++
1853 lines
90 KiB
C++
class MainMenu;
|
||
|
||
class MiniOverlay : public tsl::Gui {
|
||
private:
|
||
char GPU_Load_c[32] = "";
|
||
char Rotation_SpeedLevel_c[64] = "";
|
||
char RAM_var_compressed_c[128] = "";
|
||
char Battery_c[64] = "";
|
||
char soc_temperature_c[64] = "";
|
||
char skin_temperature_c[64] = "";
|
||
char CPU_temp_c[32];
|
||
char GPU_temp_c[32];
|
||
char RAM_temp_c[32];
|
||
|
||
uint32_t rectangleWidth;
|
||
char Variables[512];
|
||
size_t fontsize;
|
||
MiniSettings settings;
|
||
bool Initialized = false;
|
||
ApmPerformanceMode performanceMode = ApmPerformanceMode_Invalid;
|
||
uint64_t systemtickfrequency_impl = systemtickfrequency;
|
||
int frameOffsetX, frameOffsetY;
|
||
int topPadding, bottomPadding;
|
||
bool isDragging = false;
|
||
|
||
bool skipOnce = true;
|
||
bool runOnce = true;
|
||
|
||
size_t framePadding = 10;
|
||
static constexpr int screenWidth = 1280;
|
||
static constexpr int screenHeight = 720;
|
||
|
||
struct ButtonState {
|
||
std::atomic<bool> minusDragActive{false};
|
||
std::atomic<bool> plusDragActive{false};
|
||
} buttonState;
|
||
|
||
Thread touchPollThread;
|
||
std::atomic<bool> touchPollRunning{false};
|
||
public:
|
||
MiniOverlay() {
|
||
CPU_temp_c[0] = '\0';
|
||
GPU_temp_c[0] = '\0';
|
||
RAM_temp_c[0] = '\0';
|
||
|
||
tsl::hlp::requestForeground(false);
|
||
disableJumpTo = true;
|
||
//tsl::initializeUltrahandSettings();
|
||
PowerConsumption = 0.0f;
|
||
batTimeEstimate = -1;
|
||
strcpy(Battery_c, "-.-- W-.-% [--:--]"); // Default display
|
||
|
||
GetConfigSettings(&settings);
|
||
apmGetPerformanceMode(&performanceMode);
|
||
if (performanceMode == ApmPerformanceMode_Normal) {
|
||
fontsize = settings.handheldFontSize;
|
||
}
|
||
else fontsize = settings.dockedFontSize;
|
||
|
||
if (settings.disableScreenshots) {
|
||
tsl::gfx::Renderer::get().removeScreenshotStacks();
|
||
}
|
||
mutexInit(&mutex_BatteryChecker);
|
||
mutexInit(&mutex_Misc);
|
||
//alphabackground = 0x0;
|
||
frameOffsetX = settings.frameOffsetX;
|
||
frameOffsetY = settings.frameOffsetY;
|
||
framePadding = settings.framePadding;
|
||
topPadding = 5;
|
||
bottomPadding = 2;
|
||
|
||
if (ult::limitedMemory) {
|
||
tsl::gfx::Renderer::get().setLayerPos(std::max(std::min((int)(frameOffsetX*1.5 + 0.5) - tsl::impl::currentUnderscanPixels.first, 1280-32 - tsl::impl::currentUnderscanPixels.first), 0), 0);
|
||
}
|
||
|
||
FullMode = false;
|
||
TeslaFPS = settings.refreshRate;
|
||
systemtickfrequency_impl /= settings.refreshRate;
|
||
deactivateOriginalFooter = true;
|
||
realVoltsPolling = settings.realVolts;
|
||
StartThreads();
|
||
|
||
// Get initial battery readings BEFORE starting threads
|
||
if (R_SUCCEEDED(psmCheck) && R_SUCCEEDED(i2cCheck)) {
|
||
uint16_t data = 0;
|
||
float tempA = 0.0;
|
||
|
||
// Get initial power consumption
|
||
Max17050ReadReg(MAX17050_AvgCurrent, &data);
|
||
tempA = (1.5625 / (max17050SenseResistor * max17050CGain)) * (s16)data;
|
||
PowerConsumption = tempA * batVoltageAvg / 1000000.0; // Rough initial estimate
|
||
|
||
// Get initial battery info
|
||
psmGetBatteryChargeInfoFields(psmService, &_batteryChargeInfoFields);
|
||
|
||
// Get initial time estimate
|
||
if (tempA >= 0) {
|
||
batTimeEstimate = -1;
|
||
} else {
|
||
Max17050ReadReg(MAX17050_TTE, &data);
|
||
const float batteryTimeEstimateInMinutes = (5.625 * data) / 60;
|
||
if (batteryTimeEstimateInMinutes > (99.0*60.0)+59.0) {
|
||
batTimeEstimate = (99*60)+59;
|
||
} else {
|
||
batTimeEstimate = (int16_t)batteryTimeEstimateInMinutes;
|
||
}
|
||
}
|
||
} else {
|
||
// Fallback if checks failed
|
||
PowerConsumption = 0.0f;
|
||
batTimeEstimate = -1;
|
||
_batteryChargeInfoFields = {0};
|
||
}
|
||
|
||
// Now format the initial Battery_c string
|
||
char remainingBatteryLife[8];
|
||
const float drawW = (fabsf(PowerConsumption) < 0.01f) ? 0.0f : PowerConsumption;
|
||
|
||
if (batTimeEstimate >= 0 && !(drawW <= 0.01f && drawW >= -0.01f)) {
|
||
snprintf(remainingBatteryLife, sizeof(remainingBatteryLife),
|
||
"%d:%02d", batTimeEstimate / 60, batTimeEstimate % 60);
|
||
} else {
|
||
strcpy(remainingBatteryLife, "--:--");
|
||
}
|
||
|
||
if (!settings.invertBatteryDisplay) {
|
||
snprintf(Battery_c, sizeof(Battery_c),
|
||
"%.2f W%.1f%% [%s]",
|
||
drawW,
|
||
(float)_batteryChargeInfoFields.RawBatteryCharge / 1000.0f,
|
||
remainingBatteryLife);
|
||
} else {
|
||
snprintf(Battery_c, sizeof(Battery_c),
|
||
"%.1f%% [%s]%.2f W",
|
||
(float)_batteryChargeInfoFields.RawBatteryCharge / 1000.0f,
|
||
remainingBatteryLife,
|
||
drawW);
|
||
}
|
||
|
||
|
||
|
||
|
||
// Start touch polling thread for instant response at low FPS
|
||
touchPollRunning.store(true, std::memory_order_release);
|
||
threadCreate(&touchPollThread, [](void* arg) -> void {
|
||
MiniOverlay* overlay = static_cast<MiniOverlay*>(arg);
|
||
|
||
// Allow only Player 1 and handheld mode
|
||
const HidNpadIdType id_list[2] = { HidNpadIdType_No1, HidNpadIdType_Handheld };
|
||
|
||
// Configure HID system to only listen to these IDs
|
||
hidSetSupportedNpadIdType(id_list, 2);
|
||
|
||
// Configure input for up to 2 supported controllers (P1 + Handheld)
|
||
padConfigureInput(2, HidNpadStyleSet_NpadStandard | HidNpadStyleTag_NpadSystemExt);
|
||
|
||
// Initialize separate pad states for both controllers
|
||
PadState pad_p1;
|
||
PadState pad_handheld;
|
||
padInitialize(&pad_p1, HidNpadIdType_No1);
|
||
padInitialize(&pad_handheld, HidNpadIdType_Handheld);
|
||
|
||
u64 minusHoldStart = 0;
|
||
u64 plusHoldStart = 0;
|
||
static constexpr u64 HOLD_THRESHOLD_NS = 500'000'000ULL;
|
||
|
||
HidTouchScreenState state = {0};
|
||
bool inputDetected;
|
||
size_t actualEntryCount;
|
||
|
||
while (overlay->touchPollRunning.load(std::memory_order_acquire)) {
|
||
// Only poll when rendering and not dragging
|
||
{
|
||
inputDetected = false;
|
||
|
||
// Check touch in bounds
|
||
if (hidGetTouchScreenStates(&state, 1) && state.count > 0) {
|
||
const int touchX = state.touches[0].x;
|
||
const int touchY = state.touches[0].y;
|
||
|
||
// Calculate bounds (same logic as handleInput)
|
||
const uint32_t margin = (overlay->fontsize * 4);
|
||
const int overlayX = overlay->frameOffsetX;
|
||
const int overlayY = overlay->frameOffsetY;
|
||
const int overlayWidth = margin + overlay->rectangleWidth + (overlay->fontsize / 3);
|
||
|
||
// Calculate height from Variables string
|
||
actualEntryCount = 1;
|
||
for (size_t i = 0; overlay->Variables[i] != '\0'; i++) {
|
||
if (overlay->Variables[i] == '\n') {
|
||
actualEntryCount++;
|
||
}
|
||
}
|
||
const int overlayHeight = ((overlay->fontsize + overlay->settings.spacing) * actualEntryCount) +
|
||
(overlay->fontsize / 3) + overlay->settings.spacing +
|
||
overlay->topPadding + overlay->bottomPadding;
|
||
|
||
// Add touch padding
|
||
const int touchPadding = 4;
|
||
const int touchableX = overlayX - touchPadding;
|
||
const int touchableY = overlayY - touchPadding;
|
||
const int touchableWidth = overlayWidth + (touchPadding * 2);
|
||
const int touchableHeight = overlayHeight + (touchPadding * 2);
|
||
|
||
// Check if touch is within bounds
|
||
if (touchX >= touchableX && touchX <= touchableX + touchableWidth &&
|
||
touchY >= touchableY && touchY <= touchableY + touchableHeight) {
|
||
inputDetected = true;
|
||
}
|
||
}
|
||
|
||
// Poll buttons from both controllers
|
||
padUpdate(&pad_p1);
|
||
padUpdate(&pad_handheld);
|
||
//const u64 keysHeld_p1 = padGetButtons(&pad_p1);
|
||
//const u64 keysHeld_handheld = padGetButtons(&pad_handheld);
|
||
|
||
// Combine input from both controllers
|
||
const u64 keysHeld = padGetButtons(&pad_p1) | padGetButtons(&pad_handheld);
|
||
const u64 now = armTicksToNs(armGetSystemTick());
|
||
|
||
// Track MINUS hold duration
|
||
if ((keysHeld & KEY_MINUS) && !(keysHeld & ~KEY_MINUS & ALL_KEYS_MASK)) {
|
||
if (minusHoldStart == 0) {
|
||
minusHoldStart = now;
|
||
}
|
||
if (now - minusHoldStart >= HOLD_THRESHOLD_NS) {
|
||
// Long enough to start drag
|
||
inputDetected = true;
|
||
overlay->buttonState.minusDragActive.exchange(true, std::memory_order_acq_rel);
|
||
}
|
||
}
|
||
|
||
// Track PLUS hold duration
|
||
else if ((keysHeld & KEY_PLUS) && !(keysHeld & ~KEY_PLUS & ALL_KEYS_MASK)) {
|
||
if (plusHoldStart == 0) {
|
||
plusHoldStart = now;
|
||
}
|
||
if (now - plusHoldStart >= HOLD_THRESHOLD_NS) {
|
||
// Long enough to start drag
|
||
inputDetected = true;
|
||
overlay->buttonState.plusDragActive.exchange(true, std::memory_order_acq_rel);
|
||
}
|
||
}
|
||
|
||
else {
|
||
minusHoldStart = plusHoldStart = 0;
|
||
overlay->buttonState.minusDragActive.exchange(false, std::memory_order_acq_rel);
|
||
overlay->buttonState.plusDragActive.exchange(false, std::memory_order_acq_rel);
|
||
}
|
||
|
||
// Disable rendering on any input, re-enable when no input
|
||
static bool resetOnce = true;
|
||
if (inputDetected) {
|
||
if (resetOnce && isRendering) {
|
||
isRendering = false;
|
||
leventSignal(&renderingStopEvent);
|
||
resetOnce = false;
|
||
}
|
||
} else {
|
||
resetOnce = true;
|
||
}
|
||
}
|
||
|
||
if (ult::limitedMemory) {
|
||
static auto lastUnderscanPixels = std::make_pair(0, 0);
|
||
|
||
if (lastUnderscanPixels != tsl::impl::currentUnderscanPixels) {
|
||
for (int i = 0; i < 2; i++) {
|
||
tsl::gfx::Renderer::get().updateLayerSize();
|
||
tsl::gfx::Renderer::get().setLayerPos(std::max(std::min((int)(overlay->frameOffsetX*1.5 + 0.5) - tsl::impl::currentUnderscanPixels.first, 1280-32 - tsl::impl::currentUnderscanPixels.first), 0), 0);
|
||
}
|
||
}
|
||
lastUnderscanPixels = tsl::impl::currentUnderscanPixels;
|
||
}
|
||
|
||
svcSleepThread(32000000ULL); // 32ms polling
|
||
}
|
||
}, this, NULL, 0x1000, 0x2B, -2);
|
||
threadStart(&touchPollThread);
|
||
}
|
||
~MiniOverlay() {
|
||
// Stop touch polling thread
|
||
touchPollRunning.store(false, std::memory_order_release);
|
||
threadWaitForExit(&touchPollThread);
|
||
threadClose(&touchPollThread);
|
||
|
||
CloseThreads();
|
||
FullMode = true;
|
||
fixForeground = true;
|
||
//tsl::hlp::requestForeground(true);
|
||
//alphabackground = 0xD;
|
||
deactivateOriginalFooter = false;
|
||
}
|
||
|
||
resolutionCalls m_resolutionRenderCalls[8] = {0};
|
||
resolutionCalls m_resolutionViewportCalls[8] = {0};
|
||
resolutionCalls m_resolutionOutput[8] = {0};
|
||
uint8_t resolutionLookup = 0;
|
||
bool resolutionShow = false;
|
||
|
||
virtual tsl::elm::Element* createUI() override {
|
||
|
||
auto* Status = new tsl::elm::CustomDrawer([this](tsl::gfx::Renderer *renderer, u16 x, u16 y, u16 w, u16 h) {
|
||
//static constexpr u16 frameWidth = 448;
|
||
|
||
// Cache parsed settings and calculations
|
||
static std::vector<std::string> showKeys;
|
||
static std::string lastShowSetting;
|
||
static bool needsRecalc = true;
|
||
static std::vector<std::string> labelLines; // Store individual label lines
|
||
static std::string lastVariables; // Track changes in Variables content
|
||
static size_t entryCount = 0;
|
||
static uint32_t cachedHeight = 0;
|
||
static int cachedBaseX = 0, cachedBaseY = 0;
|
||
static bool lastGameRunning = false; // Track game state changes
|
||
|
||
// Check if we need to recalculate due to content changes
|
||
const bool contentChanged = (std::string(Variables) != lastVariables) ||
|
||
(GameRunning != lastGameRunning);
|
||
|
||
// Only recalculate if settings changed or content changed
|
||
if (settings.show != lastShowSetting || !Initialized || contentChanged) {
|
||
showKeys.clear();
|
||
|
||
// Parse once and cache
|
||
size_t start = 0, end = 0;
|
||
const std::string& show = settings.show;
|
||
while ((end = show.find('+', start)) != std::string::npos) {
|
||
showKeys.emplace_back(show.substr(start, end - start));
|
||
start = end + 1;
|
||
}
|
||
if (start < show.length()) {
|
||
showKeys.emplace_back(show.substr(start));
|
||
}
|
||
|
||
lastShowSetting = settings.show;
|
||
lastVariables = Variables;
|
||
lastGameRunning = GameRunning;
|
||
needsRecalc = true;
|
||
}
|
||
|
||
// Initial width calculation (only once)
|
||
if (!Initialized) {
|
||
|
||
rectangleWidth = 0;
|
||
//std::pair<u32, u32> dimensions;
|
||
u32 width;
|
||
|
||
for (const auto& key : showKeys) {
|
||
if (key == "CPU") {
|
||
//dimensions = renderer->drawString("[100%,100%,100%,100%]@4444.4", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
if (!settings.realVolts) {
|
||
if (settings.showFullCPU)
|
||
width = renderer->getTextDimensions("[100%,100%,100%,100%]@4444.4", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("100%@4444.4", false, fontsize).first;
|
||
} else {
|
||
if (settings.showFullCPU)
|
||
width = renderer->getTextDimensions("[100%,100%,100%,100%]@4444.4444 mV", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("100%@4444.4444 mV", false, fontsize).first;
|
||
}
|
||
if (settings.realTemps) {
|
||
width += renderer->getTextDimensions(" 88.8°C ", false, fontsize).first;
|
||
}
|
||
} else if (key == "RAM" && settings.ramInfoMode == "Bandwidth" && R_SUCCEEDED(hocclkCheck)) {
|
||
width = renderer->getTextDimensions("99.9GB/s@4444.4", false, fontsize).first;
|
||
if (settings.realVolts) {
|
||
if (isMariko) {
|
||
if (settings.showVDD2 && settings.decimalVDD2 && settings.showVDDQ)
|
||
width += renderer->getTextDimensions("4444.4444 mV 4444 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && !settings.decimalVDD2 && settings.showVDDQ)
|
||
width += renderer->getTextDimensions("4444 mV 4444 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && settings.decimalVDD2)
|
||
width += renderer->getTextDimensions("4444.4 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && !settings.decimalVDD2)
|
||
width += renderer->getTextDimensions("4444 mV", false, fontsize).first;
|
||
else if (settings.showVDDQ)
|
||
width += renderer->getTextDimensions("4444 mV", false, fontsize).first;
|
||
} else {
|
||
if (settings.decimalVDD2)
|
||
width += renderer->getTextDimensions("4444.44 mV", false, fontsize).first;
|
||
else
|
||
width += renderer->getTextDimensions("4444 mV", false, fontsize).first;
|
||
}
|
||
}
|
||
if (settings.realTemps) {
|
||
width += renderer->getTextDimensions(" 88.8°C ", false, fontsize).first;
|
||
}
|
||
} else if (key == "GPU" || (key == "RAM" && settings.showpartLoad && R_SUCCEEDED(hocclkCheck))) {
|
||
//dimensions = renderer->drawString("100.0%@4444.4", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
|
||
if (!settings.showpartLoadCPUGPU) {
|
||
if (!settings.realVolts) {
|
||
width = renderer->getTextDimensions("100%@4444.4", false, fontsize).first;
|
||
} else {
|
||
width = renderer->getTextDimensions("100%@4444.4444 mV", false, fontsize).first;
|
||
}
|
||
} else {
|
||
if (!settings.realVolts) {
|
||
width = renderer->getTextDimensions("100%[100%,100%]@4444.4", false, fontsize).first;
|
||
} else {
|
||
width = renderer->getTextDimensions("100%[100%,100%]@4444.4444 mV", false, fontsize).first;
|
||
}
|
||
}
|
||
if (settings.realTemps) {
|
||
width += renderer->getTextDimensions(" 88.8°C ", false, fontsize).first;
|
||
}
|
||
} else if (key == "RAM" && (!settings.showpartLoad || R_FAILED(hocclkCheck))) {
|
||
//dimensions = renderer->drawString("44444444MB@4444.4", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
if (!settings.realVolts) {
|
||
width = renderer->getTextDimensions("100%@4444.4", false, fontsize).first;
|
||
if (settings.realTemps) {
|
||
width += renderer->getTextDimensions(" 88.8°C ", false, fontsize).first;
|
||
}
|
||
} else {
|
||
if (isMariko) {
|
||
if (settings.showVDD2 && settings.decimalVDD2 && settings.showVDDQ)
|
||
width = renderer->getTextDimensions("100%@4444.44444.4444 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && !settings.decimalVDD2 && settings.showVDDQ)
|
||
width = renderer->getTextDimensions("100%@4444.44444444 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && settings.decimalVDD2)
|
||
width = renderer->getTextDimensions("100%@4444.44444.4 mV", false, fontsize).first;
|
||
else if (settings.showVDD2 && !settings.decimalVDD2)
|
||
width = renderer->getTextDimensions("100%@4444.44444 mV", false, fontsize).first;
|
||
else if (settings.showVDDQ)
|
||
width = renderer->getTextDimensions("100%@4444.4444 mV", false, fontsize).first;
|
||
} else {
|
||
if (settings.decimalVDD2)
|
||
width = renderer->getTextDimensions("100%@4444.44444.4 mV", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("100%@4444.44444 mV", false, fontsize).first;
|
||
}
|
||
}
|
||
} else if (key == "SOC") { // new block
|
||
if (!settings.realVolts)
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88°C (100%)", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88°C", false, fontsize).first;
|
||
else
|
||
if (settings.showSOCVoltage) {
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88°C 100%444 mV", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88°C444 mV", false, fontsize).first;
|
||
} else {
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88°C 100%", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88°C", false, fontsize).first;
|
||
}
|
||
} else if (key == "TMP") {
|
||
//dimensions = renderer->drawString("88.8\u00B0C88.8\u00B0C88.8\u00B0C (100%)", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
if (!settings.realVolts) {
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C 100%", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C", false, fontsize).first;
|
||
} else {
|
||
if (settings.showSOCVoltage) {
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C 100%444 mV", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C444 mV", false, fontsize).first;
|
||
} else {
|
||
if (settings.showFanPercentage)
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C 100%", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("88\u00B0C 88\u00B0C 88\u00B0C", false, fontsize).first;
|
||
}
|
||
}
|
||
} else if (key == "BAT") {
|
||
//dimensions = renderer->drawString("-44.44 W100.0% [44:44]", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
width = renderer->getTextDimensions("-44.44 W100.0% [44:44]", false, fontsize).first;
|
||
} else if (key == "FPS") {
|
||
//dimensions = renderer->drawString("444.4", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
width = renderer->getTextDimensions("444.4", false, fontsize).first;
|
||
} else if (key == "RES") {
|
||
//dimensions = renderer->drawString("3840x21603840x2160", false, 0, 0, fontsize, renderer->a(0x0000));
|
||
if (settings.showFullResolution)
|
||
width = renderer->getTextDimensions("3840x21603840x2160", false, fontsize).first;
|
||
else
|
||
width = renderer->getTextDimensions("2160p2160p", false, fontsize).first;
|
||
} else if (key == "READ") {
|
||
// Calculate width for read speed display
|
||
width = renderer->getTextDimensions("999.99 MiB/s", false, fontsize).first;
|
||
} else if (key == "MEM") {
|
||
// Calculate width for system memory display (System + divider + memory value)
|
||
const u32 memValueWidth = renderer->getTextDimensions("999.9 MB "+ult::FREE, false, fontsize).first;
|
||
const u32 dividerWidth = renderer->getTextDimensions(ult::DIVIDER_SYMBOL, false, fontsize).first;
|
||
const u32 systemWidth = renderer->getTextDimensions("System", false, fontsize).first;
|
||
width = memValueWidth + dividerWidth + systemWidth;
|
||
} else if (key == "DTC" && settings.showDTC) {
|
||
// Calculate width based on the datetime format
|
||
// Use multiple sample dates to ensure longest possible textual output
|
||
|
||
char sampleDateTime[64];
|
||
size_t maxWidth = 0;
|
||
|
||
// Representative dates that produce long names in most locales
|
||
constexpr int testDays[][3] = {
|
||
{2025, 11, 26}, // Wednesday – longest weekday
|
||
{2025, 9, 30}, // September – long month
|
||
{2025, 12, 31}, // December – another long month
|
||
{2025, 2, 28}, // February – to cover locales where it's long
|
||
};
|
||
|
||
struct tm t = {};
|
||
for (auto &d : testDays) {
|
||
|
||
t.tm_year = d[0] - 1900;
|
||
t.tm_mon = d[1] - 1;
|
||
t.tm_mday = d[2];
|
||
t.tm_hour = 23;
|
||
t.tm_min = 59;
|
||
t.tm_sec = 59;
|
||
mktime(&t); // normalize (sets weekday, etc.)
|
||
|
||
strftime(sampleDateTime, sizeof(sampleDateTime), settings.dtcFormat.c_str(), &t);
|
||
const size_t w = renderer->getTextDimensions(std::string(sampleDateTime) + " ", false, fontsize).first;
|
||
if (w > maxWidth)
|
||
maxWidth = w;
|
||
}
|
||
|
||
width = maxWidth;
|
||
|
||
} else {
|
||
continue;
|
||
}
|
||
|
||
if (rectangleWidth < width) {
|
||
rectangleWidth = width;
|
||
}
|
||
}
|
||
Initialized = true;
|
||
needsRecalc = true;
|
||
}
|
||
|
||
// Recalculate layout when needed (including content changes)
|
||
if (needsRecalc) {
|
||
// Build label lines array for individual centering
|
||
labelLines.clear();
|
||
entryCount = 0;
|
||
uint16_t flags = 0;
|
||
|
||
bool shouldAdd;
|
||
std::string labelText;
|
||
for (const auto& key : showKeys) {
|
||
shouldAdd = false;
|
||
labelText = "";
|
||
|
||
if (key == "CPU" && !(flags & 1)) {
|
||
shouldAdd = true;
|
||
labelText = "CPU";
|
||
flags |= 1;
|
||
} else if (key == "GPU" && !(flags & 2)) {
|
||
shouldAdd = true;
|
||
labelText = "GPU";
|
||
flags |= 2;
|
||
} else if (key == "RAM" && !(flags & 4)) {
|
||
shouldAdd = true;
|
||
labelText = "RAM";
|
||
flags |= 4;
|
||
} else if (key == "SOC" && !(flags & 8)) {
|
||
shouldAdd = true;
|
||
labelText = "SOC";
|
||
flags |= 8;
|
||
} else if (key == "TMP" && !(flags & 16)) {
|
||
shouldAdd = true;
|
||
labelText = "TMP";
|
||
flags |= 16;
|
||
} else if ((key == "BAT" || key == "DRAW") && !(flags & 32)) {
|
||
shouldAdd = true;
|
||
labelText = "BAT";
|
||
flags |= 32;
|
||
} else if (key == "FPS" && !(flags & 64) && GameRunning) {
|
||
shouldAdd = true;
|
||
labelText = "FPS";
|
||
flags |= 64;
|
||
} else if (key == "RES" && !(flags & 128) && GameRunning) {
|
||
shouldAdd = true;
|
||
labelText = "RES";
|
||
flags |= 128;
|
||
resolutionShow = true;
|
||
} else if (key == "READ" && !(flags & 512) && GameRunning) {
|
||
shouldAdd = true;
|
||
labelText = "READ";
|
||
flags |= 512;
|
||
} else if (key == "DTC" && !(flags & 256) && settings.showDTC) {
|
||
shouldAdd = true;
|
||
labelText = settings.useDTCSymbol ? "\uE007" : "DTC";
|
||
flags |= 256;
|
||
} else if (key == "MEM" && !(flags & 1024)) {
|
||
shouldAdd = true;
|
||
labelText = "MEM";
|
||
flags |= 1024;
|
||
}
|
||
|
||
|
||
if (shouldAdd) {
|
||
labelLines.push_back(labelText);
|
||
entryCount++;
|
||
|
||
//if (settings.realVolts && key != "BAT" && key != "DRAW" && key != "FPS" && key != "RES") {
|
||
// labelLines.push_back(""); // Empty line for voltage info
|
||
// entryCount++;
|
||
//}
|
||
}
|
||
}
|
||
|
||
// Calculate actual entry count from Variables string
|
||
size_t actualEntryCount = 1; // Start with 1 for the first line
|
||
for (size_t i = 0; Variables[i] != '\0'; i++) {
|
||
if (Variables[i] == '\n') {
|
||
actualEntryCount++;
|
||
}
|
||
}
|
||
|
||
// Use the actual entry count for height calculation
|
||
cachedHeight = ((fontsize + settings.spacing) * actualEntryCount) + (fontsize / 3) + settings.spacing + topPadding + bottomPadding;
|
||
//const uint32_t margin = (fontsize * 4);
|
||
|
||
cachedBaseX = 0;
|
||
cachedBaseY = 0;
|
||
|
||
needsRecalc = false;
|
||
}
|
||
|
||
// Fast rendering using cached values
|
||
const uint32_t margin = (fontsize * 4);
|
||
|
||
// Draw background
|
||
const tsl::Color bgColor = !isDragging
|
||
? settings.backgroundColor // full opacity
|
||
: settings.focusBackgroundColor;
|
||
|
||
int clippingOffsetX = 0, clippingOffsetY = 0;
|
||
|
||
int _frameOffsetX = ult::limitedMemory ? std::max(0, frameOffsetX - (1280-448)) : frameOffsetX;
|
||
|
||
// Check X bounds and calculate clipping offset
|
||
if (cachedBaseX + _frameOffsetX < int(framePadding)) {
|
||
clippingOffsetX = framePadding - (cachedBaseX + _frameOffsetX);
|
||
} else if ((cachedBaseX + _frameOffsetX + margin + rectangleWidth + (fontsize / 3)) > screenWidth - framePadding) {
|
||
clippingOffsetX = (screenWidth - framePadding) - (cachedBaseX + _frameOffsetX + margin + rectangleWidth + (fontsize / 3));
|
||
}
|
||
|
||
// Check Y bounds and calculate clipping offset
|
||
if (cachedBaseY + frameOffsetY < int(framePadding)) {
|
||
clippingOffsetY = framePadding - (cachedBaseY + frameOffsetY);
|
||
} else if ((cachedBaseY + frameOffsetY + cachedHeight) > screenHeight - framePadding) {
|
||
clippingOffsetY = (screenHeight - framePadding) - (cachedBaseY + frameOffsetY + cachedHeight);
|
||
}
|
||
|
||
// Apply to all drawing calls
|
||
renderer->drawRoundedRectSingleThreaded(
|
||
cachedBaseX + _frameOffsetX + clippingOffsetX,
|
||
cachedBaseY + frameOffsetY + clippingOffsetY,
|
||
margin + rectangleWidth + (fontsize / 3),
|
||
cachedHeight,
|
||
16,
|
||
a(bgColor)
|
||
);
|
||
|
||
|
||
// Split Variables into lines for individual positioning
|
||
std::vector<std::string> variableLines;
|
||
const std::string variablesStr(Variables);
|
||
size_t start = 0;
|
||
size_t pos = 0;
|
||
while ((pos = variablesStr.find('\n', start)) != std::string::npos) {
|
||
variableLines.push_back(variablesStr.substr(start, pos - start));
|
||
start = pos + 1;
|
||
}
|
||
if (start < variablesStr.length()) {
|
||
variableLines.push_back(variablesStr.substr(start));
|
||
}
|
||
|
||
// Draw each label and variable line individually
|
||
uint32_t currentY = cachedBaseY + fontsize + settings.spacing + topPadding;
|
||
size_t labelIndex = 0;
|
||
|
||
static const std::vector<std::string> specialChars = {""};
|
||
static uint32_t labelWidth, labelCenterX;
|
||
static std::vector<std::string> _variableLines;
|
||
if (!delayUpdate)
|
||
_variableLines = variableLines;
|
||
for (size_t i = 0; i < _variableLines.size() && labelIndex < labelLines.size(); i++) {
|
||
// Check for RES label without corresponding RES data
|
||
if (labelIndex < labelLines.size() && labelLines[labelIndex] == "RES") {
|
||
//const std::string& currentLine = _variableLines[i];
|
||
// Check if this data line looks like resolution data (contains 'x' or 'p')
|
||
bool isResolutionData = m_resolutionOutput[0].width;
|
||
|
||
if (!isResolutionData) {
|
||
// This is RES label but data isn't resolution data - skip the label
|
||
++labelIndex;
|
||
// Don't increment i, so the current data line gets paired with the next label
|
||
--i;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Draw label (centered in label region)
|
||
if (!labelLines[labelIndex].empty()) {
|
||
labelWidth = renderer->getTextDimensions(labelLines[labelIndex], false, fontsize).first;
|
||
labelCenterX = cachedBaseX + (margin / 2) - (labelWidth / 2);
|
||
renderer->drawString(labelLines[labelIndex], false, labelCenterX + _frameOffsetX + clippingOffsetX, currentY + frameOffsetY + clippingOffsetY, fontsize, settings.catColor);
|
||
}
|
||
|
||
// Determine rendering method based on label type
|
||
const std::string& currentLine = _variableLines[i];
|
||
const int baseX = cachedBaseX + margin + _frameOffsetX + clippingOffsetX;
|
||
const int baseY = currentY + frameOffsetY + clippingOffsetY;
|
||
|
||
if (settings.useDynamicColors) {
|
||
if (labelIndex < labelLines.size() && labelLines[labelIndex] == "CPU") {
|
||
std::string dataStr = currentLine;
|
||
|
||
const size_t degreesPos = dataStr.find("°");
|
||
if (degreesPos != std::string::npos && settings.realTemps && realCPU_Temp != 0) {
|
||
size_t tempStart = dataStr.rfind(' ', degreesPos);
|
||
if (tempStart != std::string::npos) {
|
||
tempStart++;
|
||
const size_t cPos = dataStr.find("C", degreesPos);
|
||
if (cPos != std::string::npos) {
|
||
const size_t tempEnd = cPos + 1;
|
||
|
||
const std::string preTempPart = dataStr.substr(0, tempStart);
|
||
const std::string tempPart = dataStr.substr(tempStart, tempEnd - tempStart);
|
||
const std::string postTempPart = dataStr.substr(tempEnd);
|
||
|
||
const float temp = realCPU_Temp / 1000.0f;
|
||
const tsl::Color tempColor = tsl::GradientColor(temp);
|
||
|
||
int currentX = baseX;
|
||
if (!preTempPart.empty()) {
|
||
renderer->drawStringWithColoredSections(preTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
currentX += renderer->getTextDimensions(preTempPart, false, fontsize).first;
|
||
}
|
||
|
||
renderer->drawStringWithColoredSections(tempPart, false, specialChars, currentX, baseY, fontsize, tempColor, settings.separatorColor);
|
||
|
||
if (!postTempPart.empty()) {
|
||
currentX += renderer->getTextDimensions(tempPart, false, fontsize).first;
|
||
renderer->drawStringWithColoredSections(postTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
} else if (labelIndex < labelLines.size() && labelLines[labelIndex] == "GPU") {
|
||
std::string dataStr = currentLine;
|
||
|
||
const size_t degreesPos = dataStr.find("°");
|
||
if (degreesPos != std::string::npos && settings.realTemps && realGPU_Temp != 0) {
|
||
size_t tempStart = dataStr.rfind(' ', degreesPos);
|
||
if (tempStart != std::string::npos) {
|
||
tempStart++;
|
||
const size_t cPos = dataStr.find("C", degreesPos);
|
||
if (cPos != std::string::npos) {
|
||
const size_t tempEnd = cPos + 1;
|
||
|
||
const std::string preTempPart = dataStr.substr(0, tempStart);
|
||
const std::string tempPart = dataStr.substr(tempStart, tempEnd - tempStart);
|
||
const std::string postTempPart = dataStr.substr(tempEnd);
|
||
|
||
const float temp = realGPU_Temp / 1000.0f;
|
||
const tsl::Color tempColor = tsl::GradientColor(temp);
|
||
|
||
int currentX = baseX;
|
||
if (!preTempPart.empty()) {
|
||
renderer->drawStringWithColoredSections(preTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
currentX += renderer->getTextDimensions(preTempPart, false, fontsize).first;
|
||
}
|
||
|
||
renderer->drawStringWithColoredSections(tempPart, false, specialChars, currentX, baseY, fontsize, tempColor, settings.separatorColor);
|
||
|
||
if (!postTempPart.empty()) {
|
||
currentX += renderer->getTextDimensions(tempPart, false, fontsize).first;
|
||
renderer->drawStringWithColoredSections(postTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
} else if (labelIndex < labelLines.size() && labelLines[labelIndex] == "RAM") {
|
||
std::string dataStr = currentLine;
|
||
|
||
const size_t degreesPos = dataStr.find("°");
|
||
if (degreesPos != std::string::npos && settings.realTemps && realRAM_Temp != 0) {
|
||
size_t tempStart = dataStr.rfind(' ', degreesPos);
|
||
if (tempStart != std::string::npos) {
|
||
tempStart++;
|
||
const size_t cPos = dataStr.find("C", degreesPos);
|
||
if (cPos != std::string::npos) {
|
||
const size_t tempEnd = cPos + 1;
|
||
|
||
const std::string preTempPart = dataStr.substr(0, tempStart);
|
||
const std::string tempPart = dataStr.substr(tempStart, tempEnd - tempStart);
|
||
const std::string postTempPart = dataStr.substr(tempEnd);
|
||
|
||
const float temp = realRAM_Temp / 1000.0f;
|
||
const tsl::Color tempColor = tsl::GradientColor(temp);
|
||
|
||
int currentX = baseX;
|
||
if (!preTempPart.empty()) {
|
||
renderer->drawStringWithColoredSections(preTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
currentX += renderer->getTextDimensions(preTempPart, false, fontsize).first;
|
||
}
|
||
|
||
renderer->drawStringWithColoredSections(tempPart, false, specialChars, currentX, baseY, fontsize, tempColor, settings.separatorColor);
|
||
|
||
if (!postTempPart.empty()) {
|
||
currentX += renderer->getTextDimensions(tempPart, false, fontsize).first;
|
||
renderer->drawStringWithColoredSections(postTempPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
} else if (labelIndex < labelLines.size() && labelLines[labelIndex] == "SOC") {
|
||
// SOC temperature rendering with gradient
|
||
const size_t degreesPos = currentLine.find("°");
|
||
if (degreesPos != std::string::npos) {
|
||
// Find the 'C' after the degrees symbol
|
||
const size_t cPos = currentLine.find("C", degreesPos);
|
||
if (cPos != std::string::npos) {
|
||
const size_t tempEnd = cPos + 1; // Include the 'C'
|
||
|
||
// Extract temperature value and apply gradient
|
||
const int temp = atoi(currentLine.c_str());
|
||
const tsl::Color tempColor = tsl::GradientColor((float)temp);
|
||
|
||
// Split into temperature part and remaining part
|
||
const std::string tempPart = currentLine.substr(0, tempEnd);
|
||
const std::string restPart = currentLine.substr(tempEnd);
|
||
|
||
// Render temperature with gradient color
|
||
int currentX = baseX;
|
||
renderer->drawString(tempPart, false, currentX, baseY, fontsize, tempColor);
|
||
|
||
// Render remaining text with normal color
|
||
if (!restPart.empty()) {
|
||
currentX += renderer->getTextDimensions(tempPart, false, fontsize).first;
|
||
renderer->drawStringWithColoredSections(restPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
// Fallback: no C found after degrees, render normally
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
// Fallback: no degrees symbol found, render normally
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
} else if (labelIndex < labelLines.size() && labelLines[labelIndex] == "TMP") {
|
||
// TMP multiple temperatures rendering with gradients
|
||
int currentX = baseX;
|
||
size_t pos = 0;
|
||
bool parseSuccess = true;
|
||
|
||
// Parse up to 3 temperatures in the format "XX°C XX°C XX°C (XX%)"
|
||
for (int tempCount = 0; tempCount < 3 && parseSuccess && pos < currentLine.length(); tempCount++) {
|
||
// Find start of current temperature (skip any leading spaces)
|
||
while (pos < currentLine.length() && currentLine[pos] == ' ') {
|
||
renderer->drawString(" ", false, currentX, baseY, fontsize, settings.textColor);
|
||
currentX += renderer->getTextDimensions(" ", false, fontsize).first;
|
||
pos++;
|
||
}
|
||
|
||
if (pos >= currentLine.length()) break;
|
||
|
||
// Find degrees symbol
|
||
const size_t degreesPos = currentLine.find("°", pos);
|
||
if (degreesPos == std::string::npos) {
|
||
parseSuccess = false;
|
||
break;
|
||
}
|
||
|
||
// Find 'C' after degrees symbol
|
||
const size_t cPos = currentLine.find("C", degreesPos);
|
||
if (cPos == std::string::npos) {
|
||
parseSuccess = false;
|
||
break;
|
||
}
|
||
|
||
const size_t tempEnd = cPos + 1; // Include the 'C'
|
||
|
||
// Extract and render temperature with gradient
|
||
const std::string tempPart = currentLine.substr(pos, tempEnd - pos);
|
||
const int temp = atoi(tempPart.c_str());
|
||
const tsl::Color tempColor = tsl::GradientColor((float)temp);
|
||
|
||
renderer->drawString(tempPart, false, currentX, baseY, fontsize, tempColor);
|
||
currentX += renderer->getTextDimensions(tempPart, false, fontsize).first;
|
||
|
||
pos = tempEnd;
|
||
}
|
||
|
||
// Render any remaining text (like " (50%)" or voltage info)
|
||
if (pos < currentLine.length()) {
|
||
std::string restPart = currentLine.substr(pos);
|
||
renderer->drawStringWithColoredSections(restPart, false, specialChars, currentX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
// If parsing failed, fall back to normal rendering
|
||
if (!parseSuccess) {
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else if (labelIndex < labelLines.size() && labelLines[labelIndex] == "MEM") {
|
||
// MEM memory rendering with gradient color
|
||
// Extract numeric value for color determination
|
||
float sysValue = 0.0f;
|
||
sscanf(currentLine.c_str(), "%f", &sysValue);
|
||
|
||
// Determine unit from string
|
||
const bool isGB = (currentLine.find("GB") != std::string::npos);
|
||
const float sysValueMB = isGB ? (sysValue * 1024.0f) : sysValue;
|
||
|
||
// Apply same color scheme as in drawMemoryWidget
|
||
tsl::Color sysColor = settings.textColor;
|
||
if (sysValueMB >= 9.0f) {
|
||
sysColor = settings.useDynamicColors ? tsl::healthyRamTextColor : settings.textColor;
|
||
} else if (sysValueMB >= 3.0f) {
|
||
sysColor = settings.useDynamicColors ? tsl::neutralRamTextColor : settings.textColor;
|
||
} else {
|
||
sysColor = settings.useDynamicColors ? tsl::badRamTextColor : settings.textColor;
|
||
}
|
||
|
||
// Draw the memory value with color on the left
|
||
int currentX = baseX;
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, currentX, baseY, fontsize, sysColor, settings.separatorColor);
|
||
currentX += renderer->getTextDimensions(currentLine, false, fontsize).first;
|
||
|
||
// Draw separator " | "
|
||
renderer->drawString(ult::DIVIDER_SYMBOL, false, currentX, baseY, fontsize, settings.separatorColor);
|
||
currentX += renderer->getTextDimensions(ult::DIVIDER_SYMBOL, false, fontsize).first;
|
||
|
||
// Draw "System" on the right
|
||
renderer->drawString("System", false, currentX, baseY, fontsize, settings.textColor);
|
||
} else {
|
||
// Normal rendering for all other line types (CPU, GPU, RAM, BAT, FPS, RES, DTC)
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
} else {
|
||
// Normal rendering for all other line types (CPU, GPU, RAM, BAT, FPS, RES, DTC)
|
||
renderer->drawStringWithColoredSections(currentLine, false, specialChars, baseX, baseY, fontsize, settings.textColor, settings.separatorColor);
|
||
}
|
||
|
||
currentY += fontsize + settings.spacing;
|
||
++labelIndex;
|
||
}
|
||
});
|
||
|
||
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("", "");
|
||
rootFrame->setContent(Status);
|
||
return rootFrame;
|
||
}
|
||
|
||
virtual void update() override {
|
||
if (triggerExitNow)
|
||
return;
|
||
|
||
if (!SaltySD) {
|
||
SaltySD = CheckPort();
|
||
if (SaltySD) {
|
||
LoadSharedMemory();
|
||
threadCreate(&t6, CheckIfGameRunning, NULL, NULL, 0x1000, 0x38, -2);
|
||
threadStart(&t6);
|
||
}
|
||
}
|
||
|
||
// Handle performance mode changes
|
||
apmGetPerformanceMode(&performanceMode);
|
||
if (performanceMode == ApmPerformanceMode_Normal) {
|
||
if (fontsize != settings.handheldFontSize) {
|
||
Initialized = false;
|
||
fontsize = settings.handheldFontSize;
|
||
}
|
||
}
|
||
else if (performanceMode == ApmPerformanceMode_Boost) {
|
||
if (fontsize != settings.dockedFontSize) {
|
||
Initialized = false;
|
||
fontsize = settings.dockedFontSize;
|
||
}
|
||
}
|
||
|
||
// Parse show settings to determine what to calculate
|
||
std::vector<std::string> showKeys;
|
||
{
|
||
size_t start = 0, end = 0;
|
||
const std::string& show = settings.show;
|
||
while ((end = show.find('+', start)) != std::string::npos) {
|
||
showKeys.emplace_back(show.substr(start, end - start));
|
||
start = end + 1;
|
||
}
|
||
if (start < show.length()) {
|
||
showKeys.emplace_back(show.substr(start));
|
||
}
|
||
}
|
||
|
||
// Helper to check if a key is active
|
||
static auto isActive = [&showKeys](const std::string& key) {
|
||
return std::find(showKeys.begin(), showKeys.end(), key) != showKeys.end();
|
||
};
|
||
|
||
mutexLock(&mutex_Misc);
|
||
|
||
// Variables to store formatted strings
|
||
char MINI_CPU_compressed_c[42] = "";
|
||
char MINI_CPU_volt_c[16] = "";
|
||
char MINI_GPU_Load_c[32] = "";
|
||
char MINI_GPU_volt_c[20] = "";
|
||
char MINI_RAM_var_compressed_c[35] = "";
|
||
char MINI_RAM_volt_c[32] = "";
|
||
char MINI_MEM_RAM_c[32] = "";
|
||
char MINI_SOC_volt_c[16] = "";
|
||
|
||
// Only process CPU if needed
|
||
if (isActive("CPU")) {
|
||
char MINI_CPU_Usage0[7], MINI_CPU_Usage1[7], MINI_CPU_Usage2[7], MINI_CPU_Usage3[7];
|
||
|
||
const uint64_t idle0 = idletick0.load(std::memory_order_acquire);
|
||
const uint64_t idle1 = idletick1.load(std::memory_order_acquire);
|
||
const uint64_t idle2 = idletick2.load(std::memory_order_acquire);
|
||
const uint64_t idle3 = idletick3.load(std::memory_order_acquire);
|
||
|
||
auto formatMiniUsage = [this](char* buf, size_t size, uint64_t idletick) {
|
||
if (idletick > systemtickfrequency_impl) {
|
||
strcpy(buf, "0%");
|
||
} else {
|
||
snprintf(buf, size, "%.0f%%",
|
||
(1.0 - (static_cast<double>(idletick) / systemtickfrequency_impl)) * 100.0);
|
||
}
|
||
};
|
||
|
||
formatMiniUsage(MINI_CPU_Usage0, sizeof(MINI_CPU_Usage0), idle0);
|
||
formatMiniUsage(MINI_CPU_Usage1, sizeof(MINI_CPU_Usage1), idle1);
|
||
formatMiniUsage(MINI_CPU_Usage2, sizeof(MINI_CPU_Usage2), idle2);
|
||
formatMiniUsage(MINI_CPU_Usage3, sizeof(MINI_CPU_Usage3), idle3);
|
||
|
||
if (settings.showFullCPU) {
|
||
if (settings.realFrequencies && realCPU_Hz) {
|
||
snprintf(MINI_CPU_compressed_c, sizeof(MINI_CPU_compressed_c),
|
||
"[%s,%s,%s,%s]@%hu.%hhu",
|
||
MINI_CPU_Usage0, MINI_CPU_Usage1, MINI_CPU_Usage2, MINI_CPU_Usage3,
|
||
realCPU_Hz / 1000000, (realCPU_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_CPU_compressed_c, sizeof(MINI_CPU_compressed_c),
|
||
"[%s,%s,%s,%s]@%hu.%hhu",
|
||
MINI_CPU_Usage0, MINI_CPU_Usage1, MINI_CPU_Usage2, MINI_CPU_Usage3,
|
||
CPU_Hz / 1000000, (CPU_Hz / 100000) % 10);
|
||
}
|
||
} else {
|
||
double usage0 = 0, usage1 = 0, usage2 = 0, usage3 = 0;
|
||
sscanf(MINI_CPU_Usage0, "%lf%%", &usage0);
|
||
sscanf(MINI_CPU_Usage1, "%lf%%", &usage1);
|
||
sscanf(MINI_CPU_Usage2, "%lf%%", &usage2);
|
||
sscanf(MINI_CPU_Usage3, "%lf%%", &usage3);
|
||
|
||
double maxUsage = std::max({usage0, usage1, usage2, usage3});
|
||
|
||
if (settings.realFrequencies && realCPU_Hz) {
|
||
snprintf(MINI_CPU_compressed_c, sizeof(MINI_CPU_compressed_c),
|
||
"%.0f%%@%hu.%hhu", maxUsage,
|
||
realCPU_Hz / 1000000, (realCPU_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_CPU_compressed_c, sizeof(MINI_CPU_compressed_c),
|
||
"%.0f%%@%hu.%hhu", maxUsage,
|
||
CPU_Hz / 1000000, (CPU_Hz / 100000) % 10);
|
||
}
|
||
}
|
||
|
||
if (settings.realVolts) {
|
||
const uint32_t mv = realCPU_mV / 1000;
|
||
snprintf(MINI_CPU_volt_c, sizeof(MINI_CPU_volt_c), "%u mV", mv);
|
||
}
|
||
}
|
||
|
||
if (settings.realTemps && realCPU_Temp != 0 && CPU_temp_c[0] != '\0') {
|
||
char temp_buffer[48];
|
||
snprintf(temp_buffer, sizeof(temp_buffer), " %s", CPU_temp_c);
|
||
strncat(MINI_CPU_compressed_c, temp_buffer, sizeof(MINI_CPU_compressed_c) - strlen(MINI_CPU_compressed_c) - 1);
|
||
}
|
||
|
||
// Only process GPU if needed
|
||
if (isActive("GPU")) {
|
||
if (settings.realFrequencies && realGPU_Hz) {
|
||
snprintf(MINI_GPU_Load_c, sizeof(MINI_GPU_Load_c),
|
||
"%hu%%%s%hu.%hhu",
|
||
GPU_Load_u / 10, "@",
|
||
realGPU_Hz / 1000000, (realGPU_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_GPU_Load_c, sizeof(MINI_GPU_Load_c),
|
||
"%hu%%%s%hu.%hhu",
|
||
GPU_Load_u / 10, "@",
|
||
GPU_Hz / 1000000, (GPU_Hz / 100000) % 10);
|
||
}
|
||
|
||
// Handle sleep exit
|
||
if (settings.sleepExit) {
|
||
const auto GPU_Hz_int = int(GPU_Hz / 1000000);
|
||
static auto lastGPU_Hz_int = GPU_Hz_int;
|
||
if (GPU_Hz_int == 0 && lastGPU_Hz_int != 0) {
|
||
isRendering = false;
|
||
leventSignal(&renderingStopEvent);
|
||
triggerExitNow = true;
|
||
mutexUnlock(&mutex_Misc);
|
||
return;
|
||
}
|
||
lastGPU_Hz_int = GPU_Hz_int;
|
||
}
|
||
|
||
if (settings.realVolts) {
|
||
const uint32_t mv = realGPU_mV / 1000;
|
||
snprintf(MINI_GPU_volt_c, sizeof(MINI_GPU_volt_c), "%u mV", mv);
|
||
}
|
||
}
|
||
|
||
if (settings.realTemps && realGPU_Temp != 0 && GPU_temp_c[0] != '\0') {
|
||
char temp_buffer[48];
|
||
snprintf(temp_buffer, sizeof(temp_buffer), " %s", GPU_temp_c);
|
||
strncat(MINI_GPU_Load_c, temp_buffer, sizeof(MINI_GPU_Load_c) - strlen(MINI_GPU_Load_c) - 1);
|
||
}
|
||
|
||
// Only process RAM if needed
|
||
if (isActive("RAM")) {
|
||
if (!settings.showpartLoad) {
|
||
const float ramTotalGiB = (RAM_Total_application_u + RAM_Total_applet_u +
|
||
RAM_Total_system_u + RAM_Total_systemunsafe_u) /
|
||
(1024.0f * 1024.0f);
|
||
const float ramUsedGiB = (RAM_Used_application_u + RAM_Used_applet_u +
|
||
RAM_Used_system_u + RAM_Used_systemunsafe_u) /
|
||
(1024.0f * 1024.0f);
|
||
|
||
if (settings.realFrequencies && realRAM_Hz) {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%.0f/%.0fMB@%hu.%hhu",
|
||
ramUsedGiB, ramTotalGiB,
|
||
realRAM_Hz / 1000000, (realRAM_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%.0f/%.0fMB@%hu.%hhu",
|
||
ramUsedGiB, ramTotalGiB,
|
||
RAM_Hz / 1000000, (RAM_Hz / 100000) % 10);
|
||
}
|
||
} else {
|
||
if (settings.ramInfoMode == "Bandwidth" && R_SUCCEEDED(hocclkCheck)) {
|
||
// Bandwidth mode: show GB/s from context (partLoad values are in MB/s)
|
||
const uint32_t ramFreq = settings.realFrequencies && realRAM_Hz ? realRAM_Hz : RAM_Hz;
|
||
const unsigned bwAll = partLoad[HocClkPartLoad_RamBWAll] / 1000;
|
||
const unsigned bwAllD = (partLoad[HocClkPartLoad_RamBWAll] % 1000) / 100;
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u.%uGB/s@%hu.%hhu",
|
||
bwAll, bwAllD,
|
||
ramFreq / 1000000, (ramFreq / 100000) % 10);
|
||
} else {
|
||
unsigned partLoadInt;
|
||
|
||
if (R_SUCCEEDED(hocclkCheck)) {
|
||
partLoadInt = partLoad[HocClkPartLoad_EMC] / 10;
|
||
|
||
if (settings.showpartLoadCPUGPU) {
|
||
unsigned ramCpuLoadInt = partLoad[HocClkPartLoad_EMCCpu] / 10;
|
||
int RAM_GPU_Load = partLoad[HocClkPartLoad_EMC] - partLoad[HocClkPartLoad_EMCCpu];
|
||
unsigned ramGpuLoadInt = RAM_GPU_Load / 10;
|
||
|
||
if (settings.realFrequencies && realRAM_Hz) {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%[%u%%,%u%%]@%hu.%hhu",
|
||
partLoadInt, ramCpuLoadInt, ramGpuLoadInt,
|
||
realRAM_Hz / 1000000, (realRAM_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%[%u%%,%u%%]@%hu.%hhu",
|
||
partLoadInt, ramCpuLoadInt, ramGpuLoadInt,
|
||
RAM_Hz / 1000000, (RAM_Hz / 100000) % 10);
|
||
}
|
||
} else {
|
||
if (settings.realFrequencies && realRAM_Hz) {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%@%hu.%hhu", partLoadInt,
|
||
realRAM_Hz / 1000000, (realRAM_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%@%hu.%hhu", partLoadInt,
|
||
RAM_Hz / 1000000, (RAM_Hz / 100000) % 10);
|
||
}
|
||
}
|
||
} else {
|
||
const uint64_t RAM_Total_all = RAM_Total_application_u + RAM_Total_applet_u +
|
||
RAM_Total_system_u + RAM_Total_systemunsafe_u;
|
||
const uint64_t RAM_Used_all = RAM_Used_application_u + RAM_Used_applet_u +
|
||
RAM_Used_system_u + RAM_Used_systemunsafe_u;
|
||
partLoadInt = (RAM_Total_all > 0) ? (unsigned)((RAM_Used_all * 100) / RAM_Total_all) : 0;
|
||
|
||
if (settings.realFrequencies && realRAM_Hz) {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%@%hu.%hhu", partLoadInt,
|
||
realRAM_Hz / 1000000, (realRAM_Hz / 100000) % 10);
|
||
} else {
|
||
snprintf(MINI_RAM_var_compressed_c, sizeof(MINI_RAM_var_compressed_c),
|
||
"%u%%@%hu.%hhu", partLoadInt,
|
||
RAM_Hz / 1000000, (RAM_Hz / 100000) % 10);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (settings.realVolts) {
|
||
const float mv_vdd2_f = realVDD2_mV / 1000.0f; // VDD2
|
||
const uint32_t mv_vddq = realVDDQ_mV / 1000; // VDDQ
|
||
const uint32_t mv_vdd2_i = realVDD2_mV / 1000;
|
||
|
||
if (isMariko) {
|
||
if (settings.showVDDQ && settings.showVDD2) {
|
||
if (settings.decimalVDD2)
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c),
|
||
"%.1f mV%u mV", mv_vdd2_f, mv_vddq);
|
||
else
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c),
|
||
"%u mV%u mV", mv_vdd2_i, mv_vddq);
|
||
} else if (settings.showVDDQ) {
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c), "%u mV", mv_vddq);
|
||
} else if (settings.showVDD2) {
|
||
if (settings.decimalVDD2)
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c), "%.1f mV", mv_vdd2_f);
|
||
else
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c), "%u mV", mv_vdd2_i);
|
||
}
|
||
} else {
|
||
if (settings.decimalVDD2)
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c), "%.1f mV", mv_vdd2_f);
|
||
else
|
||
snprintf(MINI_RAM_volt_c, sizeof(MINI_RAM_volt_c), "%u mV", mv_vdd2_i);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (settings.realTemps && realRAM_Temp != 0 && RAM_temp_c[0] != '\0') {
|
||
char temp_buffer[48];
|
||
snprintf(temp_buffer, sizeof(temp_buffer), " %s", RAM_temp_c);
|
||
strncat(MINI_RAM_var_compressed_c, temp_buffer, sizeof(MINI_RAM_var_compressed_c) - strlen(MINI_RAM_var_compressed_c) - 1);
|
||
}
|
||
|
||
// Only process MEM if needed
|
||
if (isActive("MEM")) {
|
||
const u64 freeRamBytes = RAM_Total_system_u - RAM_Used_system_u;
|
||
|
||
float value;
|
||
const char* unit;
|
||
|
||
if (freeRamBytes >= 1024ULL * 1024 * 1024) {
|
||
value = static_cast<float>(freeRamBytes) / (1024.0f * 1024.0f * 1024.0f);
|
||
unit = "GB";
|
||
} else {
|
||
value = static_cast<float>(freeRamBytes) / (1024.0f * 1024.0f);
|
||
unit = "MB";
|
||
}
|
||
|
||
int decimalPlaces;
|
||
if (value >= 1000.0f) {
|
||
decimalPlaces = 0;
|
||
} else if (value >= 100.0f) {
|
||
decimalPlaces = 1;
|
||
} else if (value >= 10.0f) {
|
||
decimalPlaces = 2;
|
||
} else {
|
||
decimalPlaces = 3;
|
||
}
|
||
|
||
snprintf(MINI_MEM_RAM_c, sizeof(MINI_MEM_RAM_c), "%.*f %s %s",
|
||
decimalPlaces, value, unit, ult::FREE.c_str());
|
||
}
|
||
|
||
// Only process temperature/fan data if SOC or TMP is active
|
||
if (isActive("TMP") || isActive("SOC")) {
|
||
const int duty = safeFanDuty((int)Rotation_Duty);
|
||
|
||
if (settings.realVolts && settings.showSOCVoltage) {
|
||
const uint32_t mv = realSOC_mV / 1000;
|
||
snprintf(MINI_SOC_volt_c, sizeof(MINI_SOC_volt_c), "%u mV", mv);
|
||
}
|
||
|
||
if (isActive("SOC")) {
|
||
if (settings.showFanPercentage) {
|
||
snprintf(soc_temperature_c, sizeof(soc_temperature_c),
|
||
"%d°C %d%%", (int)SOC_temperatureF, duty);
|
||
} else {
|
||
snprintf(soc_temperature_c, sizeof(soc_temperature_c),
|
||
"%d°C", (int)SOC_temperatureF);
|
||
}
|
||
}
|
||
|
||
if (isActive("TMP")) {
|
||
if (settings.showFanPercentage) {
|
||
snprintf(skin_temperature_c, sizeof(skin_temperature_c),
|
||
"%d\u00B0C %d\u00B0C %hu\u00B0C %d%%",
|
||
(int)SOC_temperatureF, (int)PCB_temperatureF,
|
||
skin_temperaturemiliC / 1000, duty);
|
||
} else {
|
||
snprintf(skin_temperature_c, sizeof(skin_temperature_c),
|
||
"%d\u00B0C %d\u00B0C %hu\u00B0C",
|
||
(int)SOC_temperatureF, (int)PCB_temperatureF,
|
||
skin_temperaturemiliC / 1000);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (settings.realTemps) {
|
||
if (realCPU_Temp != 0) {
|
||
snprintf(CPU_temp_c, sizeof(CPU_temp_c), "%u°C", static_cast<u32>(realCPU_Temp / 1000.0));
|
||
}
|
||
if (realGPU_Temp != 0) {
|
||
snprintf(GPU_temp_c, sizeof(GPU_temp_c), "%u°C", static_cast<u32>(realGPU_Temp / 1000.0));
|
||
}
|
||
if (realRAM_Temp != 0) {
|
||
snprintf(RAM_temp_c, sizeof(RAM_temp_c), "%u°C", static_cast<u32>(realRAM_Temp / 1000.0));
|
||
}
|
||
}
|
||
|
||
// Only process resolution if RES is active and game is running
|
||
if (isActive("RES") && GameRunning && NxFps) {
|
||
if (!resolutionLookup) {
|
||
(NxFps->renderCalls[0].calls) = 0xFFFF;
|
||
resolutionLookup = 1;
|
||
} else if (resolutionLookup == 1) {
|
||
if ((NxFps->renderCalls[0].calls) != 0xFFFF) resolutionLookup = 2;
|
||
} else {
|
||
memcpy(&m_resolutionRenderCalls, &(NxFps->renderCalls), sizeof(m_resolutionRenderCalls));
|
||
memcpy(&m_resolutionViewportCalls, &(NxFps->viewportCalls), sizeof(m_resolutionViewportCalls));
|
||
qsort(m_resolutionRenderCalls, 8, sizeof(resolutionCalls), compare);
|
||
qsort(m_resolutionViewportCalls, 8, sizeof(resolutionCalls), compare);
|
||
memset(&m_resolutionOutput, 0, sizeof(m_resolutionOutput));
|
||
size_t out_iter = 0;
|
||
bool found = false;
|
||
for (size_t i = 0; i < 8; i++) {
|
||
for (size_t x = 0; x < 8; x++) {
|
||
if (m_resolutionRenderCalls[i].width == 0) break;
|
||
if ((m_resolutionRenderCalls[i].width == m_resolutionViewportCalls[x].width) &&
|
||
(m_resolutionRenderCalls[i].height == m_resolutionViewportCalls[x].height)) {
|
||
m_resolutionOutput[out_iter].width = m_resolutionRenderCalls[i].width;
|
||
m_resolutionOutput[out_iter].height = m_resolutionRenderCalls[i].height;
|
||
m_resolutionOutput[out_iter].calls = (m_resolutionRenderCalls[i].calls > m_resolutionViewportCalls[x].calls)
|
||
? m_resolutionRenderCalls[i].calls : m_resolutionViewportCalls[x].calls;
|
||
out_iter++;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found && m_resolutionRenderCalls[i].width != 0) {
|
||
m_resolutionOutput[out_iter].width = m_resolutionRenderCalls[i].width;
|
||
m_resolutionOutput[out_iter].height = m_resolutionRenderCalls[i].height;
|
||
m_resolutionOutput[out_iter].calls = m_resolutionRenderCalls[i].calls;
|
||
out_iter++;
|
||
}
|
||
found = false;
|
||
if (out_iter == 8) break;
|
||
}
|
||
if (out_iter < 8) {
|
||
const size_t out_iter_s = out_iter;
|
||
for (size_t x = 0; x < 8; x++) {
|
||
for (size_t y = 0; y < out_iter_s; y++) {
|
||
if (m_resolutionViewportCalls[x].width == 0) break;
|
||
if ((m_resolutionViewportCalls[x].width == m_resolutionOutput[y].width) &&
|
||
(m_resolutionViewportCalls[x].height == m_resolutionOutput[y].height)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found && m_resolutionViewportCalls[x].width != 0) {
|
||
m_resolutionOutput[out_iter].width = m_resolutionViewportCalls[x].width;
|
||
m_resolutionOutput[out_iter].height = m_resolutionViewportCalls[x].height;
|
||
m_resolutionOutput[out_iter].calls = m_resolutionViewportCalls[x].calls;
|
||
out_iter++;
|
||
}
|
||
found = false;
|
||
if (out_iter == 8) break;
|
||
}
|
||
}
|
||
qsort(m_resolutionOutput, 8, sizeof(resolutionCalls), compare);
|
||
}
|
||
} else if (!GameRunning && resolutionLookup != 0) {
|
||
resolutionLookup = 0;
|
||
}
|
||
|
||
mutexUnlock(&mutex_Misc);
|
||
|
||
// Battery/power draw - always update since BAT/DRAW might be displayed
|
||
if (isActive("BAT") || isActive("DRAW")) {
|
||
char remainingBatteryLife[8];
|
||
const float drawW = (fabsf(PowerConsumption) < 0.01f) ? 0.0f : PowerConsumption;
|
||
|
||
mutexLock(&mutex_BatteryChecker);
|
||
|
||
if (batTimeEstimate >= 0 && !(drawW <= 0.01f && drawW >= -0.01f)) {
|
||
snprintf(remainingBatteryLife, sizeof(remainingBatteryLife),
|
||
"%d:%02d", batTimeEstimate / 60, batTimeEstimate % 60);
|
||
} else {
|
||
strcpy(remainingBatteryLife, "--:--");
|
||
}
|
||
|
||
if (!settings.invertBatteryDisplay) {
|
||
snprintf(Battery_c, sizeof(Battery_c),
|
||
"%.2f W%.1f%% [%s]",
|
||
drawW,
|
||
(float)_batteryChargeInfoFields.RawBatteryCharge / 1000.0f,
|
||
remainingBatteryLife);
|
||
} else {
|
||
snprintf(Battery_c, sizeof(Battery_c),
|
||
"%.1f%% [%s]%.2f W",
|
||
(float)_batteryChargeInfoFields.RawBatteryCharge / 1000.0f,
|
||
remainingBatteryLife,
|
||
drawW);
|
||
}
|
||
|
||
mutexUnlock(&mutex_BatteryChecker);
|
||
}
|
||
|
||
// Build Variables string
|
||
char Temp[512] = "";
|
||
uint16_t flags = 0;
|
||
|
||
for (const auto& key : showKeys) {
|
||
if (key == "CPU" && !(flags & 1)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, MINI_CPU_compressed_c);
|
||
if (settings.realVolts && MINI_CPU_volt_c[0]) {
|
||
strcat(Temp, "");
|
||
strcat(Temp, MINI_CPU_volt_c);
|
||
}
|
||
flags |= 1;
|
||
}
|
||
else if (key == "GPU" && !(flags & 2)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, MINI_GPU_Load_c);
|
||
if (settings.realVolts && MINI_GPU_volt_c[0]) {
|
||
strcat(Temp, "");
|
||
strcat(Temp, MINI_GPU_volt_c);
|
||
}
|
||
flags |= 2;
|
||
}
|
||
else if (key == "RAM" && !(flags & 4)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, MINI_RAM_var_compressed_c);
|
||
if (settings.realVolts && MINI_RAM_volt_c[0]) {
|
||
strcat(Temp, "");
|
||
strcat(Temp, MINI_RAM_volt_c);
|
||
}
|
||
flags |= 4;
|
||
}
|
||
else if (key == "SOC" && !(flags & 8)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, soc_temperature_c);
|
||
if (settings.realVolts && settings.showSOCVoltage && MINI_SOC_volt_c[0]) {
|
||
strcat(Temp, "");
|
||
strcat(Temp, MINI_SOC_volt_c);
|
||
}
|
||
flags |= 8;
|
||
}
|
||
else if (key == "TMP" && !(flags & 16)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, skin_temperature_c);
|
||
if (settings.realVolts && settings.showSOCVoltage && MINI_SOC_volt_c[0]) {
|
||
strcat(Temp, "");
|
||
strcat(Temp, MINI_SOC_volt_c);
|
||
}
|
||
flags |= 16;
|
||
}
|
||
else if ((key == "BAT" || key == "DRAW") && !(flags & 32)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, Battery_c);
|
||
flags |= 32;
|
||
}
|
||
else if (key == "FPS" && !(flags & 64) && GameRunning) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
char Temp_s[24];
|
||
snprintf(Temp_s, sizeof(Temp_s), "%2.1f [%2.1f - %2.1f]", FPSavg, FPSmin, FPSmax);
|
||
strcat(Temp, Temp_s);
|
||
flags |= 64;
|
||
}
|
||
else if (key == "RES" && !(flags & 128) && GameRunning && m_resolutionOutput[0].width) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
char Temp_s[32] = "";
|
||
static std::pair<uint16_t, uint16_t> old_res[2];
|
||
|
||
uint16_t w0 = m_resolutionOutput[0].width;
|
||
uint16_t h0 = m_resolutionOutput[0].height;
|
||
uint16_t w1 = m_resolutionOutput[1].width;
|
||
uint16_t h1 = m_resolutionOutput[1].height;
|
||
|
||
if (w0 || w1) {
|
||
if (w0 && w1) {
|
||
if ((w0 == old_res[1].first && h0 == old_res[1].second) ||
|
||
(w1 == old_res[0].first && h1 == old_res[0].second)) {
|
||
std::swap(w0, w1);
|
||
std::swap(h0, h1);
|
||
}
|
||
}
|
||
|
||
|
||
// Format based on whether we show full resolution or just height (p)
|
||
if (settings.showFullResolution) {
|
||
if (!w1 || !h1)
|
||
snprintf(Temp_s, sizeof(Temp_s), "%dx%d", w0 ? w0 : w1, h0 ? h0 : h1);
|
||
else
|
||
snprintf(Temp_s, sizeof(Temp_s), "%dx%d%dx%d", w0, h0, w1, h1);
|
||
} else {
|
||
if (!w1 || !h1)
|
||
snprintf(Temp_s, sizeof(Temp_s), "%dp", h0 ? h0 : h1);
|
||
else
|
||
snprintf(Temp_s, sizeof(Temp_s), "%dp%dp", h0, h1);
|
||
}
|
||
|
||
// Update last-frame cache
|
||
old_res[0] = {m_resolutionOutput[0].width, m_resolutionOutput[0].height};
|
||
old_res[1] = {m_resolutionOutput[1].width, m_resolutionOutput[1].height};
|
||
|
||
strcat(Temp, Temp_s);
|
||
flags |= 128;
|
||
}
|
||
}
|
||
else if (key == "READ" && !(flags & 512) && GameRunning && NxFps) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
char Temp_s[24];
|
||
if ((NxFps->readSpeedPerSecond) != 0.f) {
|
||
snprintf(Temp_s, sizeof(Temp_s), "%.2f MiB/s", (NxFps->readSpeedPerSecond) / 1048576.f);
|
||
} else {
|
||
strcpy(Temp_s, "n/d");
|
||
}
|
||
strcat(Temp, Temp_s);
|
||
flags |= 512;
|
||
}
|
||
else if (key == "DTC" && !(flags & 256) && settings.showDTC) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
char dateTimeStr[64];
|
||
time_t rawtime = time(NULL);
|
||
struct tm *timeinfo = localtime(&rawtime);
|
||
strftime(dateTimeStr, sizeof(dateTimeStr), settings.dtcFormat.c_str(), timeinfo);
|
||
strcat(Temp, dateTimeStr);
|
||
flags |= 256;
|
||
}
|
||
else if (key == "MEM" && !(flags & 1024)) {
|
||
if (Temp[0]) strcat(Temp, "\n");
|
||
strcat(Temp, MINI_MEM_RAM_c);
|
||
flags |= 1024;
|
||
}
|
||
}
|
||
|
||
strcpy(Variables, Temp);
|
||
|
||
// Enable rendering if needed
|
||
if (!skipOnce) {
|
||
if (runOnce) {
|
||
isRendering = true;
|
||
leventClear(&renderingStopEvent);
|
||
runOnce = false;
|
||
}
|
||
} else {
|
||
skipOnce = false;
|
||
}
|
||
}
|
||
|
||
virtual bool handleInput(u64 keysDown, u64 keysHeld, const HidTouchState &touchPos, HidAnalogStickState joyStickPosLeft, HidAnalogStickState joyStickPosRight) override {
|
||
// Static variables to maintain drag state between function calls
|
||
static bool oldTouchDetected = false;
|
||
static bool oldMinusHeld = false;
|
||
static bool oldPlusHeld = false;
|
||
|
||
static HidTouchState initialTouchPos = {0};
|
||
static int initialFrameOffsetX = 0;
|
||
static int initialFrameOffsetY = 0;
|
||
static constexpr int TOUCH_THRESHOLD = 8;
|
||
static bool hasMoved = false;
|
||
//static u64 lastTouchTime = 0;
|
||
//static constexpr u64 TOUCH_DEBOUNCE_NS = 16000000ULL; // 16ms debounce
|
||
|
||
// Get current time for debouncing
|
||
//const u64 currentTime = armTicksToNs(armGetSystemTick());
|
||
|
||
// Better touch detection - check if coordinates are within reasonable screen bounds
|
||
const bool currentTouchDetected = (touchPos.x > 0 && touchPos.y > 0 &&
|
||
touchPos.x < screenWidth && touchPos.y < screenHeight);
|
||
|
||
// Debounce touch detection
|
||
//if (currentTouchDetected && !oldTouchDetected) {
|
||
// if (currentTime - lastTouchTime < TOUCH_DEBOUNCE_NS) {
|
||
// currentTouchDetected = false; // Ignore rapid touch changes
|
||
// } else {
|
||
// lastTouchTime = currentTime;
|
||
// }
|
||
//}
|
||
|
||
static bool clearOnRelease = false;
|
||
|
||
if (clearOnRelease && !isRendering) {
|
||
clearOnRelease = false;
|
||
isRendering = true;
|
||
leventClear(&renderingStopEvent);
|
||
}
|
||
|
||
// Calculate overlay bounds
|
||
const uint32_t margin = (fontsize * 4);
|
||
|
||
// Cache bounds calculation
|
||
static int cachedBaseX = 0;
|
||
static int cachedBaseY = 0;
|
||
static uint32_t cachedOverlayHeight = 0;
|
||
static bool boundsNeedUpdate = true;
|
||
static std::string lastVariables = "";
|
||
|
||
// Only recalculate bounds when Variables change or first time
|
||
if (boundsNeedUpdate || std::string(Variables) != lastVariables) {
|
||
// Calculate actual entry count from Variables string
|
||
size_t actualEntryCount = 1;
|
||
for (size_t i = 0; Variables[i] != '\0'; i++) {
|
||
if (Variables[i] == '\n') {
|
||
actualEntryCount++;
|
||
}
|
||
}
|
||
|
||
cachedOverlayHeight = ((fontsize + settings.spacing) * actualEntryCount) +
|
||
(fontsize / 3) + settings.spacing + topPadding + bottomPadding;
|
||
|
||
// Position calculation based on settings
|
||
cachedBaseX = 0;
|
||
cachedBaseY = 0;
|
||
|
||
boundsNeedUpdate = false;
|
||
lastVariables = Variables;
|
||
}
|
||
|
||
const int overlayX = frameOffsetX;//ult::limitedMemory ? cachedBaseX + std::max(0, frameOffsetX - (1280-448)) : cachedBaseX + frameOffsetX;
|
||
const int overlayY = cachedBaseY + frameOffsetY;
|
||
const int overlayWidth = margin + rectangleWidth + (fontsize / 3);
|
||
const int overlayHeight = cachedOverlayHeight;
|
||
|
||
// Add padding to make touch detection more forgiving
|
||
static constexpr int touchPadding = 4;
|
||
const int touchableX = overlayX - touchPadding;
|
||
const int touchableY = overlayY - touchPadding;
|
||
const int touchableWidth = overlayWidth + (touchPadding * 2);
|
||
const int touchableHeight = overlayHeight + (touchPadding * 2);
|
||
|
||
// Screen boundaries for clamping
|
||
const int minX = -cachedBaseX + framePadding;
|
||
const int maxX = screenWidth - overlayWidth - cachedBaseX - framePadding;
|
||
const int minY = -cachedBaseY + framePadding;
|
||
const int maxY = screenHeight - overlayHeight - cachedBaseY - framePadding;
|
||
|
||
const bool minusDragReady = buttonState.minusDragActive.load(std::memory_order_acquire);
|
||
const bool plusDragReady = buttonState.plusDragActive.load(std::memory_order_acquire);
|
||
|
||
// Check button states
|
||
const bool currentMinusHeld = (keysHeld & KEY_MINUS) && !(keysHeld & ~KEY_MINUS & ALL_KEYS_MASK) && minusDragReady;
|
||
const bool currentPlusHeld = (keysHeld & KEY_PLUS) && !(keysHeld & ~KEY_PLUS & ALL_KEYS_MASK) && plusDragReady;
|
||
|
||
// Handle touch dragging
|
||
if (currentTouchDetected && !isDragging) {
|
||
// Touch detected and not currently dragging - check if we should start
|
||
const int touchX = touchPos.x;
|
||
const int touchY = touchPos.y;
|
||
|
||
if (!oldTouchDetected) {
|
||
// Touch just started - check if within overlay bounds
|
||
if (touchX >= touchableX && touchX <= touchableX + touchableWidth &&
|
||
touchY >= touchableY && touchY <= touchableY + touchableHeight) {
|
||
|
||
// Start touch dragging
|
||
isDragging = true;
|
||
//isRendering = false;
|
||
//leventSignal(&renderingStopEvent);
|
||
triggerRumbleClick.store(true, std::memory_order_release);
|
||
triggerOnSound.store(true, std::memory_order_release);
|
||
hasMoved = false;
|
||
initialTouchPos = touchPos;
|
||
initialFrameOffsetX = frameOffsetX;
|
||
initialFrameOffsetY = frameOffsetY;
|
||
}
|
||
}
|
||
} else if (currentTouchDetected && isDragging && !currentMinusHeld && !currentPlusHeld) {
|
||
// Continue touch dragging (only if neither MINUS nor PLUS is held)
|
||
const int touchX = touchPos.x;
|
||
const int touchY = touchPos.y;
|
||
const int deltaX = touchX - initialTouchPos.x;
|
||
const int deltaY = touchY - initialTouchPos.y;
|
||
|
||
// Check if we've moved enough to consider this a drag
|
||
if (!hasMoved) {
|
||
const int totalMovement = abs(deltaX) + abs(deltaY);
|
||
if (totalMovement >= TOUCH_THRESHOLD) {
|
||
hasMoved = true;
|
||
}
|
||
}
|
||
|
||
if (hasMoved) {
|
||
// Update frame offsets with boundary checking
|
||
const int newFrameOffsetX = std::max(minX, std::min(maxX, initialFrameOffsetX + deltaX));
|
||
const int newFrameOffsetY = std::max(minY, std::min(maxY, initialFrameOffsetY + deltaY));
|
||
|
||
frameOffsetX = newFrameOffsetX;
|
||
frameOffsetY = newFrameOffsetY;
|
||
|
||
if (ult::limitedMemory) {
|
||
tsl::gfx::Renderer::get().setLayerPos(std::max(std::min((int)(frameOffsetX*1.5 + 0.5) - tsl::impl::currentUnderscanPixels.first, 1280-32 - tsl::impl::currentUnderscanPixels.first), 0), 0);
|
||
}
|
||
boundsNeedUpdate = true;
|
||
}
|
||
} else if (!currentTouchDetected && oldTouchDetected && isDragging && !currentMinusHeld && !currentPlusHeld) {
|
||
// Touch just released and we were touch dragging
|
||
if (hasMoved) {
|
||
// Save position when touch drag ends
|
||
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
|
||
iniData["mini"]["frame_offset_x"] = std::to_string(frameOffsetX);
|
||
iniData["mini"]["frame_offset_y"] = std::to_string(frameOffsetY);
|
||
ult::saveIniFileData(configIniPath, iniData);
|
||
}
|
||
|
||
// Reset touch drag state
|
||
isDragging = false;
|
||
hasMoved = false;
|
||
//isRendering = true;
|
||
//leventClear(&renderingStopEvent);
|
||
clearOnRelease = true;
|
||
triggerRumbleDoubleClick.store(true, std::memory_order_release);
|
||
triggerOffSound.store(true, std::memory_order_release);
|
||
}
|
||
|
||
|
||
// Handle joystick dragging (MINUS + right joystick OR PLUS + left joystick)
|
||
if ((currentMinusHeld || currentPlusHeld) && !isDragging) {
|
||
// Start joystick dragging
|
||
isDragging = true;
|
||
//isRendering = false;
|
||
//leventSignal(&renderingStopEvent);
|
||
triggerRumbleClick.store(true, std::memory_order_release);
|
||
triggerOnSound.store(true, std::memory_order_release);
|
||
} else if ((currentMinusHeld || currentPlusHeld) && isDragging) {
|
||
// Continue joystick dragging
|
||
static constexpr int JOYSTICK_DEADZONE = 20;
|
||
|
||
// Choose the appropriate joystick based on which button is held
|
||
const HidAnalogStickState& activeJoystick = currentMinusHeld ? joyStickPosRight : joyStickPosLeft;
|
||
|
||
// Only move if joystick is outside deadzone
|
||
if (abs(activeJoystick.x) > JOYSTICK_DEADZONE || abs(activeJoystick.y) > JOYSTICK_DEADZONE) {
|
||
// Calculate joystick magnitude (distance from center)
|
||
const float magnitude = sqrt((float)(activeJoystick.x * activeJoystick.x + activeJoystick.y * activeJoystick.y));
|
||
const float normalizedMagnitude = magnitude / 32767.0f; // Normalize to 0-1 range
|
||
|
||
// Single smooth curve: stays very slow for wide range, then accelerates
|
||
static constexpr float baseSensitivity = 0.00008f; // Higher so small movements register
|
||
static constexpr float maxSensitivity = 0.0005f;
|
||
|
||
// Use x^8 curve - stays very low until ~70% then curves up sharply
|
||
const float curveValue = pow(normalizedMagnitude, 8.0f);
|
||
const float currentSensitivity = baseSensitivity + (maxSensitivity - baseSensitivity) * curveValue;
|
||
|
||
// Calculate movement delta with fractional accumulation
|
||
static float accumulatedX = 0.0f;
|
||
static float accumulatedY = 0.0f;
|
||
|
||
accumulatedX += (float)activeJoystick.x * currentSensitivity;
|
||
accumulatedY += -(float)activeJoystick.y * currentSensitivity;
|
||
|
||
// Extract integer movement and keep fractional part
|
||
const int deltaX = (int)accumulatedX;
|
||
const int deltaY = (int)accumulatedY;
|
||
accumulatedX -= deltaX;
|
||
accumulatedY -= deltaY;
|
||
|
||
// Update frame offsets with boundary checking
|
||
const int newFrameOffsetX = std::max(minX, std::min(maxX, frameOffsetX + deltaX));
|
||
const int newFrameOffsetY = std::max(minY, std::min(maxY, frameOffsetY + deltaY));
|
||
|
||
frameOffsetX = newFrameOffsetX;
|
||
frameOffsetY = newFrameOffsetY;
|
||
|
||
if (ult::limitedMemory) {
|
||
tsl::gfx::Renderer::get().setLayerPos(std::max(std::min((int)(frameOffsetX*1.5 + 0.5) - tsl::impl::currentUnderscanPixels.first, 1280-32 - tsl::impl::currentUnderscanPixels.first), 0), 0);
|
||
}
|
||
boundsNeedUpdate = true;
|
||
}
|
||
} else if (((!currentMinusHeld && oldMinusHeld) || (!currentPlusHeld && oldPlusHeld)) && isDragging) {
|
||
// Button just released - stop joystick dragging
|
||
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
|
||
iniData["mini"]["frame_offset_x"] = std::to_string(frameOffsetX);
|
||
iniData["mini"]["frame_offset_y"] = std::to_string(frameOffsetY);
|
||
ult::saveIniFileData(configIniPath, iniData);
|
||
isDragging = false;
|
||
//isRendering = true;
|
||
//leventClear(&renderingStopEvent);
|
||
clearOnRelease = true;
|
||
triggerRumbleDoubleClick.store(true, std::memory_order_release);
|
||
triggerOffSound.store(true, std::memory_order_release);
|
||
}
|
||
|
||
// Update state for next frame
|
||
oldTouchDetected = currentTouchDetected;
|
||
oldMinusHeld = currentMinusHeld;
|
||
oldPlusHeld = currentPlusHeld;
|
||
|
||
// Handle existing key input logic (but don't interfere with dragging)
|
||
if (!isDragging) {
|
||
if (isKeyComboPressed(keysHeld, keysDown)) {
|
||
isRendering = false;
|
||
leventSignal(&renderingStopEvent);
|
||
//triggerRumbleDoubleClick.store(true, std::memory_order_release);
|
||
skipOnce = true;
|
||
runOnce = true;
|
||
//TeslaFPS = 60;
|
||
if (skipMain) {
|
||
//lastSelectedItem = "Mini";
|
||
lastMode = "returning";
|
||
tsl::goBack();
|
||
}
|
||
else {
|
||
tsl::setNextOverlay(filepath.c_str(), "--lastSelectedItem Mini");
|
||
tsl::Overlay::get()->close();
|
||
}
|
||
return true;
|
||
}
|
||
else if (((keysDown & KEY_L) && (keysDown & KEY_ZL)) || ((keysDown & KEY_L) && (keysHeld & KEY_ZL)) || ((keysHeld & KEY_L) && (keysDown & KEY_ZL))) {
|
||
FPSmin = 254;
|
||
FPSmax = 0;
|
||
}
|
||
}
|
||
|
||
// Return true if we handled the input (during dragging)
|
||
return isDragging;
|
||
}
|
||
}; |