mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-22 06:32:20 -05:00
Fix orphaned ViGEm pads left behind by a failed target add (#52)
vigem_target_add() plugs the child device in with IOCTL_VIGEM_PLUGIN_TARGET, then waits for it to come up with IOCTL_VIGEM_WAIT_DEVICE_READY. When that wait fails, the cleanup path calls vigem_target_remove() -- but target->State is only set to VIGEM_TARGET_CONNECTED on the two success paths above it, so the removal trips its own "State != VIGEM_TARGET_CONNECTED" guard and returns without ever sending IOCTL_VIGEM_UNPLUG_TARGET. The device stays plugged into the bus with no owning process, holding an XInput slot until the machine reboots. Observed locally: a pad created at 19:57:25 outlived the app by five hours and several sessions, and every later session had to take the next serial. The rapid enable/disable churn during device cycling is what triggers the wait timeout in the first place. Mark the target connected before the cleanup so the unplug is actually sent, and pin the return to VIGEM_ERROR_TARGET_NOT_PLUGGED_IN -- a successful unplug would otherwise report VIGEM_ERROR_NONE and register a target in pTargetsList that is no longer plugged in. This is a local patch to the vendored b66d02d ViGEmClient snapshot; upstream is archived, so no future sync will bring it in. Also: - VirtualController records which ViGEm call failed and its error code; ControllerManager logs both. The old driverMissing line could not distinguish a missing bus from a failed target add, which is exactly the distinction needed to spot this leak. - ShowViGEmBalloon() now latches the way ShowElevationBalloon() already did. Every failed enable calls back into UpdateTrayIcon -- several times a second during an acquire-retry loop -- so an unavailable driver produced a wall of identical balloons. The latch clears once the driver is reachable again so a genuine mid-session disappearance still warns. UpdateTrayIcon no longer returns early on vigemMissing, so the icon and tooltip keep tracking the controller instead of freezing. - Reword that balloon: a disabled bus devnode reports the same VIGEM_ERROR_BUS_NOT_FOUND as an uninstalled one, and "is not installed" sends users looking in the wrong place. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
94f1bf94dd
commit
9ef3609da5
6 files changed
+62
-7
No files matched your search
@@ -427,7 +427,8 @@ ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut,
|
|||||||
if (sc->IsOpen()) sc->SetRumble(largeMotor, smallMotor);
|
if (sc->IsOpen()) sc->SetRumble(largeMotor, smallMotor);
|
||||||
});
|
});
|
||||||
if (!slot.vc->IsValid()) {
|
if (!slot.vc->IsValid()) {
|
||||||
EventLog::Write("GAMEMODE: ViGEm virtual controller failed (driverMissing=%d)",
|
EventLog::Write("GAMEMODE: ViGEm virtual controller failed (stage=%s, err=0x%08X, driverMissing=%d)",
|
||||||
|
slot.vc->FailStage(), slot.vc->LastError(),
|
||||||
slot.vc->IsDriverMissing() ? 1 : 0);
|
slot.vc->IsDriverMissing() ? 1 : 0);
|
||||||
if (slot.vc->IsDriverMissing()) vigemMissingOut = true;
|
if (slot.vc->IsDriverMissing()) vigemMissingOut = true;
|
||||||
slot.vc.reset();
|
slot.vc.reset();
|
||||||
|
|||||||
+15
-2
@@ -512,9 +512,13 @@ void TrayApp::RemoveTrayIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TrayApp::UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing) {
|
void TrayApp::UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing) {
|
||||||
if (vigemMissing) { ShowViGEmBalloon(); return; }
|
if (vigemMissing) ShowViGEmBalloon();
|
||||||
|
else m_vigemBalloonShown = false;
|
||||||
|
|
||||||
|
// Fall through rather than returning early — the icon and tooltip still need
|
||||||
|
// to track the controller while the driver is unavailable.
|
||||||
const wchar_t* tip = gameModeActive ? L"Steamless Controller — Steamless Mode ON"
|
const wchar_t* tip = gameModeActive ? L"Steamless Controller — Steamless Mode ON"
|
||||||
|
: vigemMissing ? L"Steamless Controller — ViGEmBus driver unavailable"
|
||||||
: connected ? L"Steamless Controller — Connected (Steamless Mode OFF)"
|
: connected ? L"Steamless Controller — Connected (Steamless Mode OFF)"
|
||||||
: L"Steamless Controller — No controller found";
|
: L"Steamless Controller — No controller found";
|
||||||
|
|
||||||
@@ -548,7 +552,14 @@ void TrayApp::OpenEventLog() {
|
|||||||
EventLog::FilePath().c_str(), nullptr, SW_SHOWNORMAL);
|
EventLog::FilePath().c_str(), nullptr, SW_SHOWNORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Latched: every failed game-mode enable calls back into UpdateTrayIcon, which
|
||||||
|
// during an acquire-retry loop is several times a second — without this the user
|
||||||
|
// gets a wall of identical balloons. UpdateTrayIcon clears the latch once the
|
||||||
|
// driver is reachable again, so a genuine mid-session disappearance still warns.
|
||||||
void TrayApp::ShowViGEmBalloon() {
|
void TrayApp::ShowViGEmBalloon() {
|
||||||
|
if (m_vigemBalloonShown) return;
|
||||||
|
m_vigemBalloonShown = true;
|
||||||
|
|
||||||
NOTIFYICONDATAW nid{};
|
NOTIFYICONDATAW nid{};
|
||||||
nid.cbSize = sizeof(nid);
|
nid.cbSize = sizeof(nid);
|
||||||
nid.hWnd = m_hwnd;
|
nid.hWnd = m_hwnd;
|
||||||
@@ -556,7 +567,9 @@ void TrayApp::ShowViGEmBalloon() {
|
|||||||
nid.uFlags = NIF_INFO;
|
nid.uFlags = NIF_INFO;
|
||||||
nid.dwInfoFlags = NIIF_WARNING;
|
nid.dwInfoFlags = NIIF_WARNING;
|
||||||
wcscpy_s(nid.szInfoTitle, L"Driver required");
|
wcscpy_s(nid.szInfoTitle, L"Driver required");
|
||||||
wcscpy_s(nid.szInfo, L"ViGEmBus is not installed. Click here to download it.");
|
wcscpy_s(nid.szInfo,
|
||||||
|
L"ViGEmBus is unavailable — not installed, or disabled in Device "
|
||||||
|
L"Manager under System devices. Click here to download it.");
|
||||||
Shell_NotifyIconW(NIM_MODIFY, &nid);
|
Shell_NotifyIconW(NIM_MODIFY, &nid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ private:
|
|||||||
int m_acquireRetries = 0;
|
int m_acquireRetries = 0;
|
||||||
ULONGLONG m_lastCycleTick = 0;
|
ULONGLONG m_lastCycleTick = 0;
|
||||||
bool m_elevationBalloonShown = false;
|
bool m_elevationBalloonShown = false;
|
||||||
|
bool m_vigemBalloonShown = false;
|
||||||
bool m_startupEnabled = false;
|
bool m_startupEnabled = false;
|
||||||
int m_startupMechanism = 0; // 0 none, 1 Run key, 2 elevated task
|
int m_startupMechanism = 0; // 0 none, 1 Run key, 2 elevated task
|
||||||
bool m_notificationsEnabled = true; // disconnect/stall balloons
|
bool m_notificationsEnabled = true; // disconnect/stall balloons
|
||||||
|
|||||||
@@ -150,10 +150,16 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback
|
|||||||
: m_platform(platform), m_rumbleCallback(std::move(rumbleCallback))
|
: m_platform(platform), m_rumbleCallback(std::move(rumbleCallback))
|
||||||
{
|
{
|
||||||
m_client = vigem_alloc();
|
m_client = vigem_alloc();
|
||||||
if (!m_client) { printf("[ViGEm] alloc failed\n"); return; }
|
if (!m_client) {
|
||||||
|
m_failStage = "vigem_alloc";
|
||||||
|
printf("[ViGEm] alloc failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
VIGEM_ERROR err = vigem_connect(static_cast<PVIGEM_CLIENT>(m_client));
|
VIGEM_ERROR err = vigem_connect(static_cast<PVIGEM_CLIENT>(m_client));
|
||||||
if (!VIGEM_SUCCESS(err)) {
|
if (!VIGEM_SUCCESS(err)) {
|
||||||
|
m_failStage = "vigem_connect";
|
||||||
|
m_lastError = static_cast<uint32_t>(err);
|
||||||
if (err == VIGEM_ERROR_BUS_NOT_FOUND || err == VIGEM_ERROR_BUS_ACCESS_FAILED)
|
if (err == VIGEM_ERROR_BUS_NOT_FOUND || err == VIGEM_ERROR_BUS_ACCESS_FAILED)
|
||||||
m_driverMissing = true;
|
m_driverMissing = true;
|
||||||
vigem_free(static_cast<PVIGEM_CLIENT>(m_client));
|
vigem_free(static_cast<PVIGEM_CLIENT>(m_client));
|
||||||
@@ -164,11 +170,17 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback
|
|||||||
m_target = (platform == ControllerPlatform::PlayStation)
|
m_target = (platform == ControllerPlatform::PlayStation)
|
||||||
? vigem_target_ds4_alloc()
|
? vigem_target_ds4_alloc()
|
||||||
: vigem_target_x360_alloc();
|
: vigem_target_x360_alloc();
|
||||||
if (!m_target) { printf("[ViGEm] target alloc failed\n"); return; }
|
if (!m_target) {
|
||||||
|
m_failStage = "vigem_target_alloc";
|
||||||
|
printf("[ViGEm] target alloc failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
err = vigem_target_add(static_cast<PVIGEM_CLIENT>(m_client),
|
err = vigem_target_add(static_cast<PVIGEM_CLIENT>(m_client),
|
||||||
static_cast<PVIGEM_TARGET>(m_target));
|
static_cast<PVIGEM_TARGET>(m_target));
|
||||||
if (!VIGEM_SUCCESS(err)) {
|
if (!VIGEM_SUCCESS(err)) {
|
||||||
|
m_failStage = "vigem_target_add";
|
||||||
|
m_lastError = static_cast<uint32_t>(err);
|
||||||
printf("[ViGEm] target_add failed: 0x%08X\n", err);
|
printf("[ViGEm] target_add failed: 0x%08X\n", err);
|
||||||
vigem_target_free(static_cast<PVIGEM_TARGET>(m_target));
|
vigem_target_free(static_cast<PVIGEM_TARGET>(m_target));
|
||||||
m_target = nullptr;
|
m_target = nullptr;
|
||||||
@@ -186,6 +198,8 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback
|
|||||||
X360Notification,
|
X360Notification,
|
||||||
this);
|
this);
|
||||||
if (!VIGEM_SUCCESS(err)) {
|
if (!VIGEM_SUCCESS(err)) {
|
||||||
|
m_failStage = "vigem_target_x360_register_notification";
|
||||||
|
m_lastError = static_cast<uint32_t>(err);
|
||||||
printf("[ViGEm] notification registration failed: 0x%08X\n", err);
|
printf("[ViGEm] notification registration failed: 0x%08X\n", err);
|
||||||
vigem_target_remove(static_cast<PVIGEM_CLIENT>(m_client),
|
vigem_target_remove(static_cast<PVIGEM_CLIENT>(m_client),
|
||||||
static_cast<PVIGEM_TARGET>(m_target));
|
static_cast<PVIGEM_TARGET>(m_target));
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ public:
|
|||||||
bool IsValid() const { return m_valid; }
|
bool IsValid() const { return m_valid; }
|
||||||
bool IsDriverMissing() const { return m_driverMissing; }
|
bool IsDriverMissing() const { return m_driverMissing; }
|
||||||
|
|
||||||
|
// Why construction failed. Only meaningful when !IsValid(); FailStage() names
|
||||||
|
// the ViGEm call that failed and LastError() carries its VIGEM_ERROR code
|
||||||
|
// (0 when the failing call returns no code, e.g. the allocators).
|
||||||
|
const char* FailStage() const { return m_failStage; }
|
||||||
|
uint32_t LastError() const { return m_lastError; }
|
||||||
|
|
||||||
void Update(const uint8_t* buf, size_t n, const BackButtonConfig& backCfg, bool backMouseEnabled);
|
void Update(const uint8_t* buf, size_t n, const BackButtonConfig& backCfg, bool backMouseEnabled);
|
||||||
void OnRumble(uint8_t largeMotor, uint8_t smallMotor);
|
void OnRumble(uint8_t largeMotor, uint8_t smallMotor);
|
||||||
|
|
||||||
@@ -40,6 +46,8 @@ private:
|
|||||||
RumbleCallback m_rumbleCallback;
|
RumbleCallback m_rumbleCallback;
|
||||||
bool m_valid = false;
|
bool m_valid = false;
|
||||||
bool m_driverMissing = false;
|
bool m_driverMissing = false;
|
||||||
|
const char* m_failStage = "none";
|
||||||
|
uint32_t m_lastError = 0;
|
||||||
|
|
||||||
// DS4 touchpad tracking
|
// DS4 touchpad tracking
|
||||||
bool m_trackpadMouseEnabled = false;
|
bool m_trackpadMouseEnabled = false;
|
||||||
|
|||||||
+20
-2
@@ -639,8 +639,26 @@ VIGEM_ERROR vigem_target_add(PVIGEM_CLIENT vigem, PVIGEM_TARGET target)
|
|||||||
|
|
||||||
//
|
//
|
||||||
// Don't leave device connected if the wait call failed
|
// Don't leave device connected if the wait call failed
|
||||||
//
|
//
|
||||||
error = vigem_target_remove(vigem, target);
|
// LOCAL PATCH (SteamlessController): as shipped, this cleanup is a
|
||||||
|
// no-op. IOCTL_VIGEM_PLUGIN_TARGET above already succeeded, so the
|
||||||
|
// child device exists, but State is only set to CONNECTED on the two
|
||||||
|
// success paths above — so vigem_target_remove() trips its own
|
||||||
|
// "State != VIGEM_TARGET_CONNECTED" guard and returns without ever
|
||||||
|
// sending IOCTL_VIGEM_UNPLUG_TARGET. The device is then orphaned in
|
||||||
|
// the bus with no owning process until the machine reboots or the bus
|
||||||
|
// is restarted. SerialNo still holds the slot that was plugged in, so
|
||||||
|
// marking the target connected lets the unplug actually go out.
|
||||||
|
//
|
||||||
|
// The error code is restored afterwards: a successful unplug would
|
||||||
|
// otherwise make vigem_target_add() report VIGEM_ERROR_NONE (and
|
||||||
|
// register the target in pTargetsList below) for a device that is no
|
||||||
|
// longer plugged in. VIGEM_ERROR_TARGET_NOT_PLUGGED_IN is what the
|
||||||
|
// unpatched code effectively returned here.
|
||||||
|
//
|
||||||
|
target->State = VIGEM_TARGET_CONNECTED;
|
||||||
|
(void)vigem_target_remove(vigem, target);
|
||||||
|
error = VIGEM_ERROR_TARGET_NOT_PLUGGED_IN;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user