202 lines
7.4 KiB
C++
202 lines
7.4 KiB
C++
/********************************************************************************
|
||
* 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<bool> rumbleActive{false};
|
||
std::atomic<bool> 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<HidNpadStyleTag>(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);
|
||
}
|
||
} |