/******************************************************************************** * File: haptics.cpp * Author: ppkantorski * Description: * This source file provides implementations for the functions declared in * haptics.hpp. These functions manage haptic feedback for the Ultrahand Overlay * using libnx’s vibration interfaces. It includes routines for initializing * rumble devices, sending vibration patterns, and handling single or double * click feedback with timing control. Thread safety is maintained through * atomic operations and synchronization mechanisms. * * For the latest updates and contributions, visit the project's GitHub repository. * (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay) * * Note: Please be aware that this notice cannot be altered or removed. It is a part * of the project's documentation and must remain intact. * * Licensed under both GPLv2 and CC-BY-4.0 * Copyright (c) 2025 ppkantorski ********************************************************************************/ #include "haptics.hpp" namespace ult { // ===== Internal state (private to this file) ===== //bool rumbleInitialized = false; static HidVibrationDeviceHandle vibHandheld; static HidVibrationDeviceHandle vibPlayer1Left; static HidVibrationDeviceHandle vibPlayer1Right; static u64 rumbleStartTick = 0; static u64 doubleClickTick = 0; static u8 doubleClickPulse = 0; // ===== Shared flags (accessible globally) ===== std::atomic rumbleActive{false}; std::atomic doubleClickActive{false}; // ===== Constants ===== static constexpr u64 RUMBLE_DURATION_NS = 30'000'000ULL; static constexpr u64 DOUBLE_CLICK_PULSE_DURATION_NS = 30'000'000ULL; static constexpr u64 DOUBLE_CLICK_GAP_NS = 100'000'000ULL; static constexpr HidVibrationValue clickDocked = { .amp_low = 0.20f, .freq_low = 100.0f, .amp_high = 0.80f, .freq_high = 300.0f }; static constexpr HidVibrationValue clickHandheld = { .amp_low = 0.25f, .freq_low = 100.0f, .amp_high = 1.0f, .freq_high = 300.0f }; static constexpr HidVibrationValue vibrationStop{0}; // ===== Internal helpers ===== static void initController(HidNpadIdType npad, HidVibrationDeviceHandle* handles, int count) { const u32 styleMask = hidGetNpadStyleSet(npad); if (styleMask) hidInitializeVibrationDevices(handles, count, npad, static_cast(styleMask)); } static void sendVibration(const HidVibrationValue* value) { if (hidGetNpadStyleSet(HidNpadIdType_Handheld)) hidSendVibrationValue(vibHandheld, value); if (hidGetNpadStyleSet(HidNpadIdType_No1)) { hidSendVibrationValue(vibPlayer1Left, value); hidSendVibrationValue(vibPlayer1Right, value); } } // ===== Public API ===== void initRumble() { //if (rumbleInitialized) return; // Try to initialize whatever is available // Don't check if controllers exist - let initController handle it initController(HidNpadIdType_Handheld, &vibHandheld, 1); HidVibrationDeviceHandle handles[2]; initController(HidNpadIdType_No1, handles, 2); vibPlayer1Left = handles[0]; vibPlayer1Right = handles[1]; // Only mark as initialized if at least one controller was found hidGetNpadStyleSet(HidNpadIdType_Handheld); hidGetNpadStyleSet(HidNpadIdType_No1); //rumbleInitialized = (handheldStyle || player1Style); // If neither exist, stay uninitialized so we retry later } //void deinitRumble() { // rumbleInitialized = false; //} void checkAndReinitRumble() { static u32 lastHandheldStyle = 0; static u32 lastPlayer1Style = 0; const u32 currentHandheldStyle = hidGetNpadStyleSet(HidNpadIdType_Handheld); const u32 currentPlayer1Style = hidGetNpadStyleSet(HidNpadIdType_No1); // If not initialized but controllers exist, try to init // This handles the boot race condition where HID reports controllers // but vibration subsystem isn't ready yet //if (!rumbleInitialized && (currentHandheldStyle || currentPlayer1Style)) { // initRumble(); //} // Reinit if controller configuration changed if (currentHandheldStyle != lastHandheldStyle || currentPlayer1Style != lastPlayer1Style) { //rumbleInitialized = false; initRumble(); } // Update last style tracking regardless lastHandheldStyle = currentHandheldStyle; lastPlayer1Style = currentPlayer1Style; } void rumbleClick() { //if (!rumbleInitialized) { // initRumble(); // if (!rumbleInitialized) return; //} sendVibration(hidGetNpadStyleSet(HidNpadIdType_Handheld) ? &clickHandheld : &clickDocked); rumbleActive.store(true, std::memory_order_release); rumbleStartTick = armGetSystemTick(); } void rumbleDoubleClick() { //if (!rumbleInitialized) { // initRumble(); // if (!rumbleInitialized) return; //} sendVibration(hidGetNpadStyleSet(HidNpadIdType_Handheld) ? &clickHandheld : &clickDocked); doubleClickActive.store(true, std::memory_order_release); doubleClickPulse = 1; doubleClickTick = armGetSystemTick(); } void processRumbleStop(u64 nowNs) { if (rumbleActive.load(std::memory_order_acquire) && nowNs - armTicksToNs(rumbleStartTick) >= RUMBLE_DURATION_NS) { sendVibration(&vibrationStop); rumbleActive.store(false, std::memory_order_release); } } void processRumbleDoubleClick(u64 nowNs) { if (!doubleClickActive.load(std::memory_order_acquire)) return; const u64 elapsed = nowNs - armTicksToNs(doubleClickTick); switch (doubleClickPulse) { case 1: if (elapsed >= DOUBLE_CLICK_PULSE_DURATION_NS) { sendVibration(&vibrationStop); doubleClickPulse = 2; doubleClickTick = armGetSystemTick(); } break; case 2: if (elapsed >= DOUBLE_CLICK_GAP_NS) { sendVibration(hidGetNpadStyleSet(HidNpadIdType_Handheld) ? &clickHandheld : &clickDocked); doubleClickPulse = 3; doubleClickTick = armGetSystemTick(); } break; case 3: if (elapsed >= DOUBLE_CLICK_PULSE_DURATION_NS) { sendVibration(&vibrationStop); doubleClickActive.store(false, std::memory_order_release); doubleClickPulse = 0; } break; } } void rumbleDoubleClickStandalone() { sendVibration(hidGetNpadStyleSet(HidNpadIdType_Handheld) ? &clickHandheld : &clickDocked); svcSleepThread(DOUBLE_CLICK_PULSE_DURATION_NS); sendVibration(&vibrationStop); svcSleepThread(DOUBLE_CLICK_GAP_NS); sendVibration(hidGetNpadStyleSet(HidNpadIdType_Handheld) ? &clickHandheld : &clickDocked); svcSleepThread(DOUBLE_CLICK_PULSE_DURATION_NS); sendVibration(&vibrationStop); } }