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>
194 lines
6.8 KiB
Rust
194 lines
6.8 KiB
Rust
//! 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,
|
|
}
|
|
|
|
/// 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,
|
|
pub right_trigger: u8,
|
|
pub left_stick_x: i16,
|
|
pub left_stick_y: i16,
|
|
pub right_stick_x: i16,
|
|
pub right_stick_y: i16,
|
|
}
|
|
|
|
/// 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;
|
|
pub const DPAD_LEFT: u16 = 0x0004;
|
|
pub const DPAD_RIGHT: u16 = 0x0008;
|
|
pub const START: u16 = 0x0010;
|
|
pub const BACK: u16 = 0x0020;
|
|
pub const LEFT_THUMB: u16 = 0x0040;
|
|
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 {
|
|
gamepad: GamepadState::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for InputSystem {
|
|
fn default() -> Self {
|
|
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);
|
|
}
|
|
}
|