Libretro Input Polling: Ports, Device IDs, Analog Ranges, and Timing
Implement libretro input with correct per-frame polling, port and device semantics, analog ranges, relative mouse deltas, and tested fallbacks.
Libretro input is a query interface, not a stream of host-controller events. The frontend maps physical devices to abstract libretro device types, calls the core’s input-poll callback to update its input view, and answers the core’s subsequent state queries. This lets one core work with a keyboard, gamepad, touchscreen, mouse, or light gun across different frontends, but only if the core treats the callback and its four query parameters as a defined protocol.
This article focuses on the core-side API contract. It complements the end-to-end controller-latency analysis, which measures the broader button-to-display path, and does not repeat the general feature-negotiation overview in the environment-callback guide.
Register and call both input callbacks
The frontend supplies two callbacks through retro_set_input_poll() and retro_set_input_state(). The first asks the frontend to poll or refresh current input. The second queries a value for a player port, a device type, a device-specific index, and a control ID. The result type is int16_t; its meaning depends on the requested device and ID, and unsupported values return zero.
The canonical header requires the core to call the poll callback at least once during each retro_run(). A typical frame loop polls, reads the values it needs for the next emulated input sample, and then advances the emulated system. The exact point in an emulated frame matters: a console may latch controls at vblank, at a CPU write, or through a serial shift register. Libretro does not decide that hardware timing for the core. The core must sample or latch its abstract input at the moment required by the emulated machine.
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;
void retro_run(void)
{
input_poll_cb();
bool player_one_a = input_state_cb(
0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A) != 0;
emulate_one_video_frame(player_one_a);
}
The example uses the ordinary RetroPad query: port zero, base device type RETRO_DEVICE_JOYPAD, index zero, and the A-button ID. Production code should keep the callback references supplied by the frontend, use them in the expected core lifecycle, and query the port and controls actually supported by the emulated system. The API does not make calls thread-safe; keep ordinary input polling on the core’s main run path rather than a random worker thread.
Parse port, device, index, and ID as separate fields
The most common implementation bug is to treat the callback parameters as one packed controller number. port identifies a player-facing input port. device identifies an abstract class such as joypad, analog, mouse, pointer, keyboard, or light gun. index selects a subdevice when the device type has one, such as the left or right analog stick. id selects a button, axis, key, or other control within that abstraction.
For a digital RetroPad, index is normally zero and id names a button. The returned value is one when a digital button is pressed and zero when it is not. The RetroPad abstraction is designed around a common console-style gamepad, so a frontend can map it to a keyboard or physical controller without forcing the core to interpret each host device’s layout. Cores that emulate a gamepad should generally use that abstraction instead of requiring a host keyboard.
Use base device IDs when calling retro_input_state_t. The header explicitly reserves polling with device subclass IDs for future definition. Some current convenience logic masks the subclass bits, but a core must not depend on that implementation detail. If an emulated system uses a multitap or another specialized controller selected through retro_set_controller_port_device(), the documented input queries still use the base joypad device type for the relevant port.
Digital and analog values have different numeric contracts
Digital joypad buttons are Boolean-like values, but analog axes are signed 16-bit values. The standard analog range is from -0x8000 through 0x7fff, with positive X pointing right and positive Y pointing down. Analog button pressure uses a non-negative range from zero to 0x7fff. Some devices can return -0x8000, so code that assumes the minimum is -32767 can mishandle the full negative endpoint.
An analog query includes both an index and an ID. The index selects the stick or analog control group; the ID selects the axis or analog button. Do not read an analog stick as though its value were a normalized float from zero to one, and do not threshold every analog value before the emulated machine has had a chance to apply its own dead-zone or sensitivity behavior. If the emulated system has only digital input, define and test a deliberate conversion threshold in the core or frontend mapping rather than pretending that the analog value is already digital.
The optional joypad-mask query can return all pressed digital buttons in one value, but it is capability-dependent. A core must first ask the environment whether RETRO_ENVIRONMENT_GET_INPUT_BITMASKS is supported, and only query RETRO_DEVICE_ID_JOYPAD_MASK when that feature is available. The callback result is signed int16_t; if the core consumes individual bits, convert it to an unsigned 16-bit value before shifting or masking. The per-button path remains the portable fallback.
Mouse, pointer, and light gun are not synonyms
RETRO_DEVICE_MOUSE reports relative motion: its X and Y values describe movement since the last poll, and the core is responsible for maintaining its own pointer position. This is appropriate for a mouse-like device where motion is incremental. The core should accumulate deltas, apply the emulated device’s range and edge rules, and avoid confusing relative motion with a screen coordinate.
RETRO_DEVICE_POINTER represents absolute positioning devices such as touchscreens or styluses. The documented coordinates range from -0x7fff at the top/left to 0x7fff at the bottom/right of the frontend’s displayed game area. A light gun has its own device type and screen-space coordinate convention; the header describes -0x8000 as out-of-bounds. Keep these values distinct. Reinterpreting an off-screen light-gun shot as the pointer’s far-left coordinate can turn a valid reload action into an accidental edge shot.
Keyboard input is also a separate abstraction. A core emulating a home computer or a title that genuinely requires text input can poll keyboard state or register the keyboard-event callback. A gamepad-oriented console core should normally leave host-key mapping to the frontend. A keycode event and a generated character are separate concepts; the frontend may produce multiple characters from one key event or a key event without a character.
Zero is not a diagnostic explanation
The callback returns zero both for a neutral digital control and for a control unsupported by the frontend or its backing device. Therefore, zero does not distinguish “the user is not pressing A” from “the core queried a device/ID the frontend cannot provide.” A useful diagnosis checks the exact tuple: port, base device type, index, and ID. It also checks the frontend’s port device assignment, core-side input descriptor, and the emulated system’s expected controller configuration.
If the core depends on a capability such as a full joypad bitmask or a specific device class, feature-detect it using the defined environment query and provide a fallback. Do not infer device support from the name of a frontend or from one machine where the user’s mapping happens to work. Keep optional input features optional unless the emulated hardware cannot run without them, and then make the requirement explicit.
Test the sampling boundary, not just button labels
Create a small test matrix for the core and at least one other frontend where possible:
- Verify that
retro_input_poll()is called everyretro_run()and that state queries happen after the intended poll. - Test port zero and each additional supported port, including the controller device type selected for that port.
- Test each digital button, simultaneous directions, rapid press/release transitions, and the optional bitmask path with its fallback.
- Test the center, positive and negative endpoints, dead-zone boundary, and analog button pressure range.
- Move a relative mouse by known deltas; verify accumulation, clipping, and edge behavior across polls.
- Test pointer coordinates at all four corners and the center, then test light-gun off-screen state separately.
- Test keyboard state and event callbacks only for systems that actually emulate a keyboard.
For timing-sensitive systems, record when the core polls, when it latches the emulated controller, and which emulated CPU/frame boundary consumes the result. A frontend’s physical polling rate and operating-system event queue are separate timing layers; a test that confirms the callback returned a value does not prove a particular button-to-photon latency. That broader measurement belongs to the display path, not to the input ABI alone.
The core is responsible for translating the abstract state into the original hardware’s register, serial, or latch behavior. Frontends are responsible for mapping a real input source to the abstraction. Keeping those responsibilities separate makes the interface portable and makes failures diagnosable: first validate the queried tuple and poll boundary, then validate the console-specific input hardware model.
Related:
- Controller Input Latency: Tracing the Path From Button to Pixel
- Libretro’s Environment Callback: How Cores Negotiate Features with Frontends
Sources: