Skip to content

Steam coexistence strategies (discussion) - #49

Draft
ddeverill wants to merge 11 commits into
mainfrom
steam-coexistence
Draft

ddeverill wants to merge 11 commits into
mainfrom
steam-coexistence

Conversation

@ddeverill

@ddeverill ddeverill commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Draft — not for merge. Splitting @danbosscher's Steam coexistence work out
of #44 so the puck activation fix can land on its own, and so this can be
discussed at its own pace. All eight commits are preserved verbatim with
original authorship.

The problem being solved

Since the firmware update, "could not get an exclusive handle" is no longer
reliable proof that Steam owns the controller — something in Windows keeps a
compatible write handle open on the live collection even with Steam closed.
Sharing the handle with a running Steam produced duplicate input or a virtual
pad that received nothing, depending on startup order.

What's in here

  • Three selectable policies under a Debug: Steam coexistence menu: yield to
    Steam, reserve the controller via Steam's controller_blacklist, or launch
    Steam with -nojoy
  • Fails closed: only take the controller when Steam is absent or there is
    current-session proof Steam cannot see it, yielding immediately if that
    proof disappears
  • config.vdf read/modify/write with a backup and atomic replace, plus
    uninstall-time restoration
  • Tray single-click toggle, Nereid 0x1305 support, sleep handling, extra
    diagnostics

Open questions before any of this lands

1. Is exclusivity actually unobtainable? This design rests on that premise,
but two bugs were making the device cycle a silent no-op: the scheduled task
pointed at a stale helper that didn't know the current PIDs, and
DIF_PROPERTYCHANGE reports success while not restarting a devnode that has an
open handle. Both are fixed on main — cycling is disable/enable now, and
cycle.log distinguishes CYCLED from FELL BACK TO RESTART. Testing was
done from a build folder, which almost certainly had no registered cycle task
at all. Worth re-measuring before designing around the premise.

2. It removes the handoff mode that we want to make default in the future. SetSteamMayOwnController
is true whenever Steam runs, idle or in-game, and a shared claim is then
refused — so "Steamless while Steam idles, hand off during games" stops working
unless the user blacklists the controller.

3. Writing config.vdf is persistent state outside our app. The
implementation is careful, but if the app dies between apply and restore, the
user's Steam silently can't see their controller with nothing pointing back at
us. Documenting Steam's own blacklist UI may be the better trade.

4. Uninstall must not be blocked. Abort on failed restoration means a user
whose Steam won't shut down cleanly can't remove the app — worse than leaving
the setting behind.

5. What's actually causing the doubled D-pad? Three candidates with different
fixes: Steam reading the same physical device (inherent to sharing, since HID
delivers reports to every open handle); Steam re-processing our ViGEm pad via
Xbox Configuration Support; or lizard mode oscillating because both processes
hold write access and our keepalive fights Steam's mappings every 2s.
Separating them: press D-pad in Notepad, count devices in joy.cpl, and toggle
off Xbox Configuration Support.

Rebasing

This currently overlaps with #44 because both share the puck fix. Once #44
merges, replay just the coexistence commits onto main:

git rebase --onto main a532804 steam-coexistence

🤖 Generated with Claude Code

danbosscher and others added 11 commits August 1, 2026 15:32
…dpoint

# Conflicts:
#	src/main.cpp
#	src/steam/SteamController.cpp
The shared-access fallback fixed a real problem — something in Windows
holds a compatible write handle on the live puck collection even with
Steam closed — but applied unconditionally it also swallowed the signal
the Steam handoff depends on.

A failed exclusive claim is precisely how the app detects that Steam is
holding the controller: game mode failing to activate is what escalates
TryAcquireController to the device cycle that takes the handle back. With
the fallback always succeeding, the app would instead report success and
drive the controller alongside Steam, both sending competing lizard-mode
feature reports, and would never cycle. None of the PR's validation would
catch it, since every check was run with Steam closed.

Gate it on whether steam.exe is running. TrayApp already watches that for
the auto modes, so it now pushes presence into ControllerManager from the
WM_STEAMSTATE handler — outside ApplySteamState, which returns early in
Manual mode, because this decision matters in every mode.

