Forward a physical controller connected to a Linux host into a Windows VM over LAN, where it appears as a virtual Xbox 360 controller through ViGEmBus.
Pop!_OS / Linux host
physical controller -> /dev/input/js*
C++ sender -> UDP packets
Windows VM
C# receiver -> ViGEmBus
virtual Xbox 360 controller
Working end to end:
- Linux sender reads a selected
/dev/input/js*controller. - Sender maps the Linux controller state into Xbox-style packets.
- Sender streams fixed-size UDP packets at a configurable tick rate.
- Windows receiver decodes those packets.
- Receiver updates a ViGEmBus virtual Xbox 360 controller.
- Receiver releases all inputs if packets stop for about 250 ms.
- Both sender and receiver support silent mode.
This project is not affiliated with Microsoft, Xbox, ViGEmBus, Nefarius Software Solutions, or any game publisher/platform. ViGEmBus is a separate dependency and is not bundled with this repository.
Use this only on networks and systems you control. The receiver accepts unauthenticated UDP packets on the configured port.
Linux host:
bashg++make- a controller exposed as
/dev/input/js*
Windows VM:
- ViGEmBus installed and running
- .NET 8 SDK/runtime for development, or the published standalone
.exe - inbound UDP allowed for the selected port, default
26760
Check ViGEmBus on Windows:
Get-Service ViGEmBusStart the Windows receiver first:
cd <repo>\XboxControllerForwarder\Receiver\XboxForwarderReceiver
dotnet restore
dotnet run -- --port 26760Find the Windows VM IP:
ipconfigStart the Linux sender:
cd XboxControllerForwarder/Sender
./XboxForwarder.sh --host <WINDOWS_VM_IP> --port 26760The sender prompts for a controller on first run and stores the selection locally.
Silent mode:
./XboxForwarder.sh --host <WINDOWS_VM_IP> --port 26760 --silentdotnet run -- --port 26760 --silentTo publish a Windows .exe from Linux:
cd XboxControllerForwarder
./Receiver/PublishWindowsReceiver.shOutput:
dist/windows-receiver/XboxForwarderReceiver.exe
Generated files under dist/ are release artifacts. Do not commit them to git; attach them to a GitHub Release or copy them directly to the Windows VM.
Copy dist/windows-receiver/ to the Windows VM, then run:
.\XboxForwarderReceiver.exe --port 26760Silent mode:
.\XboxForwarderReceiver.exe --port 26760 --silentTo publish an executable that defaults to silent mode:
./Receiver/PublishWindowsReceiverSilent.shOutput:
dist/windows-receiver-silent/XboxForwarderReceiverSilent.exe
Run:
.\XboxForwarderReceiverSilent.exe --port 26760Normal run:
cd XboxControllerForwarder/Sender
./XboxForwarder.sh --host <WINDOWS_VM_IP> --port 26760Choose a different controller:
./XboxForwarder.sh --select --host <WINDOWS_VM_IP> --port 26760Bypass stored selection:
./XboxForwarder.sh --device /dev/input/js0 --host <WINDOWS_VM_IP> --port 26760Useful sender flags:
--device /dev/input/js0
--host <ip-or-hostname>
--port 26760
--hz 125
--config controller.yaml
--axis-layout default
--axis-layout legacy
--axis-map lx,ly,rx,ry,lt,rt,dpx,dpy
--silent
The launcher automatically loads Sender/controller.yaml. It maps semantic controls
to stable Linux input identities rather than fragile /dev/input/js* positions:
axes:
left_stick_x: ABS_X
left_stick_y: ABS_Y
right_stick_x: ABS_RX
right_stick_y: ABS_RY
left_trigger: ABS_Z
right_trigger: ABS_RZ
dpad_x: ABS_HAT0X
dpad_y: ABS_HAT0YYou can edit that file to rearrange controls, or select another config explicitly:
./XboxForwarder.sh --host <WINDOWS_VM_IP> --config my-controller.yamlAt connection time the sender asks the Linux joystick API which numeric position each identity occupies. It resolves the mapping again after reconnecting, so a changed enumeration does not cross sticks and triggers.
The older fixed numeric layouts remain available as compatibility overrides:
./XboxForwarder.sh --host <WINDOWS_VM_IP> --axis-layout legacy
./XboxForwarder.sh --host <WINDOWS_VM_IP> --axis-map 0,1,3,4,2,5,6,7Development run:
cd <repo>\XboxControllerForwarder\Receiver\XboxForwarderReceiver
dotnet restore
dotnet run -- --port 26760Run an existing build:
dotnet .\bin\Debug\net8.0\XboxForwarderReceiver.dll --port 26760Run the published executable:
.\XboxForwarderReceiver.exe --port 26760Receiver flags:
--port 26760
--silent
Use this to test the sender without Windows.
Terminal 1:
cd XboxControllerForwarder/Sender
./PacketInspector.sh --port 26760Terminal 2:
cd XboxControllerForwarder/Sender
./XboxForwarder.sh --host 127.0.0.1 --port 26760The inspector prints decoded packet state.
Windows receiver gets no packets:
- Use the Windows VM IP, not
localhost, from the Linux sender. - Allow inbound UDP on port
26760in Windows Firewall. - Confirm the VM network mode allows host-to-guest traffic.
- Test the sender locally with
PacketInspector.sh.
Virtual controller does not appear:
-
Confirm ViGEmBus is installed and running:
Get-Service ViGEmBus -
Restart the receiver after installing ViGEmBus.
-
Try running the receiver from an elevated PowerShell.
Linux sender cannot open the controller:
ls /dev/input/js*
./XboxForwarder.sh --select --host <WINDOWS_VM_IP>Controls are mapped incorrectly:
-
Check the sender's
Last: Axis map:line to see the identities Linux reported. -
Edit
Sender/controller.yamlso each semantic control points to the intendedABS_*identity, then restart the sender. -
For a device that does not expose useful Linux identities, use the fixed numeric compatibility map:
./XboxForwarder.sh --host <WINDOWS_VM_IP> --axis-map 0,1,3,4,2,5,6,7
XboxControllerForwarder/
Sender/ Linux C++ sender
Receiver/XboxForwarderReceiver/ Windows C# receiver
Receiver/PublishWindowsReceiver.sh
Receiver/PublishWindowsReceiverSilent.sh
LICENSE
README.md
MIT. See LICENSE.