diff --git a/CMakeLists.txt b/CMakeLists.txt index c2f0a38..cc7b10c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,21 @@ if (MSVC) target_compile_options(SteamlessDeviceCycle PRIVATE /W4 /WX /utf-8) endif() +# Directional pad checks — the sector maths wraps at 0/360, holds a sector +# against hysteresis, and latches a press's zone, none of which fail loudly +add_executable(TrackpadDirectionProbe + src/probe/TrackpadDirectionProbe.cpp + src/app/TrackpadInput.cpp + src/app/InputInjection.cpp + src/app/EventLog.cpp +) +target_include_directories(TrackpadDirectionProbe PRIVATE src) +target_link_libraries(TrackpadDirectionProbe PRIVATE SteamController) +if (MSVC) + target_compile_options(TrackpadDirectionProbe PRIVATE /W4 /WX /utf-8) + set_target_properties(TrackpadDirectionProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") +endif() + # TrackpadZoneProbe — measures where a thumb lands when it clicks a pad, so the # Directional Pad ring boundary is calibrated rather than guessed. add_executable(TrackpadZoneProbe @@ -164,7 +179,7 @@ add_executable(SteamlessController WIN32 src/app/TrayApp.cpp src/app/ControllerManager.cpp src/app/VirtualController.cpp - src/app/TrackpadMouse.cpp + src/app/TrackpadInput.cpp src/app/KeyInput.cpp src/app/TouchKeyboard.cpp src/app/InputInjection.cpp diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index 4a2f608..ca0e9c9 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -3,7 +3,7 @@ #include "InputInjection.h" #include "ViGEmBusInfo.h" #include "VirtualController.h" -#include "TrackpadMouse.h" +#include "TrackpadInput.h" #include "KeyInput.h" #include "TouchKeyboard.h" #include "steam/SteamController.h" @@ -35,8 +35,8 @@ struct ControllerManager::Slot { std::unique_ptr sc; std::unique_ptr vc; // One per physical pad — they are configured independently. - TrackpadMouse leftPad; - TrackpadMouse rightPad; + TrackpadInput leftPad; + TrackpadInput rightPad; std::thread readThread; std::atomic readRunning{false}; bool gameModeActive = false; @@ -377,6 +377,8 @@ void ControllerManager::ApplyPadSettings(Slot& slot) { slot.rightPad.SetMode(m_profile.rightPad.mode); slot.leftPad.SetScrollDirection(m_profile.leftPad.scrollDir); slot.rightPad.SetScrollDirection(m_profile.rightPad.scrollDir); + slot.leftPad.SetDiagonals(m_profile.leftPad.diagonals); + slot.rightPad.SetDiagonals(m_profile.rightPad.diagonals); // The virtual controller reads pad modes straight off the profile it is // handed every frame, so there is nothing to push to it here. } @@ -404,7 +406,7 @@ void ControllerManager::SetProfile(const ControllerProfile& profile) { // Live update: the ReadLoop reads m_profile on every frame, so paddle and // pad-click changes take effect on the very next report. Movement modes - // live inside the per-slot TrackpadMouse objects, so those are pushed. + // live inside the per-slot TrackpadInput objects, so those are pushed. for (auto& slot : m_slots) ApplyPadSettings(*slot); } @@ -1175,7 +1177,7 @@ void ControllerManager::ReadLoop(Slot* slot) { // Edge-detected so each press sends exactly one down and each release // exactly one up. // - // Pad clicks ride this same path rather than living in TrackpadMouse, + // Pad clicks ride this same path rather than living in TrackpadInput, // which is what makes a pad's click independent of its movement mode — // the click used to be hardcoded to a left button and only fired while // that pad was driving the mouse. diff --git a/src/app/TrackpadInput.cpp b/src/app/TrackpadInput.cpp new file mode 100644 index 0000000..4a7b288 --- /dev/null +++ b/src/app/TrackpadInput.cpp @@ -0,0 +1,282 @@ +#include "TrackpadInput.h" +#include "InputInjection.h" +#include "steam/SteamController.h" +#include +#include +#include +#include + +void TrackpadInput::SetMode(TrackpadMode mode) { + if (mode == m_mode) return; + m_mode = mode; + Reset(); // carried remainders and touch state mean nothing to the new mode +} + +void TrackpadInput::SetDiagonals(DiagonalMode d) { + if (d == m_diagonals) return; + m_diagonals = d; + // The sector numbering means something different either side of this, so a + // held one cannot carry over. Dropping it re-resolves against the new + // geometry on the next frame; the directions it produced are released by + // the same edge detection that handles any other change. + m_sector = -1; + m_dirs = DirNone; +} + +void TrackpadInput::Reset() { + m_touching = false; + m_prevX = 0; + m_prevY = 0; + m_remX = 0.0f; + m_remY = 0.0f; + m_scrollRemX = 0.0f; + m_scrollRemY = 0.0f; + m_haveRef = false; + m_travelSent = 0; + + m_dirs = DirNone; + m_sector = -1; + m_clickHeld = false; + m_clickInCentre = true; +} + +// Tracks whether accepted movement is reaching the cursor. Re-arms whenever the +// cursor does move, so only a genuinely pinned cursor accumulates travel. +void TrackpadInput::NoteMovementSent(long px, bool haveCursor, + long cursorX, long cursorY) { + if (!haveCursor) { + // No cursor position to compare against — GetCursorPos itself fails + // off the input desktop, and InputInjection reports that separately. + m_haveRef = false; + m_travelSent = 0; + return; + } + + if (!m_haveRef || cursorX != m_refCursorX || cursorY != m_refCursorY) { + m_refCursorX = cursorX; + m_refCursorY = cursorY; + m_haveRef = true; + m_travelSent = 0; + } + + m_travelSent += px; + if (m_travelSent >= STUCK_TRAVEL_PX) { + POINT at{ m_refCursorX, m_refCursorY }; + InputInjection::LogCursorNotMoving(m_travelSent, at); + m_travelSent = 0; // re-arm; the log call rate-limits itself + } +} + +void TrackpadInput::UpdatePointer(int dx, int dy) { + // Carry sub-pixel remainders between frames — per-frame deltas scaled by + // sensitivity are often below one pixel, and truncating them each frame + // would discard slow movement entirely. + const float fx = static_cast(dx) * SENSITIVITY + m_remX; + const float fy = static_cast(dy) * SENSITIVITY + m_remY; + const LONG ix = static_cast(fx); + const LONG iy = static_cast(fy); + m_remX = fx - static_cast(ix); + m_remY = fy - static_cast(iy); + if (ix == 0 && iy == 0) return; + + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = MOUSEEVENTF_MOVE; + input.mi.dx = ix; + input.mi.dy = iy; + // Read the cursor before sending: SendInput queues the event rather than + // applying it, so a position read straight after would still be the old one. + POINT before{}; + const bool haveBefore = GetCursorPos(&before) != FALSE; + if (InputInjection::Send(input, "trackpad-move")) { + NoteMovementSent(std::labs(ix) + std::labs(iy), + haveBefore, before.x, before.y); + } +} + +// Wheel events are quantised to WHEEL_DELTA notches, which is much coarser +// than a pad frame's movement — so the same remainder-carry the pointer path +// uses is what makes slow scrolling work at all here. +// +// Takes raw pad deltas (Y growing upward). Natural scrolling means the +// content follows the finger, which is the opposite sense to the wheel's own +// convention that positive is "away from the user" — hence the inversion +// here rather than at the call site. +void TrackpadInput::UpdateScroll(int dx, int dy) { + if (m_scrollDir == ScrollDirection::Natural) { dx = -dx; dy = -dy; } + + const float fy = static_cast(dy) * SCROLL_SENSITIVITY + m_scrollRemY; + const float fx = static_cast(dx) * SCROLL_SENSITIVITY + m_scrollRemX; + const LONG iy = static_cast(fy); + const LONG ix = static_cast(fx); + m_scrollRemY = fy - static_cast(iy); + m_scrollRemX = fx - static_cast(ix); + + if (iy != 0) { + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = MOUSEEVENTF_WHEEL; + input.mi.mouseData = static_cast(iy); + InputInjection::Send(input, "trackpad-scroll"); + } + if (ix != 0) { + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = MOUSEEVENTF_HWHEEL; + input.mi.mouseData = static_cast(ix); + InputInjection::Send(input, "trackpad-hscroll"); + } +} + +// --------------------------------------------------------------------------- +// Directional pad +// --------------------------------------------------------------------------- + +// Shortest angular distance between two bearings, in degrees, always 0..180. +static double AngleDelta(double a, double b) { + const double d = std::fmod(std::fabs(a - b), 360.0); + return d > 180.0 ? 360.0 - d : d; +} + +// Which directions a sector presses. Eight-way diagonals press two, which is +// what a real d-pad does and what makes diagonal movement in a game possible; +// four-way rounds to the nearer single direction. +static uint8_t SectorDirs(int sector, DiagonalMode diagonals) { + if (sector < 0) return DirNone; + if (diagonals == DiagonalMode::FourWay) { + switch (sector) { + case 0: return DirRight; + case 1: return DirUp; + case 2: return DirLeft; + default: return DirDown; + } + } + switch (sector) { + case 0: return DirRight; + case 1: return DirUp | DirRight; + case 2: return DirUp; + case 3: return DirUp | DirLeft; + case 4: return DirLeft; + case 5: return DirDown | DirLeft; + case 6: return DirDown; + default: return DirDown | DirRight; + } +} + +int TrackpadInput::ResolveSector(double angleDeg) const { + const bool four = m_diagonals == DiagonalMode::FourWay; + const int count = four ? 4 : 8; + const double step = four ? 90.0 : 45.0; + const double half = step / 2.0; + + // Hold the sector already being pressed until the thumb has cleared its + // boundary by the hysteresis margin, so a thumb resting on a boundary + // stays where it is instead of alternating. + if (m_sector >= 0 + && AngleDelta(angleDeg, static_cast(m_sector) * step) + <= half + SECTOR_HYSTERESIS_DEG) + return m_sector; + + int s = static_cast(std::floor((angleDeg + half) / step)) % count; + if (s < 0) s += count; + return s; +} + +// Directions come from the click rather than from touch, so that resting a +// thumb on the pad presses nothing, and so the pad's three signals stay +// disjoint: touch is not zoned, a click in the middle is the click binding, +// and a click out in the ring is a direction. +void TrackpadInput::UpdateDirections(bool clicked, int16_t x, int16_t y) { + if (!clicked) { + m_clickHeld = false; + m_sector = -1; + m_dirs = DirNone; + return; + } + + const double r = std::hypot(static_cast(x), static_cast(y)); + + if (!m_clickHeld) { + // Rising edge. Decide the zone once, here, from the position at the + // click itself — measurement says the centroid moves ~120 units in the + // frame before a click and ~350 across three, against a boundary of + // 12000 and a gap of nearly 12000 between deliberate centre and + // deliberate direction presses. Looking back for a cleaner position + // would be machinery for a correction two orders of magnitude smaller + // than the thing it corrects. + m_clickHeld = true; + m_clickInCentre = r < kPadRingRadius; + m_sector = -1; + } + + if (m_clickInCentre) { + m_dirs = DirNone; // this press is the click binding, not a direction + return; + } + + // A ring press keeps following the thumb around the ring, which is what + // lets a roll from up to right register as both in turn and what gives the + // direction-change haptic something to fire on. It stops re-resolving once + // the thumb slides inside the boundary: the zone is latched, so this press + // can never become a centre click, and the angle gets noisy near the + // middle where a small wobble spans whole sectors. + if (r >= kPadRingRadius) { + double angle = std::atan2(static_cast(y), static_cast(x)) + * 180.0 / 3.14159265358979323846; + if (angle < 0.0) angle += 360.0; + m_sector = ResolveSector(angle); + } + m_dirs = SectorDirs(m_sector, m_diagonals); +} + +void TrackpadInput::Update(const uint8_t* buf, size_t n) { + // Only three modes read the pad's position at all. A pad set to None or + // feeding the DS4 touchpad has nothing for this class to work out, and a + // single button is the click bit alone, which ControllerManager reads for + // itself alongside every other binding. + const bool wantsMovement = m_mode == TrackpadMode::MousePointer + || m_mode == TrackpadMode::ScrollWheel; + const bool wantsDirections = m_mode == TrackpadMode::DirectionalPad; + if (n < 30 || (!wantsMovement && !wantsDirections)) return; + + const uint8_t b2 = buf[4]; + const uint8_t b3 = buf[5]; + + const bool touching = m_isLeftPad + ? (b3 & SteamController::BTN_TP_LT) != 0 + : (b2 & SteamController::BTN_TP_RT) != 0; + + int16_t x = 0, y = 0; + if (m_isLeftPad) { + memcpy(&x, buf + 18, 2); + memcpy(&y, buf + 20, 2); + } else { + memcpy(&x, buf + 24, 2); + memcpy(&y, buf + 26, 2); + } + + if (wantsDirections) { + const bool clicked = m_isLeftPad + ? (b3 & SteamController::BTN_TP_LT_CLICK) != 0 + : (b2 & SteamController::BTN_TP_RT_CLICK) != 0; + UpdateDirections(clicked, x, y); + return; + } + + if (touching && m_touching) { + const int dxRaw = static_cast(x - m_prevX); + const int dyRaw = static_cast(y - m_prevY); + if (dxRaw != 0 || dyRaw != 0) { + if (m_mode == TrackpadMode::MousePointer) { + // Pad Y grows upward, screen Y grows downward. + UpdatePointer(dxRaw, -dyRaw); + } else { + // Raw deltas — UpdateScroll owns the direction convention. + UpdateScroll(dxRaw, dyRaw); + } + } + } + + if (touching) { m_prevX = x; m_prevY = y; } + m_touching = touching; +} diff --git a/src/app/TrackpadInput.h b/src/app/TrackpadInput.h new file mode 100644 index 0000000..6b77255 --- /dev/null +++ b/src/app/TrackpadInput.h @@ -0,0 +1,112 @@ +#pragma once +#include +#include +#include "TrackpadConfig.h" + +// What ONE physical trackpad's finger position means. One instance per pad, so +// the two pads can be configured independently — this used to be a single +// instance switched between pads by a bool, which structurally could not +// represent "both pads doing something". +// +// Two jobs, both of them "turn a position into something". The pointer and +// scroll modes drive the desktop directly from here. Directional pad mode +// resolves nothing itself: it works out which direction the thumb is pressing +// and hands that out through Directions(), because a direction has to reach +// both the virtual gamepad and SendInput and neither of those belongs here. +// +// No binding is dispatched from this class. A pad's click, touch and +// directions are all BackButtonBindings, dispatched by ControllerManager +// alongside the back paddles, which is what lets them be the same kind of +// thing as a paddle whatever this pad's mode is. +class TrackpadInput { +public: + // Which physical pad to read. Set once when the slot is created. + void SetPad(bool isLeftPad) { m_isLeftPad = isLeftPad; } + void SetMode(TrackpadMode mode); + void SetScrollDirection(ScrollDirection dir) { m_scrollDir = dir; } + void SetDiagonals(DiagonalMode d); + + void Update(const uint8_t* buf, size_t n); + void Reset(); + + // Which directions this pad is pressing right now, as PadDir bits. Always + // empty unless the pad is a directional pad with its click held. + uint8_t Directions() const { return m_dirs; } + + // Where the click being held landed: inside the centre circle, where the + // press is the pad's click binding, or out in the ring, where it is a + // direction instead. True for any pad that is not a directional pad, so a + // caller can gate the click binding on this unconditionally. + // + // Meaningless while no click is held, which costs nothing — every caller + // is already testing the click bit. + bool ClickInCentre() const { return m_clickInCentre; } + +private: + void UpdatePointer(int dx, int dy); + void UpdateScroll(int dx, int dy); + void NoteMovementSent(long px, bool haveCursor, long cursorX, long cursorY); + // Resolves the eight- or four-way sector a press at this angle belongs to, + // holding the current sector until the thumb clears its boundary by the + // hysteresis margin. Reads m_sector; does not write it. + int ResolveSector(double angleDeg) const; + void UpdateDirections(bool clicked, int16_t x, int16_t y); + + bool m_isLeftPad = false; + TrackpadMode m_mode = TrackpadMode::None; + ScrollDirection m_scrollDir = ScrollDirection::Natural; + DiagonalMode m_diagonals = DiagonalMode::EightWay; + + bool m_touching = false; + int16_t m_prevX = 0; + int16_t m_prevY = 0; + float m_remX = 0.0f; // sub-pixel movement carry + float m_remY = 0.0f; + float m_scrollRemX = 0.0f; // sub-detent scroll carry + float m_scrollRemY = 0.0f; + + // Movement Windows accepted, checked against the cursor actually moving. + // A refused SendInput reports itself; motion that is accepted and then + // discarded — by a cursor clip a game left behind, or a low-level hook + // filtering injected input — looks identical to the user and otherwise + // leaves no trace at all. Records where the cursor was and how much travel + // has been sent since; when the travel adds up and the position has not + // changed, that is worth a log line. + long m_refCursorX = 0; + long m_refCursorY = 0; + bool m_haveRef = false; + long m_travelSent = 0; + + // Enough sent movement that a still cursor cannot be a coincidence. + static constexpr long STUCK_TRAVEL_PX = 120; + + static constexpr float SENSITIVITY = 0.01125f; + // Chosen so a full swipe across the pad is a few notches rather than a + // page-length fling: each pad axis saturates at +/-32767, and one wheel + // detent is WHEEL_DELTA (120). + static constexpr float SCROLL_SENSITIVITY = 0.02f; + + // ---- Directional pad ---- + + // The directions currently pressed, and the sector they came from. The + // sector is kept separately because hysteresis needs to know which one is + // being held, which the bitmask cannot say: up-left and left-up are the + // same two bits. + uint8_t m_dirs = DirNone; + int m_sector = -1; // -1 = none held + + // A click is being held, and where it landed. The zone is decided once, on + // the press, and never revisited: sliding from the ring into the centre + // mid-press would otherwise release a direction and fire the click + // binding, which is one physical press meaning two different things — the + // exact collision the zone split exists to prevent. + bool m_clickHeld = false; + bool m_clickInCentre = true; + + // How far past a sector boundary the thumb has to travel before the + // direction changes. Without it a thumb parked on a boundary alternates + // between two directions at the report rate, and since directions are + // edge-dispatched that is a stream of key down/up pairs rather than a + // cosmetic flicker. + static constexpr double SECTOR_HYSTERESIS_DEG = 8.0; +}; diff --git a/src/app/TrackpadMouse.cpp b/src/app/TrackpadMouse.cpp deleted file mode 100644 index 6fcd709..0000000 --- a/src/app/TrackpadMouse.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "TrackpadMouse.h" -#include "InputInjection.h" -#include "steam/SteamController.h" -#include -#include -#include -#include - -void TrackpadMouse::SetMode(TrackpadMode mode) { - if (mode == m_mode) return; - m_mode = mode; - Reset(); // carried remainders and touch state mean nothing to the new mode -} - -void TrackpadMouse::Reset() { - m_touching = false; - m_prevX = 0; - m_prevY = 0; - m_remX = 0.0f; - m_remY = 0.0f; - m_scrollRemX = 0.0f; - m_scrollRemY = 0.0f; - m_haveRef = false; - m_travelSent = 0; -} - -// Tracks whether accepted movement is reaching the cursor. Re-arms whenever the -// cursor does move, so only a genuinely pinned cursor accumulates travel. -void TrackpadMouse::NoteMovementSent(long px, bool haveCursor, - long cursorX, long cursorY) { - if (!haveCursor) { - // No cursor position to compare against — GetCursorPos itself fails - // off the input desktop, and InputInjection reports that separately. - m_haveRef = false; - m_travelSent = 0; - return; - } - - if (!m_haveRef || cursorX != m_refCursorX || cursorY != m_refCursorY) { - m_refCursorX = cursorX; - m_refCursorY = cursorY; - m_haveRef = true; - m_travelSent = 0; - } - - m_travelSent += px; - if (m_travelSent >= STUCK_TRAVEL_PX) { - POINT at{ m_refCursorX, m_refCursorY }; - InputInjection::LogCursorNotMoving(m_travelSent, at); - m_travelSent = 0; // re-arm; the log call rate-limits itself - } -} - -void TrackpadMouse::UpdatePointer(int dx, int dy) { - // Carry sub-pixel remainders between frames — per-frame deltas scaled by - // sensitivity are often below one pixel, and truncating them each frame - // would discard slow movement entirely. - const float fx = static_cast(dx) * SENSITIVITY + m_remX; - const float fy = static_cast(dy) * SENSITIVITY + m_remY; - const LONG ix = static_cast(fx); - const LONG iy = static_cast(fy); - m_remX = fx - static_cast(ix); - m_remY = fy - static_cast(iy); - if (ix == 0 && iy == 0) return; - - INPUT input{}; - input.type = INPUT_MOUSE; - input.mi.dwFlags = MOUSEEVENTF_MOVE; - input.mi.dx = ix; - input.mi.dy = iy; - // Read the cursor before sending: SendInput queues the event rather than - // applying it, so a position read straight after would still be the old one. - POINT before{}; - const bool haveBefore = GetCursorPos(&before) != FALSE; - if (InputInjection::Send(input, "trackpad-move")) { - NoteMovementSent(std::labs(ix) + std::labs(iy), - haveBefore, before.x, before.y); - } -} - -// Wheel events are quantised to WHEEL_DELTA notches, which is much coarser -// than a pad frame's movement — so the same remainder-carry the pointer path -// uses is what makes slow scrolling work at all here. -// -// Takes raw pad deltas (Y growing upward). Natural scrolling means the -// content follows the finger, which is the opposite sense to the wheel's own -// convention that positive is "away from the user" — hence the inversion -// here rather than at the call site. -void TrackpadMouse::UpdateScroll(int dx, int dy) { - if (m_scrollDir == ScrollDirection::Natural) { dx = -dx; dy = -dy; } - - const float fy = static_cast(dy) * SCROLL_SENSITIVITY + m_scrollRemY; - const float fx = static_cast(dx) * SCROLL_SENSITIVITY + m_scrollRemX; - const LONG iy = static_cast(fy); - const LONG ix = static_cast(fx); - m_scrollRemY = fy - static_cast(iy); - m_scrollRemX = fx - static_cast(ix); - - if (iy != 0) { - INPUT input{}; - input.type = INPUT_MOUSE; - input.mi.dwFlags = MOUSEEVENTF_WHEEL; - input.mi.mouseData = static_cast(iy); - InputInjection::Send(input, "trackpad-scroll"); - } - if (ix != 0) { - INPUT input{}; - input.type = INPUT_MOUSE; - input.mi.dwFlags = MOUSEEVENTF_HWHEEL; - input.mi.mouseData = static_cast(ix); - InputInjection::Send(input, "trackpad-hscroll"); - } -} - -void TrackpadMouse::Update(const uint8_t* buf, size_t n) { - // Pad clicks are handled by ControllerManager, alongside every other - // binding — this is movement only. Only the two desktop-driving modes - // produce anything here; None and DS4Touchpad are both "not the desktop's". - if (n < 30 - || (m_mode != TrackpadMode::MousePointer && m_mode != TrackpadMode::ScrollWheel)) - return; - - const uint8_t b2 = buf[4]; - const uint8_t b3 = buf[5]; - - const bool touching = m_isLeftPad - ? (b3 & SteamController::BTN_TP_LT) != 0 - : (b2 & SteamController::BTN_TP_RT) != 0; - - int16_t x = 0, y = 0; - if (m_isLeftPad) { - memcpy(&x, buf + 18, 2); - memcpy(&y, buf + 20, 2); - } else { - memcpy(&x, buf + 24, 2); - memcpy(&y, buf + 26, 2); - } - - if (touching && m_touching) { - const int dxRaw = static_cast(x - m_prevX); - const int dyRaw = static_cast(y - m_prevY); - if (dxRaw != 0 || dyRaw != 0) { - if (m_mode == TrackpadMode::MousePointer) { - // Pad Y grows upward, screen Y grows downward. - UpdatePointer(dxRaw, -dyRaw); - } else { - // Raw deltas — UpdateScroll owns the direction convention. - UpdateScroll(dxRaw, dyRaw); - } - } - } - - if (touching) { m_prevX = x; m_prevY = y; } - m_touching = touching; -} diff --git a/src/app/TrackpadMouse.h b/src/app/TrackpadMouse.h deleted file mode 100644 index 4783efb..0000000 --- a/src/app/TrackpadMouse.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once -#include -#include -#include "TrackpadConfig.h" - -// Desktop input driven by ONE physical trackpad. One instance per pad, so the -// two pads can be configured independently — this used to be a single instance -// switched between pads by a bool, which structurally could not represent -// "both pads doing something". -// -// Only movement lives here. The pad's click is a binding like any other and is -// dispatched by ControllerManager alongside the back paddles, so it works the -// same whatever this pad's movement mode is. -class TrackpadMouse { -public: - // Which physical pad to read. Set once when the slot is created. - void SetPad(bool isLeftPad) { m_isLeftPad = isLeftPad; } - void SetMode(TrackpadMode mode); - void SetScrollDirection(ScrollDirection dir) { m_scrollDir = dir; } - - void Update(const uint8_t* buf, size_t n); - void Reset(); - -private: - void UpdatePointer(int dx, int dy); - void UpdateScroll(int dx, int dy); - void NoteMovementSent(long px, bool haveCursor, long cursorX, long cursorY); - - bool m_isLeftPad = false; - TrackpadMode m_mode = TrackpadMode::None; - ScrollDirection m_scrollDir = ScrollDirection::Natural; - - bool m_touching = false; - int16_t m_prevX = 0; - int16_t m_prevY = 0; - float m_remX = 0.0f; // sub-pixel movement carry - float m_remY = 0.0f; - float m_scrollRemX = 0.0f; // sub-detent scroll carry - float m_scrollRemY = 0.0f; - - // Movement Windows accepted, checked against the cursor actually moving. - // A refused SendInput reports itself; motion that is accepted and then - // discarded — by a cursor clip a game left behind, or a low-level hook - // filtering injected input — looks identical to the user and otherwise - // leaves no trace at all. Records where the cursor was and how much travel - // has been sent since; when the travel adds up and the position has not - // changed, that is worth a log line. - long m_refCursorX = 0; - long m_refCursorY = 0; - bool m_haveRef = false; - long m_travelSent = 0; - - // Enough sent movement that a still cursor cannot be a coincidence. - static constexpr long STUCK_TRAVEL_PX = 120; - - static constexpr float SENSITIVITY = 0.01125f; - // Chosen so a full swipe across the pad is a few notches rather than a - // page-length fling: the pad's axes span roughly +/-32000, and one wheel - // detent is WHEEL_DELTA (120). - static constexpr float SCROLL_SENSITIVITY = 0.02f; -}; diff --git a/src/probe/TrackpadDirectionProbe.cpp b/src/probe/TrackpadDirectionProbe.cpp new file mode 100644 index 0000000..261cb48 --- /dev/null +++ b/src/probe/TrackpadDirectionProbe.cpp @@ -0,0 +1,292 @@ +// Console diagnostic: drives TrackpadInput with synthesized state reports and +// checks that a directional pad resolves the direction a thumb is actually +// pressing. +// +// Worth checking rather than eyeballing, because three separate things here +// are quietly easy to get wrong and none of them fail loudly: +// +// - Angles wrap. The RIGHT sector straddles 0/360, and a modulo on a +// negative intermediate lands in the wrong sector rather than erroring. +// - Hysteresis holds the current sector. Held too eagerly it never lets go +// and the pad sticks in one direction; held too weakly it does nothing and +// a thumb on a boundary alternates at the report rate, which — because +// directions are edge-dispatched — is a stream of key down/up pairs. +// - The zone is latched at the press and must never be revisited, or sliding +// between centre and ring mid-press makes one physical click mean both a +// direction and the click binding. +// +// Feeding whole reports rather than calling the resolver directly also checks +// the byte offsets and button bits, which is where a left-pad reader ends up +// reading right-pad coordinates. + +#include "app/TrackpadInput.h" +#include "steam/SteamController.h" +#include +#include +#include + +namespace { + +int g_failures = 0; + +void Check(bool ok, const char* what) { + if (!ok) ++g_failures; + printf(" [%s] %s\n", ok ? "ok" : "FAIL", what); +} + +std::string DirsName(uint8_t d) { + if (d == DirNone) return "none"; + std::string s; + if (d & DirUp) s += "Up "; + if (d & DirDown) s += "Down "; + if (d & DirLeft) s += "Left "; + if (d & DirRight) s += "Right "; + s.pop_back(); + return s; +} + +// A state report carrying one pad's position and click state. Everything the +// resolver reads lives in the first 30 bytes; the rest stays zero. +struct Report { + uint8_t buf[64] = {}; + + Report(bool leftPad, bool touching, bool clicked, int16_t x, int16_t y) { + buf[0] = SteamController::REPORT_STATE; + if (leftPad) { + if (touching) buf[5] |= SteamController::BTN_TP_LT; + if (clicked) buf[5] |= SteamController::BTN_TP_LT_CLICK; + std::memcpy(buf + 18, &x, 2); + std::memcpy(buf + 20, &y, 2); + } else { + if (touching) buf[4] |= SteamController::BTN_TP_RT; + if (clicked) buf[4] |= SteamController::BTN_TP_RT_CLICK; + std::memcpy(buf + 24, &x, 2); + std::memcpy(buf + 26, &y, 2); + } + } +}; + +// Feeds one frame and returns what the pad resolved. +uint8_t Feed(TrackpadInput& pad, bool leftPad, bool clicked, int16_t x, int16_t y) { + Report r(leftPad, /*touching=*/true, clicked, x, y); + pad.Update(r.buf, 30); + return pad.Directions(); +} + +TrackpadInput MakePad(bool leftPad, DiagonalMode diagonals = DiagonalMode::EightWay) { + TrackpadInput pad; + pad.SetPad(leftPad); + pad.SetMode(TrackpadMode::DirectionalPad); + pad.SetDiagonals(diagonals); + return pad; +} + +// A point at the given bearing, far enough out to be a ring press. +void AtAngle(double deg, int radius, int16_t& x, int16_t& y) { + const double rad = deg * 3.14159265358979323846 / 180.0; + x = static_cast(std::cos(rad) * radius); + y = static_cast(std::sin(rad) * radius); +} + +void CheckAngle(bool leftPad, double deg, uint8_t want) { + TrackpadInput pad = MakePad(leftPad); + int16_t x = 0, y = 0; + AtAngle(deg, 25000, x, y); + const uint8_t got = Feed(pad, leftPad, /*clicked=*/true, x, y); + char label[96]; + snprintf(label, sizeof(label), "%5.1f deg -> %-16s (wanted %s)", + deg, DirsName(got).c_str(), DirsName(want).c_str()); + Check(got == want, label); +} + +} // namespace + +int main() { + printf("Eight-way sectors, right pad\n"); + CheckAngle(false, 0.0, DirRight); + CheckAngle(false, 45.0, DirUp | DirRight); + CheckAngle(false, 90.0, DirUp); + CheckAngle(false, 135.0, DirUp | DirLeft); + CheckAngle(false, 180.0, DirLeft); + CheckAngle(false, 225.0, DirDown | DirLeft); + CheckAngle(false, 270.0, DirDown); + CheckAngle(false, 315.0, DirDown | DirRight); + // The RIGHT sector straddles the wrap, so both sides of it have to land in + // the same place. + CheckAngle(false, 20.0, DirRight); + CheckAngle(false, 340.0, DirRight); + CheckAngle(false, 359.9, DirRight); + + printf("\nThe left pad reads its own coordinates\n"); + CheckAngle(true, 90.0, DirUp); + CheckAngle(true, 180.0, DirLeft); + { + // A report carrying only right-pad data must leave a left pad idle — + // this is the check that a swapped offset would fail. + TrackpadInput pad = MakePad(true); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Report r(/*leftPad=*/false, true, true, x, y); + pad.Update(r.buf, 30); + Check(pad.Directions() == DirNone, "left pad ignores a right-pad press"); + } + + printf("\nFour-way rounds diagonals to one direction\n"); + { + // The boundary sits at 45 degrees, so a press either side of it rounds + // to the nearer cardinal and never presses two at once. + TrackpadInput pad = MakePad(false, DiagonalMode::FourWay); + int16_t x = 0, y = 0; + AtAngle(40.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirRight, "40 deg rounds to Right alone"); + } + { + TrackpadInput pad = MakePad(false, DiagonalMode::FourWay); + int16_t x = 0, y = 0; + AtAngle(50.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, "50 deg rounds to Up alone"); + } + { + // The same bearing that is a diagonal in eight-way. + TrackpadInput eight = MakePad(false); + TrackpadInput four = MakePad(false, DiagonalMode::FourWay); + int16_t x = 0, y = 0; + AtAngle(46.0, 25000, x, y); + Check(Feed(eight, false, true, x, y) == (DirUp | DirRight), + "46 deg is Up+Right in eight-way"); + Check(Feed(four, false, true, x, y) == DirUp, + "46 deg is Up alone in four-way"); + } + + printf("\nThe centre is the click binding, not a direction\n"); + { + TrackpadInput pad = MakePad(false); + Check(Feed(pad, false, true, 0, 0) == DirNone, "dead centre presses nothing"); + Check(pad.ClickInCentre(), "and reports itself as a centre click"); + } + { + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, kPadRingRadius - 500, x, y); + Check(Feed(pad, false, true, x, y) == DirNone, "just inside the ring is still centre"); + } + { + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, kPadRingRadius + 500, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, "just outside the ring is a direction"); + Check(!pad.ClickInCentre(), "and reports itself as a ring click"); + } + + printf("\nTouch alone presses nothing — directions come from the click\n"); + { + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Check(Feed(pad, false, /*clicked=*/false, x, y) == DirNone, + "a thumb resting out in the ring presses nothing"); + } + + printf("\nThe zone latches at the press and is never revisited\n"); + { + // Press in the ring, then slide into the centre while still held. The + // direction has to survive: converting to a centre click here would + // make one physical press mean two things. + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, "ring press gives Up"); + Check(Feed(pad, false, true, 0, 0) == DirUp, "sliding to dead centre keeps Up"); + Check(!pad.ClickInCentre(), "and it is still a ring click"); + } + { + // The mirror: press in the centre, slide out to the rim while held. + TrackpadInput pad = MakePad(false); + Check(Feed(pad, false, true, 0, 0) == DirNone, "centre press gives nothing"); + int16_t x = 0, y = 0; + AtAngle(90.0, 30000, x, y); + Check(Feed(pad, false, true, x, y) == DirNone, "sliding to the rim still gives nothing"); + Check(pad.ClickInCentre(), "and it is still a centre click"); + } + + printf("\nReleasing clears everything\n"); + { + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Feed(pad, false, true, x, y); + Check(Feed(pad, false, /*clicked=*/false, x, y) == DirNone, "release drops the direction"); + // A fresh press re-decides the zone, so the pad is usable again. + Check(Feed(pad, false, true, 0, 0) == DirNone, "next press re-reads the zone as centre"); + Check(pad.ClickInCentre(), "and says so"); + } + + printf("\nHysteresis: a thumb on a boundary does not alternate\n"); + { + // The Up/Up-Right boundary sits at 67.5 degrees. Hold Up, then creep + // across it — the direction must not change until the thumb has + // cleared the boundary by the margin. + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, "starts on Up"); + + AtAngle(66.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, + "1.5 deg past the boundary still Up"); + AtAngle(60.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirUp, + "7.5 deg past the boundary still Up"); + AtAngle(56.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == (DirUp | DirRight), + "11.5 deg past the boundary finally switches"); + // Having switched, it must be just as reluctant to switch back, or the + // thumb sitting where it is would flip on the next frame. + AtAngle(60.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == (DirUp | DirRight), + "and does not immediately switch back"); + } + { + // Hysteresis must not become a trap: a deliberate move to the opposite + // side of the pad has to register. + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Feed(pad, false, true, x, y); + AtAngle(270.0, 25000, x, y); + Check(Feed(pad, false, true, x, y) == DirDown, "Up to Down still registers"); + } + + printf("\nOther modes resolve no directions at all\n"); + for (auto mode : { TrackpadMode::None, TrackpadMode::MousePointer, + TrackpadMode::ScrollWheel, TrackpadMode::DS4Touchpad, + TrackpadMode::SingleButton }) { + TrackpadInput pad; + pad.SetPad(false); + pad.SetMode(mode); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Report r(false, true, true, x, y); + pad.Update(r.buf, 30); + char label[80]; + snprintf(label, sizeof(label), "mode %-8s presses no direction", TrackpadModeId(mode)); + Check(pad.Directions() == DirNone, label); + // Every non-directional pad has to answer "centre", so callers can gate + // the click binding on it without asking what mode they are in. + snprintf(label, sizeof(label), "mode %-8s reports a centre click", TrackpadModeId(mode)); + Check(pad.ClickInCentre(), label); + } + + printf("\nA short report is ignored rather than read past\n"); + { + TrackpadInput pad = MakePad(false); + int16_t x = 0, y = 0; + AtAngle(90.0, 25000, x, y); + Report r(false, true, true, x, y); + pad.Update(r.buf, 20); + Check(pad.Directions() == DirNone, "29 bytes or fewer resolves nothing"); + } + + printf("\n%s (%d failure(s))\n", g_failures ? "FAILED" : "All checks passed", g_failures); + return g_failures ? 1 : 0; +}