chore: merge hoc monitor and remove unnessesary files

This commit is contained in:
souldbminersmwc
2025-12-12 16:53:20 -05:00
parent 39d3739059
commit 90ed29998d
224 changed files with 17643 additions and 64401 deletions

View File

@@ -0,0 +1,220 @@
class OtherMenu;
class BatteryOverlay : public tsl::Gui {
private:
// Separated value buffers
char actualCapacity_c[32] = "";
char designedCapacity_c[32] = "";
char batteryTemp_c[32] = "";
char rawCharge_c[32] = "";
char batteryAge_c[32] = "";
char voltageAvg_c[64] = "";
char currentFlow_c[64] = "";
char powerFlow_c[64] = "";
char remainingTime_c[16] = "";
char inputCurrentLimit_c[32] = "";
char vbusCurrentLimit_c[32] = "";
char chargeVoltageLimit_c[32] = "";
char chargeCurrentLimit_c[32] = "";
char chargerType_c[32] = "";
char chargerMaxVoltage_c[32] = "";
char chargerMaxCurrent_c[32] = "";
bool skipOnce = true;
bool runOnce = true;
bool isChargerConnected = false;
FullSettings settings;
public:
BatteryOverlay() {
GetConfigSettings(&settings);
disableJumpTo = true;
mutexInit(&mutex_BatteryChecker);
StartBatteryThread();
//tsl::elm::g_disableMenuCacheOnReturn.store(true, std::memory_order_release);
}
~BatteryOverlay() {
CloseBatteryThread();
fixForeground = true;
}
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 Y_OFFSET = 40;
static constexpr u16 X_OFFSET = 20;
static const u16 LABEL_X = 20 + X_OFFSET;
static const u16 VALUE_X = 240+ X_OFFSET;
static const u16 START_Y = 155 + Y_OFFSET;
static constexpr u16 LINE_HEIGHT = 18;
static constexpr u8 FONT_SIZE = 18;
static const tsl::Color LABEL_COLOR_1= settings.catColor1;
static const tsl::Color LABEL_COLOR_2 = settings.catColor2;
static const tsl::Color VALUE_COLOR = settings.textColor;
renderer->drawString("Battery Stats", false, LABEL_X, 120 + Y_OFFSET, 20, LABEL_COLOR_1);
u16 currentY = START_Y;
// Actual Capacity
renderer->drawString("Actual Capacity", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(actualCapacity_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Designed Capacity
renderer->drawString("Designed Capacity", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(designedCapacity_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Temperature
renderer->drawString("Temperature", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(batteryTemp_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Raw Charge
renderer->drawString("Raw Charge", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(rawCharge_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Age
renderer->drawString("Age", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(batteryAge_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Voltage
renderer->drawString("Voltage", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(voltageAvg_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Current Flow
renderer->drawString("Current Flow", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(currentFlow_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Power Flow
renderer->drawString("Power Flow", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(powerFlow_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Remaining Time
renderer->drawString("Remaining Time", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(remainingTime_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charger-specific fields (only shown when charger is connected)
if (isChargerConnected) {
currentY += 3*LINE_HEIGHT;
renderer->drawString("Charger Stats", false, LABEL_X, currentY, 20, LABEL_COLOR_1);
currentY += 2*LINE_HEIGHT;
// Input Current Limit
renderer->drawString("Input Current Limit", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(inputCurrentLimit_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// VBUS Current Limit
renderer->drawString("VBUS Current Limit", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(vbusCurrentLimit_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charge Voltage Limit
renderer->drawString("Voltage Limit", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(chargeVoltageLimit_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charge Current Limit
renderer->drawString("Current Limit", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(chargeCurrentLimit_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charger Type
renderer->drawString("Type", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(chargerType_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charger Max Voltage
renderer->drawString("Max Voltage", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(chargerMaxVoltage_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
currentY += LINE_HEIGHT;
// Charger Max Current
renderer->drawString("Max Current", false, LABEL_X, currentY, FONT_SIZE, LABEL_COLOR_2);
renderer->drawString(chargerMaxCurrent_c, false, VALUE_X, currentY, FONT_SIZE, VALUE_COLOR);
}
});
//tsl::elm::g_disableMenuCacheOnReturn.store(true, std::memory_order_release);
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("Status Monitor", APP_VERSION, true);
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
mutexLock(&mutex_BatteryChecker);
char tempBatTimeEstimate[8] = "--:--";
if (batTimeEstimate >= 0) {
snprintf(tempBatTimeEstimate, sizeof(tempBatTimeEstimate), "%d:%02d", batTimeEstimate / 60, batTimeEstimate % 60);
}
const BatteryChargeInfoFieldsChargerType ChargerConnected = hosversionAtLeast(17,0,0) ?
((BatteryChargeInfoFields17*)&_batteryChargeInfoFields)->ChargerType :
_batteryChargeInfoFields.ChargerType;
const int32_t ChargerVoltageLimit = hosversionAtLeast(17,0,0) ?
((BatteryChargeInfoFields17*)&_batteryChargeInfoFields)->ChargerVoltageLimit :
_batteryChargeInfoFields.ChargerVoltageLimit;
const int32_t ChargerCurrentLimit = hosversionAtLeast(17,0,0) ?
((BatteryChargeInfoFields17*)&_batteryChargeInfoFields)->ChargerCurrentLimit :
_batteryChargeInfoFields.ChargerCurrentLimit;
isChargerConnected = (ChargerConnected != 0);
// Format all values
snprintf(actualCapacity_c, sizeof(actualCapacity_c), "%.0f mAh", actualFullBatCapacity);
snprintf(designedCapacity_c, sizeof(designedCapacity_c), "%.0f mAh", designedFullBatCapacity);
snprintf(batteryTemp_c, sizeof(batteryTemp_c), "%.1f\u00B0C", (float)_batteryChargeInfoFields.BatteryTemperature / 1000);
snprintf(rawCharge_c, sizeof(rawCharge_c), "%.1f%%", (float)_batteryChargeInfoFields.RawBatteryCharge / 1000);
snprintf(batteryAge_c, sizeof(batteryAge_c), "%.1f%%", (float)_batteryChargeInfoFields.BatteryAge / 1000);
snprintf(voltageAvg_c, sizeof(voltageAvg_c), "%.0f mV (%ds)", batVoltageAvg, batteryFiltered ? 45 : 5);
snprintf(currentFlow_c, sizeof(currentFlow_c), "%+.0f mA (%ss)", batCurrentAvg, batteryFiltered ? "11.25" : "5");
snprintf(powerFlow_c, sizeof(powerFlow_c), "%+.3f W%s", PowerConsumption, batteryFiltered ? "" : " (5s)");
snprintf(remainingTime_c, sizeof(remainingTime_c), "%s", tempBatTimeEstimate);
if (isChargerConnected) {
snprintf(inputCurrentLimit_c, sizeof(inputCurrentLimit_c), "%d mA", _batteryChargeInfoFields.InputCurrentLimit);
snprintf(vbusCurrentLimit_c, sizeof(vbusCurrentLimit_c), "%d mA", _batteryChargeInfoFields.VBUSCurrentLimit);
snprintf(chargeVoltageLimit_c, sizeof(chargeVoltageLimit_c), "%d mV", _batteryChargeInfoFields.ChargeVoltageLimit);
snprintf(chargeCurrentLimit_c, sizeof(chargeCurrentLimit_c), "%d mA", _batteryChargeInfoFields.ChargeCurrentLimit);
snprintf(chargerType_c, sizeof(chargerType_c), "%u", ChargerConnected);
snprintf(chargerMaxVoltage_c, sizeof(chargerMaxVoltage_c), "%u mV", ChargerVoltageLimit);
snprintf(chargerMaxCurrent_c, sizeof(chargerMaxCurrent_c), "%u mA", ChargerCurrentLimit);
}
mutexUnlock(&mutex_BatteryChecker);
if (!skipOnce) {
if (runOnce) {
isRendering = true;
leventClear(&renderingStopEvent);
runOnce = false;
}
} else {
skipOnce = false;
}
}
virtual bool handleInput(uint64_t keysDown, uint64_t keysHeld, touchPosition touchInput, JoystickPosition leftJoyStick, JoystickPosition rightJoyStick) override {
if (keysDown & KEY_B) {
isRendering = false;
leventSignal(&renderingStopEvent);
triggerRumbleDoubleClick.store(true, std::memory_order_release);
triggerExitSound.store(true, std::memory_order_release);
skipOnce = true;
runOnce = true;
lastSelectedItem = "Battery/Charger";
lastMode = "";
tsl::swapTo<OtherMenu>();
return true;
}
return false;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,525 @@
class MainMenu;
class com_FPS : public tsl::Gui {
private:
char FPSavg_c[8];
FpsCounterSettings settings;
size_t fontsize = 0;
ApmPerformanceMode performanceMode = ApmPerformanceMode_Invalid;
bool skipOnce = true;
bool runOnce = true;
// Repositioning variables
int frameOffsetX = 0;
int frameOffsetY = 0;
bool isDragging = false;
size_t framePadding = 10;
static constexpr int screenWidth = 1280;
static constexpr int screenHeight = 720;
static constexpr int border = 8;
bool originalUseRightAlignment = ult::useRightAlignment;
struct ButtonState {
std::atomic<bool> minusDragActive{false};
std::atomic<bool> plusDragActive{false};
} buttonState;
Thread touchPollThread;
std::atomic<bool> touchPollRunning{false};
// Store actual rendered dimensions
size_t actualTextWidth = 0;
size_t actualTotalWidth = 0;
size_t actualTotalHeight = 0;
public:
com_FPS() {
tsl::hlp::requestForeground(false);
disableJumpTo = true;
GetConfigSettings(&settings);
apmGetPerformanceMode(&performanceMode);
if (performanceMode == ApmPerformanceMode_Normal) {
fontsize = settings.handheldFontSize;
}
else if (performanceMode == ApmPerformanceMode_Boost) {
fontsize = settings.dockedFontSize;
}
// Load saved frame offsets
frameOffsetX = settings.frameOffsetX;
frameOffsetY = settings.frameOffsetY;
framePadding = settings.framePadding;
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;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().removeScreenshotStacks();
}
deactivateOriginalFooter = true;
StartFPSCounterThread();
// Start touch polling thread for instant response at low FPS
touchPollRunning.store(true, std::memory_order_release);
threadCreate(&touchPollThread, [](void* arg) -> void {
com_FPS* overlay = static_cast<com_FPS*>(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;
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;
// Use actual dimensions, fallback to estimate if not yet rendered
size_t totalWidth = overlay->actualTotalWidth;
size_t totalHeight = overlay->actualTotalHeight;
if (totalWidth == 0) {
// Fallback calculation
size_t approxFontSize = overlay->fontsize;
if (approxFontSize == 0) approxFontSize = 50;
const size_t textWidth = approxFontSize * 4;
const size_t margin = (approxFontSize / 8);
const size_t innerWidth = textWidth + margin;
const size_t innerHeight = approxFontSize;
totalWidth = innerWidth + (2 * border);
totalHeight = innerHeight + (2 * border);
}
// Apply frame offsets
const int overlayX = overlay->frameOffsetX;
const int overlayY = overlay->frameOffsetY;
// Touch padding
const int touchPadding = 4;
const int touchableX = overlayX - touchPadding;
const int touchableY = overlayY - touchPadding;
const int touchableWidth = totalWidth + (touchPadding * 2);
const int touchableHeight = totalHeight + (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);
// 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) {
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) {
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(16000000ULL*2); // 16ms polling
}
}, this, NULL, 0x1000, 0x2B, -2);
threadStart(&touchPollThread);
}
~com_FPS() {
// Stop touch polling thread
touchPollRunning.store(false, std::memory_order_release);
threadWaitForExit(&touchPollThread);
threadClose(&touchPollThread);
TeslaFPS = 60;
EndFPSCounterThread();
FullMode = true;
fixForeground = true;
ult::useRightAlignment = originalUseRightAlignment;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().addScreenshotStacks();
}
deactivateOriginalFooter = 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) {
// Calculate text dimensions
const auto [textWidth, textHeight] = renderer->getTextDimensions(
(FPSavg != 254.0) ? FPSavg_c : "--", false, fontsize
);
const size_t margin = (fontsize / 8);
// Inner rectangle dimensions (content area)
const size_t innerWidth = textWidth + margin;
const size_t innerHeight = textHeight;
// Total dimensions including border
const size_t totalWidth = innerWidth + (2 * border);
const size_t totalHeight = innerHeight + (2 * border);
// Store actual dimensions for input handling
actualTextWidth = textWidth;
actualTotalWidth = totalWidth;
actualTotalHeight = totalHeight;
// Calculate position with frame offsets
//int posX = frameOffsetX;
//int posY = frameOffsetY;
int _frameOffsetX = ult::limitedMemory ? std::max(0, frameOffsetX - (1280-448)) : frameOffsetX;
// Clamp to screen bounds (accounting for total size including border)
const int posX = std::max(int(framePadding), std::min(_frameOffsetX, static_cast<int>(screenWidth - totalWidth - framePadding)));
const int posY = std::max(int(framePadding), std::min(frameOffsetY, static_cast<int>(screenHeight - totalHeight - framePadding)));
// Draw the rounded rectangle (background)
const tsl::Color bgColor = !isDragging
? settings.backgroundColor
: settings.focusBackgroundColor;
renderer->drawRoundedRectSingleThreaded(
posX,
posY,
totalWidth,
totalHeight,
16,
aWithOpacity(bgColor)
);
// Calculate centered text position within the bordered area
const int textX = posX + border + (margin / 2);
const int textY = posY + border + (fontsize - margin);
// Draw the text
renderer->drawString(
(FPSavg != 254.0) ? FPSavg_c : "--",
false,
textX,
textY,
fontsize,
settings.textColor
);
});
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("", "");
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
apmGetPerformanceMode(&performanceMode);
if (performanceMode == ApmPerformanceMode_Normal) {
fontsize = settings.handheldFontSize;
}
else if (performanceMode == ApmPerformanceMode_Boost) {
fontsize = settings.dockedFontSize;
}
if (settings.useIntegerCounter) {
snprintf(FPSavg_c, sizeof FPSavg_c, "%d", (int)round(useOldFPSavg ? FPSavg_old : FPSavg));
} else {
snprintf(FPSavg_c, sizeof FPSavg_c, "%2.1f", useOldFPSavg ? FPSavg_old : FPSavg);
}
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;
// Touch detection
const bool currentTouchDetected = (touchPos.x > 0 && touchPos.y > 0 &&
touchPos.x < screenWidth && touchPos.y < screenHeight);
static bool clearOnRelease = false;
if (clearOnRelease && !isRendering) {
clearOnRelease = false;
isRendering = true;
leventClear(&renderingStopEvent);
}
// Use actual dimensions from last render, fallback to estimate if not available
size_t totalWidth = actualTotalWidth;
size_t totalHeight = actualTotalHeight;
if (totalWidth == 0) {
// Fallback calculation if not yet rendered
const size_t textWidth = fontsize * 4;
const size_t margin = (fontsize / 8);
const size_t innerWidth = textWidth + margin;
const size_t innerHeight = fontsize + (margin / 2);
totalWidth = innerWidth + (2 * border);
totalHeight = innerHeight + (2 * border);
}
// Current overlay position
const int overlayX = frameOffsetX;
const int overlayY = frameOffsetY;
// Touch detection area (with padding for easier interaction)
static constexpr int touchPadding = 4;
const int touchableX = overlayX - touchPadding;
const int touchableY = overlayY - touchPadding;
const int touchableWidth = totalWidth + (touchPadding * 2);
const int touchableHeight = totalHeight + (touchPadding * 2);
// Screen boundaries for clamping (accounting for total size)
const int minX = framePadding;
const int maxX = screenWidth - totalWidth - framePadding;
const int minY = framePadding;
const int maxY = screenHeight - totalHeight - 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) {
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;
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
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
frameOffsetX = std::max(minX, std::min(maxX, initialFrameOffsetX + deltaX));
frameOffsetY = std::max(minY, std::min(maxY, initialFrameOffsetY + deltaY));
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);
}
}
} else if (!currentTouchDetected && oldTouchDetected && isDragging && !currentMinusHeld && !currentPlusHeld) {
// Touch just released
if (hasMoved) {
// Save position when touch drag ends
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
iniData["fps-counter"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["fps-counter"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
}
// Reset touch drag state
isDragging = false;
hasMoved = false;
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;
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
const float magnitude = sqrt((float)(activeJoystick.x * activeJoystick.x + activeJoystick.y * activeJoystick.y));
const float normalizedMagnitude = magnitude / 32767.0f;
// Smooth curve for sensitivity
static constexpr float baseSensitivity = 0.00008f;
static constexpr float maxSensitivity = 0.0005f;
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;
const int deltaX = (int)accumulatedX;
const int deltaY = (int)accumulatedY;
accumulatedX -= deltaX;
accumulatedY -= deltaY;
// Update frame offsets with boundary checking
frameOffsetX = std::max(minX, std::min(maxX, frameOffsetX + deltaX));
frameOffsetY = std::max(minY, std::min(maxY, frameOffsetY + deltaY));
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);
}
}
} else if (((!currentMinusHeld && oldMinusHeld) || (!currentPlusHeld && oldPlusHeld)) && isDragging) {
// Button just released - stop joystick dragging
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
iniData["fps-counter"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["fps-counter"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
isDragging = false;
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);
runOnce = true;
skipOnce = true;
TeslaFPS = 60;
lastSelectedItem = "FPS Counter";
lastMode = "";
if (skipMain) {
lastMode = "return";
tsl::goBack();
}
else {
tsl::setNextOverlay(filepath.c_str(), "--lastSelectedItem 'FPS Counter'");
tsl::Overlay::get()->close();
}
return true;
}
}
// Return true if we handled the input (during dragging)
return isDragging;
}
};

View File

@@ -0,0 +1,726 @@
class MainMenu;
class com_FPSGraph : public tsl::Gui {
private:
uint8_t refreshRate = 0;
char FPSavg_c[8];
FpsGraphSettings settings;
uint64_t systemtickfrequency_impl = systemtickfrequency;
uint32_t cnt = 0;
char CPU_Load_c[12] = " -";
char GPU_Load_c[12] = " -";
char RAM_Load_c[12] = " -";
char SOC_TEMP_c[12] = " -";
char PCB_TEMP_c[12] = " -";
char SKIN_TEMP_c[12] = " -";
bool skipOnce = true;
bool runOnce = true;
// Repositioning variables (matching Mini)
int frameOffsetX = 0;
int frameOffsetY = 0;
bool isDragging = false;
size_t framePadding = 10;
static constexpr int screenWidth = 1280;
static constexpr int screenHeight = 720;
static constexpr int border = 8;
bool originalUseRightAlignment = ult::useRightAlignment;
struct ButtonState {
std::atomic<bool> minusDragActive{false};
std::atomic<bool> plusDragActive{false};
} buttonState;
Thread touchPollThread;
std::atomic<bool> touchPollRunning{false};
// Store actual rendered dimensions (including border)
size_t actualTotalWidth = 0;
size_t actualTotalHeight = 0;
public:
bool isStarted = false;
com_FPSGraph() {
tsl::hlp::requestForeground(false);
disableJumpTo = true;
GetConfigSettings(&settings);
if (R_SUCCEEDED(SaltySD_Connect())) {
if (R_FAILED(SaltySD_GetDisplayRefreshRate(&refreshRate)))
refreshRate = 0;
svcSleepThread(100'000);
SaltySD_Term();
}
// Load saved frame offsets
frameOffsetX = settings.frameOffsetX;
frameOffsetY = settings.frameOffsetY;
framePadding = settings.framePadding;
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;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().removeScreenshotStacks();
}
deactivateOriginalFooter = true;
mutexInit(&mutex_Misc);
StartInfoThread();
StartFPSCounterThread();
// Start touch polling thread for instant response at low FPS
touchPollRunning.store(true, std::memory_order_release);
threadCreate(&touchPollThread, [](void* arg) -> void {
com_FPSGraph* overlay = static_cast<com_FPSGraph*>(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;
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;
// Use actual dimensions, fallback to estimate if not yet rendered
size_t totalWidth = overlay->actualTotalWidth;
size_t totalHeight = overlay->actualTotalHeight;
if (totalWidth == 0) {
// Fallback calculation
const s16 refresh_rate_offset = (overlay->refreshRate < 100) ? 21 : 28;
const s16 info_width = overlay->settings.showInfo ? (6 + overlay->rectangle_width/2 - 4) : 0;
const s16 content_width = overlay->rectangle_width + refresh_rate_offset + info_width;
const s16 content_height = overlay->rectangle_height + 12;
totalWidth = content_width + (2 * border);
totalHeight = content_height + (2 * border);
}
// Apply frame offsets (base position already includes border offset)
const int overlayX = overlay->base_x + overlay->frameOffsetX - border;
const int overlayY = overlay->base_y + overlay->frameOffsetY - border;
// Touch padding
const int touchPadding = 4;
const int touchableX = overlayX - touchPadding;
const int touchableY = overlayY - touchPadding;
const int touchableWidth = totalWidth + (touchPadding * 2);
const int touchableHeight = totalHeight + (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);
// 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) {
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) {
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(16000000ULL*2); // 16ms polling
}
}, this, NULL, 0x1000, 0x2B, -2);
threadStart(&touchPollThread);
}
~com_FPSGraph() {
// Stop touch polling thread
touchPollRunning.store(false, std::memory_order_release);
threadWaitForExit(&touchPollThread);
threadClose(&touchPollThread);
EndInfoThread();
EndFPSCounterThread();
FullMode = true;
fixForeground = true;
ult::useRightAlignment = originalUseRightAlignment;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().addScreenshotStacks();
}
deactivateOriginalFooter = false;
}
struct stats {
s16 value;
bool zero_rounded;
};
std::vector<stats> readings;
s16 base_y = 0;
s16 base_x = 0;
s16 rectangle_width = 180;
s16 rectangle_height = 60;
s16 rectangle_x = 15;
s16 rectangle_y = 5;
s16 rectangle_range_max = 60;
s16 rectangle_range_min = 0;
char legend_max[4] = "60";
char legend_min[2] = "0";
s32 range = std::abs(rectangle_range_max - rectangle_range_min) + 1;
s16 x_end = rectangle_x + rectangle_width;
s16 y_old = rectangle_y+rectangle_height;
s16 y_30FPS = rectangle_y+(rectangle_height / 2);
s16 y_60FPS = rectangle_y;
bool isAbove = 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) {
// Calculate content dimensions (what goes inside the border)
const s16 refresh_rate_offset = (refreshRate < 100) ? 21 : 28;
const s16 info_width = settings.showInfo ? (6 + rectangle_width/2 - 4) : 6;
const s16 content_width = rectangle_width + refresh_rate_offset + info_width;
const s16 content_height = rectangle_height + 12;
// Total dimensions including border
const size_t totalWidth = content_width + (2 * border);
const size_t totalHeight = content_height + (2 * border);
// Store actual dimensions for input handling
actualTotalWidth = totalWidth;
actualTotalHeight = totalHeight;
if (refreshRate && refreshRate < 240) {
rectangle_height = refreshRate;
rectangle_range_max = refreshRate;
if (refreshRate < 100) {
rectangle_x = 15;
legend_max[0] = 0x30 + (refreshRate / 10);
legend_max[1] = 0x30 + (refreshRate % 10);
legend_max[2] = 0;
}
else {
rectangle_x = 22;
legend_max[0] = 0x30 + (refreshRate / 100);
legend_max[1] = 0x30 + ((refreshRate / 10) % 10);
legend_max[2] = 0x30 + (refreshRate % 10);
}
y_30FPS = rectangle_y+(rectangle_height / 2);
range = std::abs(rectangle_range_max - rectangle_range_min) + 1;
};
int _frameOffsetX = ult::limitedMemory ? std::max(0, frameOffsetX - (1280-448)) : frameOffsetX;
// Calculate position with frame offsets (for the rounded rect, which includes border)
int posX = base_x + _frameOffsetX - border;
int posY = base_y + frameOffsetY - border;
// Clamp to screen bounds (accounting for total size including border)
posX = std::max(int(framePadding), std::min(posX, static_cast<int>(screenWidth - totalWidth - framePadding)));
posY = std::max(int(framePadding), std::min(posY, static_cast<int>(screenHeight - totalHeight - framePadding)));
// Draw the rounded rectangle (background)
const tsl::Color bgColor = !isDragging
? settings.backgroundColor
: settings.focusBackgroundColor;
renderer->drawRoundedRectSingleThreaded(
posX,
posY,
totalWidth,
totalHeight,
16,
aWithOpacity(bgColor)
);
posX += 4;
// Content drawing position (inside the border)
const int final_base_x = posX + border;
const int final_base_y = posY + border;
const s16 size = (refreshRate > 60 || !refreshRate) ? 63 : (s32)(63.0/(60.0/refreshRate));
const auto width = renderer->getTextDimensions(FPSavg_c, false, size).first;
const s16 pos_y = size + final_base_y + rectangle_y + ((rectangle_height - size) / 2);
const s16 pos_x = final_base_x + rectangle_x + ((rectangle_width - width) / 2);
if (FPSavg != 254.0)
renderer->drawString(FPSavg_c, false, pos_x, pos_y-5, size, settings.fpsColor);
renderer->drawEmptyRect(final_base_x+(rectangle_x - 1)+2, final_base_y+(rectangle_y - 1), rectangle_width + 2, rectangle_height + 4, aWithOpacity(settings.borderColor));
renderer->drawDashedLine(final_base_x+rectangle_x+2, final_base_y+y_30FPS, final_base_x+rectangle_x+rectangle_width, final_base_y+y_30FPS, 6, aWithOpacity(settings.dashedLineColor));
renderer->drawString(&legend_max[0], false, final_base_x+(rectangle_x-((refreshRate < 100) ? 15 : 22)), final_base_y+(rectangle_y+7), 10, (settings.maxFPSTextColor));
renderer->drawString(&legend_min[0], false, final_base_x+(rectangle_x-10), final_base_y+(rectangle_y+rectangle_height+3), 10, settings.minFPSTextColor);
size_t last_element = readings.size() - 1;
s16 offset = 0;
if (refreshRate >= 100) offset = 7;
static s32 y_on_range;
static tsl::Color color = {0};
for (s16 x = x_end; x > static_cast<s16>(x_end-readings.size()); x--) {
y_on_range = readings[last_element].value + std::abs(rectangle_range_min) + 1;
if (y_on_range < 0) {
y_on_range = 0;
}
else if (y_on_range > range) {
isAbove = true;
y_on_range = range;
}
const s16 y = rectangle_y + static_cast<s16>(std::lround((float)rectangle_height * ((float)(range - y_on_range) / (float)range)));
color = (settings.mainLineColor);
if (y == y_old && !isAbove && readings[last_element].zero_rounded) {
if ((y == y_30FPS || y == y_60FPS))
color = (settings.perfectLineColor);
else
color = (settings.dashedLineColor);
}
if (x == x_end) {
y_old = y;
}
renderer->drawLine(final_base_x+x+offset, final_base_y+y, final_base_x+x+offset, final_base_y+y_old, color);
isAbove = false;
y_old = y;
last_element--;
}
if (settings.showInfo) {
const s16 info_x = final_base_x+rectangle_width+rectangle_x + 6 +8;
const s16 info_y = final_base_y + 3;
const s16 fontSize = 11;
// Get line height from font size (we'll use the actual rendered height)
const auto testDimensions = renderer->getTextDimensions("A", false, fontSize);
const s16 lineHeight = testDimensions.second;
// Starting Y position for first line
const s16 startY = info_y + lineHeight;
// Value X position (offset from labels)
const s16 value_x = info_x + 40;
static constexpr s16 SPACING = 1;
// Compute gradient colors for temperatures
const tsl::Color socColor = settings.useDynamicColors ? tsl::GradientColor(SOC_temperatureF) : settings.textColor;
const tsl::Color pcbColor = settings.useDynamicColors ? tsl::GradientColor(PCB_temperatureF) : settings.textColor;
const tsl::Color skinColor = settings.useDynamicColors ? tsl::GradientColor(static_cast<float>(skin_temperaturemiliC) / 1000.0f) : settings.textColor;
// Draw each label and value pair on the same baseline
// Line 0: CPU
renderer->drawString("CPU", false, info_x, startY, fontSize, settings.catColor);
renderer->drawString(CPU_Load_c, false, value_x, startY, fontSize, settings.textColor);
// Line 1: GPU
renderer->drawString("GPU", false, info_x, startY + lineHeight+SPACING, fontSize, settings.catColor);
renderer->drawString(GPU_Load_c, false, value_x, startY + lineHeight+SPACING, fontSize, settings.textColor);
// Line 2: RAM
renderer->drawString("RAM", false, info_x, startY + lineHeight * 2+2*SPACING, fontSize, settings.catColor);
renderer->drawString(RAM_Load_c, false, value_x, startY + lineHeight * 2+2*SPACING, fontSize, settings.textColor);
// Line 3: SOC (with gradient color)
renderer->drawString("SOC", false, info_x, startY + lineHeight * 3+3*SPACING, fontSize, settings.catColor);
renderer->drawString(SOC_TEMP_c, false, value_x, startY + lineHeight * 3+3*SPACING, fontSize, socColor);
// Line 4: PCB (with gradient color)
renderer->drawString("PCB", false, info_x, startY + lineHeight * 4+4*SPACING, fontSize, settings.catColor);
renderer->drawString(PCB_TEMP_c, false, value_x, startY + lineHeight * 4+4*SPACING, fontSize, pcbColor);
// Line 5: SKIN (with gradient color)
renderer->drawString("Skin", false, info_x, startY + lineHeight * 5+5*SPACING, fontSize, settings.catColor);
renderer->drawString(SKIN_TEMP_c, false, value_x, startY + lineHeight * 5+5*SPACING, fontSize, skinColor);
}
});
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("", "");
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
cnt++;
if (cnt >= TeslaFPS)
cnt = 0;
///FPS
stats temp = {0, false};
static uint64_t lastFrame = 0;
snprintf(FPSavg_c, sizeof FPSavg_c, "%2.1f", FPSavg);
const uint8_t SaltySharedDisplayRefreshRate = *(uint8_t*)((uintptr_t)shmemGetAddr(&_sharedmemory) + 1);
if (SaltySharedDisplayRefreshRate)
refreshRate = SaltySharedDisplayRefreshRate;
else refreshRate = 60;
if (FPSavg < 254) {
snprintf(FPSavg_c, sizeof(FPSavg_c), "%.1f", useOldFPSavg ? FPSavg_old : FPSavg);
if (lastFrame == lastFrameNumber) return;
else lastFrame = lastFrameNumber;
if ((s16)(readings.size()) >= rectangle_width) {
readings.erase(readings.begin());
}
const float whole = std::round(useOldFPSavg ? FPSavg_old : FPSavg);
temp.value = static_cast<s16>(std::lround(useOldFPSavg ? FPSavg_old : FPSavg));
if ((useOldFPSavg ? FPSavg_old : FPSavg) < whole+0.04 && (useOldFPSavg ? FPSavg_old : FPSavg) > whole-0.05) {
temp.zero_rounded = true;
}
readings.push_back(temp);
}
else {
if (readings.size()) {
readings.clear();
readings.shrink_to_fit();
lastFrame = 0;
}
FPSavg_c[0] = 0;
}
if (cnt)
return;
mutexLock(&mutex_Misc);
// Format temperature strings separately for proper alignment
snprintf(SOC_TEMP_c, sizeof SOC_TEMP_c, "%2.1f\u00B0C", SOC_temperatureF);
snprintf(PCB_TEMP_c, sizeof PCB_TEMP_c, "%2.1f\u00B0C", PCB_temperatureF);
snprintf(SKIN_TEMP_c, sizeof SKIN_TEMP_c, "%2d.%d\u00B0C",
skin_temperaturemiliC / 1000, (skin_temperaturemiliC / 100) % 10);
// Atomically snapshot each idle tick once
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);
// Clamp values to systemtickfrequency_impl (avoid div-by-zero / runaway)
const uint64_t safe0 = std::min(idle0, systemtickfrequency_impl);
const uint64_t safe1 = std::min(idle1, systemtickfrequency_impl);
const uint64_t safe2 = std::min(idle2, systemtickfrequency_impl);
const uint64_t safe3 = std::min(idle3, systemtickfrequency_impl);
// Compute per-core CPU usage
const double cpu_usage0 = (1.0 - (static_cast<double>(safe0) / systemtickfrequency_impl)) * 100.0;
const double cpu_usage1 = (1.0 - (static_cast<double>(safe1) / systemtickfrequency_impl)) * 100.0;
const double cpu_usage2 = (1.0 - (static_cast<double>(safe2) / systemtickfrequency_impl)) * 100.0;
const double cpu_usage3 = (1.0 - (static_cast<double>(safe3) / systemtickfrequency_impl)) * 100.0;
// Compute max core load (the highest usage)
const double cpu_usageM = std::max({cpu_usage0, cpu_usage1, cpu_usage2, cpu_usage3});
// Format output strings
snprintf(CPU_Load_c, sizeof(CPU_Load_c), "%.1f%%", cpu_usageM);
snprintf(GPU_Load_c, sizeof(GPU_Load_c), "%d.%d%%", GPU_Load_u / 10, GPU_Load_u % 10);
snprintf(RAM_Load_c, sizeof(RAM_Load_c), "%hu.%hhu%%",
ramLoad[SysClkRamLoad_All] / 10,
ramLoad[SysClkRamLoad_All] % 10);
mutexUnlock(&mutex_Misc);
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;
// Touch detection
const bool currentTouchDetected = (touchPos.x > 0 && touchPos.y > 0 &&
touchPos.x < screenWidth && touchPos.y < screenHeight);
static bool clearOnRelease = false;
if (clearOnRelease && !isRendering) {
clearOnRelease = false;
isRendering = true;
leventClear(&renderingStopEvent);
}
// Use actual dimensions from last render, fallback to estimate if not available
size_t totalWidth = actualTotalWidth;
size_t totalHeight = actualTotalHeight;
if (totalWidth == 0) {
// Fallback calculation if not yet rendered
const s16 refresh_rate_offset = (refreshRate < 100) ? 21 : 28;
const s16 info_width = settings.showInfo ? (6 + rectangle_width/2 - 4) : 0;
const s16 content_width = rectangle_width + refresh_rate_offset + info_width;
const s16 content_height = rectangle_height + 12;
totalWidth = content_width + (2 * border);
totalHeight = content_height + (2 * border);
}
// Current overlay position (top-left of rounded rect)
const int overlayX = base_x + frameOffsetX - border;
const int overlayY = base_y + frameOffsetY - border;
// Touch detection area (with padding for easier interaction)
static constexpr int touchPadding = 4;
const int touchableX = overlayX - touchPadding;
const int touchableY = overlayY - touchPadding;
const int touchableWidth = totalWidth + (touchPadding * 2);
const int touchableHeight = totalHeight + (touchPadding * 2);
// Screen boundaries for clamping (accounting for total size)
const int minX = -(base_x - border) + framePadding;
const int maxX = screenWidth - totalWidth - (base_x - border) - framePadding;
const int minY = -(base_y - border) + framePadding;
const int maxY = screenHeight - totalHeight - (base_y - border) - 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) {
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;
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
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
frameOffsetX = std::max(minX, std::min(maxX, initialFrameOffsetX + deltaX));
frameOffsetY = std::max(minY, std::min(maxY, initialFrameOffsetY + deltaY));
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);
}
}
} else if (!currentTouchDetected && oldTouchDetected && isDragging && !currentMinusHeld && !currentPlusHeld) {
// Touch just released
if (hasMoved) {
// Save position when touch drag ends
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
iniData["fps-graph"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["fps-graph"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
}
// Reset touch drag state
isDragging = false;
hasMoved = false;
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;
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
const float magnitude = sqrt((float)(activeJoystick.x * activeJoystick.x + activeJoystick.y * activeJoystick.y));
const float normalizedMagnitude = magnitude / 32767.0f;
// Smooth curve for sensitivity
static constexpr float baseSensitivity = 0.00008f;
static constexpr float maxSensitivity = 0.0005f;
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;
const int deltaX = (int)accumulatedX;
const int deltaY = (int)accumulatedY;
accumulatedX -= deltaX;
accumulatedY -= deltaY;
// Update frame offsets with boundary checking
frameOffsetX = std::max(minX, std::min(maxX, frameOffsetX + deltaX));
frameOffsetY = std::max(minY, std::min(maxY, frameOffsetY + deltaY));
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);
}
}
} else if (((!currentMinusHeld && oldMinusHeld) || (!currentPlusHeld && oldPlusHeld)) && isDragging) {
// Button just released - stop joystick dragging
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
iniData["fps-graph"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["fps-graph"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
isDragging = false;
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);
runOnce = true;
skipOnce = true;
TeslaFPS = 60;
lastSelectedItem = "FPS Graph";
lastMode = "";
if (skipMain) {
lastMode = "return";
tsl::goBack();
}
else {
tsl::setNextOverlay(filepath.c_str(), "--lastSelectedItem 'FPS Graph'");
tsl::Overlay::get()->close();
}
return true;
}
}
// Return true if we handled the input (during dragging)
return isDragging;
}
};

View File

@@ -0,0 +1,635 @@
class MainMenu;
class FullOverlay : public tsl::Gui {
private:
char DeltaCPU_c[12] = "";
char DeltaGPU_c[12] = "";
char DeltaRAM_c[12] = "";
char RealCPU_Hz_c[64] = "";
char RealGPU_Hz_c[64] = "";
char RealRAM_Hz_c[64] = "";
char GPU_Load_c[32] = "";
char Rotation_SpeedLevel_c[64] = "";
char RAM_compressed_c[64] = "";
char RAM_var_compressed_c[128] = "";
char RAM_percentage_var_compressed_c[128] = "";
char CPU_Hz_c[64] = "";
char GPU_Hz_c[64] = "";
char RAM_Hz_c[64] = "";
char CPU_compressed_c[160] = "";
char SOC_temperature_c[32] = "";
char PCB_temperature_c[32] = "";
char skin_temperature_c[32] = "";
char BatteryDraw_c[64] = "";
char FPS_var_compressed_c[64] = "";
char RAM_load_c[64] = "";
char Resolutions_c[64] = "";
char readSpeed_c[32] = "";
// New separated value buffers for CPU cores
char CPU_Core0_c[16] = "";
char CPU_Core1_c[16] = "";
char CPU_Core2_c[16] = "";
char CPU_Core3_c[16] = "";
// New separated value buffers for FPS
char PFPS_value_c[16] = "";
char FPS_value_c[16] = "";
static constexpr uint8_t COMMON_MARGIN = 20;
FullSettings settings;
uint64_t systemtickfrequency_impl = systemtickfrequency;
std::string formattedKeyCombo = keyCombo;
std::string message;
const std::vector<std::string> KEY_SYMBOLS = {
"\uE0E4", "\uE0E5", "\uE0E6", "\uE0E7",
"\uE0E8", "\uE0E9", "\uE0ED", "\uE0EB",
"\uE0EE", "\uE0EC", "\uE0E0", "\uE0E1",
"\uE0E2", "\uE0E3", "\uE08A", "\uE08B",
"\uE0B6", "\uE0B5"
};
bool skipOnce = true;
bool runOnce = true;
bool originalUseRightAlignment = ult::useRightAlignment;
public:
FullOverlay() {
disableJumpTo = true;
GetConfigSettings(&settings);
mutexInit(&mutex_BatteryChecker);
mutexInit(&mutex_Misc);
tsl::hlp::requestForeground(false);
TeslaFPS = settings.refreshRate;
systemtickfrequency_impl /= settings.refreshRate;
idletick0.store(systemtickfrequency_impl, std::memory_order_relaxed);
idletick1.store(systemtickfrequency_impl, std::memory_order_relaxed);
idletick2.store(systemtickfrequency_impl, std::memory_order_relaxed);
idletick3.store(systemtickfrequency_impl, std::memory_order_relaxed);
if (settings.setPosRight) {
const auto [horizontalUnderscanPixels, verticalUnderscanPixels] = tsl::gfx::getUnderscanPixels();
tsl::gfx::Renderer::get().setLayerPos(1280-32 - horizontalUnderscanPixels, 0);
ult::useRightAlignment = true;
} else {
tsl::gfx::Renderer::get().setLayerPos(0, 0);
ult::useRightAlignment = false;
}
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().removeScreenshotStacks();
}
deactivateOriginalFooter = true;
formatButtonCombination(formattedKeyCombo);
//message = "Press " + formattedKeyCombo + " to Exit";
realVoltsPolling = false;
StartThreads();
}
~FullOverlay() {
CloseThreads();
fixForeground = true;
ult::useRightAlignment = originalUseRightAlignment;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().addScreenshotStacks();
}
deactivateOriginalFooter = false;
}
resolutionCalls m_resolutionRenderCalls[8] = {0};
resolutionCalls m_resolutionViewportCalls[8] = {0};
resolutionCalls m_resolutionOutput[8] = {0};
uint8_t resolutionLookup = 0;
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 auto targetFreqWidth = renderer->getTextDimensions("Target Frequency", false, 15).first;
//static auto realFreqWidth = renderer->getTextDimensions("Real Frequency", false, 15).first;
//static auto freqWidth = std::max(targetFreqWidth, realFreqWidth);
//static auto batteryLabelWidth = renderer->getTextDimensions("Battery Power Flow", false, 15).first;
//static auto fanLabelWidth = renderer->getTextDimensions("Fan Rotation Level", false, 15).first;
//static auto boardWidth = std::max(batteryLabelWidth, fanLabelWidth);
static constexpr size_t valueOffset = 150+10;
static constexpr size_t deltaOffset = 246+10;
static constexpr size_t ramPercentageOffset = 350+10;
//Print strings
///CPU
if (R_SUCCEEDED(clkrstCheck) || R_SUCCEEDED(pcvCheck)) {
uint32_t height_offset = 155;
if (realCPU_Hz && settings.showRealFreqs) {
height_offset = 162;
}
renderer->drawString("CPU Usage", false, COMMON_MARGIN, 120, 20, (settings.catColor1));
if (settings.showTargetFreqs) {
//static auto targetFreqWidth = renderer->getTextDimensions("Target Frequency: ", false, 15).first;
renderer->drawString("Target Frequency", false, COMMON_MARGIN, height_offset, 15, (settings.catColor2));
renderer->drawString(CPU_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset, 15, (settings.textColor));
}
if (realCPU_Hz && settings.showRealFreqs) {
//static auto realFreqWidth = renderer->getTextDimensions("Real Frequency: ", false, 15).first;
renderer->drawString("Real Frequency", false, COMMON_MARGIN, height_offset - 15, 15, (settings.catColor2));
renderer->drawString(RealCPU_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset - 15, 15, (settings.textColor));
if (settings.showDeltas && settings.showTargetFreqs) {
renderer->drawString(DeltaCPU_c, false, COMMON_MARGIN + deltaOffset, height_offset - 7, 15, (settings.textColor));
}
else if (settings.showDeltas && !settings.showTargetFreqs) {
renderer->drawString(DeltaCPU_c, false, COMMON_MARGIN + deltaOffset, height_offset - 15, 15, (settings.textColor));
}
}
else if (realCPU_Hz && settings.showDeltas && (settings.showRealFreqs || settings.showTargetFreqs)) {
renderer->drawString(DeltaCPU_c, false, COMMON_MARGIN + deltaOffset, height_offset, 15, (settings.textColor));
}
// CPU Core labels and values
static auto core0Width = renderer->getTextDimensions("Core 0 ", false, 15).first;
static auto core1Width = renderer->getTextDimensions("Core 1 ", false, 15).first;
static auto core2Width = renderer->getTextDimensions("Core 2 ", false, 15).first;
static auto core3Width = renderer->getTextDimensions("Core 3 ", false, 15).first;
const uint32_t core_height = height_offset + 30;
renderer->drawString("Core 0 ", false, COMMON_MARGIN, core_height, 15, (settings.catColor2));
renderer->drawString(CPU_Core0_c, false, COMMON_MARGIN + core0Width, core_height, 15, (settings.textColor));
renderer->drawString("Core 1 ", false, COMMON_MARGIN, core_height + 15, 15, (settings.catColor2));
renderer->drawString(CPU_Core1_c, false, COMMON_MARGIN + core1Width, core_height + 15, 15, (settings.textColor));
renderer->drawString("Core 2 ", false, COMMON_MARGIN, core_height + 30, 15, (settings.catColor2));
renderer->drawString(CPU_Core2_c, false, COMMON_MARGIN + core2Width, core_height + 30, 15, (settings.textColor));
renderer->drawString("Core 3 ", false, COMMON_MARGIN, core_height + 45, 15, (settings.catColor2));
renderer->drawString(CPU_Core3_c, false, COMMON_MARGIN + core3Width, core_height + 45, 15, (settings.textColor));
}
///GPU
if (R_SUCCEEDED(clkrstCheck) || R_SUCCEEDED(pcvCheck) || R_SUCCEEDED(nvCheck)) {
uint32_t height_offset = 320-8;
if (realGPU_Hz && settings.showRealFreqs) {
height_offset = 327-8;
}
renderer->drawString("GPU Usage", false, COMMON_MARGIN, 285-8, 20, (settings.catColor1));
if (R_SUCCEEDED(clkrstCheck) || R_SUCCEEDED(pcvCheck)) {
if (settings.showTargetFreqs) {
//static auto targetFreqWidth = renderer->getTextDimensions("Target Frequency: ", false, 15).first;
renderer->drawString("Target Frequency", false, COMMON_MARGIN, height_offset, 15, (settings.catColor2));
renderer->drawString(GPU_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset, 15, (settings.textColor));
}
if (realCPU_Hz && settings.showRealFreqs) {
//static auto realFreqWidth = renderer->getTextDimensions("Real Frequency: ", false, 15).first;
renderer->drawString("Real Frequency", false, COMMON_MARGIN, height_offset - 15, 15, (settings.catColor2));
renderer->drawString(RealGPU_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset - 15, 15, (settings.textColor));
if (settings.showDeltas && settings.showTargetFreqs) {
renderer->drawString(DeltaGPU_c, false, COMMON_MARGIN + deltaOffset, height_offset - 7, 15, (settings.textColor));
}
else if (settings.showDeltas && !settings.showTargetFreqs) {
renderer->drawString(DeltaGPU_c, false, COMMON_MARGIN + deltaOffset, height_offset - 15, 15, (settings.textColor));
}
}
else if (realGPU_Hz && settings.showDeltas && (settings.showRealFreqs || settings.showTargetFreqs)) {
renderer->drawString(DeltaGPU_c, false, COMMON_MARGIN + deltaOffset, height_offset, 15, (settings.textColor));
}
}
if (R_SUCCEEDED(nvCheck)) {
//static auto loadWidth = renderer->getTextDimensions("Load: ", false, 15).first;
renderer->drawString("Load", false, COMMON_MARGIN, height_offset + 15, 15, (settings.catColor2));
renderer->drawString(GPU_Load_c, false, COMMON_MARGIN + valueOffset, height_offset + 15, 15, (settings.textColor));
}
}
static std::vector<std::string> specialChars = {""};
///RAM
if (R_SUCCEEDED(clkrstCheck) || R_SUCCEEDED(pcvCheck) || R_SUCCEEDED(Hinted)) {
uint32_t height_offset = 410;
if (realRAM_Hz && settings.showRealFreqs) {
height_offset += 7;
}
renderer->drawString("RAM Usage", false, COMMON_MARGIN, 375, 20, (settings.catColor1));
if (R_SUCCEEDED(clkrstCheck) || R_SUCCEEDED(pcvCheck)) {
if (settings.showTargetFreqs) {
//static auto targetFreqWidth = renderer->getTextDimensions("Target Frequency: ", false, 15).first;
renderer->drawString("Target Frequency", false, COMMON_MARGIN, height_offset, 15, (settings.catColor2));
renderer->drawString(RAM_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset, 15, (settings.textColor));
}
if (realRAM_Hz && settings.showRealFreqs) {
//static auto realFreqWidth = renderer->getTextDimensions("Real Frequency: ", false, 15).first;
renderer->drawString("Real Frequency", false, COMMON_MARGIN, height_offset - 15, 15, (settings.catColor2));
renderer->drawString(RealRAM_Hz_c, false, COMMON_MARGIN + valueOffset, height_offset - 15, 15, (settings.textColor));
if (settings.showDeltas && settings.showTargetFreqs) {
renderer->drawString(DeltaRAM_c, false, COMMON_MARGIN + deltaOffset, height_offset - 7, 15, (settings.textColor));
}
else if (settings.showDeltas && !settings.showTargetFreqs) {
renderer->drawString(DeltaRAM_c, false, COMMON_MARGIN + deltaOffset, height_offset - 15, 15, (settings.textColor));
}
}
else if (realRAM_Hz && settings.showDeltas && (settings.showRealFreqs || settings.showTargetFreqs)) {
renderer->drawString(DeltaRAM_c, false, COMMON_MARGIN + deltaOffset, height_offset, 15, (settings.textColor));
}
if (R_SUCCEEDED(sysclkCheck)) {
static std::vector<std::string> ramLoadColoredChars = {"CPU", "GPU"};
//static auto loadLabelWidth = renderer->getTextDimensions("Load: ", false, 15).first;
renderer->drawString("Load", false, COMMON_MARGIN, height_offset+15, 15, (settings.catColor2));
renderer->drawStringWithColoredSections(RAM_load_c, false, ramLoadColoredChars, COMMON_MARGIN + valueOffset, height_offset+15, 15, (settings.textColor), settings.catColor2);
}
}
if (R_SUCCEEDED(Hinted)) {
//static auto textWidth = renderer->getTextDimensions("Total \nApplication \nApplet \nSystem \nSystem Unsafe ", false, 15).first;
renderer->drawString("Total\nApplication\nApplet\nSystem\nSystem Unsafe", false, COMMON_MARGIN, height_offset + 40, 15, (settings.catColor2));
renderer->drawString(RAM_var_compressed_c, false, COMMON_MARGIN + valueOffset, height_offset + 40, 15, (settings.textColor));
renderer->drawString(RAM_percentage_var_compressed_c, false, ramPercentageOffset, height_offset + 40, 15, (settings.textColor));
}
}
///Thermal
if (R_SUCCEEDED(i2cCheck) || R_SUCCEEDED(tcCheck) || R_SUCCEEDED(pwmCheck)) {
renderer->drawString("Board", false, 20, 550+2, 20, (settings.catColor1));
if (R_SUCCEEDED(i2cCheck)) {
renderer->drawString("Battery Power Flow", false, COMMON_MARGIN, 575+2, 15, (settings.catColor2));
renderer->drawStringWithColoredSections(BatteryDraw_c, false, specialChars, COMMON_MARGIN + valueOffset, 575+2, 15, (settings.textColor), settings.separatorColor);
}
if (R_SUCCEEDED(pwmCheck)) {
renderer->drawString("Fan Rotation Level", false, COMMON_MARGIN, 590+2, 15, (settings.catColor2));
renderer->drawString(Rotation_SpeedLevel_c, false, COMMON_MARGIN + valueOffset, 590+2, 15, (settings.textColor));
}
if (R_SUCCEEDED(i2cCheck) || R_SUCCEEDED(tcCheck)) {
static auto socLabelWidth = renderer->getTextDimensions("SOC ", false, 15).first;
static auto pcbLabelWidth = renderer->getTextDimensions("PCB ", false, 15).first;
static auto maxLabelWidth = std::max(socLabelWidth, pcbLabelWidth);
static auto skinLabelWidth = renderer->getTextDimensions("Skin ", false, 15).first;
// Compute gradient colors for temperatures
const tsl::Color socColor = settings.useDynamicColors ? tsl::GradientColor(SOC_temperatureF) : settings.textColor;
const tsl::Color pcbColor = settings.useDynamicColors ? tsl::GradientColor(PCB_temperatureF) : settings.textColor;
const tsl::Color skinColor = settings.useDynamicColors ? tsl::GradientColor(static_cast<float>(skin_temperaturemiliC) / 1000.0f) : settings.textColor;
renderer->drawString("Temperatures", false, COMMON_MARGIN, 605+2, 15, (settings.catColor2));
// SOC - starts at valueOffset next to "Temperatures"
uint32_t current_x = COMMON_MARGIN + valueOffset;
renderer->drawString("SOC ", false, current_x, 605+2, 15, (settings.catColor2));
current_x += maxLabelWidth;
renderer->drawString(SOC_temperature_c, false, current_x, 605+2, 15, socColor);
// SKIN - same spacing to the right
current_x += renderer->getTextDimensions(SOC_temperature_c, false, 15).first + 15;
renderer->drawString("Skin ", false, current_x, 605+2, 15, (settings.catColor2));
current_x += skinLabelWidth;
renderer->drawString(skin_temperature_c, false, current_x, 605+2, 15, skinColor);
// PCB - below SOC on next line
current_x = COMMON_MARGIN + valueOffset;
renderer->drawString("PCB ", false, current_x, 620+2, 15, (settings.catColor2));
current_x += maxLabelWidth;
renderer->drawString(PCB_temperature_c, false, current_x, 620+2, 15, pcbColor);
}
}
///FPS
if (GameRunning) {
const uint32_t width_offset = valueOffset;
if (settings.showFPS || settings.showRES || settings.showRDSD) {
renderer->drawString("Game", false, COMMON_MARGIN + width_offset, 185+12, 20, (settings.catColor1));
}
uint32_t height = 210+12;
if (settings.showFPS == true) {
static auto pfpsWidth = renderer->getTextDimensions("PFPS ", false, 15).first;
static auto fpsWidth = renderer->getTextDimensions("FPS ", false, 15).first;
renderer->drawString("PFPS ", false, COMMON_MARGIN + width_offset, height, 15, (settings.catColor2));
renderer->drawString(PFPS_value_c, false, COMMON_MARGIN + width_offset + pfpsWidth, height, 15, (settings.textColor));
// Calculate position for "FPS " label - add some spacing
const uint32_t fps_x_offset = COMMON_MARGIN + width_offset + pfpsWidth + renderer->getTextDimensions(PFPS_value_c, false, 15).first + 15;
renderer->drawString("FPS ", false, fps_x_offset, height, 15, (settings.catColor2));
renderer->drawString(FPS_value_c, false, fps_x_offset + fpsWidth, height, 15, (settings.textColor));
height += 15;
}
if ((settings.showRES == true) && NxFps && SharedMemoryUsed && (NxFps -> API >= 1)) {
static auto resLabelWidth = renderer->getTextDimensions("Resolutions ", false, 15).first;
renderer->drawString("Resolutions ", false, COMMON_MARGIN + width_offset, height, 15, (settings.catColor2));
renderer->drawStringWithColoredSections(Resolutions_c, false, specialChars, COMMON_MARGIN + width_offset + resLabelWidth, height, 15, (settings.textColor), settings.separatorColor);
height += 15;
}
if (settings.showRDSD == true) {
static auto readLabelWidth = renderer->getTextDimensions("Read Speed ", false, 15).first;
renderer->drawString("Read Speed ", false, COMMON_MARGIN + width_offset, height, 15, (settings.catColor2));
renderer->drawString(readSpeed_c, false, COMMON_MARGIN + width_offset + readLabelWidth, height, 15, (settings.textColor));
}
}
//renderer->drawStringWithColoredSections(message, false, KEY_SYMBOLS, 30, 693, 23, a(tsl::bottomTextColor), a(tsl::buttonColor));
static const auto pressWidth = renderer->getTextDimensions("Press ", false, 23).first;
static const auto keyComboWidth = renderer->getTextDimensions(formattedKeyCombo.c_str(), false, 23).first;
static constexpr u16 baseX = 30;
static constexpr u16 baseY = 693;
static constexpr u8 fontSize = 23;
// Draw "Press "
renderer->drawString("Press ", false, baseX, baseY, fontSize, (tsl::bottomTextColor));
// Draw formatted key combo with colored sections
renderer->drawStringWithColoredSections(formattedKeyCombo, false, KEY_SYMBOLS, baseX + pressWidth, baseY, fontSize, (tsl::bottomTextColor), (tsl::buttonColor));
// Draw " to Exit"
renderer->drawString(" to Exit", false, baseX + pressWidth + keyComboWidth, baseY, fontSize, (tsl::bottomTextColor));
});
auto rootFrame = new tsl::elm::HeaderOverlayFrame("Status Monitor", APP_VERSION);
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
//Make stuff ready to print
///CPU
if (systemtickfrequency_impl > 0) {
const uint64_t idle0_val = std::min(idletick0.load(std::memory_order_acquire), systemtickfrequency_impl);
const uint64_t idle1_val = std::min(idletick1.load(std::memory_order_acquire), systemtickfrequency_impl);
const uint64_t idle2_val = std::min(idletick2.load(std::memory_order_acquire), systemtickfrequency_impl);
const uint64_t idle3_val = std::min(idletick3.load(std::memory_order_acquire), systemtickfrequency_impl);
const float usage0 = std::clamp(100.0f * (1.0f - float(idle0_val) / systemtickfrequency_impl), 0.0f, 100.0f);
const float usage1 = std::clamp(100.0f * (1.0f - float(idle1_val) / systemtickfrequency_impl), 0.0f, 100.0f);
const float usage2 = std::clamp(100.0f * (1.0f - float(idle2_val) / systemtickfrequency_impl), 0.0f, 100.0f);
const float usage3 = std::clamp(100.0f * (1.0f - float(idle3_val) / systemtickfrequency_impl), 0.0f, 100.0f);
// Format individual core values
snprintf(CPU_Core0_c, sizeof(CPU_Core0_c), "%.2f%%", usage0);
snprintf(CPU_Core1_c, sizeof(CPU_Core1_c), "%.2f%%", usage1);
snprintf(CPU_Core2_c, sizeof(CPU_Core2_c), "%.2f%%", usage2);
snprintf(CPU_Core3_c, sizeof(CPU_Core3_c), "%.2f%%", usage3);
}
mutexLock(&mutex_Misc);
snprintf(CPU_Hz_c, sizeof(CPU_Hz_c), "%u.%u MHz", CPU_Hz / 1000000, (CPU_Hz / 100000) % 10);
if (realCPU_Hz) {
snprintf(RealCPU_Hz_c, sizeof(RealCPU_Hz_c), "%u.%u MHz", realCPU_Hz / 1000000, (realCPU_Hz / 100000) % 10);
const int32_t deltaCPU = (int32_t)(realCPU_Hz / 1000) - (CPU_Hz / 1000);
snprintf(DeltaCPU_c, sizeof(DeltaCPU_c), "Δ %d.%u", deltaCPU / 1000, abs(deltaCPU / 100) % 10);
}
///GPU
snprintf(GPU_Hz_c, sizeof GPU_Hz_c, "%u.%u MHz", GPU_Hz / 1000000, (GPU_Hz / 100000) % 10);
if (realGPU_Hz) {
snprintf(RealGPU_Hz_c, sizeof(RealGPU_Hz_c), "%u.%u MHz", realGPU_Hz / 1000000, (realGPU_Hz / 100000) % 10);
const int32_t deltaGPU = (int32_t)(realGPU_Hz / 1000) - (GPU_Hz / 1000);
snprintf(DeltaGPU_c, sizeof(DeltaGPU_c), "Δ %d.%u", deltaGPU / 1000, abs(deltaGPU / 100) % 10);
}
snprintf(GPU_Load_c, sizeof GPU_Load_c, "%u.%u%%", GPU_Load_u / 10, GPU_Load_u % 10);
///RAM
snprintf(RAM_Hz_c, sizeof RAM_Hz_c, "%u.%u MHz", RAM_Hz / 1000000, (RAM_Hz / 100000) % 10);
if (realRAM_Hz) {
snprintf(RealRAM_Hz_c, sizeof(RealRAM_Hz_c), "%u.%u MHz", realRAM_Hz / 1000000, (realRAM_Hz / 100000) % 10);
const int32_t deltaRAM = (int32_t)(realRAM_Hz / 1000) - (RAM_Hz / 1000);
snprintf(DeltaRAM_c, sizeof(DeltaRAM_c), "Δ %d.%u", deltaRAM / 1000, abs(deltaRAM / 100) % 10);
}
const float RAM_Total_application_f = (float)RAM_Total_application_u / 1024 / 1024;
const float RAM_Total_applet_f = (float)RAM_Total_applet_u / 1024 / 1024;
const float RAM_Total_system_f = (float)RAM_Total_system_u / 1024 / 1024;
const float RAM_Total_systemunsafe_f = (float)RAM_Total_systemunsafe_u / 1024 / 1024;
const float RAM_Total_all_f = RAM_Total_application_f + RAM_Total_applet_f + RAM_Total_system_f + RAM_Total_systemunsafe_f;
const float RAM_Used_application_f = (float)RAM_Used_application_u / 1024 / 1024;
const float RAM_Used_applet_f = (float)RAM_Used_applet_u / 1024 / 1024;
const float RAM_Used_system_f = (float)RAM_Used_system_u / 1024 / 1024;
const float RAM_Used_systemunsafe_f = (float)RAM_Used_systemunsafe_u / 1024 / 1024;
const float RAM_Used_all_f = RAM_Used_application_f + RAM_Used_applet_f + RAM_Used_system_f + RAM_Used_systemunsafe_f;
// Compute percentages
const int RAMPct_all = (int)((RAM_Used_all_f / RAM_Total_all_f) * 100.0f );
const int RAMPct_app = (int)((RAM_Used_application_f / RAM_Total_application_f) * 100.0f );
const int RAMPct_applet = (int)((RAM_Used_applet_f / RAM_Total_applet_f) * 100.0f );
const int RAMPct_system = (int)((RAM_Used_system_f / RAM_Total_system_f) * 100.0f );
const int RAMPct_systemunsafe = (int)((RAM_Used_systemunsafe_f/ RAM_Total_systemunsafe_f)* 100.0f );
snprintf(RAM_var_compressed_c, sizeof(RAM_var_compressed_c),
"%.1f MB / %.1f MB\n"
"%.1f MB / %.1f MB\n"
"%.1f MB / %.1f MB\n"
"%.1f MB / %.1f MB\n"
"%.1f MB / %.1f MB",
RAM_Used_all_f, RAM_Total_all_f,
RAM_Used_application_f, RAM_Total_application_f,
RAM_Used_applet_f, RAM_Total_applet_f,
RAM_Used_system_f, RAM_Total_system_f,
RAM_Used_systemunsafe_f, RAM_Total_systemunsafe_f
);
// 2. Percentages only (newlines preserved)
snprintf(RAM_percentage_var_compressed_c, sizeof(RAM_percentage_var_compressed_c),
"(%d%%)\n"
"(%d%%)\n"
"(%d%%)\n"
"(%d%%)\n"
"(%d%%)",
RAMPct_all,
RAMPct_app,
RAMPct_applet,
RAMPct_system,
RAMPct_systemunsafe
);
if (R_SUCCEEDED(sysclkCheck)) {
const int RAM_GPU_Load = ramLoad[SysClkRamLoad_All] - ramLoad[SysClkRamLoad_Cpu];
snprintf(RAM_load_c, sizeof RAM_load_c,
"%u.%u%% CPU %u.%u%% GPU %u.%u%%",
ramLoad[SysClkRamLoad_All] / 10, ramLoad[SysClkRamLoad_All] % 10,
ramLoad[SysClkRamLoad_Cpu] / 10, ramLoad[SysClkRamLoad_Cpu] % 10,
RAM_GPU_Load / 10, RAM_GPU_Load % 10);
}
///Thermal
snprintf(SOC_temperature_c, sizeof SOC_temperature_c, "%.1f\u00B0C", SOC_temperatureF);
snprintf(PCB_temperature_c, sizeof PCB_temperature_c, "%.1f\u00B0C", PCB_temperatureF);
snprintf(skin_temperature_c, sizeof skin_temperature_c, "%d.%d\u00B0C", skin_temperaturemiliC / 1000, (skin_temperaturemiliC / 100) % 10);
snprintf(Rotation_SpeedLevel_c, sizeof Rotation_SpeedLevel_c, "%.1f%%", Rotation_Duty);
///FPS
if (settings.showFPS == true) {
snprintf(PFPS_value_c, sizeof PFPS_value_c, "%1u", FPS);
snprintf(FPS_value_c, sizeof FPS_value_c, "%.1f", useOldFPSavg ? FPSavg_old : FPSavg);
}
//Resolutions
if ((settings.showRES == true) && GameRunning && NxFps) {
if (!resolutionLookup) {
(NxFps -> renderCalls[0].calls) = 0xFFFF;
resolutionLookup = 1;
}
else if (resolutionLookup == 1) {
if ((NxFps -> renderCalls[0].calls) != 0xFFFF) resolutionLookup = 2;
}
else {
if (NxFps && SharedMemoryUsed) {
memcpy(&m_resolutionRenderCalls, &(NxFps -> renderCalls), sizeof(m_resolutionRenderCalls));
memcpy(&m_resolutionViewportCalls, &(NxFps -> viewportCalls), sizeof(m_resolutionViewportCalls));
} else {
memset(&m_resolutionRenderCalls, 0, sizeof(m_resolutionRenderCalls));
memset(&m_resolutionViewportCalls, 0, 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);
static std::pair<uint16_t, uint16_t> old_res[2];
// Only swap if BOTH resolutions exist (prevent swapping with empty slot)
if (m_resolutionOutput[0].width && m_resolutionOutput[1].width) {
if ((m_resolutionOutput[0].width == old_res[1].first && m_resolutionOutput[0].height == old_res[1].second) ||
(m_resolutionOutput[1].width == old_res[0].first && m_resolutionOutput[1].height == old_res[0].second)) {
const uint16_t swap_width = m_resolutionOutput[0].width;
const uint16_t swap_height = m_resolutionOutput[0].height;
m_resolutionOutput[0].width = m_resolutionOutput[1].width;
m_resolutionOutput[0].height = m_resolutionOutput[1].height;
m_resolutionOutput[1].width = swap_width;
m_resolutionOutput[1].height = swap_height;
}
}
//if (!m_resolutionOutput[1].width) {
// snprintf(Resolutions_c, sizeof(Resolutions_c), "%dx%d", m_resolutionOutput[0].width, m_resolutionOutput[0].height);
//}
//else {
// snprintf(Resolutions_c, sizeof(Resolutions_c), "%dx%d%dx%d", m_resolutionOutput[0].width, m_resolutionOutput[0].height, m_resolutionOutput[1].width, m_resolutionOutput[1].height);
//}
if (!m_resolutionOutput[1].width || !m_resolutionOutput[0].width) {
if (!m_resolutionOutput[1].width)
snprintf(Resolutions_c, sizeof(Resolutions_c), "%dx%d", m_resolutionOutput[0].width, m_resolutionOutput[0].height);
else snprintf(Resolutions_c, sizeof(Resolutions_c), "%dx%d", m_resolutionOutput[1].width, m_resolutionOutput[1].height);
}
else snprintf(Resolutions_c, sizeof(Resolutions_c),"%dx%d%dx%d", m_resolutionOutput[0].width, m_resolutionOutput[0].height, m_resolutionOutput[1].width, m_resolutionOutput[1].height);
old_res[0] = std::make_pair(m_resolutionOutput[0].width, m_resolutionOutput[0].height);
old_res[1] = std::make_pair(m_resolutionOutput[1].width, m_resolutionOutput[1].height);
}
if (settings.showRDSD == true && GameRunning && NxFps) {
if ((NxFps -> readSpeedPerSecond) != 0.f) snprintf(readSpeed_c, sizeof(readSpeed_c), "%.2f MiB/s", (NxFps -> readSpeedPerSecond) / 1048576.f);
else snprintf(readSpeed_c, sizeof(readSpeed_c), "n/d");
}
}
else if (!GameRunning && resolutionLookup != 0) {
resolutionLookup = 0;
}
mutexUnlock(&mutex_Misc);
/* ── Battery / power draw ───────────────────────────────────── */
char remainingBatteryLife[8];
/* Normalise "-0.00" → "0.00" W */
const float drawW = (fabsf(PowerConsumption) < 0.01f) ? 0.0f
: PowerConsumption;
mutexLock(&mutex_BatteryChecker);
/* keep "--:--" whenever estimate is negative */
if (batTimeEstimate >= 0 && !(drawW <= 0.01f && drawW >= -0.01f)) {
snprintf(remainingBatteryLife, sizeof(remainingBatteryLife),
"%d:%02d", batTimeEstimate / 60, batTimeEstimate % 60);
} else {
strcpy(remainingBatteryLife, "--:--");
}
const float batteryPercent = (float)_batteryChargeInfoFields.RawBatteryCharge / 1000.0f;
snprintf(BatteryDraw_c, sizeof(BatteryDraw_c),
"%.2f W%.0f%% [%s]",
drawW,
batteryPercent,
remainingBatteryLife);
mutexUnlock(&mutex_BatteryChecker);
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 {
if (isKeyComboPressed(keysHeld, keysDown)) {
isRendering = false;
leventSignal(&renderingStopEvent);
triggerRumbleDoubleClick.store(true, std::memory_order_release);
triggerExitSound.store(true, std::memory_order_release);
skipOnce = true;
runOnce = true;
TeslaFPS = 60;
lastSelectedItem = "Full";
lastMode = "";
tsl::swapTo<MainMenu>();
return true;
}
return false;
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
class OtherMenu;
void StartMiscThread() {
// Wait for existing thread to exit
threadWaitForExit(&t0);
// Clear the thread exit event for new thread
leventClear(&threadexit);
// Close and recreate thread
threadClose(&t0);
threadCreate(&t0, Misc2, NULL, NULL, 0x1000, 0x3F, 3);
threadStart(&t0);
}
void EndMiscThread() {
// Signal the thread exit event
leventSignal(&threadexit);
// Wait for thread to exit
threadWaitForExit(&t0);
// Close thread handle
threadClose(&t0);
}
class MiscOverlay : public tsl::Gui {
private:
char DSP_Load_c[16];
// Separated value buffers for NV clocks
char NVDEC_value_c[16] = "";
char NVENC_value_c[16] = "";
char NVJPG_value_c[16] = "";
char Nifm_pass[96];
FullSettings settings;
public:
MiscOverlay() {
GetConfigSettings(&settings);
disableJumpTo = true;
smInitialize();
nifmCheck = nifmInitialize(NifmServiceType_Admin);
if (R_SUCCEEDED(mmuInitialize())) {
nvdecCheck = mmuRequestInitialize(&nvdecRequest, MmuModuleId(5), 8, false);
nvencCheck = mmuRequestInitialize(&nvencRequest, MmuModuleId(6), 8, false);
nvjpgCheck = mmuRequestInitialize(&nvjpgRequest, MmuModuleId(7), 8, false);
}
if (R_SUCCEEDED(audsnoopInitialize()))
audsnoopCheck = audsnoopEnableDspUsageMeasurement();
smExit();
StartMiscThread();
//tsl::elm::g_disableMenuCacheOnReturn.store(true, std::memory_order_release);
}
~MiscOverlay() {
EndMiscThread();
nifmExit();
mmuRequestFinalize(&nvdecRequest);
mmuRequestFinalize(&nvencRequest);
mmuRequestFinalize(&nvjpgRequest);
mmuExit();
if (R_SUCCEEDED(audsnoopCheck)) {
audsnoopDisableDspUsageMeasurement();
}
audsnoopExit();
}
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 X_OFFSET = 30;
static constexpr u16 NV_VALUE_X = 120; // Aligned X position for NV values
///DSP
if (R_SUCCEEDED(audsnoopCheck)) {
renderer->drawString(DSP_Load_c, false, X_OFFSET, 120, 20, (settings.textColor));
}
//Multimedia engines
if (R_SUCCEEDED(nvdecCheck | nvencCheck | nvjpgCheck)) {
renderer->drawString("Multimedia Clock Rates", false, X_OFFSET, 165, 20, (settings.catColor1));
u16 currentY = 185;
if (R_SUCCEEDED(nvdecCheck)) {
renderer->drawString("NVDEC", false, X_OFFSET+15, currentY, 15, (settings.catColor2));
renderer->drawString(NVDEC_value_c, false, NV_VALUE_X, currentY, 15, (settings.textColor));
currentY += 15;
}
if (R_SUCCEEDED(nvencCheck)) {
renderer->drawString("NVENC", false, X_OFFSET+15, currentY, 15, (settings.catColor2));
renderer->drawString(NVENC_value_c, false, NV_VALUE_X, currentY, 15, (settings.textColor));
currentY += 15;
}
if (R_SUCCEEDED(nvjpgCheck)) {
renderer->drawString("NVJPG", false, X_OFFSET+15, currentY, 15, (settings.catColor2));
renderer->drawString(NVJPG_value_c, false, NV_VALUE_X, currentY, 15, (settings.textColor));
}
}
if (R_SUCCEEDED(nifmCheck)) {
renderer->drawString("Network", false, X_OFFSET, 255, 20, (settings.catColor1));
if (!Nifm_internet_rc) {
if (NifmConnectionType == NifmInternetConnectionType_WiFi) {
renderer->drawString("Type: Wi-Fi", false, X_OFFSET, 280, 18, (settings.catColor2));
if (!Nifm_profile_rc) {
if (Nifm_showpass)
renderer->drawString(Nifm_pass, false, X_OFFSET, 305, 15, (settings.textColor));
else
renderer->drawString("Press Y to show password", false, X_OFFSET, 305, 15, (settings.textColor));
}
}
else if (NifmConnectionType == NifmInternetConnectionType_Ethernet)
renderer->drawString("Type: Ethernet", false, X_OFFSET, 280, 18, (settings.textColor));
}
else
renderer->drawString("Type: Not connected", false, X_OFFSET, 280, 18, (settings.textColor));
}
});
//tsl::elm::g_disableMenuCacheOnReturn.store(true, std::memory_order_release);
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("Status Monitor", APP_VERSION, true);
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
snprintf(DSP_Load_c, sizeof DSP_Load_c, "DSP usage: %u%%", DSP_Load_u);
// Format just the values for NV clocks
snprintf(NVDEC_value_c, sizeof(NVDEC_value_c), "%.1f MHz", (float)NVDEC_Hz / 1000000);
snprintf(NVENC_value_c, sizeof(NVENC_value_c), "%.1f MHz", (float)NVENC_Hz / 1000000);
snprintf(NVJPG_value_c, sizeof(NVJPG_value_c), "%.1f MHz", (float)NVJPG_Hz / 1000000);
char pass_temp1[25] = "";
char pass_temp2[25] = "";
char pass_temp3[17] = "";
if (Nifm_profile.wireless_setting_data.passphrase_len > 48) {
memcpy(&pass_temp1, &(Nifm_profile.wireless_setting_data.passphrase[0]), 24);
memcpy(&pass_temp2, &(Nifm_profile.wireless_setting_data.passphrase[24]), 24);
memcpy(&pass_temp3, &(Nifm_profile.wireless_setting_data.passphrase[48]), 16);
}
else if (Nifm_profile.wireless_setting_data.passphrase_len > 24) {
memcpy(&pass_temp1, &(Nifm_profile.wireless_setting_data.passphrase[0]), 24);
memcpy(&pass_temp2, &(Nifm_profile.wireless_setting_data.passphrase[24]), 24);
}
else {
memcpy(&pass_temp1, &(Nifm_profile.wireless_setting_data.passphrase[0]), 24);
}
snprintf(Nifm_pass, sizeof Nifm_pass, "%s\n%s\n%s", pass_temp1, pass_temp2, pass_temp3);
static bool skipOnce = true;
if (!skipOnce) {
static bool runOnce = true;
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 {
if (keysHeld & KEY_Y) {
Nifm_showpass = true;
}
else Nifm_showpass = false;
if (keysDown & KEY_B) {
isRendering = false;
leventSignal(&renderingStopEvent);
triggerRumbleDoubleClick.store(true, std::memory_order_release);
triggerExitSound.store(true, std::memory_order_release);
lastSelectedItem = "Miscellaneous";
lastMode = "";
tsl::swapTo<OtherMenu>();
return true;
}
return false;
}
};

View File

@@ -0,0 +1,581 @@
class MainMenu;
class ResolutionsOverlay : public tsl::Gui {
private:
char Resolutions_c[512] = {0};
char Resolutions2_c[512] = {0};
ResolutionSettings settings;
bool skipOnce = true;
bool runOnce = true;
// Repositioning variables (matching Mini)
int frameOffsetX = 0;
int frameOffsetY = 0;
bool isDragging = false;
size_t framePadding = 10;
static constexpr int screenWidth = 1280;
static constexpr int screenHeight = 720;
bool originalUseRightAlignment = ult::useRightAlignment;
struct ButtonState {
std::atomic<bool> minusDragActive{false};
std::atomic<bool> plusDragActive{false};
} buttonState;
Thread touchPollThread;
std::atomic<bool> touchPollRunning{false};
std::atomic<bool> inputDetected{false};
public:
ResolutionsOverlay() {
tsl::hlp::requestForeground(false);
disableJumpTo = true;
GetConfigSettings(&settings);
// Load saved frame offsets
frameOffsetX = settings.frameOffsetX;
frameOffsetY = settings.frameOffsetY;
framePadding = settings.framePadding;
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);
}
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().removeScreenshotStacks();
}
deactivateOriginalFooter = true;
FullMode = false;
TeslaFPS = settings.refreshRate;
StartFPSCounterThread();
// Start touch polling thread for instant response at low FPS
touchPollRunning.store(true, std::memory_order_release);
threadCreate(&touchPollThread, [](void* arg) -> void {
ResolutionsOverlay* overlay = static_cast<ResolutionsOverlay*>(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};
while (overlay->touchPollRunning.load(std::memory_order_acquire)) {
// Only poll when rendering and not dragging
{
overlay->inputDetected.store(false, std::memory_order_release);
// 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 int overlayX = overlay->frameOffsetX;
const int overlayY = overlay->frameOffsetY;
// Overlay dimensions based on game state
int overlayWidth, overlayHeight;
overlayWidth = 360-20;
overlayHeight = 200;
// 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) {
overlay->inputDetected.store(true, std::memory_order_release);
}
}
// Poll buttons from both controllers
padUpdate(&pad_p1);
padUpdate(&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
overlay->inputDetected.store(true, std::memory_order_release);
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
overlay->inputDetected.store(true, std::memory_order_release);
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 (overlay->inputDetected.load(std::memory_order_acquire)) {
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(16000000ULL*2); // 16ms polling
}
}, this, NULL, 0x1000, 0x2B, -2);
threadStart(&touchPollThread);
}
~ResolutionsOverlay() {
// Stop touch polling thread
touchPollRunning.store(false, std::memory_order_release);
threadWaitForExit(&touchPollThread);
threadClose(&touchPollThread);
EndFPSCounterThread();
TeslaFPS = 60;
if (settings.disableScreenshots) {
tsl::gfx::Renderer::get().addScreenshotStacks();
}
deactivateOriginalFooter = false;
ult::useRightAlignment = originalUseRightAlignment;
fixForeground = true;
FullMode = true;
}
resolutionCalls m_resolutionRenderCalls[8] = {0};
resolutionCalls m_resolutionViewportCalls[8] = {0};
bool gameStart = false;
uint8_t resolutionLookup = 0;
u64 lastGameSeenTick = 0;
bool waitingForGame = true;
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) {
int base_y = 0;
int base_x = 0;
int clippingOffsetX = 0, clippingOffsetY = 0;
int total_width = 360 - 20;
int total_height = 200;
// Check clipping bounds (same as before)
if (base_x + frameOffsetX < int(framePadding))
clippingOffsetX = framePadding - (base_x + frameOffsetX);
else if ((base_x + frameOffsetX + total_width) > static_cast<int>(screenWidth - framePadding))
clippingOffsetX = (screenWidth - framePadding) - (base_x + frameOffsetX + total_width);
if (base_y + frameOffsetY < int(framePadding))
clippingOffsetY = framePadding - (base_y + frameOffsetY);
else if ((base_y + frameOffsetY + total_height) > static_cast<int>(screenHeight - framePadding))
clippingOffsetY = (screenHeight - framePadding) - (base_y + frameOffsetY + total_height);
int _frameOffsetX = ult::limitedMemory ? std::max(0, frameOffsetX - (1280-448)) : frameOffsetX;
const int final_base_x = base_x + _frameOffsetX + clippingOffsetX;
const int final_base_y = base_y + frameOffsetY + clippingOffsetY;
const tsl::Color bgColor = !isDragging ? settings.backgroundColor : settings.focusBackgroundColor;
const u64 curTick = armGetSystemTick();
// guard: ensure lastGameSeenTick is initialized to something reasonable
if (lastGameSeenTick == 0) lastGameSeenTick = curTick;
// threshold in ns (100 ms)
static constexpr u64 CHECK_NS = 2000000000ULL;
// Game detected
if (gameStart && NxFps && NxFps->API >= 1 && (Resolutions_c[0] != '\0' && Resolutions2_c[0] != '\0')) {
lastGameSeenTick = curTick;
waitingForGame = true; // reset waiting state so next missing cycle shows "Checking..."
renderer->drawRoundedRectSingleThreaded(final_base_x, final_base_y, total_width, total_height, 16, aWithOpacity(bgColor));
int xOffset = 10;
int yOffset = 10;
renderer->drawString("Depth", false, xOffset + final_base_x + 20, yOffset + final_base_y + 20, 20, settings.catColor);
renderer->drawString(Resolutions_c, false, xOffset + final_base_x + 20, yOffset + final_base_y + 55, 18, settings.textColor);
renderer->drawString("Viewport", false, xOffset + final_base_x + 180, yOffset + final_base_y + 20, 20, settings.catColor);
renderer->drawString(Resolutions2_c, false, xOffset + final_base_x + 180, yOffset + final_base_y + 55, 18, settings.textColor);
}
// Game not detected
else {
renderer->drawRoundedRectSingleThreaded(final_base_x, final_base_y, total_width, total_height, 16, aWithOpacity(bgColor));
// Check elapsed time since last game detection
u64 elapsed_ns = armTicksToNs(curTick - lastGameSeenTick);
const bool under100ms = (elapsed_ns < CHECK_NS); // 100ms
std::string msg;
if (under100ms && waitingForGame)
msg = "Checking for game...";
else {
msg = "Game is not running\nor is incompatible.";
waitingForGame = false;
}
const auto [textWidth, textHeight] = renderer->getTextDimensions(msg, false, 20);
const int text_x = final_base_x + (total_width - textWidth) / 2;
const int text_y = final_base_y + (total_height) / 2;
renderer->drawString(msg, false, text_x, (under100ms && waitingForGame) ? text_y+textHeight/2 : text_y, 20, (under100ms && waitingForGame) ? 0xFFFF : 0xF00F);
}
});
tsl::elm::HeaderOverlayFrame* rootFrame = new tsl::elm::HeaderOverlayFrame("", "");
rootFrame->setContent(Status);
return rootFrame;
}
virtual void update() override {
if (gameStart && NxFps) {
if (!resolutionLookup) {
NxFps -> renderCalls[0].calls = 0xFFFF;
resolutionLookup = 1;
}
else if (resolutionLookup == 1) {
if ((NxFps -> renderCalls[0].calls) != 0xFFFF) resolutionLookup = 2;
else return;
}
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);
snprintf(Resolutions_c, sizeof Resolutions_c,
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d",
m_resolutionRenderCalls[0].width, m_resolutionRenderCalls[0].height, m_resolutionRenderCalls[0].calls,
m_resolutionRenderCalls[1].width, m_resolutionRenderCalls[1].height, m_resolutionRenderCalls[1].calls,
m_resolutionRenderCalls[2].width, m_resolutionRenderCalls[2].height, m_resolutionRenderCalls[2].calls,
m_resolutionRenderCalls[3].width, m_resolutionRenderCalls[3].height, m_resolutionRenderCalls[3].calls,
m_resolutionRenderCalls[4].width, m_resolutionRenderCalls[4].height, m_resolutionRenderCalls[4].calls,
m_resolutionRenderCalls[5].width, m_resolutionRenderCalls[5].height, m_resolutionRenderCalls[5].calls,
m_resolutionRenderCalls[6].width, m_resolutionRenderCalls[6].height, m_resolutionRenderCalls[6].calls,
m_resolutionRenderCalls[7].width, m_resolutionRenderCalls[7].height, m_resolutionRenderCalls[7].calls
);
snprintf(Resolutions2_c, sizeof Resolutions2_c,
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d\n"
"%dx%d, %d",
m_resolutionViewportCalls[0].width, m_resolutionViewportCalls[0].height, m_resolutionViewportCalls[0].calls,
m_resolutionViewportCalls[1].width, m_resolutionViewportCalls[1].height, m_resolutionViewportCalls[1].calls,
m_resolutionViewportCalls[2].width, m_resolutionViewportCalls[2].height, m_resolutionViewportCalls[2].calls,
m_resolutionViewportCalls[3].width, m_resolutionViewportCalls[3].height, m_resolutionViewportCalls[3].calls,
m_resolutionViewportCalls[4].width, m_resolutionViewportCalls[4].height, m_resolutionViewportCalls[4].calls,
m_resolutionViewportCalls[5].width, m_resolutionViewportCalls[5].height, m_resolutionViewportCalls[5].calls,
m_resolutionViewportCalls[6].width, m_resolutionViewportCalls[6].height, m_resolutionViewportCalls[6].calls,
m_resolutionViewportCalls[7].width, m_resolutionViewportCalls[7].height, m_resolutionViewportCalls[7].calls
);
}
if (FPSavg < 254) {
gameStart = true;
}
else {
gameStart = false;
resolutionLookup = false;
}
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;
// 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);
static bool clearOnRelease = false;
if (clearOnRelease && !isRendering) {
clearOnRelease = false;
isRendering = true;
leventClear(&renderingStopEvent);
}
// Calculate overlay bounds
// Cache bounds calculation
static int cachedBaseX = 0;
static int cachedBaseY = 0;
static bool boundsNeedUpdate = true;
// Only recalculate bounds when needed
if (boundsNeedUpdate) {
cachedBaseX = 0;
cachedBaseY = 0;
boundsNeedUpdate = false;
}
const int overlayX = cachedBaseX + frameOffsetX;
const int overlayY = cachedBaseY + frameOffsetY;
// Overlay dimensions based on game state
int overlayWidth, overlayHeight;
overlayWidth = 360-20;
overlayHeight = 200;
// 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) {
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;
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
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
if (hasMoved) {
// Save position when touch drag ends
auto iniData = ult::getParsedDataFromIniFile(configIniPath);
iniData["game_resolutions"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["game_resolutions"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
}
// Reset touch drag state
isDragging = false;
hasMoved = false;
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;
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
const float magnitude = sqrt((float)(activeJoystick.x * activeJoystick.x + activeJoystick.y * activeJoystick.y));
const float normalizedMagnitude = magnitude / 32767.0f;
// Smooth curve for sensitivity
static constexpr float baseSensitivity = 0.00008f;
static constexpr float maxSensitivity = 0.0005f;
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;
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["game_resolutions"]["frame_offset_x"] = std::to_string(frameOffsetX);
iniData["game_resolutions"]["frame_offset_y"] = std::to_string(frameOffsetY);
ult::saveIniFileData(configIniPath, iniData);
isDragging = false;
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);
runOnce = true;
skipOnce = true;
TeslaFPS = 60;
lastSelectedItem = "Game Resolutions";
lastMode = "";
if (skipMain) {
lastMode = "return";
tsl::goBack();
}
else {
tsl::setNextOverlay(filepath.c_str(), "--lastSelectedItem 'Game Resolutions'");
tsl::Overlay::get()->close();
}
return true;
}
}
// Return true if we handled the input (during dragging)
return isDragging;
}
};