Merge main into PR branch; wire rumble into multi-slot architecture

Conflicts resolved:
- ControllerManager.cpp: take main's slot-based EnableGameMode/
  DisableGameMode dispatch; wire the rumble callback into
  EnableGameModeSlot and add SetRumble(0,0) to DisableGameModeSlot
  so rumble stops cleanly before vc is torn down.
- VirtualController.h: keep both RumbleCallback/OnRumble (ours) and
  the new Update(backCfg, backMouseEnabled) signature (main's).

Co-Authored-By: Dylan Deverill <ddeverill@gmail.com>
This commit is contained in:
Dylan Deverill committed 2026-06-20 23:13:24 -07:00
commit 0ce2b57a36
17 files changed
+1320 -241

No files matched your search

+270 -76
View File
@@ -1,122 +1,316 @@
#include "ControllerManager.h"
#include "VirtualController.h"
#include "TrackpadMouse.h"
#include "steam/SteamController.h"
#include <Windows.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <thread>
static std::unique_ptr<SteamController> g_ctrl;
// ---------------------------------------------------------------------------
// Slot
// ---------------------------------------------------------------------------
struct ControllerManager::Slot {
std::wstring path;
std::unique_ptr<SteamController> sc;
std::unique_ptr<VirtualController> vc;
TrackpadMouse trackpad;
std::thread readThread;
std::atomic<bool> readRunning{false};
bool gameModeActive = false;
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))
{
TryOpen();
SyncDevices();
}
ControllerManager::~ControllerManager() {
Close(/*restoreLizard=*/true);
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() {
if (!m_connected)
TryOpen();
else if (g_ctrl && !g_ctrl->IsOpen())
Close(/*restoreLizard=*/false);
SyncDevices();
}
void ControllerManager::EnableGameMode() {
if (!m_connected || m_gameModeActive) return;
if (!g_ctrl->DisableLizardMode()) return;
m_virtual = std::make_unique<VirtualController>(
[](uint8_t largeMotor, uint8_t smallMotor) {
if (!g_ctrl || !g_ctrl->IsOpen()) return;
const uint16_t leftSpeed = static_cast<uint16_t>(static_cast<uint32_t>(largeMotor) * 257u);
const uint16_t rightSpeed = static_cast<uint16_t>(static_cast<uint32_t>(smallMotor) * 257u);
g_ctrl->SetRumble(leftSpeed, rightSpeed);
});
if (!m_virtual->IsValid()) {
bool missing = m_virtual->IsDriverMissing();
m_virtual.reset();
g_ctrl->EnableLizardMode();
if (missing) m_onStateChanged(m_connected, m_gameModeActive, /*vigemMissing=*/true);
return;
}
m_gameModeActive = true;
m_trackpad.Reset();
m_trackpad.SetTrackpadEnabled(m_trackpadMouseEnabled);
m_trackpad.SetBackButtonsEnabled(m_backButtonsEnabled);
m_trackpad.SetUseLeftTrackpad(m_useLeftTrackpad);
StartReadLoop();
m_onStateChanged(m_connected, m_gameModeActive, false);
bool anyVigemMissing = false;
for (auto& slot : m_slots)
EnableGameModeSlot(*slot, anyVigemMissing);
NotifyStateChanged(anyVigemMissing);
}
void ControllerManager::DisableGameMode() {
if (!m_gameModeActive) return;
StopReadLoop();
if (g_ctrl && g_ctrl->IsOpen())
g_ctrl->SetRumble(0, 0);
m_trackpad.Reset();
m_virtual.reset();
g_ctrl->EnableLizardMode();
m_gameModeActive = false;
m_onStateChanged(m_connected, m_gameModeActive, false);
for (auto& slot : m_slots)
DisableGameModeSlot(*slot);
NotifyStateChanged();
}
void ControllerManager::SetTrackpadMouseEnabled(bool enabled) {
m_trackpadMouseEnabled = enabled;
m_trackpad.SetTrackpadEnabled(enabled);
}
void ControllerManager::SetBackButtonsEnabled(bool enabled) {
m_backButtonsEnabled = enabled;
m_trackpad.SetBackButtonsEnabled(enabled);
for (auto& slot : m_slots)
slot->trackpad.SetTrackpadEnabled(enabled);
}
void ControllerManager::SetUseLeftTrackpad(bool enabled) {
m_useLeftTrackpad = enabled;
m_trackpad.SetUseLeftTrackpad(enabled);
for (auto& slot : m_slots)
slot->trackpad.SetUseLeftTrackpad(enabled);
}
void ControllerManager::TryOpen() {
if (!g_ctrl) g_ctrl = std::make_unique<SteamController>();
if (g_ctrl->Open()) {
m_connected = true;
m_onStateChanged(m_connected, m_gameModeActive, false);
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::StartButtonCapture(std::function<void(BackButtonAction)> callback) {
std::lock_guard<std::mutex> lk(m_captureMutex);
m_captureCallback = std::move(callback);
m_capturing = true;
}
void ControllerManager::StopButtonCapture() {
m_capturing = false;
std::lock_guard<std::mutex> 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>();
slot->path = path;
slot->sc = std::make_unique<SteamController>();
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();
}
}
void ControllerManager::Close(bool restoreLizard) {
StopReadLoop();
m_virtual.reset();
if (g_ctrl) {
if (restoreLizard && m_gameModeActive)
g_ctrl->EnableLizardMode();
g_ctrl->Close();
// ---------------------------------------------------------------------------
// Game mode per-slot helpers
// ---------------------------------------------------------------------------
void ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut) {
if (slot.gameModeActive) return;
if (!slot.sc->DisableLizardMode()) return;
slot.vc = std::make_unique<VirtualController>(
[sc = slot.sc.get()](uint8_t largeMotor, uint8_t smallMotor) {
if (!sc->IsOpen()) return;
const uint16_t leftSpeed = static_cast<uint16_t>(static_cast<uint32_t>(largeMotor) * 257u);
const uint16_t rightSpeed = static_cast<uint16_t>(static_cast<uint32_t>(smallMotor) * 257u);
sc->SetRumble(leftSpeed, rightSpeed);
});
if (!slot.vc->IsValid()) {
if (slot.vc->IsDriverMissing()) vigemMissingOut = true;
slot.vc.reset();
slot.sc->EnableLizardMode();
return;
}
m_connected = false;
m_gameModeActive = false;
m_onStateChanged(m_connected, m_gameModeActive, false);
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::StartReadLoop() {
m_readRunning = true;
m_readThread = std::thread(&ControllerManager::ReadLoop, this);
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();
slot.gameModeActive = false;
}
void ControllerManager::StopReadLoop() {
m_readRunning = false;
if (m_readThread.joinable())
m_readThread.join();
// ---------------------------------------------------------------------------
// Read loop
// ---------------------------------------------------------------------------
void ControllerManager::StartReadLoop(Slot& slot) {
slot.readRunning = true;
slot.readThread = std::thread(&ControllerManager::ReadLoop, this, &slot);
}
void ControllerManager::ReadLoop() {
void ControllerManager::StopReadLoop(Slot& slot) {
slot.readRunning = false;
if (slot.readThread.joinable())
slot.readThread.join();
}
void ControllerManager::ReadLoop(Slot* slot) {
uint8_t buf[64];
while (m_readRunning) {
size_t n = g_ctrl->ReadReport(buf, sizeof(buf), /*timeoutMs=*/32);
uint8_t prevBuf[64] = {};
bool hasPrev = false;
while (slot->readRunning) {
size_t n = slot->sc->ReadReport(buf, sizeof(buf), /*timeoutMs=*/32);
if (n == 0) continue;
if (buf[0] != SteamController::REPORT_STATE) continue;
if (m_virtual) m_virtual->Update(buf, n);
m_trackpad.Update(buf, n);
if (slot->vc) slot->vc->Update(buf, n, m_backConfig, m_backButtonsEnabled);
slot->trackpad.Update(buf, n);
// 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<std::mutex> 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);
}