Repository navigation
Conversation
…SteamOS as itself
…feature report 1 comes back as 65 bytes)
…nager until it is granted
…state goes to SteamOS as a wired Steam Controller (28de:1302)
…-pad steamcontroller): only a Steam Controller passed through as itself, other controllers as Xbox 360; Automatic passes it through too, no longer behind Pass a DualSense through
…team Controllers apart from DualSenses
fxgl
left a comment
There was a problem hiding this comment.
Thanks! Passing the Steam Controller through as itself is very welcome. The BLE bus mapping, re-presenting a Puck slot as a wired 28de:1302, and the per-device input buffer lifetime all look right. Two issues break setups that work today, so I can't merge it as is:
-
The Steam Controller takes player 1's slot unconditionally (
Gamepad.swift:379-382). The Steam Controller branch inwanted()runs before everything else. It doesn't check that player 1's GameController controller is the Steam Controller, and there's no equivalent of thepassthroughAllowed()guard the DualSense has (Gamepad.swift:634-636). With the default additional controllers on:- DualSense is player 1 and the Steam Controller (via the Puck) is player 2. Slot 0 gets the uhid Steam Controller, slot 1 gets an Xbox 360 pad from the same Steam Controller via GameController. Every press arrives twice, and the DualSense disappears.
- DualSense plus a Steam Controller over BLE (GameController doesn't see it). Player 1 is the DualSense, but slot 0 passes the Steam Controller through, so the DualSense is dead in SteamOS. This works before the PR.
- Two Steam Controllers on one Puck. The first live slot in HID order is passed through, and it may not be player 1's controller.
Suggested fix: take the Steam Controller branch only if player 1 has no GameController controller or it is the Steam Controller itself. Don't take it when another player's GameController controller is the Steam Controller (mirroring
passthroughAllowed). Could you log how GameController names the Puck's controller (vendorName/productCategory), so it can be matched? -
The Input Monitoring retry never opens anything (
HIDPassthrough.swift:128, 136-148).IOHIDManagerOpenon a manager that is already open returnskIOReturnSuccessright away and doesn't touch its devices. I disassembled it on macOS 27.0.1; IOKitUser'sIOHIDManager.chas the samerequire(!manager->isOpen). The TCC denial is also cached per IOHIDDevice.- So 3 s after start the timer logs "HID manager opened (Input Monitoring granted)", even before the user has clicked anything, and stops. The devices stay unopened until the launcher restarts or the device is reconnected.
- The matching callback also fires for devices that failed to open, and the result is ignored (
{ ctx, _, _, device in). Non-Puck devices are alwayslive, so an unopened BLE controller is passed through as a dead device: no input, EIO on every GET_REPORT. Together with (1), it also displaces player 1. - Devices that appear after start never call
awaitPermission.
Suggested fix: pass
resultintoadded, and don't add devices that failed to open (or don't treat them as live); log the reason once. For granting without a restart, pollIOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGrantedand then recreate the manager (close, then a newstart()). Or drop the timer and log "restart the launcher after granting". Please check this on hardware: README en/ru/zh currently say the controller opens as soon as access is granted.
Worth fixing too:
- Liveness counts any report except 0x79 (
HIDPassthrough.swift:180). SDL treats 0x46 as wireless status as well, and the state reports are 0x42/0x45/0x47. A whitelist of state report IDs avoids phantom uhid devices if the Puck sends 0x46 on an empty slot. - The 2 s liveness window assumes the controller keeps sending state. In lizard mode (before Steam in the guest takes the controller), it may only send keyboard/mouse reports when something changes; then it would flap between uhid and the Xbox pad. Could you share a log of an untouched controller over 10+ s, before Steam starts and in a game?
- The Puck (28de:1304) is matched unconditionally (
HIDPassthrough.swift:33, 110-113). Every Puck owner now gets the "receive keystrokes from any application" prompt, even with Xbox 360 or DualSense selected. Match Valve devices only for auto/steamcontroller, or document the prompt.
Nits:
puckTimernever stops after the Puck is gone.--helpfor--paddoesn't mentionsteamcontroller.- The README's
pad statetext still says "connected DualSenses". - Use a monotonic clock (
ProcessInfo.systemUptime) instead ofDate()for liveness. - Please squash it into one commit, which is how PRs land here.
Checked and fine:
- Both BLE transport strings map to BUS_BLUETOOTH.
- 1302/1303 are handled by hid-steam and SDL, and SDL's interface 2-5 check justifies re-presenting the slot as wired 1302/USB.
- GET_REPORT headroom returns the real length to the guest.
- DualSense passthrough still doesn't need Input Monitoring.
- The new setting value is safe on rollback.
- xcstrings is complete, and README en/ru/zh are in sync.
- It merges cleanly with current main and builds.
Steam Controller (2026) currently reaches SteamOS only as the launcher's Xbox 360 uinput pad, and only through its Puck: over Bluetooth LE GameController does not show it at all. This passes it through as itself over the existing uhid path (the same one the DualSense uses), so Steam drives its trackpads, gyro, back buttons and haptics with its own driver. The guest side needed no changes:
fx-pad/uhid already take any vendor/product.What changes
28de:1303): passed through with its own IDs and the Bluetooth bus (Bluetooth Low Energytransport mapped toBUS_BLUETOOTH). It does not need a GameController controller.28de:1304): interfaces 2–5 are the controller slots, with the controller's own 372-byte descriptor. SDL (SDL_hidapi_steam_triton.c) and Steam take the Puck's controllers only by USB interface number 2–5, which a uhid device does not have. So the slot that carries controller state goes to SteamOS as a wired Steam Controller (28de:1302, USB bus), which they take by IDs. A slot without controller state for 2 s is removed (controller off, moved to another slot or to Bluetooth), and the Puck's GameController pad takes over. Wireless status reports (0x79) do not count as state.--pad steamcontroller): only a Steam Controller passed through, any other controller drives an Xbox 360 pad. Automatic passes a Steam Controller through too. It no longer depends on Pass a DualSense through.IOHIDManagerOpenreturnskIOReturnNotPermitted, the launcher callsIOHIDRequestAccessand retries every 3 s, so granting it takes effect without a restart.kIOReturnNoSpaceand Steam never initialised the controller. GET_REPORT buffers now get 64 bytes over the descriptor's sizes.pad statecounts Steam Controllers apart from DualSenses. README en/ru/zh: “Steam Controller passthrough”.Tested
On an M3 Max, macOS 27.0.1, with 1.9.4 (patch rebased onto
mainwithout conflicts; the launcher builds on both):SteamOS sees the Steam Controller itself, passed through, input0x45, feature reports without errors; Steam shows the controller and it works.Puck slot 3: controller state (report 0x42, 54 bytes)→SteamOS sees the Valve Steam Controller itself, no IOReturn errors; works in Steam.Not tested: several Steam Controllers on one Puck, and a Steam Controller as player 2–4. Passthrough goes to player 1 only, as for the DualSense.