From b1285ba560443a43f9c41a6109c08fc59b2fea2f Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 1 May 2026 16:30:03 +0200 Subject: [PATCH] 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) --- crates/xenia-debugger/src/lib.rs | 21 ++++- crates/xenia-hid/Cargo.toml | 1 + crates/xenia-hid/src/lib.rs | 150 ++++++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/crates/xenia-debugger/src/lib.rs b/crates/xenia-debugger/src/lib.rs index 0cf6d2b..abac8f8 100644 --- a/crates/xenia-debugger/src/lib.rs +++ b/crates/xenia-debugger/src/lib.rs @@ -42,15 +42,30 @@ impl Debugger { } } + /// Tier-3 perf: single branch that the hot interpreter loop checks + /// before dispatching to [`pre_step`]/[`post_step`]. When the + /// debugger is in "cold run" mode (not paused, no breakpoints, + /// `StepMode::Run`, in-memory trace off), both hooks become dead + /// code and we can skip the HashMap lookup + step-mode match + Vec + /// maintenance entirely. The compiler reliably branch-predicts the + /// stable branch direction across millions of instructions. + #[inline] + pub fn wants_hooks(&self) -> bool { + self.trace_enabled + || self.paused + || self.break_pending + || !matches!(self.step_mode, StepMode::Run) + || !self.breakpoints.is_empty() + } + /// Called before each instruction executes. pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { // Check breakpoints - if let Some(bp) = self.breakpoints.get(&ctx.pc) { - if bp.enabled { + if let Some(bp) = self.breakpoints.get(&ctx.pc) + && bp.enabled { self.break_pending = true; tracing::info!("Breakpoint hit at {:#010x}", ctx.pc); } - } } /// Called after each instruction executes. diff --git a/crates/xenia-hid/Cargo.toml b/crates/xenia-hid/Cargo.toml index b47a0a5..174f843 100644 --- a/crates/xenia-hid/Cargo.toml +++ b/crates/xenia-hid/Cargo.toml @@ -6,5 +6,6 @@ license.workspace = true [dependencies] xenia-types = { workspace = true } +xenia-memory = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } diff --git a/crates/xenia-hid/src/lib.rs b/crates/xenia-hid/src/lib.rs index 576fc5f..ca3197d 100644 --- a/crates/xenia-hid/src/lib.rs +++ b/crates/xenia-hid/src/lib.rs @@ -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` 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); + } +}