A RED4ext plugin that fixes the Cyberpunk 2077 (2.11+) freeze and input-hint flicker with mixed controls - any setup that sends both gamepad and mouse/keyboard input at the same time:
- Steam Controller / Steam Deck with touchpad or gyro mapped to mouse
- DualShock/DualSense or other pads with gyro-to-mouse
- gamepad in one hand, mouse in the other
- analog keyboards (e.g. Wooting) mapping movement keys to a virtual stick
- any similar remapper setup (Steam Input, JoyShockMapper, reWASD, ...)
In vanilla, every such input flips the game's "current input device" back and forth, the on-screen button hints flicker between keyboard and gamepad variants, and after 20-60 minutes of play the game hard-freezes and is then killed by its own watchdog. CDPR has declined to fix it ("unsupported configuration"). Full story and investigation: bug.md, also published as a post on r/cyberpunkgame with further discussion and other players' reports.
With the plugin, the UI is pinned to the device of your choice; the flicker and the freeze are gone. The raw input layer is untouched - mouse, cursor and all inputs keep working exactly as before.
DISCLAIMER: this plugin and most of the content of this repository - the code, the reverse engineering documented in RE-notes.md, the analysis tooling and the documentation - were AI-generated by Anthropic's Claude (Fable 5), working under my direction, with every build tested in-game by me. I want to be upfront about this so you can judge for yourself; the notes document the evidence behind every decision.
- Install RED4ext.
- Extract the archive into the game folder, or copy its
red4ext/plugins/MixedInputFix/folder (MixedInputFix.dll, optionalMixedInputFix.ini,scripts/) there by hand.
The scripts/ folder holds the script API declarations and
needs redscript; without redscript the
fix itself works the same, only the API is unavailable.
Built and tested on game versions 2.31 and 2.5 (GOG). Expected to work on Steam and Epic builds of the same patches: all code addresses resolve through the game's address library at runtime.
Code addresses are patch-stable, struct offsets are not, so the plugin reads the game executable's file version at startup and picks its offsets from a per-build table. On a build it does not know, it falls back to the newest offsets it has and verifies them against the running game before changing anything - most hotfixes do not move these structs, so this usually just works. If verification fails, the offset-dependent layers stay disabled and the log says so, rather than writing to a wrong address. A code address that cannot be resolved at all still fails loudly at startup, with a RED4ext error naming the hash.
MixedInputFix.ini next to the DLL. Missing file or entry = defaults.
-
enabled(defaulttrue) -falsemakes the plugin attach nothing; the game runs fully vanilla. The kill switch if a game patch misbehaves. -
mode(defaultcontroller) - which device the UI is pinned to:controller: UI locks to the gamepad after its first input. For gamepad-primary setups (Steam Controller/Deck, gyro aim). The game is 100% vanilla until a gamepad is used.kbm: UI locks to keyboard/mouse from boot. For KBM-primary setups with auxiliary analog inputs (e.g. movement/driving on a stick).inhibited: pins nothing (vanilla behaviour) until a script mod switches the mode through the script API.
Script mods can change the mode at runtime; it returns to this value when a save is loaded or the game returns to the main menu.
-
verbose(defaultfalse) - per-event logging for debugging.
Once engaged, every device-dependent choice the game makes follows the pinned device - not just how the UI looks:
- UI visuals: button-hint icons, contextual HUD hint layouts (KBM and gamepad variants differ - some actions are one key on keyboard but a button combo on a pad), shard/codex popup layouts, gamepad-specific hint bars (world map etc.).
- UI behavior: the CrystalCoat vehicle color editor keeps one navigation paradigm instead of flickering between its KBM and gamepad variants; the phone HUD and the settings menu's DualSense detection follow the pinned device too.
- Gameplay branches that check the input device: the game scripts also consult "last used device" for behavior, and those checks follow the pin as well - e.g. crouch/dodge handling (on gamepad, dodge and crouch share a button, so whether a dodge leaves you crouched follows different rules than on keyboard) and body-carry interactions.
This is deliberate - the paradigm follows the device you actually think in. Worth knowing: in vanilla mixed input these checks were effectively random - each one read whichever device happened to send the last input event, so behavior like dodge-exit crouching flip-flopped mid-play. The plugin makes all of them deterministic and mutually consistent with the chosen mode.
One consequence is worth calling out. While the UI is pinned to the gamepad,
the keyboard menu keys stop responding to a tap: holding M still opens the
map, but J/P/K/I do nothing at all. The game gates those five actions
on the last-used device and picks the hold variant for a gamepad, because
the pad's Start button opens the player menu on tap and the map on hold - and
of the five, only the map declares a hold event, so the rest are left with no
working path. Not a plugin bug; the derivation is in
RE-notes.md.
The optional MixedInputMenuKeys mod restores them without altering the Start button - see Optional mods.
Separate from the pin - and present in vanilla, reproducible with
enabled=false - if a second analog device is active while you play (the mouse
in controller mode, a stick/analog in kbm mode) the game's native input
layer can drop crouch and dodge specifically while that device streams;
every other action still taps fine. The button-to-action mapping is resolved in
layers - the default layer alone for KBM, or the default layer plus one selected
controller layout (Legacy/Agile/Alternative) for a gamepad - and the pin does
not govern that resolver, so the non-pinned device's stream makes it re-layer
and lose bindings, or their release edges, that live only in the momentarily
inactive layer. The controller layouts also behave differently at this native
level, by a mechanism we have not fully reverse-engineered.
Workaround for kbm mode: set the controller layout to Alternative (crouch
and dodge become separate single-press buttons), then in
r6/config/inputUserMappings.xml add your keyboard crouch key to
ToggleCrouch_Button_Alternative and Dive_Button_Alternative, and your dodge
key to Dodge_Button_Alternative. The controller-mode mirror is confirmed
fixable the same way - give the pad's crouch/dodge separate single-press buttons
in the default layer (mirror Alternative onto it) - though that is a heavier
edit to the base controller layout. Full derivation in RE-notes.md.
Game 2.5 made it worse: even with the bindings fixed, dodge (and, less often, crouch) stops responding while the other device streams. The engine still delivers each press but drops its release, and the game only acts on a tap (press + quick release), so nothing happens until the stream stops. The optional, heavily experimental MixedInputCrouchDodgeWorkaround mod makes dodge and toggle crouch act on the press instead, and ships the binding edits above as an input_loader file - see Optional mods.
Redscript mods can switch the mode at runtime, e.g. to pin the controller
only while driving, or to get the vanilla mouse cursor in photo mode. The
declarations ship with the plugin (scripts/ next to the DLL):
MixedInputFix.SetMode(MixedInputFixMode.KBM); // Inhibited / Controller / KBM
let saved = MixedInputFix.GetMode(); // Disabled if enabled=false
MixedInputFix.SetMode(saved);
MixedInputFix.ResetMode(); // back to the ini mode- A new mode takes hold on the next input of its device (pad for
Controller, keyboard/mouse forKBM) - immediately if that device is already in use. Until then the game runs vanilla, so every switch is a normal device change for the UI. Inhibitedpins nothing - vanilla behaviour, including the device flipping behind the freeze. Fine for occasional use (photo mode); preferKBMfor long stretches.- The runtime mode is never saved. It resets to the ini mode whenever the player detaches (loading a save, quitting to the main menu), so mods should re-apply theirs when their situation starts again.
- Last call wins; there is no stacking between mods. Use
GetMode()to save and restore around a temporary change. - With
enabled=falseevery call is refused (SetMode/ResetModereturnfalse,GetModereturnsDisabled). - To compile without the plugin installed, wrap calls in
@if(ModuleExists("MixedInputFix")).
From the Cyber Engine Tweaks
console, call the underlying natives with the mode as a number (0 =
inhibited, 1 = controller, 2 = kbm, 3 = disabled from GetMode only):
Game.MixedInputFix_SetMode(2) -- kbm
print(Game.MixedInputFix_GetMode()) -- 2
Game.MixedInputFix_ResetMode() -- back to the ini modeThe same calls can be bound to a CET hotkey or used from a CET mod.
Small redscript mods in mods/, each a separate download with its own
version. Copy <Mod>.reds to <game>/r6/scripts/<Mod>/ (or extract its
archive into the game folder). All need
redscript. Mods with custom bindings
also ship input/<Mod>.xml, which goes to <game>/r6/input/ and needs
input_loader.
-
MixedInputMenuKeys - restores the keyboard menu shortcuts in controller mode (see above). Inert unless something pins the device, so it is safe to leave installed.
-
MixedInputInhibitOnPhotoMode - inhibits the plugin while photo mode is open (vanilla photo mode with the mouse cursor) and restores the previous mode on exit. Uses the script API.
-
MixedInputVehicleControllerFpsKbm - controller mode in vehicles, kbm mode on foot: drive with the pad, fight with the mouse. Uses the script API. Deliberately minimal: until you first leave a vehicle, the ini mode applies.
-
MixedInputBindings - two bindings that pin a device: pad A pins the controller, Space pins kbm - i.e. jump on foot, handbrake in vehicles, so you switch schemes by pressing that button on the device you pick up. Rebind them in
MixedInputBindings.xml. Uses the script API and needs input_loader. -
MixedInputCrouchDodgeWorkaround - heavily experimental. Dodge and toggle crouch (crouch, stand up, slide, air dash) act on the button press instead of a tap, so they keep working while the other device streams (see above). Requires the Alternative controller layout - with any other layout it changes nothing - and its
MixedInputCrouchDodgeWorkaround.xmlbindings (needs input_loader). Before 0.2 it was named MixedInputOnPress and shipped an editedr6/config/inputUserMappings.xml: deleter6/scripts/MixedInputOnPress/and restore the.orig. Unlike the others it is not inert without the pin: with Alternative, dodge and crouch act on press for everyone. It replaces several vanilla locomotion methods, so it must be updated for each game patch and conflicts with other mods that replace the same methods. -
MixedInputQuickMeleeWorkaround - experimental. Quick melee and the dual-pistol Valentino's Frenzy (same button) act on the button press instead of a tap, for the same reason: while the other device streams, the release is lost and quick melee never fires. With the Alternative layout quick melee (tap) and the scanner (hold) share LB, and only the release tells them apart, so one of the two has to leave the controller. The archive ships two variants of
MixedInputQuickMeleeWorkaround.xml(needs input_loader). Copy exactly one intor6/input/. input_loader also reads subfolders ofr6/input/, so the variants ship outside it:r6/input-lb-does-only-quick-melee/- LB is quick melee, and the scanner is removed from the controller, in every layout. Bind it to a keyboard key: Tab by default, or set "Scanner (hold)" in the keybinding settings.r6/input-lb-does-only-scanner/- LB is the scanner, and quick melee is removed from the controller: Q by default, or set "Quick Melee". The engine still delays the scanner on Alternative's LB by about 0.3 s, which the mod works around.
Either way, a Steam Input / Deckhand back button mapped to that key works too, and the scanner comes on as soon as it is pressed (the XMLs drop its short hold delay). Not inert without the pin: quick melee acts on press for everyone. It replaces several vanilla quick melee and Frenzy methods, so it must be re-checked for each game patch.
MixedInputInhibitOnPhotoMode and MixedInputVehicleControllerFpsKbm are also meant as examples of the script API.
Check <game>/red4ext/logs/mixedinputfix-*.log:
- startup:
MixedInputFix 1.1.0 active (mode:controller game:3.0.88.46235 offsets:2.5/exact writer:on scriptKBM:on scriptPad:on scriptPS5:on latch:on+on)-offsets:<patch>/exactmeans the plugin recognises this game build;/fallbackmeans it is running on an unlisted one and is verifying the offsets it guessed (see Install) - first gamepad input:
gamepad detected (device 5)(5 = XInput; other pad types 2..9), and in controller modecontroller mode engaged: UI device locked to gamepad (device 5) - mode changes through the script API:
mode kbm -> controller (script), then the... mode engagedline once that device is used - on exit:
session summary: suppressed=... scriptKBM=... fallbackRestores=... [fallback:...]warnings should be rare (session init at most); frequent ones mean a game patch added a new writer path - please report them.
powershell -ExecutionPolicy Bypass -File build.ps1
Requires MSVC (vcvars64.bat path at the top of build.ps1) and the
header-only RED4ext.SDK submodule (git submodule update --init). Output and
all intermediates land in build/.
Note: the submodule deliberately tracks the psiberx fork of RED4ext.SDK (the variant used by Codeware) rather than upstream WopsS - it is what this plugin was developed and tested against; upstream has minor macro differences.
Short version - three cooperating layers, all bound via RED4ext address-library hashes (full reverse-engineering record with the derivations, evidence and failed approaches: RE-notes.md):
- Writer suppression (the core fix). The ink UI resolves all button-hint
icons from a single device field (
[[InkSystem]+0x210]+0x1F8in 2.5,input::EInputDevice). That field has exactly one steady-state writer, an InkSystem method that reads the current device and, on change, rewrites the field and broadcasts device-change notifications - the flicker, and the per-event UI churn whose accumulation causes the freeze (reproduced and measured, see RE-notes). The plugin hooks the writer, derives the would-be value the same way the writer does, and skips it entirely when it would switch away from the pinned device: no write, no broadcasts, no churn. - Script-side stickiness. Game scripts read the device through the
natives
PlayerLastUsedKBM/Pad/PS5Pad(a separate mechanism - it drives the contextual HUD hint layouts, popup layouts etc.). All script calls funnel through their registered native handlers; the plugin hooks those and forces the answers to match the pinned device. - Fallback latch (watchdog). Independent hooks observe the field and restore the pinned value if anything outside the suppressed writer ever changes it (e.g. session-init code), logging a warning - so hook coverage is verifiable from any user's log. If the writer hook ever failed to attach on some game build, this latch alone still provides the fix at v0.6 level (proven in testing).
| hook | addr-lib hash | purpose |
|---|---|---|
| device writer (InkSystem method) | 3719303744 | core fix: suppress switches off the pinned device |
PlayerLastUsedKBM native handler |
1037639234 | script-side device answer |
PlayerLastUsedPad native handler |
1291656829 | script-side device answer |
PlayerLastUsedPS5Pad native handler |
2647269205 | script-side device answer (DualSense detection) |
hint icon refresh (inkInputDisplayController) |
4042789608 | fallback latch + coverage watchdog |
inkSystem::ProcessInputEvents |
673454961 | fallback latch, once per UI input pump |
| InkSystem instance pointer | 2635337807 | (data, not a hook) |
The hashes survive game patches; the four struct offsets involved
(2.5: 0x210/0x1F8/0x300/vtbl 0x228; 2.31 had 0x1E0 and 0x218
for the second and fourth) are per-patch and may need
re-derivation - the procedure is documented in RE-notes.md ("Porting to a
new game patch") and the tools/ directory contains the analysis scripts
that make it a ~10 minute job.
Create an empty file MixedInputFix.diag next to the installed DLL and start
the game: the plugin arms a hardware write-watchpoint on the device field and
logs every writer's instruction address as exe+0x... (the RIP is the
instruction after the write; the plugin's own writes are filtered;
auto-disarms after ~400 events). One short mixed-input session is enough;
delete the marker afterwards. The offsets feed directly into
tools/dumpfunc.py. This is how the writer was originally found.
-
0.1-0.5 - failed approaches (kept honest in RE-notes.md so nobody retries them: device-activation hook fires on the wrong layer; pointer-mode suppression breaks the cursor).
-
0.6 - read-side latch on the hint device field. First working version: flicker gone, no freeze in long sessions. Regression: gamepad hint bars on some screens (world map) could hide and never re-show.
-
0.7 - added the opt-in hardware-watchpoint diagnostic; located the field's single writer.
-
0.8 - writer-level suppression: switches off the pinned device are skipped inside the game's own writer - no hide broadcasts, no churn. Fixed the world-map regression at the root.
-
0.9 / 0.9.1 - script-side stickiness via the
PlayerLastUsed*native handlers: fixed the contextual HUD hint widget flickering between its KBM and gamepad layout variants; all script device checks made consistent. -
1.0.0 - release:
MixedInputFix.ini(enabled,mode,verbose) with the newkbmmode (mirror-image stickiness for KBM-primary players), quiet-by-default logging with a session summary on exit, fail-soft hook attachment. Validated: vanilla freeze reproduced in ~25 min of intensive mixed input; with the fix, multi-hour sessions run clean at well past the vanilla freeze threshold with zero fallback events. -
1.1.0 - game 2.5 support, plus:
- game-build awareness: struct offsets are selected from a per-version table keyed on the executable's file version (2.31 and 2.5), and an unlisted build falls back to the newest known offsets, verified against the running game before the fix engages (if they fail, the plugin stays fully vanilla);
- script API for redscript mods to switch the mode at
runtime, and a new
inhibitedmode; - two new optional mods built on it: MixedInputInhibitOnPhotoMode and MixedInputVehicleControllerFpsKbm.
MIT.