xenia-hid + xenia-debugger: gamepad serializer; debugger fast-skip hook

xenia-hid grows a guest-facing X_INPUT_GAMEPAD writer (big-endian on
the wire, host-neutral GamepadState in memory) so XamInputGetState in
the kernel and the UI input thread share one POD snapshot type. Adds
the GUIDE button flag.

xenia-debugger gains Debugger::wants_hooks(), a single-branch fast
path the hot interpreter loop checks to skip the pre_step/post_step
HashMap+match work when the debugger is in cold-run mode (no bps, no
trace, StepMode::Run, not paused). Part of the Tier-3 perf landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-01 16:30:03 +02:00
parent 79eb52c378
commit b1285ba560
3 changed files with 167 additions and 5 deletions

View File

@@ -1,9 +1,24 @@
//! Human input device system.
//!
//! Holds the guest-facing `X_INPUT_*` struct layouts (big-endian, matching the
//! Xbox 360 ABI exactly) and the host-neutral `GamepadState` snapshot used by
//! both the UI (writer) and the kernel's `XamInputGetState` handler (reader).
use xenia_memory::{GuestMemory, MemoryAccess};
/// Human input device system stub.
pub struct InputSystem {
pub gamepad: GamepadState,
}
#[derive(Default, Clone, Copy)]
/// Host-side gamepad snapshot.
///
/// Kept as POD so it can be dropped into an `AtomicCell<GamepadState>` for
/// lock-free UI→CPU state transfer. Layout intentionally matches the in-memory
/// subset of `X_INPUT_GAMEPAD` (minus endianness), but the wire serializer in
/// [`write_input_state`] handles the big-endian conversion explicitly.
#[derive(Default, Clone, Copy, Debug)]
#[repr(C)]
pub struct GamepadState {
pub buttons: u16,
pub left_trigger: u8,
@@ -14,7 +29,8 @@ pub struct GamepadState {
pub right_stick_y: i16,
}
/// Xbox 360 button flags
/// Xbox 360 button flags. Values match `X_INPUT_GAMEPAD_BUTTON` in
/// `xenia-canary/src/xenia/hid/input.h`.
pub mod buttons {
pub const DPAD_UP: u16 = 0x0001;
pub const DPAD_DOWN: u16 = 0x0002;
@@ -26,12 +42,28 @@ pub mod buttons {
pub const RIGHT_THUMB: u16 = 0x0080;
pub const LEFT_SHOULDER: u16 = 0x0100;
pub const RIGHT_SHOULDER: u16 = 0x0200;
pub const GUIDE: u16 = 0x0400;
pub const A: u16 = 0x1000;
pub const B: u16 = 0x2000;
pub const X: u16 = 0x4000;
pub const Y: u16 = 0x8000;
}
/// Xbox guest error codes returned by `XamInput*`.
pub mod errors {
/// ERROR_SUCCESS
pub const SUCCESS: u32 = 0;
/// ERROR_DEVICE_NOT_CONNECTED
pub const DEVICE_NOT_CONNECTED: u32 = 0x48F;
/// ERROR_EMPTY (used by XamInputGetKeystroke when no event queued)
pub const EMPTY: u32 = 0x10D2;
}
/// Sub-types that games query via `X_INPUT_CAPABILITIES::SubType`.
pub mod subtype {
pub const GAMEPAD: u8 = 0x01;
}
impl InputSystem {
pub fn new() -> Self {
Self {
@@ -45,3 +77,117 @@ impl Default for InputSystem {
Self::new()
}
}
/// Serialize a [`GamepadState`] into the 16-byte big-endian `X_INPUT_STATE`
/// struct at `out_ptr` in guest memory.
///
/// Layout (matches `xenia-canary/src/xenia/hid/input.h`):
///
/// | Offset | Size | Field | Endianness |
/// |--------|------|----------------|------------|
/// | 0x00 | 4 | packet_number | BE u32 |
/// | 0x04 | 2 | buttons | BE u16 |
/// | 0x06 | 1 | left_trigger | u8 |
/// | 0x07 | 1 | right_trigger | u8 |
/// | 0x08 | 2 | thumb_lx | BE i16 |
/// | 0x0A | 2 | thumb_ly | BE i16 |
/// | 0x0C | 2 | thumb_rx | BE i16 |
/// | 0x0E | 2 | thumb_ry | BE i16 |
pub fn write_input_state(
mem: &GuestMemory,
out_ptr: u32,
packet_number: u32,
state: &GamepadState,
) {
if out_ptr == 0 {
return;
}
mem.write_u32(out_ptr, packet_number);
mem.write_u16(out_ptr + 0x04, state.buttons);
mem.write_u8(out_ptr + 0x06, state.left_trigger);
mem.write_u8(out_ptr + 0x07, state.right_trigger);
mem.write_u16(out_ptr + 0x08, state.left_stick_x as u16);
mem.write_u16(out_ptr + 0x0A, state.left_stick_y as u16);
mem.write_u16(out_ptr + 0x0C, state.right_stick_x as u16);
mem.write_u16(out_ptr + 0x0E, state.right_stick_y as u16);
}
/// Serialize an `X_INPUT_CAPABILITIES` block for a standard wired controller.
///
/// Layout (20 bytes, matches `xenia-canary`):
///
/// | Offset | Size | Field |
/// |--------|------|------------------|
/// | 0x00 | 1 | Type (gamepad=1) |
/// | 0x01 | 1 | SubType |
/// | 0x02 | 2 | Flags |
/// | 0x04 | 12 | Gamepad state |
/// | 0x10 | 2 | Vibration.left |
/// | 0x12 | 2 | Vibration.right |
pub fn write_input_capabilities(mem: &GuestMemory, out_ptr: u32) {
if out_ptr == 0 {
return;
}
// Type = DEVTYPE_GAMEPAD (1), SubType = STANDARD (1), Flags = 0
mem.write_u8(out_ptr, 1);
mem.write_u8(out_ptr + 0x01, subtype::GAMEPAD);
mem.write_u16(out_ptr + 0x02, 0);
// Gamepad capabilities: buttons = all standard bits, triggers+sticks = full range.
// Games typically AND the gamepad mask to decide which controls exist; advertising
// everything is the safe default.
mem.write_u16(out_ptr + 0x04, 0xF3FF); // buttons: all except GUIDE
mem.write_u8(out_ptr + 0x06, 0xFF); // left_trigger range
mem.write_u8(out_ptr + 0x07, 0xFF); // right_trigger range
mem.write_u16(out_ptr + 0x08, 0xFFFFu16); // lx
mem.write_u16(out_ptr + 0x0A, 0xFFFFu16); // ly
mem.write_u16(out_ptr + 0x0C, 0xFFFFu16); // rx
mem.write_u16(out_ptr + 0x0E, 0xFFFFu16); // ry
// Vibration: both motors full range
mem.write_u16(out_ptr + 0x10, 0xFFFFu16);
mem.write_u16(out_ptr + 0x12, 0xFFFFu16);
}
#[cfg(test)]
mod tests {
use super::*;
use xenia_memory::page_table::MemoryProtect;
#[test]
fn write_input_state_layout_is_big_endian() {
let mut mem = GuestMemory::new().unwrap();
let rw = MemoryProtect::READ | MemoryProtect::WRITE;
mem.alloc(0x4000_0000, 0x1000, rw).unwrap();
let state = GamepadState {
buttons: buttons::A | buttons::START, // 0x1010
left_trigger: 0x80,
right_trigger: 0x40,
left_stick_x: 0x0102,
left_stick_y: -1,
right_stick_x: 0x0304,
right_stick_y: 0x0506,
};
write_input_state(&mut mem, 0x4000_0000, 0xDEAD_BEEF, &state);
// packet_number BE
assert_eq!(mem.read_u8(0x4000_0000), 0xDE);
assert_eq!(mem.read_u8(0x4000_0001), 0xAD);
assert_eq!(mem.read_u8(0x4000_0002), 0xBE);
assert_eq!(mem.read_u8(0x4000_0003), 0xEF);
// buttons BE (0x1010 → high byte 0x10 first)
assert_eq!(mem.read_u8(0x4000_0004), 0x10);
assert_eq!(mem.read_u8(0x4000_0005), 0x10);
// triggers
assert_eq!(mem.read_u8(0x4000_0006), 0x80);
assert_eq!(mem.read_u8(0x4000_0007), 0x40);
// thumb_lx BE
assert_eq!(mem.read_u8(0x4000_0008), 0x01);
assert_eq!(mem.read_u8(0x4000_0009), 0x02);
// thumb_ly = -1 → 0xFFFF
assert_eq!(mem.read_u8(0x4000_000A), 0xFF);
assert_eq!(mem.read_u8(0x4000_000B), 0xFF);
// thumb_rx BE
assert_eq!(mem.read_u8(0x4000_000C), 0x03);
assert_eq!(mem.read_u8(0x4000_000D), 0x04);
assert_eq!(mem.read_u8(0x4000_000E), 0x05);
assert_eq!(mem.read_u8(0x4000_000F), 0x06);
}
}