← Blog
DRIVERS · P2 INPUT · RETROPIE

P2 Xbox One BT Bridge — Two-Player RetroPie Input Now Wired

June 22, 2026 · sigil-drivers · Sigil-Docs
drivers input retropie bluetooth p2 netplay

core_input_p2.sg (sigil-drivers 6796bff) bridges the P2 Xbox One BT controller slot into the emulator core input ABI. It reads the Xbox One BT state from xbox_bt.sg, translates it to the canonical hid_gamepad layout, and exposes core_set_input_p2(buttons) / core_get_p2_buttons() for the RetroPie emulator core. 13/13 PASS, x86-runverify. Two-player local input is now wired end-to-end; SRDX Mode-2 rollback netplay can inject remote P2 input through the same ABI.


Why a dedicated P2 bridge

The emulator core ABI uses an 8-byte hid_gamepad report format: axes as 8-bit values (0–255, center 127), d-pad as a hat switch (0=N…7=NW, 8=center), and buttons packed into two bytes (buttons_lo at byte 5, buttons_hi at byte 6). The Xbox One BT driver (xbox_bt.sg) uses 16-bit axes (0–65535, center 32767), separate d-pad bits, and a flat button bitmask. The P2 bridge handles the translation once, cleanly, in one place — so the core never sees the controller's raw format.

The architecture is also capability-clean: the emulator core calls core_get_p2_buttons() and gets a canonical button word. It does not hold a reference to the Xbox BT state, does not peek at BT hardware registers, and does not need to know which controller type P2 is using. If P2 switches from an Xbox controller to a Switch Pro or DS4, the bridge is the only thing that changes.


Translation pipeline

Axis scaling: 16-bit → 8-bit

Xbox One BT reports axes in the range 0–65535 (center 32767). The hid_gamepad ABI uses 0–255 (center 127). The bridge scales via integer division: axis_hid = axis_xbt / 256. For the four axes (LX, LY, RX, RY) this produces a uniform mapping with no fractional loss at the 8-bit resolution the core uses.

D-pad → hat switch translation

The Xbox BT d-pad is four independent bits: DUP=1 (bit0), DDN=2 (bit1), DLT=4 (bit2), DRT=8 (bit3). The hid_gamepad hat switch uses a single value: 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=center.

The bridge uses a 16-entry code-lookup table indexed by the 4-bit d-pad value. This avoids nested variable mutation (no if/else if chain that mutates a running hat value) — the lookup is a single read from the table. Diagonal combinations (e.g. DUP|DRT = 0b1001 = 9 → NE = 1) are handled correctly.

// d-pad to hat lookup (index = DUP|DDN|DLT|DRT bitfield)
hat_table = [8,0,4,8, 6,7,5,8, 2,1,3,8, 8,8,8,8]
// Examples:
// 0b0001 (DUP only) → 0 (N)
// 0b1001 (DUP|DRT) → 1 (NE)
// 0b0100 (DLT only) → 6 (W)
// 0b0000 (none)     → 8 (center)

Button packing: blo / bhi

Xbox buttons are a flat 16-bit bitmask. The bridge packs them into two hid_gamepad bytes:

hid bytehid bitXbox sourceXbox constant
buttons_lobit0 (A)XBT_BTN_A4096 (bit12)
buttons_lobit1 (B)XBT_BTN_B8192 (bit13)
buttons_lobit2 (X)XBT_BTN_X16384 (bit14)
buttons_lobit3 (Y)XBT_BTN_Y32768 (bit15)
buttons_lobit4 (LB)XBT_BTN_LB256 (bit8)
buttons_lobit5 (RB)XBT_BTN_RB512 (bit9)
buttons_lobit6 (View/Back)XBT_BTN_VIEW32 (bit5)
buttons_lobit7 (Menu/Start)XBT_BTN_MENU16 (bit4)
buttons_hibit0 (LT digital)lt > 511trigger threshold
buttons_hibit1 (RT digital)rt > 511trigger threshold
buttons_hibit2 (LS click)XBT_BTN_LS64 (bit6)
buttons_hibit3 (RS click)XBT_BTN_RS128 (bit7)

All bit-extraction uses explicit parentheses — (btns / XBT_BTN_A) % 2 rather than implicit precedence — matching the sigilOS driver coding convention established in the d-pad and stick drivers.


Driver API

core_p2_init()
Initialize P2 bridge. Sets the internal p2_buttons word to 0 (no buttons held). Sim-safe — no MMIO on x86/QEMU; reads from xbox_bt.sg state at 0x9A0000.
core_update_p2()
Read current Xbox BT P2 state (lx/ly/rx/ry/btns/lt/rt from xbt_state), run axis scaling + d-pad lookup + button packing, write result into p2_buttons. Called once per emulator frame before core_get_p2_buttons().
core_set_input_p2(buttons)
Override p2_buttons with a pre-packed button word. Used by SRDX Mode-2 rollback: when a late P2 input arrives from the remote player, the host calls core_set_input_p2(remote_buttons) before replaying the divergent frames. The core sees the corrected input without knowing whether it came from a local controller or the SRDX IPC channel.
core_get_p2_buttons()
Return current p2_buttons word. Called by the emulator core at its input poll point. Returns the same canonical format regardless of whether P2 is a local Xbox BT controller or an injected SRDX remote input.

The SRDX Mode-2 connection

Mode-2 rollback netplay requires that both endpoints can inject arbitrary P2 input into the emulator core at any frame boundary. core_set_input_p2(buttons) is the injection point. When the SRDX session layer receives a late P2 input packet from the remote player, it calls:

core_loadstate(snap_slot)         // roll back to pre-divergence frame
core_set_input_p2(remote_buttons) // inject corrected P2 input
core_run_frame()                  // replay with corrected state
core_savestate(snap_slot)         // update the snap point

The core never distinguishes between local and remote P2 input — it just reads core_get_p2_buttons(). The capability model is clean: the SRDX session layer holds the IPC cap to receive remote input, and the core holds only the gamepad cap. Neither holds the other's cap.

With core_input_p2.sg wired, the local 2-player input path is complete end-to-end: Xbox BT P2 → bridge → p2_buttons → core. The Mode-2 remote injection path is also complete: SRDX IPC → core_set_input_p2 → core. The only remaining piece for live Mode-2 netplay is the two-Pi SRDX session over real hardware.


Test results — 13/13 PASS

CheckWhat it verifies
init / zero statep2_buttons = 0 at init
LX axis center32767 / 256 = 127
LX axis max65535 / 256 = 255
LX axis min0 / 256 = 0
d-pad NDUP only → hat 0
d-pad NE diagonalDUP|DRT → hat 1
d-pad centerno bits → hat 8
A buttonXBT_BTN_A bit → blo bit0
Menu/Start buttonXBT_BTN_MENU → blo bit7
LT digital thresholdlt=512 → bhi bit0 set
LT below thresholdlt=511 → bhi bit0 clear
set/get round-tripcore_set_input_p2(0xAB)core_get_p2_buttons() = 0xAB
update from xbt statefull xbt state → packed p2_buttons correct