Turn your phone into a gamepad for your PC.
JAXController — Joint adaptive Xinput Controller — is a cross-platform virtual gamepad system that transforms your Android device into a fully functional game controller for Windows and Linux. It delivers low-latency, system-level input injection over Wi-Fi — no drivers, no dongles, no compromise.
JAX-V4 is designed in three performance tiers to match every use case, from casual browsing to competitive esports.
| Tier | Name | Target | Status |
|---|---|---|---|
| Low | JAX CORE | Casual gaming, lightweight apps, retro emulators, indie titles | Experimental |
| Mid | JAX FLASH | Mainstream PC gaming, media control, productivity, modern AAA | Experimental |
| High | JAX FORGE | AAA titles, precision input, low-latency competitive play, rhythm games | Experimental |
All tiers are currently experimental. Features, APIs, and behavior may change before stable release.
- JAX CORE: Optimized for responsiveness over absolute fidelity. Ideal for emulators, pixel-art games, and casual mobile ports where responsiveness matters more than feature completeness.
- JAX FLASH: The general-purpose tier. Balances input fidelity, polling rate, and feature set for the broadest compatibility with modern PC games.
- JAX FORGE: Maximum fidelity and minimum latency. Targets high-refresh-rate competitive titles, fighting games, and precision-dependent genres where every millisecond counts.
- Connect: The PC server starts a TCP listener and displays its IP and port as a QR code. The Android client scans the QR code (or enters the address manually) and opens a TCP socket.
- Serialize: Every frame, the mobile client packs the current gamepad state — buttons, triggers, thumbsticks — into a compact binary message using Colfer serialization from the shared
VGP_Data_Exchangeschema. - Receive & Parse: The server reads bytes off the socket into a ring buffer.
parse_gamepad_state()deserializes complete Colfer packets into avgp_data_exchange_gamepad_readingstruct, handling incomplete data, schema mismatches, and oversized payloads. - Execute: The parsed reading is handed to the active Executor:
- GamepadExecutor maps the reading onto a virtual XInput gamepad (Windows: ViGEmBus/XUSB; Linux: libevdev uinput) and injects it at the OS level.
- KeyboardMouseExecutor applies the active keymap profile to convert buttons/thumbsticks/triggers into keyboard keys and mouse actions (including smooth cursor movement via stepped
SendInput/libevdev).
- Repeat: The client streams readings at display refresh rate; the server processes each one in order. No cloud, no tracking, no telemetry.
┌─────────────────────────────────────────────────────────────────────┐
│ Android Client │
│ ┌─────────────┐ ┌───────────────┐ ┌────────────────────────┐ │
│ │ Compose UI │ │ GamepadState │ │ Colfer Serializer │ │
│ │ (screens, │──▶│ (buttons, │──▶│ (VGP_Data_Exchange │ │
│ │ composables│ │ axes, touch) │ │ schema) │ │
│ └─────────────┘ └──────┬────────┘ └───────────┬────────────┘ │
│ │ │ │
│ TCP Socket binary payload │
└───────────────────────────┼────────────────────────┼───────────────┘
│ │
local Wi-Fi / USB RNDIS Colfer encoded
│ │
┌───────────────────────────▼────────────────────────▼───────────────┐
│ PC Server (Qt 6 / C++20) │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌───────────────────┐ │
│ │ QTcpServer │───▶│ Data Buffer │───▶│ Parser │ │
│ │ + QR code │ │ + readyRead │ │ parse_gamepad_ │ │
│ │ generation │ │ event loop │ │ state() │ │
│ └──────────────┘ └────────────────┘ └────────┬──────────┘ │
│ │ │
│ vgp_data_exchange_ │
│ gamepad_reading │
│ │ │
│ ┌───────────────▼───────────┐ │
│ │ ExecutorInterface │ │
│ │ (strategy pattern) │ │
│ └───────────┬───────────────┘ │
│ │ │
│ ┌────────────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────────▼────────┐ ┌────────▼────────┐ │ │
│ │ GamepadExecutor│ │ KeyboardMouse │ │ │
│ │ │ │ Executor │ │ │
│ │ - Button map │ │ - Keymap profile│ │ │
│ │ - Thumbsticks │ │ - Thumb→mouse │ │ │
│ │ - Triggers │ │ - Thumb→keys │ │ │
│ │ - deadzone/ │ │ - Trigger→keys │ │ │
│ │ response │ │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ ┌───────────────┼────────────┐ │ │ │
│ │ │ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──▼─────────▼──┐ │ │
│ │ GamepadSim │ │KeyboardSim │ │ MouseSim │ │ │
│ │ (Windows) │ │(Windows + │ │ (Windows + │ │ │
│ │ - ViGEmBus │ │ Linux) │ │ Linux) │ │ │
│ │ - XUSB │ │ - SendInput│ │ - SendInput │ │ │
│ │ - report │ │ - libevdev │ │ - libevdev │ │ │
│ │ injection │ │ uinput │ │ uinput │ │ │
│ └─────────────┘ └────────────┘ └───────────────┘ │ │
│ │
│ SettingsSingleton (QSettings) stores executor mode, port, │
│ keymap profiles, thumbstick/trigger mappings per profile. │
└─────────────────────────────────────────────────────────────────────┘
- Language: Kotlin, Jetpack Compose
- Build: Gradle (Android Gradle Plugin)
- UI: Compose screens for main menu, gamepad layout, connection (manual IP + QR scan), settings, button customization
- Networking:
ConnectionViewModelmanages aSocketand aLinkedBlockingQueue<NetworkCommand>; a coroutine-based command processor serializes sends/receives so Colfer packets never interleave - Input: Touch-driven virtual gamepad with configurable buttons, analog sticks, triggers, and D-pad; haptic feedback via
HapticUtils
- Protocol: TCP, local-network only
- Connection: Server binds
QHostAddress::AnyIPv4; mobile connects by IP/port. Port is configurable viaSettingsSingleton - QR flow: Server renders each global IP as a QR code (
createQR) encodingip:port; mobile scans and resolves both parts - Latency tuning:
LowDelayOptionis set on the socket; Nagle's algorithm is disabled to keep small gamepad packets unbundled - Alternative transport: Android USB tethering (RNDIS/NCM) works without code changes because the socket layer is IP-agnostic
- Schema:
GamePadReading.colfdefines the single active message typeGamepadReading - Fields:
buttons_up(bitmask),buttons_down(bitmask),left_trigger,right_trigger,left_thumbstick_x/y,right_thumbstick_x/y(allfloat) - Serialization: Colfer — compact, schema-enforced, zero-copy friendly. C and Java code is generated from the same
.colfsource, guaranteeing both ends agree on layout - Button enum:
GamepadButtonsmirrorsWindows.Gaming.Input.GamepadButtonsbit flags (Menu, View, A/B/X/Y, DPad, shoulders, thumbsticks)
- Language: C++20 with Qt6 (Core, Widgets, Network, Gui)
- Build: CMake 3.20+, Ninja; platform presets for Windows and Linux
- Entry:
main.cpp→MainWindow→Serverwidget - Networking:
Serverowns aQTcpServer. OnnewConnectionit grabs the pending socket, pauses accepting, and connectsreadyReadtoserveClient().serveClient()appends todataBufferand loops callingparse_gamepad_state()until no complete packet remains - Parser:
parse_gamepad_state()callsvgp_data_exchange_gamepad_reading_unmarshal()and translates Colfer error codes (EWOULDBLOCK,EFBIG,EILSEQ) intoParseResult::FailureReason - Executor: Strategy pattern (
ExecutorInterface) with two implementations:KeyboardMouseExecutor: reads the activeKeymapProfilefromSettingsSingleton; maps buttons viabuttonMap(), converts thumbstick deflection into either smooth mouse steps (usingcircleToSquare+ per-stepmoveMouseByOffset) or directional key holds, and maps triggers to key presses with aTHRESHOLDcutoffGamepadExecutor: feeds readings straight intoGamepadInjector; maps JAX button flags to XUSB (Windows) or Linuxinput.hcodes, clamps thumbsticks to [-1, 1] and triggers to [0, 1], then callsinject()
GamepadInjector- Windows: Allocates a ViGEm client, connects to
ViGEmBus, creates an XUSB (Xbox 360) target, and pushesXUSB_REPORTupdates viavigem_target_x360_update - Linux: Creates a
libevdevdevice + uinput fd; tracks button state to emit press/release events; writes thumbstick/trigger absolute values through the uinput event interface
- Windows: Allocates a ViGEm client, connects to
KeyboardInjector- Windows: wraps
SendInputwith scan-code translation - Linux: writes key events through a libevdev uinput keyboard device
- Windows: wraps
MouseInjector- Windows: wraps
SendInputfor absolute/relative movement, button state, and scroll - Linux: writes relative motion and button events through a libevdev uinput mouse device
- Windows: wraps
SettingsSingletonwrapsQSettingsand exposes: executor type, TCP port, active keymap profile path, mouse sensitivityKeymapProfileloads/saves INI-style profiles mapping eachGamepadButtonsentry to anInputKeyCode(native virtual key / Linux keycode) plus optional display names; also stores per-thumbstick behavior (direction keys vs mouse move), per-trigger mappings, and per-thumbstick mouse-move toggles
| Component | Role |
|---|---|
| VirtualGamePad-PC | C++/Qt6 server — GUI, TCP listener, QR display, Colfer parsing, executor dispatch, platform input injection |
| VirtualGamePad-Mobile | Android client — Compose UI, touch gamepad, QR scanner, Colfer serialization, TCP socket management |
| VGP_Data_Exchange | Shared Colfer schema + generated C/Java code for GamepadReading and GamepadButtons |
| ViGEmBus | Windows kernel driver dependency (Xbox 360 virtual device) — installed separately, not bundled |
- Cross-platform: Windows 10/11 (SendInput + ViGEmBus) and Linux (libevdev + uinput)
- Dual execution: native virtual gamepad or keyboard/mouse fallback via customizable keymap profiles
- Multi-instance multiplayer: each PC instance listens independently; multiple phones can connect to separate servers
- Local-only networking: TCP over LAN or USB tethering, no cloud, no telemetry
- QR-code pairing: one scan connects phone to server
- Tunable per-profile mappings: buttons, triggers, thumbsticks (directional keys, smooth mouse, or raw gamepad axes)
- Portable or installable builds (controlled by CMake option)
- Open-source and auditable
| Area | Technology |
|---|---|
| PC Server | C++20, Qt6 (Core, Widgets, Network, Gui), CMake, Ninja |
| Mobile Client | Kotlin, Jetpack Compose, Android Gradle Plugin |
| Serialization | Colfer binary protocol (shared .colf schema) |
| Windows Input | SendInput (keyboard/mouse), ViGEmBus + XUSB (virtual gamepad) |
| Linux Input | libevdev, uinput (keyboard, mouse, gamepad) |
| QR Generation | Nayuki's QR-Code-generator (C++) |
| Documentation | Doxygen |
- Windows: Visual Studio 2022 (MSVC) or MinGW, Qt 6, CMake 3.20+, Ninja, ViGEmBus driver
- Linux: GCC 10+, Qt 6, CMake 3.20+, Ninja,
libevdev-dev, access to/dev/uinput(add user toinputgroup or run as root)
cd VirtualGamePad-PC
cmake --preset <preset-name> -B build
cmake --build build --config Releasecd VirtualGamePad-Mobile
./gradlew assembleReleaseFor full build instructions and preset names, see VirtualGamePad-PC/Build.md.
JAX-V4 is in active development. The entire stack — JAX CORE, JAX FLASH, and JAX FORGE — remains experimental.
Planned work includes:
- Stabilizing all three tiers
- Expanded platform coverage
- Performance tuning and latency reduction
- Enhanced profile management and community sharing
- USB RNDIS transport evaluation for lower latency than Wi-Fi
- Input smoothing (stick deadzone, response curves) for touch-native thumbsticks
For usage, downloads, and troubleshooting, visit the official JAX-V4 website — the best free virtual controller.
These are corrections made to the JAX-V4 fork beyond the original VirtualGamePad-Mobile / VirtualGamePad-PC repositories.
- File:
VirtualGamePad-PC/src/simulation/linux/gamepadSim.cpp - Problem: The Linux
GamepadInjectorregistered the LT/RT triggers onABS_HAT2X/ABS_HAT2Y(a secondary hat-switch axis), not the standard analog trigger axes.ABS_HAT2X/Yis intended for a second D-pad-style hat, not analog triggers, so games and SDL/evdev-based input libraries never read trigger data from those axes. This caused LT/RT to be silently dropped or misread cross-platform vs. Windows (ViGEm/XUSB), exactly the kind of "input clipping / cross-talk" that appears only on Linux. - Fix: Triggers now register (and write) on
ABS_Z(LT) andABS_RZ(RT) — the same axes the kernelxpaddriver uses for Xbox-style controllers. The float→[0,255] data path is unchanged, so the fix is isolated to which axis constant is registered and is low-risk. - Status: Shipped. Affects only the Linux
GamepadExecutorpath.
- File:
VirtualGamePad-Mobile/.../ui/composables/Trigger.kt - Problem: Triggers were captured as binary on/off (
gamepadState.LeftTrigger = 1f/0fon press/release) via ComposeisPressed, so a light tap reported the same value as a full press. TheGamepadReadingschema already carriesleft_trigger/right_triggerasfloat[0,1], so only the touch capture was missing — the transport and Linux side already supported analog. - Fix: Dropped Material
Button/interactionSource/collectIsPressedAsStateentirely (its click/ripple semantics anddetectDragGesturestouch-slop delay fight continuous position tracking). Replaced withawaitEachGesture+ rawpointerInput, readingchange.position.ydirectly each frame and writing a normalizedcoerceIn(0f, 1f)pressure vector based on vertical offset inside the trigger box. Touch-down reports immediately (no slop delay), overshoot clamps for free, and aDisposableEffectsafety net zeroes the trigger if the composable leaves mid-press. Same "make an input continuous" category as the still-open stick deadzone/response-curve fix inDEV_NOTES.md; both should be tested in one on-device session. - Test:
GamepadInputE2ETest.ktupdated from the old> 0.5fbinary assertion to verify both a near-max full press (> 0.9f) and a light top press (0.01f..0.4f), confirming the analog gradient end-to-end. - Status: Shipped.
Every release build is scanned before publishing. As of the current build, 1 out of 69 vendors flags VGamepadPC.exe — a generic heuristic label from one engine, not a confirmed detection. Every major vendor (Microsoft Defender, Kaspersky, ESET, Malwarebytes, BitDefender, and 60+ others) shows it clean. That single flag is a known pattern for small, unsigned, low-reputation executables that do input injection — the heuristic reacts to what the code touches (keyboard/gamepad injection APIs), not to actual malicious behavior. Check it yourself rather than taking our word for it.
VirusTotal — VGamepadPC.exe VirusTotal — GitHub repo
The flagging engine is MaxSecure, with the label Trojan.Malware.300983.susgen — the susgen suffix literally means "suspicious, generic." That's not "we identified known malware," it's "this file's behavior pattern resembles a broad category we watch." MaxSecure in particular has a well-documented reputation for aggressive heuristic flagging on small, unsigned executables.
Every engine that actually matters caught nothing — Microsoft Defender, Kaspersky, ESET, BitDefender, Malwarebytes, CrowdStrike, all 68 others: clean. If this were a real trojan, you'd expect several major vendors to agree, not one outlier.
The behavioral trigger is almost certainly your app's legitimate function: input injection. Any tool that calls SendInput, opens a raw socket to accept remote commands, and interacts with driver-level APIs looks structurally similar to a RAT (remote access trojan) to a heuristic scanner — because on a byte-pattern level, "receive network input → inject keystrokes" is exactly the shape of a RAT, even though your version does it consensually, locally, with the user driving both ends.
Zero reputation is the other half of it. Every AV vendor weights "how many people have run this exact binary hash before, and did anything bad happen" — a fresh CI build with no install history starts at zero trust, so any heuristic near-miss gets flagged rather than suppressed. This is why code signing genuinely helps here, not just for silencing SmartScreen — a signed binary from a consistent publisher identity builds exactly the reputation history that makes heuristics stop firing.
