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:
Dylan DeverillandClaude Opus 5 authored and GitHub committed 2026-08-05 01:15:29 -07:00
1 parent 94f1bf94dd
commit 9ef3609da5
6 files changed
+62 -7

No files matched your search

+2 -1
View File
@@ -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
View File
@@ -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);
} }
+1
View File
@@ -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
+16 -2
View File
@@ -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));
+8
View File
@@ -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
View File
@@ -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;
} }
} }