Also cheapen the state-report probe. It costs the full 250ms timeout on an
empty puck slot and runs per slot, while the acquire path retries in a
burst of eleven — three empty slots would have blocked the UI thread for
roughly eight seconds. Slots found silent are skipped for 1.5s, so the
burst probes each at most once. And WaitForStateReport now stops when a
read returns instantly rather than timing out, instead of spinning on a
dead handle for the rest of the window.

The skip message no longer says "puck", since the gate applies to every
transport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ddeverill

Copy link
Copy Markdown
Owner Author

I asked it to move the changes to this branch to save me some time and it decided to put a summary at the top instead of me :) Also it took my feedback on the uninstall stuff way more seriously than I meant. Less worried about it if we need to go that path. Claude makes it seem like it's the end of the world.

@ddeverill

Copy link
Copy Markdown
Owner Author

Ok, finally got the puck and other high priority parts merged in and tested. As I mentioned in the other PR, It'd be great if you could uninstall/reinstall with 1.9 and from there, see if you're still seeing the issue with release and requisition not happening. If you are, let's keep digging in here and figure out the right path forward. Like you, I don't have a ton of folks with Steam Controllers but I've tested on 3 different machines and had one user that's reached out take a peek at the update and it's working for them as well. Hopefully you're just encountering the same issue that I was when I was building out BT support and this resolves it!

Also as a heads up, it can take up to 10s to get the controller back when you close a steam game, there's logic to poll the open slots on the puck and it can take a bit.

@danbosscher

danbosscher commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

codex

Retested exactly as requested against a genuinely clean v1.9 install, and the release/requisition issue still reproduces on this machine.

Clean baseline

  • Uninstalled the existing packaged 1.8 copy.
  • Removed the development Run entry, old HKCU\Software\SteamlessController settings, old app data/logs, and the stale helper task.
  • Downloaded the official v1.9 release asset and verified SHA-256 2b6f8d9e443f9ef21bf4434b2f99db6cd31e9c0dfb01d672595949f39b1068a4.
  • Installed v1.9. The new task is Highest, Ready, and points to C:\Program Files\SteamlessController\SteamlessDeviceCycle.exe.
  • Tested the puck (VID 28DE / PID 1304), with the live controller on mi_02.

Results

  1. Steam running, idle, fresh 1.9 start: Steamless sees live reports on mi_02, but its exclusive claim is blocked. It cycles and retries, then fails to enable game mode.
  2. Steam stopped: Steamless immediately uses its shared fallback and enables game mode. XInput shows exactly one connected slot and the controller reports battery/input normally.
  3. Steam restarted idle: Steamless remains active, as expected for “Off ONLY while in Steam game”.
  4. Real Steam game launched (Megabonk, app 3405340): handoff works. The watcher reports inGame, Steamless disables ViGEm, closes its handles, and cycles the puck.
  5. Game closed: after the ~6 second debounce, Steamless attempts to reclaim. Steam wins the live endpoint after every re-enumeration. At 21:10:06.894, Steamless logs AUTO: giving up acquiring after 3 device cycles.

The important part is that this is no longer the stale/no-op helper problem. cycle.log confirms successful cycles of the live endpoint:

21:09:42.065  START: 4 interface(s) to cycle
21:09:52.368  CYCLED (transport=dongle err=0) ...mi_02...
21:09:52.368  DONE: exit=0
...
21:10:02.875  CYCLED (transport=dongle err=0) ...mi_02...
21:10:02.876  DONE: exit=0

Immediately after each mi_02 disconnect/reconnect, the app returns to:

GAMEMODE: exclusive claim blocked while Steam is running ...mi_02...

One timing detail may matter: cycling all four puck interfaces takes about 10 seconds here. The app's 4-second retry fires “attempt 2” while the first scheduled task is still running, so the log counts three attempts but only two helper invocations actually run. Even after the completed mi_02 cycles, though, Steam still reacquires first.

So my result is: release works; requisition while Steam remains running still fails on this puck/machine with clean 1.9. Steamless only recovers once steam.exe exits.

@danbosscher

danbosscher commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

codex

Correction/extra control run: I found that an earlier SteamProbe.exe --help invocation had treated the unsupported --help argument as normal probe mode and survived the shell timeout. It was holding the empty mi_04 slot. That explains the first mi_04 FELL BACK TO RESTART and inflated part of the ~10s helper timing in my previous comment.

