#include "ControllerManager.h" #include "VirtualController.h" #include "TrackpadMouse.h" #include "steam/SteamController.h" #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Slot // --------------------------------------------------------------------------- struct ControllerManager::Slot { std::wstring path; std::unique_ptr sc; std::unique_ptr vc; TrackpadMouse trackpad; std::thread readThread; std::atomic readRunning{false}; bool gameModeActive = false; // Haptic edge-detection state for touch (movement ticks). bool hapticWasRightTouching = false; bool hapticWasLeftTouching = false; // Click haptic state machine — two states per trackpad. // WaitingForPress: will fire the press haptic the first time the click bit // goes high; ignores any further highs until release fires. // WaitingForRelease: will fire the release haptic the first time the click bit // goes low; ignores any further lows until next press fires. // This fires exactly once per downstroke and once per upstroke regardless of // how many times the click bit chatters around the threshold. enum class ClickState { WaitingForPress, WaitingForRelease }; ClickState hapticRightClickState = ClickState::WaitingForPress; ClickState hapticLeftClickState = ClickState::WaitingForPress; // 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; // Grace period after first touch — suppresses movement ticks while the press // gesture completes so it can't bleed a TICK in just before the CLICK fires. // Counts down from TOUCH_GRACE_FRAMES to 0; ticks only fire when it is 0. static constexpr int kTouchGraceFrames = 12; // ~48ms at 250Hz int hapticRightTouchGrace = 0; int hapticLeftTouchGrace = 0; // Click haptic latch. Press: the firmware click bit must hold true for a // couple frames (filters single-frame glitches), then the press haptic // fires. Release: the click bit is IGNORED from then on — chatter around // the firmware's force threshold can't re-fire anything. The latch only // releases (and the release haptic fires) once contact area falls back // to genuinely idle — a light resting touch (~300-600), far below any // pressing level (~1400+ even when easing off mid-hold, ~4000 at click). // An absolute threshold, NOT a fraction of press area: grinding a hard // press dips area to 1000-2000, which a relative threshold read as a // release, firing press/release pairs in a crunchy stream. static constexpr int kPressConfirmFrames = 2; // ~8ms at 250Hz static constexpr float kReleaseIdleArea = 1000.0f; static constexpr int kAreaLowConfirmFrames = 8; // ~32ms at 250Hz int hapticRightClickTrueFrames = 0; int hapticLeftClickTrueFrames = 0; int hapticRightAreaLowFrames = 0; int hapticLeftAreaLowFrames = 0; // Movement-accumulator idle reset: if the finger stays still (below the // motion deadzone) this many frames, discard accumulated travel so a // primed accumulator can't discharge a spurious TICK from press wobble. static constexpr int kIdleResetFrames = 30; // ~120ms at 250Hz int hapticRightIdleFrames = 0; int hapticLeftIdleFrames = 0; // Post-release grace — after the release haptic fires, the finger is // still peeling off / settling; the centroid jumps hundreds of units per // frame during that window, which reads as motion but isn't swiping. static constexpr int kPostReleaseGraceFrames = 45; // ~180ms at 250Hz int hapticRightReleaseGrace = 0; int hapticLeftReleaseGrace = 0; Slot() = default; Slot(const Slot&) = delete; Slot& operator=(const Slot&) = delete; }; // --------------------------------------------------------------------------- // Rising-edge capture: returns true (and sets 'out') when any of the 16 // mappable XInput buttons transitions from not-pressed to pressed. // Back paddles (L4/L5/R4/R5) are excluded — they are what we are remapping, // not valid capture targets. // --------------------------------------------------------------------------- static bool DetectCapture(const uint8_t* cur, const uint8_t* prev, BackButtonAction& out) { using BA = BackButtonAction; // Helper: rising edge on a single bit in a byte. auto rose = [&](int byteIdx, uint8_t mask) -> bool { return (cur[byteIdx] & mask) && !(prev[byteIdx] & mask); }; // Triggers use 16-bit analog — fire on crossing 25% (0x2000). auto trigRose = [&](int byteIdx) -> bool { int16_t c, p; memcpy(&c, cur + byteIdx, 2); memcpy(&p, prev + byteIdx, 2); return c > 0x2000 && p <= 0x2000; }; // Check each mappable button in priority order. // Byte offsets match SteamController.h for the 0x45 report. if (rose(2, SteamController::BTN_A)) { out = BA::A; return true; } if (rose(2, SteamController::BTN_B)) { out = BA::B; return true; } if (rose(2, SteamController::BTN_X)) { out = BA::X; return true; } if (rose(2, SteamController::BTN_Y)) { out = BA::Y; return true; } if (rose(4, SteamController::BTN_LB)) { out = BA::LB; return true; } if (rose(3, SteamController::BTN_RB)) { out = BA::RB; return true; } if (trigRose(6)) { out = BA::LT; return true; } if (trigRose(8)) { out = BA::RT; return true; } if (rose(3, SteamController::BTN_DPAD_UP)) { out = BA::DPadUp; return true; } if (rose(3, SteamController::BTN_DPAD_DN)) { out = BA::DPadDown; return true; } if (rose(3, SteamController::BTN_DPAD_LT)) { out = BA::DPadLeft; return true; } if (rose(3, SteamController::BTN_DPAD_RT)) { out = BA::DPadRight; return true; } if (rose(2, SteamController::BTN_MENU)) { out = BA::Menu; return true; } if (rose(3, SteamController::BTN_VIEW)) { out = BA::View; return true; } if (rose(3, SteamController::BTN_LS)) { out = BA::L3; return true; } if (rose(2, SteamController::BTN_RS)) { out = BA::R3; return true; } return false; } // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- ControllerManager::ControllerManager(StateChangedFn onStateChanged) : m_onStateChanged(std::move(onStateChanged)) { SyncDevices(); } ControllerManager::~ControllerManager() { for (auto& slot : m_slots) { // Stop the read loop and virtual controller first (same as DisableGameModeSlot // but without the gameModeActive guard — we always want to restore lizard mode). if (slot->gameModeActive) { StopReadLoop(*slot); slot->trackpad.Reset(); slot->vc.reset(); slot->gameModeActive = false; } slot->sc->EnableLizardMode(); // always restore, even if game mode was never active } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- bool ControllerManager::IsGameModeActive() const { return std::any_of(m_slots.begin(), m_slots.end(), [](const auto& s) { return s->gameModeActive; }); } void ControllerManager::OnDeviceChange() { SyncDevices(); } void ControllerManager::EnableGameMode() { bool anyVigemMissing = false; for (auto& slot : m_slots) EnableGameModeSlot(*slot, anyVigemMissing); NotifyStateChanged(anyVigemMissing); } void ControllerManager::DisableGameMode() { for (auto& slot : m_slots) DisableGameModeSlot(*slot); NotifyStateChanged(); } void ControllerManager::ReleaseDevices() { // Disable game mode first — enables lizard mode and tears down ViGEm while // we still hold write access to the device. DisableGameMode(); // Close all device handles. Slot destructors call SteamController::Close() // which closes the HID handle, allowing another process to open it. m_slots.clear(); NotifyStateChanged(); } void ControllerManager::SetTrackpadMouseEnabled(bool enabled) { m_trackpadMouseEnabled = enabled; for (auto& slot : m_slots) { slot->trackpad.SetTrackpadEnabled(enabled); if (slot->vc) slot->vc->SetTrackpadMouseClaim(enabled, m_useLeftTrackpad); } } void ControllerManager::SetUseLeftTrackpad(bool enabled) { m_useLeftTrackpad = enabled; for (auto& slot : m_slots) { slot->trackpad.SetUseLeftTrackpad(enabled); if (slot->vc) slot->vc->SetTrackpadMouseClaim(m_trackpadMouseEnabled, enabled); } } void ControllerManager::SetBackButtonConfig(const BackButtonConfig& cfg) { m_backConfig = cfg; // Live update: the ReadLoop reads m_backConfig on every frame, so the // change takes effect on the very next report. } void ControllerManager::SetBackButtonsEnabled(bool enabled) { m_backButtonsEnabled = enabled; for (auto& slot : m_slots) slot->trackpad.SetBackButtonsEnabled(enabled); } void ControllerManager::SetControllerPlatform(ControllerPlatform platform) { if (m_controllerPlatform == platform) return; m_controllerPlatform = platform; if (IsGameModeActive()) { DisableGameMode(); EnableGameMode(); } } void ControllerManager::StartButtonCapture(std::function callback) { std::lock_guard lk(m_captureMutex); m_captureCallback = std::move(callback); m_capturing = true; } void ControllerManager::StopButtonCapture() { m_capturing = false; std::lock_guard lk(m_captureMutex); m_captureCallback = nullptr; } // --------------------------------------------------------------------------- // Device management // --------------------------------------------------------------------------- void ControllerManager::SyncDevices() { auto livePaths = SteamController::EnumerateAll(); auto it = m_slots.begin(); while (it != m_slots.end()) { bool alive = std::any_of(livePaths.begin(), livePaths.end(), [&](const auto& p) { return p == (*it)->path; }); if (!alive) { DisableGameModeSlot(**it); it = m_slots.erase(it); } else { ++it; } } for (auto const& path : livePaths) { bool already = std::any_of(m_slots.begin(), m_slots.end(), [&](const auto& s) { return s->path == path; }); if (!already) OpenSlot(path); } NotifyStateChanged(); } void ControllerManager::OpenSlot(const std::wstring& path) { auto slot = std::make_unique(); slot->path = path; slot->sc = std::make_unique(); if (!slot->sc->Open(path)) return; m_slots.push_back(std::move(slot)); if (IsGameModeActive()) { bool dummy = false; EnableGameModeSlot(*m_slots.back(), dummy); } else { // Restore lizard mode in case a previous session crashed without cleaning up. m_slots.back()->sc->EnableLizardMode(); } } // --------------------------------------------------------------------------- // Game mode per-slot helpers // --------------------------------------------------------------------------- void ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut) { if (slot.gameModeActive) return; if (!slot.sc->ClaimExclusive()) return; if (!slot.sc->DisableLizardMode()) { slot.sc->ReleaseToShared(); return; } slot.vc = std::make_unique( m_controllerPlatform, [sc = slot.sc.get()](uint8_t largeMotor, uint8_t smallMotor) { if (sc->IsOpen()) sc->SetRumble(largeMotor, smallMotor); }); if (!slot.vc->IsValid()) { if (slot.vc->IsDriverMissing()) vigemMissingOut = true; slot.vc.reset(); slot.sc->EnableLizardMode(); return; } if (m_controllerPlatform == ControllerPlatform::PlayStation) slot.sc->SetImuEnabled(true); slot.vc->SetTrackpadMouseClaim(m_trackpadMouseEnabled, m_useLeftTrackpad); slot.gameModeActive = true; slot.trackpad.Reset(); slot.trackpad.SetTrackpadEnabled(m_trackpadMouseEnabled); slot.trackpad.SetUseLeftTrackpad(m_useLeftTrackpad); slot.trackpad.SetBackButtonsEnabled(m_backButtonsEnabled); StartReadLoop(slot); } void ControllerManager::DisableGameModeSlot(Slot& slot) { if (!slot.gameModeActive) return; StopReadLoop(slot); if (slot.sc->IsOpen()) slot.sc->SetRumble(0, 0); slot.trackpad.Reset(); slot.vc.reset(); slot.sc->EnableLizardMode(); // Reopen shared so Steam can obtain write access — game mode is no longer active. slot.sc->ReleaseToShared(); slot.gameModeActive = false; } // --------------------------------------------------------------------------- // Read loop // --------------------------------------------------------------------------- void ControllerManager::StartReadLoop(Slot& slot) { slot.readRunning = true; slot.readThread = std::thread(&ControllerManager::ReadLoop, this, &slot); } void ControllerManager::StopReadLoop(Slot& slot) { slot.readRunning = false; if (slot.readThread.joinable()) slot.readThread.join(); } void ControllerManager::ReadLoop(Slot* slot) { uint8_t buf[64]; uint8_t prevBuf[64] = {}; bool hasPrev = false; auto lastKeepalive = std::chrono::steady_clock::now(); while (slot->readRunning) { // Keepalive — the firmware silently reverts to lizard mode (and its // autonomous click haptics, which double up with ours as a crunchy // burst) after a period without host feature reports. Re-assert the // lizard-off state every couple of seconds. Must run before the // read-timeout continue below, or an idle controller would starve it. const auto nowKa = std::chrono::steady_clock::now(); if (nowKa - lastKeepalive >= std::chrono::seconds(2)) { lastKeepalive = nowKa; slot->sc->SendKeepalive(); } size_t n = slot->sc->ReadReport(buf, sizeof(buf), /*timeoutMs=*/32); if (n == 0) continue; // Battery status — update DS4 battery level; no further processing needed. if (buf[0] == SteamController::REPORT_BATTERY_STATUS) { if (n >= 3 && slot->vc) slot->vc->SetBatteryState(buf[1], buf[2]); continue; } if (!SteamController::IsStateReportId(buf[0])) continue; if (slot->vc) slot->vc->Update(buf, n, m_backConfig, m_backButtonsEnabled); slot->trackpad.Update(buf, n); // Trackpad haptics. { // Distance of deliberate travel between movement ticks. A full // top-to-bottom swipe (~65k units) should produce roughly 10 ticks. static constexpr float TRACKPAD_HAPTIC_TICK_DISTANCE = 6500.0f; // Per-frame motion deadzone: deltas smaller than this are sensor // jitter or fingertip wobble from pressing, not deliberate motion. // Discarding them keeps the accumulator from priming itself while // the finger rests or presses, which caused spurious ticks to fire // alongside the click haptic on the downstroke. static constexpr float TRACKPAD_HAPTIC_MOTION_DEADZONE = 45.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; int16_t rx = 0, ry = 0, lx = 0, ly = 0; uint16_t rArea = 0, lArea = 0; if (n >= 28) { memcpy(&rx, buf + 24, 2); memcpy(&ry, buf + 26, 2); memcpy(&lx, buf + 18, 2); memcpy(&ly, buf + 20, 2); } if (n >= 30) { memcpy(&lArea, buf + 22, 2); memcpy(&rArea, buf + 28, 2); } // Click haptic latch — press fires off the (briefly confirmed) // firmware click bit; from then on the click bit is ignored so // threshold chatter can't re-fire. The release haptic fires once // contact area returns to idle. slot->hapticRightClickTrueFrames = rc ? slot->hapticRightClickTrueFrames + 1 : 0; slot->hapticLeftClickTrueFrames = lc ? slot->hapticLeftClickTrueFrames + 1 : 0; if (slot->hapticRightClickState == Slot::ClickState::WaitingForPress && slot->hapticRightClickTrueFrames >= Slot::kPressConfirmFrames) { slot->sc->PulseTrackpadHaptic(false, true); slot->hapticRightClickState = Slot::ClickState::WaitingForRelease; slot->hapticRightAreaLowFrames = 0; slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; slot->hapticRightDistAccum = 0.0f; } else if (slot->hapticRightClickState == Slot::ClickState::WaitingForRelease) { if (!rc && static_cast(rArea) <= Slot::kReleaseIdleArea) { if (++slot->hapticRightAreaLowFrames >= Slot::kAreaLowConfirmFrames) { slot->sc->PulseTrackpadHaptic(false, true); slot->hapticRightClickState = Slot::ClickState::WaitingForPress; slot->hapticRightReleaseGrace = Slot::kPostReleaseGraceFrames; slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; slot->hapticRightDistAccum = 0.0f; } } else { slot->hapticRightAreaLowFrames = 0; } } if (slot->hapticLeftClickState == Slot::ClickState::WaitingForPress && slot->hapticLeftClickTrueFrames >= Slot::kPressConfirmFrames) { slot->sc->PulseTrackpadHaptic(true, true); slot->hapticLeftClickState = Slot::ClickState::WaitingForRelease; slot->hapticLeftAreaLowFrames = 0; slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; slot->hapticLeftDistAccum = 0.0f; } else if (slot->hapticLeftClickState == Slot::ClickState::WaitingForRelease) { if (!lc && static_cast(lArea) <= Slot::kReleaseIdleArea) { if (++slot->hapticLeftAreaLowFrames >= Slot::kAreaLowConfirmFrames) { slot->sc->PulseTrackpadHaptic(true, true); slot->hapticLeftClickState = Slot::ClickState::WaitingForPress; slot->hapticLeftReleaseGrace = Slot::kPostReleaseGraceFrames; slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; slot->hapticLeftDistAccum = 0.0f; } } else { slot->hapticLeftAreaLowFrames = 0; } } // Movement haptic — tick once per TRACKPAD_HAPTIC_TICK_DISTANCE of travel. if (n >= 28) { // On first-touch frame, seed positions, reset accumulator, and // start grace period so press-gesture motion can't bleed a TICK // just before the CLICK fires (~48ms at 250Hz). if (rt && !slot->hapticWasRightTouching) { slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; slot->hapticRightDistAccum = 0.0f; slot->hapticRightTouchGrace = Slot::kTouchGraceFrames; } if (lt && !slot->hapticWasLeftTouching) { slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; slot->hapticLeftDistAccum = 0.0f; slot->hapticLeftTouchGrace = Slot::kTouchGraceFrames; } if (slot->hapticRightTouchGrace > 0) --slot->hapticRightTouchGrace; if (slot->hapticLeftTouchGrace > 0) --slot->hapticLeftTouchGrace; if (slot->hapticRightReleaseGrace > 0) --slot->hapticRightReleaseGrace; if (slot->hapticLeftReleaseGrace > 0) --slot->hapticLeftReleaseGrace; // No ticks while the finger is pressing down (contact area // balloons far past light-touch levels) and no single-frame // centroid teleports (partial-contact artifacts) — neither is // deliberate swiping, however much distance they report. // NOTE: no minimum-area gate — a light gliding finger reads // near-zero contact area on this pad, indistinguishable from // post-lift ghost drift. Ghosts near clicks are covered by the // post-release grace instead. static constexpr float TRACKPAD_TICK_MAX_AREA = 900.0f; static constexpr float TRACKPAD_TICK_MAX_STEP = 4000.0f; // Gate on the latch state, not the raw click bit — while the // click is held (even if the bit chatters low), no ticks. if (rt && slot->hapticWasRightTouching && !rc && slot->hapticRightClickState == Slot::ClickState::WaitingForPress && slot->hapticRightTouchGrace == 0) { const float dx = static_cast(rx - slot->hapticPrevRightX); const float dy = static_cast(ry - slot->hapticPrevRightY); const float dist = std::hypot(dx, dy); if (slot->hapticRightReleaseGrace > 0 || static_cast(rArea) > TRACKPAD_TICK_MAX_AREA || dist > TRACKPAD_TICK_MAX_STEP) { // Settling after a release, pressing down, or a centroid // teleport — pause, don't punish: skip the frame without // accumulating it, but keep travel already earned so a // brief excursion mid-swipe doesn't zero the progress. // No tick can fire from a gated frame, and the press // haptic still resets the accumulator when a click lands. slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; } else if (dist >= TRACKPAD_HAPTIC_MOTION_DEADZONE) { // Below the deadzone: don't accumulate, and don't advance // the reference point — slow creep past the deadzone still // counts. If the finger stays still long enough, discard // accumulated travel (idle reset below). slot->hapticRightIdleFrames = 0; slot->hapticRightDistAccum += dist; if (slot->hapticRightDistAccum >= TRACKPAD_HAPTIC_TICK_DISTANCE) { if (slot->sc->TickTrackpadMovement(false)) { slot->hapticRightDistAccum = 0.0f; } else { // Rate-limited: hold at threshold so the tick // fires as soon as the limiter reopens. slot->hapticRightDistAccum = TRACKPAD_HAPTIC_TICK_DISTANCE; } } slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; } else if (++slot->hapticRightIdleFrames >= Slot::kIdleResetFrames) { slot->hapticRightDistAccum = 0.0f; slot->hapticPrevRightX = rx; slot->hapticPrevRightY = ry; } } if (lt && slot->hapticWasLeftTouching && !lc && slot->hapticLeftClickState == Slot::ClickState::WaitingForPress && slot->hapticLeftTouchGrace == 0) { const float dx = static_cast(lx - slot->hapticPrevLeftX); const float dy = static_cast(ly - slot->hapticPrevLeftY); const float dist = std::hypot(dx, dy); if (slot->hapticLeftReleaseGrace > 0 || static_cast(lArea) > TRACKPAD_TICK_MAX_AREA || dist > TRACKPAD_TICK_MAX_STEP) { slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; } else if (dist >= TRACKPAD_HAPTIC_MOTION_DEADZONE) { slot->hapticLeftIdleFrames = 0; slot->hapticLeftDistAccum += dist; if (slot->hapticLeftDistAccum >= TRACKPAD_HAPTIC_TICK_DISTANCE) { if (slot->sc->TickTrackpadMovement(true)) slot->hapticLeftDistAccum = 0.0f; else slot->hapticLeftDistAccum = TRACKPAD_HAPTIC_TICK_DISTANCE; } slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; } else if (++slot->hapticLeftIdleFrames >= Slot::kIdleResetFrames) { slot->hapticLeftDistAccum = 0.0f; slot->hapticPrevLeftX = lx; slot->hapticPrevLeftY = ly; } } } slot->hapticWasRightTouching = rt; slot->hapticWasLeftTouching = lt; } // Fire mouse button events for back paddles mapped to LeftMouseButton/RightMouseButton. // Uses edge detection so we only send DOWN on press and UP on release. // Skipped when backButtonsEnabled — TrackpadMouse already handles L4/R4 in that mode. if (!m_backButtonsEnabled && hasPrev) { struct PaddleEdge { bool cur; bool prev; BackButtonAction action; }; const PaddleEdge edges[] = { { n>4 && (buf[4]&SteamController::BTN_L4)!=0, (prevBuf[4]&SteamController::BTN_L4)!=0, m_backConfig.l4 }, { n>4 && (buf[4]&SteamController::BTN_L5)!=0, (prevBuf[4]&SteamController::BTN_L5)!=0, m_backConfig.l5 }, { n>2 && (buf[2]&SteamController::BTN_R4)!=0, (prevBuf[2]&SteamController::BTN_R4)!=0, m_backConfig.r4 }, { n>3 && (buf[3]&SteamController::BTN_R5)!=0, (prevBuf[3]&SteamController::BTN_R5)!=0, m_backConfig.r5 }, }; for (const auto& e : edges) { if (e.cur == e.prev) continue; DWORD flag = 0; if (e.action == BackButtonAction::LeftMouseButton) flag = e.cur ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; else if (e.action == BackButtonAction::RightMouseButton) flag = e.cur ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; if (flag) { INPUT inp{}; inp.type = INPUT_MOUSE; inp.mi.dwFlags = flag; SendInput(1, &inp, sizeof(INPUT)); } } } // Button capture for the remap window (press-to-bind). // Only fires on a rising edge to avoid repeat-triggering on hold. if (m_capturing.load() && hasPrev) { BackButtonAction captured; if (DetectCapture(buf, prevBuf, captured)) { m_capturing = false; std::lock_guard lk(m_captureMutex); if (m_captureCallback) m_captureCallback(captured); } } memcpy(prevBuf, buf, n); hasPrev = true; } } void ControllerManager::NotifyStateChanged(bool vigemMissing) { m_onStateChanged(!m_slots.empty(), IsGameModeActive(), vigemMissing); }