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 byte | hid bit | Xbox source | Xbox constant |
|---|---|---|---|
buttons_lo | bit0 (A) | XBT_BTN_A | 4096 (bit12) |
buttons_lo | bit1 (B) | XBT_BTN_B | 8192 (bit13) |
buttons_lo | bit2 (X) | XBT_BTN_X | 16384 (bit14) |
buttons_lo | bit3 (Y) | XBT_BTN_Y | 32768 (bit15) |
buttons_lo | bit4 (LB) | XBT_BTN_LB | 256 (bit8) |
buttons_lo | bit5 (RB) | XBT_BTN_RB | 512 (bit9) |
buttons_lo | bit6 (View/Back) | XBT_BTN_VIEW | 32 (bit5) |
buttons_lo | bit7 (Menu/Start) | XBT_BTN_MENU | 16 (bit4) |
buttons_hi | bit0 (LT digital) | lt > 511 | trigger threshold |
buttons_hi | bit1 (RT digital) | rt > 511 | trigger threshold |
buttons_hi | bit2 (LS click) | XBT_BTN_LS | 64 (bit6) |
buttons_hi | bit3 (RS click) | XBT_BTN_RS | 128 (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
p2_buttons word to 0 (no buttons held). Sim-safe — no MMIO on x86/QEMU; reads from xbox_bt.sg state at 0x9A0000.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().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.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
| Check | What it verifies |
|---|---|
| init / zero state | p2_buttons = 0 at init |
| LX axis center | 32767 / 256 = 127 |
| LX axis max | 65535 / 256 = 255 |
| LX axis min | 0 / 256 = 0 |
| d-pad N | DUP only → hat 0 |
| d-pad NE diagonal | DUP|DRT → hat 1 |
| d-pad center | no bits → hat 8 |
| A button | XBT_BTN_A bit → blo bit0 |
| Menu/Start button | XBT_BTN_MENU → blo bit7 |
| LT digital threshold | lt=512 → bhi bit0 set |
| LT below threshold | lt=511 → bhi bit0 clear |
| set/get round-trip | core_set_input_p2(0xAB) → core_get_p2_buttons() = 0xAB |
| update from xbt state | full xbt state → packed p2_buttons correct |