diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index e637ed5..057d8a1 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -427,7 +427,8 @@ ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut, if (sc->IsOpen()) sc->SetRumble(largeMotor, smallMotor); }); 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); if (slot.vc->IsDriverMissing()) vigemMissingOut = true; slot.vc.reset(); diff --git a/src/app/TrayApp.cpp b/src/app/TrayApp.cpp index c08d6b0..f0d2c4f 100644 --- a/src/app/TrayApp.cpp +++ b/src/app/TrayApp.cpp @@ -512,9 +512,13 @@ void TrayApp::RemoveTrayIcon() { } 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" + : vigemMissing ? L"Steamless Controller — ViGEmBus driver unavailable" : connected ? L"Steamless Controller — Connected (Steamless Mode OFF)" : L"Steamless Controller — No controller found"; @@ -548,7 +552,14 @@ void TrayApp::OpenEventLog() { 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() { + if (m_vigemBalloonShown) return; + m_vigemBalloonShown = true; + NOTIFYICONDATAW nid{}; nid.cbSize = sizeof(nid); nid.hWnd = m_hwnd; @@ -556,7 +567,9 @@ void TrayApp::ShowViGEmBalloon() { nid.uFlags = NIF_INFO; nid.dwInfoFlags = NIIF_WARNING; 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); } diff --git a/src/app/TrayApp.h b/src/app/TrayApp.h index 6d970ef..393954c 100644 --- a/src/app/TrayApp.h +++ b/src/app/TrayApp.h @@ -76,6 +76,7 @@ private: int m_acquireRetries = 0; ULONGLONG m_lastCycleTick = 0; bool m_elevationBalloonShown = false; + bool m_vigemBalloonShown = false; bool m_startupEnabled = false; int m_startupMechanism = 0; // 0 none, 1 Run key, 2 elevated task bool m_notificationsEnabled = true; // disconnect/stall balloons diff --git a/src/app/VirtualController.cpp b/src/app/VirtualController.cpp index 6cbb0cb..8f271f9 100644 --- a/src/app/VirtualController.cpp +++ b/src/app/VirtualController.cpp @@ -150,10 +150,16 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback : m_platform(platform), m_rumbleCallback(std::move(rumbleCallback)) { 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(m_client)); if (!VIGEM_SUCCESS(err)) { + m_failStage = "vigem_connect"; + m_lastError = static_cast(err); if (err == VIGEM_ERROR_BUS_NOT_FOUND || err == VIGEM_ERROR_BUS_ACCESS_FAILED) m_driverMissing = true; vigem_free(static_cast(m_client)); @@ -164,11 +170,17 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback m_target = (platform == ControllerPlatform::PlayStation) ? vigem_target_ds4_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(m_client), static_cast(m_target)); if (!VIGEM_SUCCESS(err)) { + m_failStage = "vigem_target_add"; + m_lastError = static_cast(err); printf("[ViGEm] target_add failed: 0x%08X\n", err); vigem_target_free(static_cast(m_target)); m_target = nullptr; @@ -186,6 +198,8 @@ VirtualController::VirtualController(ControllerPlatform platform, RumbleCallback X360Notification, this); if (!VIGEM_SUCCESS(err)) { + m_failStage = "vigem_target_x360_register_notification"; + m_lastError = static_cast(err); printf("[ViGEm] notification registration failed: 0x%08X\n", err); vigem_target_remove(static_cast(m_client), static_cast(m_target)); diff --git a/src/app/VirtualController.h b/src/app/VirtualController.h index 9e3d9c0..b5ff53f 100644 --- a/src/app/VirtualController.h +++ b/src/app/VirtualController.h @@ -19,6 +19,12 @@ public: bool IsValid() const { return m_valid; } 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 OnRumble(uint8_t largeMotor, uint8_t smallMotor); @@ -40,6 +46,8 @@ private: RumbleCallback m_rumbleCallback; bool m_valid = false; bool m_driverMissing = false; + const char* m_failStage = "none"; + uint32_t m_lastError = 0; // DS4 touchpad tracking bool m_trackpadMouseEnabled = false; diff --git a/third_party/ViGEmClient-b66d02d/src/ViGEmClient.cpp b/third_party/ViGEmClient-b66d02d/src/ViGEmClient.cpp index f01a4c8..ab078fe 100644 --- a/third_party/ViGEmClient-b66d02d/src/ViGEmClient.cpp +++ b/third_party/ViGEmClient-b66d02d/src/ViGEmClient.cpp @@ -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 - // - 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; } }