I killed every probe, stopped both Steam and Steamless, and reran the checks:

  • With Steam, Steamless, and all probes stopped, all four FF00:0001 puck slot collections still reject an exclusive open with ERROR_SHARING_VIOLATION; the live mi_02 slot is still streaming 0x42 reports.
  • I repeated the real Steam-game handoff/reclaim test with no probe process present.
  • Handoff still works.
  • During reclaim, cycle.log shows successful CYCLED (err=0) results for every endpoint, including live mi_02. A full helper pass is about 5.5s once the stale probe is removed.
  • Immediately after each successful mi_02 cycle, Steamless still logs exclusive claim blocked while Steam is running and ultimately gives up.

So the stale probe affected timing but not the result. The corrected finding is: the device cycle is real and succeeds, but the live endpoint is already opened again before Steamless can claim it. Separately, exclusive access is unavailable on this machine even with both applications stopped, so I am now tracing which Windows/system service owns the remaining shared HID handles.

@danbosscher

danbosscher commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

I'm on Windows 11 25H2 build 26200, Microsoft.GameInput 3.3.221.0.

@danbosscher

Copy link
Copy Markdown
Contributor

Looks like GameInput 3.4.218 is available. Upgrading to that, seeing if that fixes the issue.

@danbosscher

Copy link
Copy Markdown
Contributor

Yeah I'm hitting a dead end on other options / root causes - what Windows versions are you on?

@ddeverill

Copy link
Copy Markdown
Owner Author

I'm on 25H2, build 26200, so it should be the same as yours :/

@ddeverill

Copy link
Copy Markdown
Owner Author

One random thing for you to try while I try this out on other machines. Can you try plugging the controller in via USB and see if SteamlessController reclaims the device from Steam after you exit Megabonk? I wonder if part of this is timing. When I reboot the device internally, there is a bit of a race to claim it, but (at least unless something changed) I used to grab immediately after release so we'd always beat Steam to getting it back. I wonder if with the changes for enumerating the puck we're somehow slower now and can't snag it before Steam. USB should be super fast though.

@ddeverill

Copy link
Copy Markdown
Owner Author

Also, what does this page look like for you when SteamlessController has the controller and Steam is open? Do you just see one, or do you see many Xbox Controllers?
image

@ddeverill

Copy link
Copy Markdown
Owner Author

Ok, while I was digging in here, I genuinely found an issue with the release/reclaim logic for controllers. I've got two paths internally, a result of the fact that I added the AUTO modes later. I pushed some updates to main that unifies the logic. Would you be able to reboot your machine, and try the new version v1.10 (or MAIN)? I also found an issue where ViGEm was leaking devices and it could clog up the device list (why I earlier asked about that screen in Steam).

If this is still happening with these changes, can you attach your logs and I'll take a peek? I added some more logging to the builds.

@ddeverill

Copy link
Copy Markdown
Owner Author

One more datapoint! I went ahead and added some more logging. When SteamlessController isn't able to take the handle away from whatever app has it, I go ahead and walk the tree of active apps and see who has a handle to the controller and print that in the logs (I also print that in the popup that shows up when we're unsuccessful. Let me know what it says when it's unable to take the controller away. From all my testing, Steam never takes exclusive ownership of the controller, so I wonder if something else is running and is much more aggressive.

@ddeverill

Copy link
Copy Markdown
Owner Author

Hey @danbosscher I just put up version 1.18 tonight, and I think it might fix the issue here. I went through a lot of iterations, and the issue below was that I wasn't grabbing the controller as aggressively as I could have.

For some more details for the more technically minded. I was waiting on the signal for the device showing up to snag it (Steam likely is doing the same thing). I instead start grabbing from the moment I cycle the device - which makes it much faster generally). Also, I cycled all the devices which includes all 4 heads on the puck. This can end up causing a lot of delay, when I really should have just been cycling only the controllers that are attached, and trying to grab those slots again. No reason to assume someone's going to plug in another controller right when I'm cycling. With all that, I've dropped slot acquisition down from 100s of ms or even over a second in some cases down to single digit, low double digits. And on 3 attempts, we saw the app winning all the time even on another device that's hitting the same issues you were. Give it a shot and see if it's working for you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants