diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index ca0e9c9..8e44ec9 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -64,11 +64,21 @@ struct ControllerManager::Slot { // what the press actually sent rather than reading the binding again on // release, so rebinding mid-hold still releases cleanly. // - // Indices 0-3 are the paddles (L4, L5, R4, R5); 4-5 are the left and right - // pad clicks, which are ordinary bindings dispatched down this same path. - static constexpr size_t kBindableCount = 6; + // Indices 0-3 are the paddles (L4, L5, R4, R5); everything after them + // belongs to the trackpads, which are ordinary bindings dispatched down + // this same path. 4-5 are the pad clicks, 6-7 the pad touches, and 8-15 + // the four directions of each pad in turn. + static constexpr size_t kBindableCount = 16; BackButtonBinding paddleHeld[kBindableCount]; + // What each pad's directions were last frame. Unlike every other edge + // dispatched here these are not bits in the report — they are resolved + // from a position, so the previous report cannot be re-read for them and + // the answer has to be carried. Written every frame, including the first, + // so a direction cannot be stranded down across a mode change. + uint8_t prevLeftDirs = DirNone; + uint8_t prevRightDirs = DirNone; + // Auto-repeat for held key bindings. When the next repeat is due, and the // gap to use after that — both captured at press time from the user's // keyboard settings, so the rate cannot shift mid-hold. @@ -972,9 +982,17 @@ void ControllerManager::ReadLoop(Slot* slot) { "dropped before it reaches SendInput", n); } - if (slot->vc) slot->vc->Update(buf, n, m_profile); + // The pads first: a directional pad's directions are resolved here and + // the virtual controller is handed them, so updating it first would + // report last frame's directions — a frame of lag on every press and, + // worse, a direction still held for a frame after release. slot->leftPad.Update(buf, n); slot->rightPad.Update(buf, n); + const PadDigital resolved{ slot->leftPad.Directions(), + slot->rightPad.Directions(), + slot->leftPad.ClickInCentre(), + slot->rightPad.ClickInCentre() }; + if (slot->vc) slot->vc->Update(buf, n, m_profile, resolved); // Trackpad haptics. { @@ -1189,14 +1207,47 @@ void ControllerManager::ReadLoop(Slot* slot) { const BackButtonBinding leftPadClick = m_profile.leftPad.EffectiveClick(); const BackButtonBinding rightPadClick = m_profile.rightPad.EffectiveClick(); - struct PaddleEdge { bool cur; bool prev; const BackButtonBinding& binding; }; + // A directional pad's click is only its click binding when the + // press landed in the middle — out in the ring the same press is a + // direction, and letting both through is the one physical press + // meaning two things that the zone split exists to prevent. The + // zone is latched for the press's whole life, so this cannot flip + // mid-hold and strand a binding down. Every other mode answers + // "centre", so they gate to exactly what they did before. + const bool leftClickInCentre = resolved.leftClickInCentre; + const bool rightClickInCentre = resolved.rightClickInCentre; + + const uint8_t lDirs = resolved.leftDirs, lPrevDirs = slot->prevLeftDirs; + const uint8_t rDirs = resolved.rightDirs, rPrevDirs = slot->prevRightDirs; + const auto& lPad = m_profile.leftPad; + const auto& rPad = m_profile.rightPad; + + // Held by value rather than by reference: half of these bindings + // are returned by value from the Effective* accessors, and a + // reference member would be bound to a temporary. + struct PaddleEdge { bool cur; bool prev; BackButtonBinding binding; }; const PaddleEdge edges[] = { { n>4 && (buf[4]&SteamController::BTN_L4)!=0, (prevBuf[4]&SteamController::BTN_L4)!=0, m_profile.back.l4 }, { n>4 && (buf[4]&SteamController::BTN_L5)!=0, (prevBuf[4]&SteamController::BTN_L5)!=0, m_profile.back.l5 }, { n>2 && (buf[2]&SteamController::BTN_R4)!=0, (prevBuf[2]&SteamController::BTN_R4)!=0, m_profile.back.r4 }, { n>3 && (buf[3]&SteamController::BTN_R5)!=0, (prevBuf[3]&SteamController::BTN_R5)!=0, m_profile.back.r5 }, - { n>5 && (buf[5]&SteamController::BTN_TP_LT_CLICK)!=0, (prevBuf[5]&SteamController::BTN_TP_LT_CLICK)!=0, leftPadClick }, - { n>4 && (buf[4]&SteamController::BTN_TP_RT_CLICK)!=0, (prevBuf[4]&SteamController::BTN_TP_RT_CLICK)!=0, rightPadClick }, + { n>5 && (buf[5]&SteamController::BTN_TP_LT_CLICK)!=0 && leftClickInCentre, (prevBuf[5]&SteamController::BTN_TP_LT_CLICK)!=0 && leftClickInCentre, leftPadClick }, + { n>4 && (buf[4]&SteamController::BTN_TP_RT_CLICK)!=0 && rightClickInCentre, (prevBuf[4]&SteamController::BTN_TP_RT_CLICK)!=0 && rightClickInCentre, rightPadClick }, + // Touch is not zoned: it cannot collide with a direction, + // which comes from the click, and a thumb crosses zones + // constantly while sliding. + { n>5 && (buf[5]&SteamController::BTN_TP_LT)!=0, (prevBuf[5]&SteamController::BTN_TP_LT)!=0, lPad.EffectiveTouch() }, + { n>4 && (buf[4]&SteamController::BTN_TP_RT)!=0, (prevBuf[4]&SteamController::BTN_TP_RT)!=0, rPad.EffectiveTouch() }, + // Directions are resolved rather than read, so their previous + // state is the one carried on the slot, not a bit in prevBuf. + { (lDirs&DirUp)!=0, (lPrevDirs&DirUp)!=0, lPad.EffectiveDirection(DirUp) }, + { (lDirs&DirDown)!=0, (lPrevDirs&DirDown)!=0, lPad.EffectiveDirection(DirDown) }, + { (lDirs&DirLeft)!=0, (lPrevDirs&DirLeft)!=0, lPad.EffectiveDirection(DirLeft) }, + { (lDirs&DirRight)!=0, (lPrevDirs&DirRight)!=0, lPad.EffectiveDirection(DirRight) }, + { (rDirs&DirUp)!=0, (rPrevDirs&DirUp)!=0, rPad.EffectiveDirection(DirUp) }, + { (rDirs&DirDown)!=0, (rPrevDirs&DirDown)!=0, rPad.EffectiveDirection(DirDown) }, + { (rDirs&DirLeft)!=0, (rPrevDirs&DirLeft)!=0, rPad.EffectiveDirection(DirLeft) }, + { (rDirs&DirRight)!=0, (rPrevDirs&DirRight)!=0, rPad.EffectiveDirection(DirRight) }, }; static_assert(std::size(edges) == Slot::kBindableCount, "paddleHeld must have one entry per edge-dispatched binding"); @@ -1250,6 +1301,14 @@ void ControllerManager::ReadLoop(Slot* slot) { } } + // Deliberately outside the hasPrev guard above, alongside the report + // it is the equivalent of: the first frame of a read loop establishes + // a baseline rather than dispatching against a stale one, and every + // later frame leaves behind what the next will compare with. Skipping + // it on any frame would stand a direction up or down for good. + slot->prevLeftDirs = resolved.leftDirs; + slot->prevRightDirs = resolved.rightDirs; + memcpy(prevBuf, buf, n); hasPrev = true; } diff --git a/src/app/TrackpadConfig.h b/src/app/TrackpadConfig.h index 9e26ab4..ee83d8c 100644 --- a/src/app/TrackpadConfig.h +++ b/src/app/TrackpadConfig.h @@ -105,12 +105,23 @@ enum PadDir : uint8_t { DirRight = 1 << 3, }; -// What both pads' directions resolved to this frame. Passed to the virtual -// controller, which cannot work them out for itself — resolving a direction -// needs the hysteresis state that lives per-pad in TrackpadInput. +// What both pads resolved to this frame, for the two places a pad binding is +// delivered — the virtual controller and SendInput. Neither can work these out +// for itself: a direction and a click's zone both need the hysteresis and +// latch state that lives per-pad in TrackpadInput. +// +// Carrying the zone here rather than re-deriving it is what keeps the two +// delivery paths agreeing. It is inferrable — a directional pad only presses +// directions from a ring click, so a click with nothing under it is a centre +// one — but an inference that holds "except for one frame after the diagonals +// setting changes" is exactly the kind that gets found the hard way. struct PadDigital { uint8_t leftDirs = DirNone; uint8_t rightDirs = DirNone; + // True for any pad that is not a directional pad, so a caller can gate the + // click binding on this without asking what mode the pad is in. + bool leftClickInCentre = true; + bool rightClickInCentre = true; }; // Where a click landed on a pad, which is what keeps a directional pad's three diff --git a/src/app/VirtualController.cpp b/src/app/VirtualController.cpp index 52f7e9e..23198ad 100644 --- a/src/app/VirtualController.cpp +++ b/src/app/VirtualController.cpp @@ -282,10 +282,43 @@ void VirtualController::SetBatteryState(uint8_t levelPercent, uint8_t chargeStat } void VirtualController::Update(const uint8_t* buf, size_t n, - const ControllerProfile& profile) { + const ControllerProfile& profile, + PadDigital resolved) { if (!m_valid) return; const BackButtonConfig& backCfg = profile.back; + // Calls `apply` once for every trackpad binding the pads are currently + // pressing, so the two report paths below only have to say how one binding + // reaches their own report format. Directions and the click's zone were + // both resolved by TrackpadInput and arrive in `dirs`; touch and the click + // bit itself are read from the report here. + auto forEachPadBinding = [&](const auto& apply) { + const uint8_t b2 = n > 4 ? buf[4] : 0; + const uint8_t b3 = n > 5 ? buf[5] : 0; + + struct PadState { + const TrackpadSettings& pad; + bool touching; + bool clicked; // already narrowed to a centre press + uint8_t dirs; + }; + const PadState pads[] = { + { profile.leftPad, (b3 & SteamController::BTN_TP_LT) != 0, + (b3 & SteamController::BTN_TP_LT_CLICK) != 0 + && resolved.leftClickInCentre, resolved.leftDirs }, + { profile.rightPad, (b2 & SteamController::BTN_TP_RT) != 0, + (b2 & SteamController::BTN_TP_RT_CLICK) != 0 + && resolved.rightClickInCentre, resolved.rightDirs }, + }; + + for (const PadState& p : pads) { + if (p.touching) apply(p.pad.EffectiveTouch()); + if (p.clicked) apply(p.pad.EffectiveClick()); + for (uint8_t d : { DirUp, DirDown, DirLeft, DirRight }) + if (p.dirs & d) apply(p.pad.EffectiveDirection(static_cast(d))); + } + }; + if (m_platform == ControllerPlatform::PlayStation) { // A pad reaches the DS4 touchpad only when that is what it was set // to. Decided per pad, so one pad can drive the desktop while the @@ -373,13 +406,15 @@ void VirtualController::Update(const uint8_t* buf, size_t n, if (n > 3) { if (buf[3] & SteamController::BTN_R5) applyBack(backCfg.r5); } - // Pad clicks bound to a gamepad action reach the virtual pad the - // same way the paddles do. EffectiveClick yields nothing for a - // pad feeding the touchpad, whose press is reported below instead. - if (n > 5 && (buf[5] & SteamController::BTN_TP_LT_CLICK)) - applyBack(profile.leftPad.EffectiveClick()); - if (n > 4 && (buf[4] & SteamController::BTN_TP_RT_CLICK)) - applyBack(profile.rightPad.EffectiveClick()); + // Trackpad clicks, touches and directions bound to a gamepad + // action reach the virtual pad the same way the paddles do. The + // Effective* accessors yield nothing for a pad feeding the + // touchpad, whose press is reported below instead. + // + // Applied before the hat is worked out below, so a direction bound + // to the d-pad — which is the default — folds into it and its + // diagonals come out as diagonals rather than as two cardinals. + forEachPadBinding(applyBack); DS4_DPAD_DIRECTIONS hat = DS4_BUTTON_DPAD_NONE; if (dUp && dRt) hat = DS4_BUTTON_DPAD_NORTHEAST; @@ -492,13 +527,13 @@ void VirtualController::Update(const uint8_t* buf, size_t n, if (n > 3) { if (buf[3] & SteamController::BTN_R5) ApplyBackActionX360(backCfg.r5, report); } - // Pad clicks bound to a gamepad action. EffectiveClick still applies: - // a pad set to DS4 Touchpad has no binding to deliver, and on this - // platform no touchpad to deliver it to either. - if (n > 5 && (buf[5] & SteamController::BTN_TP_LT_CLICK)) - ApplyBackActionX360(profile.leftPad.EffectiveClick(), report); - if (n > 4 && (buf[4] & SteamController::BTN_TP_RT_CLICK)) - ApplyBackActionX360(profile.rightPad.EffectiveClick(), report); + // Trackpad clicks, touches and directions bound to a gamepad action. + // The Effective* accessors still apply: a pad set to DS4 Touchpad has + // no binding to deliver, and on this platform no touchpad to deliver + // it to either. + forEachPadBinding([&](const BackButtonBinding& b) { + ApplyBackActionX360(b, report); + }); vigem_target_x360_update(static_cast(m_client), static_cast(m_target), diff --git a/src/app/VirtualController.h b/src/app/VirtualController.h index 7d8afad..0488ae0 100644 --- a/src/app/VirtualController.h +++ b/src/app/VirtualController.h @@ -35,7 +35,11 @@ public: // for choosing what to tell the user. Empty unless a connect failed. const std::wstring& BusReport() const { return m_busReport; } - void Update(const uint8_t* buf, size_t n, const ControllerProfile& profile); + // `resolved` carries what each pad's directions and click zone came out as + // this frame. Passed in rather than worked out here because resolving them + // needs the per-pad hysteresis and latch state that lives in TrackpadInput. + void Update(const uint8_t* buf, size_t n, const ControllerProfile& profile, + PadDigital resolved); void OnRumble(uint8_t largeMotor, uint8_t smallMotor); // DS4-only: update battery level shown in the DS4 report.