Input system
NEShim supports three input sources that are combined every frame: keyboard, XInput gamepads, and Steam Input controllers. This page covers how each source works, how they interact, and how to configure them.
Overview
Every emulation frame, InputManager.PollSnapshot() produces an InputSnapshot — an immutable set of NES button names that are currently pressed. The snapshot merges all three sources, with duplicates deduplicated harmlessly by the ImmutableHashSet builder:
- Steam Input: reads active Gameplay action names from
SteamInputManager.GetActiveActions()and maps them to NES button names via a fixed constant table in code (SteamInputManager.ActionToNesButton). - XInput: reads raw gamepad state from
xinput1_4.dllfor player 0 and resolves each button through each binding’sgamepadButtonfield inconfig.json. - Keyboard: reads pressed keys and resolves them through each binding’s
keyfield inconfig.json.
Steam actions are not stored in config.json. The mapping from VDF action name to NES button name is fixed by the VDF file and lives as a compile-time constant — it is not user-configurable. Only keyboard and XInput bindings are stored in config and editable in-game.
Each entry in inputMappings maps a NES button name to two source bindings:
"P1 A": {
"key": "OemPeriod",
"gamepadButton": "A"
}
This means:
- An Xbox controller detected by Steam Input uses XInput passthrough — all input flows through XInput. Both Steam polling (empty) and XInput polling (button pressed) are checked; only the XInput path fires.
- A PS4, PS5, or Switch Pro controller requires Steam Input (these are not natively XInput devices). The Steam Gameplay action set defines the mapping;
ActionToNesButtoninSteamInputManagerconverts action names to NES buttons each frame. - A player can use a keyboard and a gamepad simultaneously.
Keyboard input
Keyboard input is driven by Windows Forms KeyDown / KeyUp events wired in MainForm:
WinForms KeyDown/KeyUp (UI thread)
→ InputManager.OnKeyDown / OnKeyUp
→ _pressedKeys (HashSet, protected by lock)
→ read on emulation thread in PollSnapshot()
Mapping keyboard keys
Each NES button in inputMappings has an optional key field. The value is a System.Windows.Forms.Keys enum member name (case-insensitive):
"P1 A": { "key": "OemPeriod", "gamepadButton": "A" }
Common key names:
| Key | Name |
|---|---|
| Letter keys | "A" through "Z" |
| Number row | "D0" through "D9" |
| Numpad | "NumPad0" through "NumPad9" |
| Arrow keys | "Up", "Down", "Left", "Right" |
| Enter | "Return" |
| Shift (right) | "RShiftKey" |
| Shift (left) | "LShiftKey" |
| Space | "Space" |
| Period | "OemPeriod" |
| Comma | "OemComma" |
| Backspace | "Back" |
| Escape | "Escape" |
| F1–F12 | "F1" through "F12" |
The full list is the System.Windows.Forms.Keys enum. The in-game keyboard rebind screen writes the correct name for you when you press a key.
Binding uniqueness
When you rebind a key, any other action previously bound to that key is automatically cleared to prevent duplicate bindings. This applies to both keyboard and gamepad bindings independently.
XInput gamepads
XInput support uses a direct P/Invoke to xinput1_4.dll (present on Windows 8+). Player 0 (the first connected controller) is always used.
Mapping XInput buttons
Each NES button in inputMappings has an optional gamepadButton field. Valid values:
| Value | Physical button |
|---|---|
"A" | A button |
"B" | B button |
"X" | X button |
"Y" | Y button |
"Start" | Start button (reserved — see below) |
"Back" | Back/Select button |
"LeftShoulder" | Left bumper (LB) |
"RightShoulder" | Right bumper (RB) |
"LeftThumb" | Left stick click |
"RightThumb" | Right stick click |
"DPadUp" | D-pad up |
"DPadDown" | D-pad down |
"DPadLeft" | D-pad left |
"DPadRight" | D-pad right |
Start is reserved. Regardless of the input mapping, pressing the gamepad Start button always opens or closes the in-game pause menu. It cannot be bound to a NES button. This prevents the player from softlocking a game that doesn’t implement its own pause.
Analog stick → D-pad conversion
The left analog stick is automatically converted to directional input using the configured deadzone (gamepadDeadzone). The conversion runs even if D-pad buttons are already mapped. The deadzone is a raw axis value in the range ±32767; the default of 8000 is about 24% deflection.
When both axes exceed the deadzone at the same time (stick pushed diagonally), behaviour is controlled by the analogStickMode developer setting:
analogStickMode | Behaviour |
|---|---|
"Cardinal" (default) | The axis with the larger absolute value wins. Only one direction registers, preventing accidental diagonal NES input in games with 4-directional movement. |
"Diagonal" | Both axes register simultaneously. Use this for games with genuine 8-directional movement. |
Menu navigation always uses cardinal mode regardless of this setting — menus are inherently 4-directional.
Steam Input
Steam Input is the recommended path for non-Xbox controllers. It maps physical hardware to abstract game actions, enabling PS4, PS5, Switch Pro, Steam Controller, and other controllers to work without XInput.
How it works
Steam Input maps physical hardware through a layer defined in a VDF (value definition) file. The game declares abstract actions (up, a_button, menu_confirm, etc.) and Steam maps the player’s physical hardware to those actions. Default mappings ship with the game so players can use supported controllers immediately. Players can override the defaults from the Steam overlay configurator at any time.
Each frame, SteamInputManager.GetActiveActions() returns the set of VDF action names currently pressed. InputManager.PollSnapshot() applies the constant mapping table (SteamInputManager.ActionToNesButton) to convert those action names to NES button names — no config lookup is needed. This table is:
| VDF action | NES button |
|---|---|
up | P1 Up |
down | P1 Down |
left | P1 Left |
right | P1 Right |
a_button | P1 A |
b_button | P1 B |
start | P1 Start |
select | P1 Select |
This mapping is fixed in code (SteamInputManager.ActionToNesButton / NesButtonToAction). It is not stored in config.json and cannot be changed without modifying both the code and the VDF file.
Action sets
NEShim defines two action sets in game_actions_<appid>.vdf:
Gameplay set
Active during emulation (when the pause menu is closed).
| Action name | Purpose |
|---|---|
up | NES D-pad Up |
down | NES D-pad Down |
left | NES D-pad Left |
right | NES D-pad Right |
a_button | NES A button |
b_button | NES B button |
start | NES Start |
select | NES Select |
Menu set
Active when the pre-game main menu or in-game pause menu is open.
| Action name | Purpose |
|---|---|
menu_up | Move cursor up |
menu_down | Move cursor down |
menu_left | Decrease volume (on Sound screen) |
menu_right | Increase volume (on Sound screen) |
menu_confirm | Activate selected item |
menu_back | Go back / cancel |
The action set is switched automatically:
SteamInputManager.ActivateGameplaySet()— called when emulation resumes (menu closed, game started).SteamInputManager.ActivateMenuSet()— called when a menu opens or the main menu is shown.
VDF file setup
The action definition file must be present alongside the executable and named game_actions_<AppID>.vdf. During development the placeholder file is named game_actions_0.vdf.
The file contains a configurations block that tells Steam which binding VDF to load for each controller type. This makes the defaults apply automatically in local development without requiring an upload to the Steamworks partner dashboard first.
Steps for production:
- Rename the file:
game_actions_0.vdf→game_actions_<YourAppID>.vdf. - Upload the file via the Steamworks partner dashboard under Steam Input → Default Configuration.
- Upload the controller binding VDF files from
controller_bindings/as the default configuration for each controller type (see Default controller bindings below).
If the VDF file is missing or Steam Input fails to initialise, SteamInputManager.IsAvailable will be false and the code falls back to XInput.
Default controller bindings
Default bindings ship in the controller_bindings/ directory alongside the executable:
| File | Controller type |
|---|---|
xbox360.vdf | Xbox 360 |
xboxone.vdf | Xbox One / Xbox Series X|S / Xbox One Elite |
neptune.vdf | Steam Deck |
ps4.vdf | PlayStation 4 DualShock 4 |
ps5.vdf | PlayStation 5 DualSense |
switch_pro.vdf | Nintendo Switch Pro Controller |
steam_controller.vdf | Valve Steam Controller |
Each file uses XInput passthrough bindings: face buttons, D-pad, Start, and Back are forwarded to the game as XInput signals, and the left analog stick passes through automatically. The existing XInput code in InputManager handles all mapping from there via config.json. Players can override the defaults at any time from the Steam overlay configurator (Shift+Tab).
Note on Switch Pro button labels. Steam Input normalises button positions across controllers using a positional mapping. The “A” button in the binding file maps to the bottom face button on the physical controller, which is B on a Switch Pro. The NES-style labels (A = right face, B = bottom face) are correct for gameplay feel.
Gamepad bindings screen behaviour
The Gamepad Controls settings screen behaves differently depending on the active controller type:
XInput / XInput passthrough controllers (Xbox, most gamepads)
SteamInputManager.IsUsingNativeActions() returns false — all input flows through XInput.
- Each binding row shows the XInput button name from
config.json(e.g.DPadUp,Y,Back). - All rows are selectable and editable.
- To rebind: select a row, press the desired physical button. The binding is saved immediately.
- Start is reserved — pressing it during rebind shows a toast and cancels the operation.
Native Steam controllers (PS4, PS5, Switch Pro, Steam Controller with full action bindings)
SteamInputManager.IsUsingNativeActions() returns true — the Gameplay action set has real digital action bindings, not XInput passthrough.
- Each binding row shows the physical button label from Steam (e.g. “Cross Button”, “Triangle Button”), queried live via
GetDigitalActionOrigins+GetStringForActionOrigin. Labels reflect the player’s current Steam controller configurator layout. - All binding rows are shown read-only (greyed out). In-game rebinding is not possible for native Steam controllers.
- To remap: open the Steam overlay (Shift+Tab) → Controller Settings, and adjust bindings there. The change is reflected live in the binding labels on next visit to this screen.
- The Back row remains active so the player can exit the screen.
Hotkeys
Hotkeys are system-level shortcuts processed by the emulation thread before gameplay input is forwarded to the NES. They use edge-triggered detection (fires once on the frame the key is first pressed, not every frame it is held).
Keyboard hotkeys
Configured via hotkeyMappings in config.json:
| Action | Default key | Description |
|---|---|---|
OpenMenu | Escape | Open or close the in-game pause menu |
SaveActiveSlot | F5 | Save to the currently selected slot |
LoadActiveSlot | F9 | Load from the currently selected slot (only if the slot is non-empty) |
SelectSlot1–SelectSlot8 | F1–F8* | Select a save slot (displays a toast) |
ToggleWindow | F11 | Toggle between fullscreen and windowed mode |
*Slots 1–4 use F1–F4; slot 5 uses F6; slots 6–8 use F7, F8, F12. F5 and F9 are reserved for save/load.
Gamepad hotkeys
Configured via gamepadHotkeyMappings in config.json:
| Action | Default button | Description |
|---|---|---|
OpenMenu | LeftShoulder | Open or close the in-game pause menu |
The gamepad Start button always opens/closes the pause menu regardless of this mapping.
Menu navigation
While a menu is open, the emulation loop does not run RunFrame. Instead, it polls for menu navigation input at approximately 60 Hz (using ManualResetEventSlim.Wait(16)).
Menu navigation combines XInput and Steam Input (edge-triggered):
| Input | Action |
|---|---|
| D-pad Up / Left stick up | Move cursor up |
| D-pad Down / Left stick down | Move cursor down |
| D-pad Left / Left stick left | Decrease volume (on Sound screen) |
| D-pad Right / Left stick right | Increase volume (on Sound screen) |
| A button | Confirm / activate selected item |
| B button or Back button | Go back |
Keyboard navigation uses the arrow keys (Up/Down for cursor movement, Left/Right for volume), Enter/Space/Z to confirm, and Escape to go back.
Mouse hover and click are also supported — hovering highlights items; clicking activates them.
Input pipeline summary
Keyboard events (UI thread)
──→ InputManager._pressedKeys (lock)
↓
Emulation thread: PollSnapshot()
├─ Steam Input: SteamInputManager.GetActiveActions()
│ └─ ImmutableHashSet<string> of VDF action names (empty if unavailable)
│ └─ SteamInputManager.ActionToNesButton (constant table)
│ └─ NES button names added directly to builder
├─ XInput: XInputHelper.GetState(0)
│ └─ Digital buttons + analog axes (empty if disconnected)
└─ Keyboard: _pressedKeys → Keys enum
↓
InputMappings loop (config.json) — keyboard + XInput only
per NES button: check binding.GamepadButton pressed on XInput
check binding.Key ∈ pressedKeys
+ analog stick → D-pad conversion (XInput only)
↓ (all sources resolved; duplicates deduplicated)
InputSnapshot (ImmutableHashSet<string> of NES button names)
↓
NesController.Update(snapshot)
↓
NES.FrameAdvance(controller, ...)