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:
@@ -42,16 +42,31 @@ 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.
|
/// Called before each instruction executes.
|
||||||
pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
||||||
// Check breakpoints
|
// Check breakpoints
|
||||||
if let Some(bp) = self.breakpoints.get(&ctx.pc) {
|
if let Some(bp) = self.breakpoints.get(&ctx.pc)
|
||||||
if bp.enabled {
|
&& bp.enabled {
|
||||||
self.break_pending = true;
|
self.break_pending = true;
|
||||||
tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
|
tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Called after each instruction executes.
|
/// Called after each instruction executes.
|
||||||
pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
xenia-types = { workspace = true }
|
xenia-types = { workspace = true }
|
||||||
|
xenia-memory = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -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.
|
/// Human input device system stub.
|
||||||
pub struct InputSystem {
|
pub struct InputSystem {
|
||||||
pub gamepad: GamepadState,
|
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 struct GamepadState {
|
||||||
pub buttons: u16,
|
pub buttons: u16,
|
||||||
pub left_trigger: u8,
|
pub left_trigger: u8,
|
||||||
@@ -14,7 +29,8 @@ pub struct GamepadState {
|
|||||||
pub right_stick_y: i16,
|
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 mod buttons {
|
||||||
pub const DPAD_UP: u16 = 0x0001;
|
pub const DPAD_UP: u16 = 0x0001;
|
||||||
pub const DPAD_DOWN: u16 = 0x0002;
|
pub const DPAD_DOWN: u16 = 0x0002;
|
||||||
@@ -26,12 +42,28 @@ pub mod buttons {
|
|||||||
pub const RIGHT_THUMB: u16 = 0x0080;
|
pub const RIGHT_THUMB: u16 = 0x0080;
|
||||||
pub const LEFT_SHOULDER: u16 = 0x0100;
|
pub const LEFT_SHOULDER: u16 = 0x0100;
|
||||||
pub const RIGHT_SHOULDER: u16 = 0x0200;
|
pub const RIGHT_SHOULDER: u16 = 0x0200;
|
||||||
|
pub const GUIDE: u16 = 0x0400;
|
||||||
pub const A: u16 = 0x1000;
|
pub const A: u16 = 0x1000;
|
||||||
pub const B: u16 = 0x2000;
|
pub const B: u16 = 0x2000;
|
||||||
pub const X: u16 = 0x4000;
|
pub const X: u16 = 0x4000;
|
||||||
pub const Y: u16 = 0x8000;
|
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 {
|
impl InputSystem {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -45,3 +77,117 @@ impl Default for InputSystem {
|
|||||||
Self::new()
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user