Manual mode could not take the controller back from a running Steam. The tray toggle called ControllerManager::EnableGameMode() once and discarded the result, so a Blocked outcome -- Steam holding the exclusive write handle -- was simply dropped. The auto modes wrapped the same call in TryAcquireController(), which escalates a blocked claim to a device cycle via the elevated helper and retries on re-arrival. Only closing Steam freed the pad in manual mode. The two paths diverged because acquiring is asynchronous: a blocked claim fires a cycle through Task Scheduler and comes back seconds later as a WM_DEVICECHANGE, so the retry loop lives in timers and message handlers rather than on the stack. Each of those re-entry points has to re-answer "do we still want control?", and the code answered it by recomputing m_autoMode != Manual && WantControl(steamState) -- deriving intent from Steam. That works in the auto modes, where Steam's state is the intent. Manual mode had nothing to derive from: its intent lived in a menu click that was never recorded anywhere. WantControl() said as much, returning false for Manual with the comment "tray toggle decides" -- and the tray toggle's decision went unstored. Store it. m_wantControl replaces the derived guard at all three re-entry points (the IDT_ACQUIRE and IDT_WAKE_POLL timers, and the arrival branch of WM_DEVICECHANGE), leaving WantControl() with a single caller in ApplySteamState. The toggle sets it and calls the shared path, so manual mode gets the retry burst, device cycling, wake polling, and the failure balloon it never had. Release is unified the same way: ReleaseControl() lifts the yield branch out of ApplySteamState, substituting m_steamWatcher.GetState() for the SteamState parameter the manual caller has no access to. Manual toggle-off now also cycles the devnode, so a running Steam sees an arrival event and re-adopts the pad instead of leaving it unclaimed. Three follow-on details: - Manual toggle-on calls EnsureCycleTaskRegistered(), matching what IDM_MODE_GAME already does, so the one-time UAC prompt lands on a menu click rather than ambushing the first cycle. - SetAutoMode(Manual) adopts IsGameModeActive() rather than inheriting a stale m_wantControl from the auto mode being left. - The toggle's menu label tracks m_wantControl in manual mode, not the live handle state. Acquisition can spend several seconds cycling, and a second click during that window should cancel rather than start over. Both AUTO: log lines in the acquire path are now reachable from manual mode, so they are relabelled ACQUIRE: -- these are exactly the lines read when diagnosing a failed handoff, and the old prefix pointed away from it. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
SteamlessController
A lightweight Windows system tray app that lets you use a Steam Controller as a standard gamepad — without Steam running.
When Steamless Mode is active, the app disables the controller's built-in keyboard/mouse emulation (lizard mode) and exposes it as a virtual Xbox 360 controller via ViGEmBus, making it compatible with any game that supports XInput or the Xbox controller.
Features
- NEW in 1.9 - Bluetooth! Use the controller wired, through the dongle, or paired over Bluetooth LE
- NEW in 1.8 - Automatically enable/disable based on if Steam is running, or a Steam game is running!
- NEW in 1.8 - Advanced debugging and local logging to help diagnose issues
- Rumble Support v2.0
- Support for multiple Steam Controllers! Long live split screen!
- New shiny UI for remapping L4/L5 R4/L5 back buttons!
- Support for Xbox OR PlayStation controllers - with PS Touchpad and Gyro plumbing.
- System tray icon shows connection and mode status
- Steamless Mode — disables lizard mode and exposes controller as Xbox 360 gamepad
- Trackpad Mouse — use the right (or left) trackpad as a mouse cursor
- Back Buttons for Clicking — map R4/R5 (or L4/L5) to left/right mouse click
- Use Left Trackpad Instead — mirror all trackpad/back-button functionality to the left side for left-handed users
- Start with Windows — launch automatically at login
- Settings persist across restarts
Requirements
To run
- Windows 10 or later (64-bit)
- ViGEmBus driver installed
- Steam Controller — wired USB (PID
0x1302), wireless dongle (PID0x1304), or Bluetooth LE (PID0x1303, pair via Windows Bluetooth settings) - Steam closed (Steam claims the controller when running)
To build
- Visual Studio 2022 with the Desktop development with C++ workload
- CMake 3.20 or later (included with Visual Studio, or install separately)
- Windows SDK 10.0.22000 or later (installed via Visual Studio Installer)
Building
git clone https://github.com/your-username/SteamlessController.git
cd SteamlessController
cmake -B build
cmake --build build --target SteamlessController
The executable will be at build\Debug\SteamlessController.exe.
For a release build:
cmake -B build/release -G "Visual Studio 18 2025"
cmake --build build/release --config Release --target SteamlessController
If you have Visual Studio 2022, replace
"Visual Studio 18 2025"with"Visual Studio 17 2022".
CMake Targets
| Target | Description |
|---|---|
SteamlessController |
Main system tray application |
SteamProbe |
Console diagnostic tool — dumps raw HID report bytes as you interact with the controller. Useful for protocol research. |
RawControllerProbe |
Checks whether Windows.Gaming.Input.RawGameController can enumerate the Steam Controller (requires WinRT). |
How it works
The Steam Controller exposes a vendor HID collection (usage page 0xFF00) that carries all game input in a 54-byte report (ID 0x42) at ~60 Hz. By default the firmware runs in lizard mode, emulating a keyboard and mouse so the controller works without drivers.
SteamlessController sends HID feature reports to disable lizard mode, then reads the raw input reports and translates them into a virtual Xbox 360 controller via ViGEmBus. A background heartbeat re-sends the disable command every 800 ms to keep lizard mode off.
The full input report layout is documented in src/steam/SteamController.h.
Third-party
- ViGEmClient — MIT License, built from source as a static library