feat(steam): raw_gadget virtual Deck — full Steam Input recognition (proven on Deck)

The interface-2 wall is climbed. packaging/linux/steam-deck-gadget/deck_raw_gadget.c
is a raw_gadget userspace emulator of a real 3-interface USB Steam Deck (28DE:1205,
mouse=0/keyboard=1/controller=2) on a dummy_hcd loopback UDC, with descriptors
captured verbatim from a physical Deck and full HID feature-report handling.

Live on a real Deck (SteamOS 3.8.11): hid-steam reads our serial (PFDECK000),
creates the Steam Deck + Motion Sensors evdevs, and Steam Input PROMOTES it —
controller.txt "Interface: 2 ... device opened ... reserving XInput slot 1" +
"input: Microsoft X-Box 360 pad 1". Stable (1 connect, 0 disconnects, no zombie);
the kernel Steam Deck evdev is then grabbed by Steam Input which exposes its own
X-Box pad, exactly like a real Deck. First time a virtual Deck is fully Steam-Input
promoted (UHID can't — it has no USB interface number, so Steam filters it).

Also includes the configfs f_hid variant (configfs_gadget_up/down.sh) — the minimal
reproducer that proved interface 2 makes Steam open+XInput-reserve the device, but
f_hid can't serve feature reports so Steam dropped it as a zombie.

Gotchas documented in the README: 7-byte vs 9-byte endpoint descriptor, no-data OUT
controls acked via zero-length EP0_READ (not WRITE, else error -110), streamer must
not start before SET_CONFIGURATION is acked. SteamOS-host only (needs dummy_hcd +
raw_gadget). Recognition proven; feeding real client reports + a host backend is next.
Not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:38:26 +00:00
parent a81f1304cd
commit 8870e85233
5 changed files with 452 additions and 0 deletions
@@ -0,0 +1,86 @@
#!/bin/bash
# PoC: stand up a REAL 3-interface USB Steam Deck (28DE:1205) on a dummy_hcd loopback UDC, with the
# controller on **interface 2** (kbd=0, mouse=1) — the structure Steam's controller driver filters
# for. Run as root on the Deck (which ships dummy_hcd + configfs f_hid). Then we check: does hid-steam
# bind interface 2, and does the Deck's own Steam promote it (controller.txt "Interface: 2")?
set -e
G=/sys/kernel/config/usb_gadget/pfdeck
echo "== modprobe dummy_hcd + libcomposite =="
modprobe dummy_hcd
modprobe libcomposite
UDC=$(ls /sys/class/udc | grep -i dummy | head -1)
echo "dummy UDC: ${UDC:-<none found!>}"
[ -n "$UDC" ] || { echo "no dummy UDC — abort"; exit 1; }
# Tear down a prior instance if present.
if [ -d "$G" ]; then
echo "" > "$G/UDC" 2>/dev/null || true
for l in "$G"/configs/c.1/hid.usb*; do [ -e "$l" ] && rm -f "$l"; done
rmdir "$G"/configs/c.1/strings/0x409 2>/dev/null || true
rmdir "$G"/configs/c.1 2>/dev/null || true
rmdir "$G"/functions/hid.usb* 2>/dev/null || true
rmdir "$G"/strings/0x409 2>/dev/null || true
rmdir "$G" 2>/dev/null || true
fi
echo "== build gadget $G =="
mkdir -p "$G"; cd "$G"
echo 0x28de > idVendor
echo 0x1205 > idProduct
echo 0x0110 > bcdDevice
echo 0x0200 > bcdUSB
mkdir -p strings/0x409
echo "Valve Software" > strings/0x409/manufacturer
echo "Steam Deck Controller" > strings/0x409/product
echo "PFDECK0001" > strings/0x409/serialnumber
# --- interface 0: boot keyboard ---
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
printf '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x03\x95\x05\x75\x01\x05\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x03\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' > functions/hid.usb0/report_desc
# --- interface 1: boot mouse ---
mkdir -p functions/hid.usb1
echo 2 > functions/hid.usb1/protocol
echo 1 > functions/hid.usb1/subclass
echo 4 > functions/hid.usb1/report_length
printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02\x95\x01\x75\x05\x81\x03\x05\x01\x09\x30\x09\x31\x15\x81\x25\x7f\x75\x08\x95\x02\x81\x06\xc0\xc0' > functions/hid.usb1/report_desc
# --- interface 2: the Steam Deck controller (STEAMDECK_RDESC) ---
mkdir -p functions/hid.usb2
echo 0 > functions/hid.usb2/protocol
echo 0 > functions/hid.usb2/subclass
echo 64 > functions/hid.usb2/report_length
printf '\x06\x00\xff\x09\x01\xa1\x01\x15\x00\x26\xff\x00\x75\x08\x95\x40\x09\x01\x81\x02\x09\x01\x95\x40\xb1\x02\xc0' > functions/hid.usb2/report_desc
# --- config, link in order so interface numbers are 0,1,2 ---
mkdir -p configs/c.1/strings/0x409
echo "Punktfunk virtual Deck" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
ln -s functions/hid.usb0 configs/c.1/
ln -s functions/hid.usb1 configs/c.1/
ln -s functions/hid.usb2 configs/c.1/
echo "== bind to $UDC =="
echo "$UDC" > UDC
sleep 2
echo ""; echo "===== VERIFY ====="
echo "--- /sys hid devices for 28DE (which interface, which driver) ---"
for d in /sys/bus/hid/devices/*28DE*; do
[ -e "$d" ] || continue
rp=$(readlink -f "$d")
echo " $(basename "$d"): bInterfaceNumber=$(cat "$rp/../bInterfaceNumber" 2>/dev/null) driver=$(basename "$(readlink -f "$d/driver" 2>/dev/null)")"
done
echo "--- hidg char devices (controller = hidg for interface 2) ---"; ls -1 /dev/hidg* 2>/dev/null
echo "--- kernel log (hid-steam bind + Steam Deck evdev) ---"
journalctl -k --since "20 seconds ago" --no-pager 2>/dev/null | grep -iE "steam|28de|1205|hid-generic" | tail -10
echo "--- /proc input: Steam Deck evdevs created? ---"
grep -c '^N: Name="Steam Deck' /proc/bus/input/devices | sed 's/^/ Steam Deck input nodes: /'
echo "--- lsusb ---"; lsusb -d 28de:1205 2>/dev/null || true
echo ""
echo "Gadget is UP. Feed a neutral controller report with: printf '\\x01\\x00\\x09\\x3c' | dd of=/dev/hidg2 ..."
echo "Tear down with: deck_gadget_down.sh"