Cross-platform gamepad tooling for anyone.
Ultimate multi-platform, multi-language controller tool for remapping and compability layering, Any input device in, any virtual controller out: Xbox, PlayStation or Nintendo. third-party gamepad, keyboard or/and mouse.
v1.0.1 Beta · Built with .NET 10 · Avalonia UI · SDL3 · HIDMaestro · ViGemBus
No pressure — using and sharing the project already helps a lot. Thank you!
GameFlow sits between your physical input — a gamepad, keyboard, mouse, or a phone on your Wi-Fi — and any game or application. It reads that input, transforms it through a saved profile (a stack of mapping rules), and writes the result to one or more virtual controllers the game sees.
This project started as a script to spare players from repetitive inputs that were damaging both their controllers and their hands. It was later rebuilt in Python as Autofire, then in C#/.NET as the architecture you're looking at now, since renamed GameFlow to separate the product from autofire the individual feature.
For the full feature reference, per-rule mechanics, and platform-by-platform verification notes, see WIKI.md.
| Category | Details |
|---|---|
| Autofire | Button and stick autofire, hysteresis-shaped so it doesn't jitter at the trigger threshold |
| Remapping | Button-to-button remap with optional source suppression; toggle rules to flip other rules on/off at runtime |
| Shift layers | "Caps Lock for your controller" — 6 activation modes (Hold, Toggle, Latch, Cycle, Sticky, No Button), long-press-to-engage, idle auto-cancel |
| SOCD cleaning | Opposite-direction key resolution — Last-wins (Snap Tap), First-wins, or Neutral, per button pair |
| Stick Trim | Hold a digital button, a stick modulates it into an analog trigger press — deadzone, ramp rate, and reset-on-release all configurable |
| Multi-source rows | Many inputs → one output. Six combine modes (Maximum, Minimum, Sum, Average, Multiply, FirstActive) or a formula — a small dependency-free expression compiler (s1 - s2, if(s1>0.5, s2, 0), clamp(...)), with 10 starter recipes |
| Gyro aiming | Local / Player / World reference frames, dual-threshold smoothing (kills tremor, keeps flicks sharp), 4 engage modes including a raw-stick gate, drift bias calibration, output to a stick or the mouse |
| Touchpad mapping | Finger-anchored virtual stick, 8-way wedge D-pad, frame-to-frame mouse mode — works on any pad with a touch surface |
| Custom scripting | Sandboxed Lua per control (MoonSharp), for logic the built-in rule types don't cover |
| Button combos | One press → a timed sequence of virtual presses |
| Freeze macro | Captures the stick vector on the rising edge of a button; optional pulse-while-frozen |
| Per-device tuning | Deadzone, anti-deadzone, full-at, sensitivity, response curve, and invert — per stick, per trigger, per slot AND per device, so the same pad feels different on two different slots. Offline slots remain editable through inheritable defaults; rumble, lighting, and adaptive-trigger settings reach assigned supported hardware through the effects thread. |
| Rumble-linked adaptive triggers | A DualSense trigger can stiffen with the game's rumble, or buzz along with it — the impulse-trigger feel, without the game knowing. Per trigger, with its own amount |
| Keyboard & mouse as gamepad | Full keyboard state (not just a handful of buttons) synthesized into a gamepad snapshot — works on Windows, Linux, and macOS |
| Motion server | DSU / Cemuhook over UDP (26760 by default) — Cemu, Dolphin, Yuzu and Ryujinx read gyro and accelerometer straight from any slot with a motion-capable pad. Off by default; enable it in the Dashboard's Motion Server section |
| Phone as controller | No app install — open a URL in any phone browser on the LAN. Dual anchored sticks, 8-way D-pad, analog triggers, rumble via the Vibration API, and the phone's own gyroscope/accelerometer feed the same gyro pipeline a DualSense uses. Up to 16 phones at once |
| Multiple virtual controllers | Each "slot" is an independent input → profile → output pipeline, with its own device assignment, output kind, and output provider |
| Profile system | Create, duplicate, rename, import, export — JSON, human-editable |
| Live dashboard | Physical and virtual controllers rendered side by side, per slot |
| Requirement | Version |
|---|---|
| .NET SDK | 10.0 or later |
| Git | Any recent version |
Gamepad/joystick input goes through SDL3 on every platform:
| Platform | File | Location |
|---|---|---|
| Windows | SDL3.dll |
Next to GameFlow.App.exe |
| Linux | libSDL3.so |
System library path, or the app directory |
| macOS | libSDL3.dylib |
System library path (e.g. /usr/local/lib), or the app directory |
An optional gamecontrollerdb.txt (SDL_GameControllerDB) in the application directory extends the built-in mapping database.
GameFlow uses HIDMaestro exclusively: user-mode, no kernel driver, no reboot. Drop HIDMaestro.Core.dll next to GameFlow.App.exe and it activates on next launch. If it isn't available, that slot has no output — GameFlow won't silently substitute something else, and the log and the slot's displayed name both say why. The app runs fine either way; the slot falls back to Preview (dashboard-only, no real device).
Windows also has the fullest platform support: keyboard/mouse-as-gamepad, mouse output (SendInput), and the only real virtual-gamepad output backend.
- Gamepad input: SDL3, works out of the box.
- Keyboard/mouse-as-gamepad: real, via direct
evdevreads (/dev/input/eventN) — not a Windows-only feature anymore. - Mouse output: real, via
uinput(creates an actual virtual input device). - Virtual gamepad output: not built yet — Preview only (see Known Limitations).
Needs the input group for device access without root:
sudo usermod -aG input $USER
# then log out and back in- Gamepad input: SDL3, works out of the box.
- Keyboard/mouse-as-gamepad: real, via
CGEventTap— reads system-wide (no per-device distinction; see the wiki). - Mouse output: real, via
CGEventPost. - Virtual gamepad output: not built yet — Preview only.
Keyboard/mouse capture needs Input Monitoring permission (System Settings → Privacy & Security → Input Monitoring) — without it the app runs fine, that one source just reads as empty.
---
git clone https://github.com/Prooxie/GameFlow.git
cd GameFlow
dotnet build GameFlow.sln
dotnet run --project src/GameFlow.AppFirst launch creates a default profile under:
| Platform | Location |
|---|---|
| Windows | %LOCALAPPDATA%/AutofireNext/ |
| Linux | \~/.local/share/AutofireNext/ |
| macOS | \~/Library/Application Support/AutofireNext/ |
Still named
AutofireNexton purpose — it predates the rename, and changing it would orphan existing installs. Nobody types or sees this path; the app itself always says GameFlow.
---
- Profiles tab → create a profile, or start from the default.
- Devices tab → confirm your physical device is listed.
- Virtual controllers panel → Add controller to create a slot, assign a device, pick an output kind and provider, enable it.
- Dashboard → confirm the virtual side (marked with a VIRTUAL badge) mirrors your input through the mapping.
A slot's own virtual output is hidden from every input picker — you can't feed a virtual controller back in as a source.
Click any virtual controller panel on the Dashboard. The panel outlines on hover, and clicking opens that slot's tuning editor — five tabs: Sticks, Triggers, Rumble, Lighting, Adaptive. The editor remains available with no physical controller connected: an offline assigned device keeps its own settings, while a slot with no assignment edits defaults inherited by devices added later. Device-specific settings override those defaults. Changes are live and effects are delivered to an assigned supported controller whenever it is connected.
On the Adaptive tab, React to rumble ties a trigger to what the game is actually doing instead of holding one configured feel. Resistance makes the trigger stiffen as the game rumbles — free at rest, reaching the strength you set at full rumble. Vibration makes the trigger buzz along with the rumble, which is how an Xbox title's impulse triggers land on a DualSense; between events the effect you configured applies unchanged. Amount scales how far the game is allowed to move it. Both read the rumble level after the Rumble tab's gain, so silencing a pad's motors silences its triggers too.
- Dashboard → Web Controller → enable it.
- Open the shown URL (e.g.
http://10.0.0.5:8080) on a phone on the same Wi-Fi. - Pick a layout, tap Enable motion for gyro aiming.
- The phone appears in the device list as an ordinary gamepad — assign it to a slot like any other device.
**Windows firewall note:** binding to all interfaces needs an admin URL ACL, or the server falls back to localhost-only and phones can't reach it (the log says which mode it's in):
netsh http add urlacl url=http://+:8080/ user=Everyone
Profiles tab → + Add rule against any control. Rules apply in declared order, every polling tick, live — no separate "apply" step.
src/GameFlow.App/appsettings.json:
{
"Runtime": {
"DashboardRefreshHz": 165,
"StartRuntimeOnLaunch": true,
"DefaultCulture": "en",
"Updates": {
"RepoOwner": "Prooxie",
"RepoName": "GameFlow",
"AssetNamePattern": "GameFlow-{tag}-{rid}.zip",
"UserAgent": "GameFlow-UpdateChecker"
}
}
}| Key | Default | Description |
|---|---|---|
DashboardRefreshHz |
60 |
UI refresh rate for the live dashboard (30–1000). This drives a full redraw of every controller surface on screen, so it is the dominant UI cost — lower it on a weak machine. Independent of the mapping tick, which polls at its own rate and is unaffected |
StartRuntimeOnLaunch |
true |
Whether the input/output runtime starts automatically |
DefaultCulture |
en |
Fallback UI language |
Updates:RepoOwner / RepoName |
Prooxie / GameFlow |
GitHub-releases update checker |
Profiles are JSON in the application data directory (see Quick Start), editable by hand or entirely through the Profiles tab.
Input: sdl (all platforms — gamepads/joysticks everywhere, plus keyboard/mouse synthesis via platform-native raw input), web (phone browsers over the LAN), demo (animated preview, no hardware), none.
Output: hidmaestro (Windows, real virtual gamepad), preview (all platforms, dashboard-only). Mouse-cursor output (separate from gamepad output) is real on all three platforms via SendInput / uinput / CGEventPost.
Older profiles referencing retired providers (XInput, GameInput, x360ce, ViGEm, PS3, and similar) migrate to sdl/hidmaestro automatically.
---
GameFlow/
├── src/
│ ├── GameFlow.App # Avalonia UI, ViewModels, Views, Localization
│ ├── GameFlow.Core # Domain models, mapping pipeline, rule types, formula engine
│ └── GameFlow.Infrastructure # SDL3, HIDMaestro, platform input/output, web controller, profiles
├── tests/
│ ├── GameFlow.Core.Tests # Pipeline, rules, formula compiler, gyro maths
│ └── GameFlow.Infrastructure.Tests # Platform interop, protocol, device catalog
└── .github/workflows/ # CI: build, test, and tag-triggered release packaging
Physical input Keyboard/mouse Phone browser
(SDL3 gamepad/joystick) (Win Raw Input / (WebSocket)
│ Linux evdev / │
│ macOS CGEventTap) │
▼ ▼ ▼
InputSource.ReadAsync() / ReadDevice() ─────────────────────┘
│
▼
DeviceSettingsProcessor (per-device deadzone/curve, before merge)
│
▼
ControllerMappingPipeline (one per active slot, isolated)
├── RuleToggleRule ├── StickTrimRule
├── SocdCleanRule ├── GyroMapRule
├── ButtonRemapRule ├── TouchpadMapRule
├── ButtonAutofireRule ├── MultiSourceMapRule (+ Formula)
├── MultiButtonAutofireRule ├── ControlScriptRule (Lua)
├── ButtonComboRule └── StickThresholdRule
├── StickAutofireRule
└── FreezeLastDirectionRule
│
▼
OutputSink.WriteAsync() (HIDMaestro · Preview)
+ IMouseOutputWriter (SendInput · uinput · CGEventPost, independent of gamepad output)
│
▼
Virtual controller seen by games — hidden from GameFlow's own
input list, so it can never be selected back in as a source.
Shift layers resolve first each tick, gating which rules are active. Every slot's rules live in its profile's JSON, applied in order, live. The dashboard reads from lock-free snapshot stores on its own timer, independent of the mapping pipeline's tick rate.
Documented here rather than discovered by surprise:
- No virtual gamepad output on Linux or macOS. Mouse output is real on both; a real virtual controller (via
uinput's gamepad mode, or DriverKit on macOS) is future work. - Bundled controller theme placement is known-imperfect on some skins — several were generated from asset-pack sprites without authoritative layout data. Fixing this properly needs template-matching each sprite against its base image; tracked, not yet done.
- CGEventTap (macOS) has no per-device keyboard/mouse distinction — one aggregate stream for the whole system, not per-physical-device like Windows/Linux.
- Remote Link, per-app profile switching — on the roadmap, not started.
- The DSU/Cemuhook motion server addresses 4 pads, not 16. That is the protocol's own limit, not an implementation shortcut — DSU has no way to describe a fifth pad. The first four enabled slots are the ones emulators see.
🇬🇧 English · 🇨🇿 Czech · 🇩🇪 German · 🇪🇸 Spanish · 🇫🇷 French · 🇮🇹 Italian · 🇵🇱 Polish · 🇷🇺 Russian — switchable live from Settings.
- Fork the repository
git checkout -b feature/my-feature- Commit your changes
- Open a Pull Request
Keep commits focused with a short description of what changed and why. See WIKI.md for how the mapping pipeline and rule system fit together before adding a new rule type.
MIT — see LICENSE.
- HIDMaestro by hifihedgehog — driver-less, user-mode virtual controller platform.
- SDL_GameControllerDB — the community gamepad mapping database SDL3 input builds on.
- VSCView THEMEENGINE by Nielk1 — the controller theme format GameFlow's controller surfaces are compatible with.
- AL2009man's Gamepad Asset Pack & Prompt Asset Pack — controller and button-prompt art.
- MoonSharp — the sandboxed Lua interpreter behind custom scripting.
Check out my other projects on GitHub, YouTube, and Twitch.
Nazzareno96 — keeping me sane during development, beta testing twitch.tv/nazzareno96
NoobKillerRoof — voicing real hardware/software pain points that inspired this project, beta testing twitch.tv/noobkillerroof