From ddb91176fe35ffb0ee4a9f6da9d8e1a32b921f63 Mon Sep 17 00:00:00 2001 From: Dylan Deverill Date: Mon, 22 Jun 2026 00:02:08 -0700 Subject: [PATCH] Add distance-based movement haptics and click release haptic for trackpad Replaces the per-frame movement buzz with a scroll-wheel-style tick that fires once per 5000 raw units of travel. Adds a matching haptic on click release to mirror Steam's press/release feel. Reseeds the movement baseline on click release so stale position delta doesn't bleed into a spurious movement tick on the same frame. Co-Authored-By: Claude Sonnet 4.6 --- src/app/ControllerManager.cpp | 90 +++++++++++++++++++++++++++++++---- src/steam/SteamController.cpp | 14 ++++-- src/steam/SteamController.h | 5 +- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index 1d714f1..19b834c 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -23,10 +24,18 @@ struct ControllerManager::Slot { bool gameModeActive = false; // Haptic edge-detection state — fire a haptic on first touch/click each gesture. - bool hapticWasRightTouching = false; - bool hapticWasLeftTouching = false; - bool hapticWasRightClicked = false; - bool hapticWasLeftClicked = false; + bool hapticWasRightTouching = false; + bool hapticWasLeftTouching = false; + bool hapticWasRightClicked = false; + bool hapticWasLeftClicked = false; + + // Previous trackpad positions and accumulated travel for distance-based haptic. + int16_t hapticPrevRightX = 0; + int16_t hapticPrevRightY = 0; + float hapticRightDistAccum = 0.0f; + int16_t hapticPrevLeftX = 0; + int16_t hapticPrevLeftY = 0; + float hapticLeftDistAccum = 0.0f; Slot() = default; Slot(const Slot&) = delete; @@ -304,18 +313,81 @@ void ControllerManager::ReadLoop(Slot* slot) { if (slot->vc) slot->vc->Update(buf, n, m_backConfig, m_backButtonsEnabled); slot->trackpad.Update(buf, n); - // Trackpad haptics — fire once per new touch/click gesture. + // Trackpad haptics. { + static constexpr float TRACKPAD_HAPTIC_TICK_DISTANCE = 5000.0f; + const uint8_t b2 = n > 4 ? buf[4] : 0; const uint8_t b3 = n > 5 ? buf[5] : 0; const bool rt = (b2 & SteamController::BTN_TP_RT) != 0; const bool lt = (b3 & SteamController::BTN_TP_LT) != 0; const bool rc = (b2 & SteamController::BTN_TP_RT_CLICK) != 0; const bool lc = (b3 & SteamController::BTN_TP_LT_CLICK) != 0; - if (rc && !slot->hapticWasRightClicked) slot->sc->PulseTrackpadHaptic(false, true); - if (lc && !slot->hapticWasLeftClicked) slot->sc->PulseTrackpadHaptic(true, true); - if (rt && !slot->hapticWasRightTouching && !rc) slot->sc->PulseTrackpadHaptic(false, false); - if (lt && !slot->hapticWasLeftTouching && !lc) slot->sc->PulseTrackpadHaptic(true, false); + + int16_t rx = 0, ry = 0, lx = 0, ly = 0; + if (n >= 28) { + memcpy(&rx, buf + 24, 2); + memcpy(&ry, buf + 26, 2); + memcpy(&lx, buf + 18, 2); + memcpy(&ly, buf + 20, 2); + } + + // Click haptic — strong pulse on press and release. + // On release, also reseed the movement baseline so distance accumulated + // while clicking doesn't bleed into a spurious movement tick. + if (rc && !slot->hapticWasRightClicked) slot->sc->PulseTrackpadHaptic(false, true); + if (lc && !slot->hapticWasLeftClicked) slot->sc->PulseTrackpadHaptic(true, true); + if (!rc && slot->hapticWasRightClicked) { + slot->sc->PulseTrackpadHaptic(false, true); + slot->hapticPrevRightX = rx; + slot->hapticPrevRightY = ry; + slot->hapticRightDistAccum = 0.0f; + } + if (!lc && slot->hapticWasLeftClicked) { + slot->sc->PulseTrackpadHaptic(true, true); + slot->hapticPrevLeftX = lx; + slot->hapticPrevLeftY = ly; + slot->hapticLeftDistAccum = 0.0f; + } + + // Movement haptic — tick once per TRACKPAD_HAPTIC_TICK_DISTANCE of travel. + if (n >= 28) { + // On first-touch frame, seed positions and reset accumulator. + if (rt && !slot->hapticWasRightTouching) { + slot->hapticPrevRightX = rx; + slot->hapticPrevRightY = ry; + slot->hapticRightDistAccum = 0.0f; + } + if (lt && !slot->hapticWasLeftTouching) { + slot->hapticPrevLeftX = lx; + slot->hapticPrevLeftY = ly; + slot->hapticLeftDistAccum = 0.0f; + } + + if (rt && slot->hapticWasRightTouching && !rc) { + const float dx = static_cast(rx - slot->hapticPrevRightX); + const float dy = static_cast(ry - slot->hapticPrevRightY); + slot->hapticRightDistAccum += std::hypot(dx, dy); + while (slot->hapticRightDistAccum >= TRACKPAD_HAPTIC_TICK_DISTANCE) { + slot->sc->TickTrackpadMovement(false); + slot->hapticRightDistAccum -= TRACKPAD_HAPTIC_TICK_DISTANCE; + } + slot->hapticPrevRightX = rx; + slot->hapticPrevRightY = ry; + } + if (lt && slot->hapticWasLeftTouching && !lc) { + const float dx = static_cast(lx - slot->hapticPrevLeftX); + const float dy = static_cast(ly - slot->hapticPrevLeftY); + slot->hapticLeftDistAccum += std::hypot(dx, dy); + while (slot->hapticLeftDistAccum >= TRACKPAD_HAPTIC_TICK_DISTANCE) { + slot->sc->TickTrackpadMovement(true); + slot->hapticLeftDistAccum -= TRACKPAD_HAPTIC_TICK_DISTANCE; + } + slot->hapticPrevLeftX = lx; + slot->hapticPrevLeftY = ly; + } + } + slot->hapticWasRightTouching = rt; slot->hapticWasLeftTouching = lt; slot->hapticWasRightClicked = rc; diff --git a/src/steam/SteamController.cpp b/src/steam/SteamController.cpp index b0c36ef..de0e36c 100644 --- a/src/steam/SteamController.cpp +++ b/src/steam/SteamController.cpp @@ -46,10 +46,11 @@ static uint16_t UnitToHapticSpeed(double value, uint16_t minimumSpeed) { static constexpr uint8_t HAPTIC_COMMAND_TICK = 1; static constexpr uint8_t HAPTIC_COMMAND_CLICK = 2; -static constexpr uint16_t TRACKPAD_CLICK_PULSE_US = 5000; -static constexpr int8_t TRACKPAD_TOUCH_GAIN_DB = -45; -static constexpr int8_t TRACKPAD_CLICK_COMMAND_GAIN_DB = 9; -static constexpr int16_t TRACKPAD_CLICK_PULSE_GAIN_DB = 40; +static constexpr uint16_t TRACKPAD_CLICK_PULSE_US = 5000; +static constexpr int8_t TRACKPAD_TOUCH_GAIN_DB = -45; +static constexpr int8_t TRACKPAD_MOVE_GAIN_DB = -50; +static constexpr int8_t TRACKPAD_CLICK_COMMAND_GAIN_DB = 9; +static constexpr int16_t TRACKPAD_CLICK_PULSE_GAIN_DB = 40; // --------------------------------------------------------------------------- // Open / Close @@ -270,6 +271,11 @@ void SteamController::PulseTrackpadHaptic(bool left, bool strongClick) { SendTrackpadPulseOutput(side, TRACKPAD_CLICK_PULSE_US, 0, 1, TRACKPAD_CLICK_PULSE_GAIN_DB); } +void SteamController::TickTrackpadMovement(bool left) { + const uint8_t side = left ? 0x01 : 0x02; + SendTrackpadCommandOutput(side, HAPTIC_COMMAND_TICK, TRACKPAD_MOVE_GAIN_DB); +} + void SteamController::ClearTrackpadHaptics() { // Pulse/command reports are one-shots — no persistent state to cancel. // Kept as a lifecycle hook for clean teardown. diff --git a/src/steam/SteamController.h b/src/steam/SteamController.h index 2c32812..0d4ba5e 100644 --- a/src/steam/SteamController.h +++ b/src/steam/SteamController.h @@ -168,9 +168,12 @@ public: void SetRumble(uint8_t largeMotor, uint8_t smallMotor); // Fire a single haptic event on one trackpad. - // strongClick = true → physical-click sensation; false → light movement tick. + // strongClick = true → physical-click sensation; false → light touch-down tick. void PulseTrackpadHaptic(bool left, bool strongClick); + // Fire a very light tick while the thumb moves on the trackpad. + void TickTrackpadMovement(bool left); + // Silence any in-flight trackpad haptic state (lifecycle cleanup hook). void ClearTrackpadHaptics();