Workspace gains a new xenia-ui member that owns the winit/wgpu window, the Xenos display pipeline (xenos_pipeline + render + texture_cache_host), HUD font/blit shaders, and the input-bridge plumbing the app uses to surface guest framebuffers and overlays. Workspace dependencies grow accordingly: rusqlite is replaced with duckdb (analysis pipeline now writes DuckDB stores), and tracing / metrics / pprof / winit / wgpu / gilrs / pollster / crossbeam / bytemuck are added at workspace level so xenia-ui and xenia-app share versions. Cargo.lock regenerated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
6.0 KiB
Rust
160 lines
6.0 KiB
Rust
//! gilrs → [`xenia_hid::GamepadState`] adapter.
|
|
|
|
use gilrs::{Axis, Button, EventType, Gilrs, GilrsBuilder};
|
|
use xenia_hid::{GamepadState, buttons};
|
|
|
|
/// Simulated-pad policy: if no physical controller is attached, advertise
|
|
/// a connected pad (with all-zero state) so games don't early-exit on
|
|
/// `XamInputGetState → ERROR_DEVICE_NOT_CONNECTED`. Set
|
|
/// `XENIA_FAKE_PAD=0` to disable and report the true no-controller status.
|
|
fn fake_pad_policy() -> bool {
|
|
match std::env::var("XENIA_FAKE_PAD").as_deref() {
|
|
Ok("0") | Ok("false") | Ok("off") => false,
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
pub struct HostInput {
|
|
ctx: Gilrs,
|
|
/// Currently-active gilrs pad id. `None` until the first Connected event.
|
|
active: Option<gilrs::GamepadId>,
|
|
/// `true` if the pad is really physical; `false` for the zeroed simulated
|
|
/// pad we advertise when no hardware is attached (see `fake_pad_policy`).
|
|
pub physical_attached: bool,
|
|
fake_allowed: bool,
|
|
}
|
|
|
|
impl HostInput {
|
|
pub fn new() -> anyhow::Result<Self> {
|
|
let ctx = GilrsBuilder::new()
|
|
.add_included_mappings(true)
|
|
.build()
|
|
.map_err(|e| anyhow::anyhow!("gilrs init failed: {e}"))?;
|
|
let active = ctx.gamepads().next().map(|(id, _)| id);
|
|
let physical_attached = active.is_some();
|
|
let fake_allowed = fake_pad_policy();
|
|
tracing::info!(
|
|
physical = physical_attached,
|
|
fake_allowed,
|
|
"input: init (pad simulation: {})",
|
|
if fake_allowed && !physical_attached {
|
|
"simulated (no physical pad)"
|
|
} else if physical_attached {
|
|
"physical"
|
|
} else {
|
|
"disabled"
|
|
}
|
|
);
|
|
Ok(Self {
|
|
ctx,
|
|
active,
|
|
physical_attached,
|
|
fake_allowed,
|
|
})
|
|
}
|
|
|
|
/// True iff a gamepad is "available" for user index 0 — either the
|
|
/// actual host pad, or a simulated one when `XENIA_FAKE_PAD != 0`.
|
|
pub fn is_connected(&self) -> bool {
|
|
if self.physical_attached
|
|
&& self
|
|
.active
|
|
.and_then(|id| self.ctx.connected_gamepad(id))
|
|
.is_some()
|
|
{
|
|
return true;
|
|
}
|
|
self.fake_allowed
|
|
}
|
|
|
|
/// Pump gilrs events until empty. Updates the internal "active pad"
|
|
/// registration; pad state is sampled by [`snapshot`].
|
|
pub fn poll(&mut self) {
|
|
while let Some(event) = self.ctx.next_event() {
|
|
match event.event {
|
|
EventType::Connected => {
|
|
if self.active.is_none() {
|
|
self.active = Some(event.id);
|
|
self.physical_attached = true;
|
|
tracing::info!("gilrs: pad connected (id={:?})", event.id);
|
|
}
|
|
}
|
|
EventType::Disconnected => {
|
|
if self.active == Some(event.id) {
|
|
self.active = None;
|
|
tracing::info!("gilrs: pad disconnected (id={:?})", event.id);
|
|
self.active = self.ctx.gamepads().next().map(|(id, _)| id);
|
|
self.physical_attached = self.active.is_some();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build an Xbox-360-layout `GamepadState` from the active gilrs pad.
|
|
/// Returns `GamepadState::default()` (all-zero) when no pad is present.
|
|
pub fn snapshot(&self) -> GamepadState {
|
|
let Some(id) = self.active else {
|
|
return GamepadState::default();
|
|
};
|
|
let Some(pad) = self.ctx.connected_gamepad(id) else {
|
|
return GamepadState::default();
|
|
};
|
|
let mut state = GamepadState::default();
|
|
|
|
let pressed = |btn: Button| pad.is_pressed(btn);
|
|
let set = |state: &mut GamepadState, btn: Button, mask: u16| {
|
|
if pressed(btn) {
|
|
state.buttons |= mask;
|
|
}
|
|
};
|
|
set(&mut state, Button::DPadUp, buttons::DPAD_UP);
|
|
set(&mut state, Button::DPadDown, buttons::DPAD_DOWN);
|
|
set(&mut state, Button::DPadLeft, buttons::DPAD_LEFT);
|
|
set(&mut state, Button::DPadRight, buttons::DPAD_RIGHT);
|
|
set(&mut state, Button::Start, buttons::START);
|
|
set(&mut state, Button::Select, buttons::BACK);
|
|
set(&mut state, Button::LeftThumb, buttons::LEFT_THUMB);
|
|
set(&mut state, Button::RightThumb, buttons::RIGHT_THUMB);
|
|
set(&mut state, Button::LeftTrigger, buttons::LEFT_SHOULDER);
|
|
set(&mut state, Button::RightTrigger, buttons::RIGHT_SHOULDER);
|
|
set(&mut state, Button::Mode, buttons::GUIDE);
|
|
set(&mut state, Button::South, buttons::A);
|
|
set(&mut state, Button::East, buttons::B);
|
|
set(&mut state, Button::West, buttons::X);
|
|
set(&mut state, Button::North, buttons::Y);
|
|
|
|
// Triggers: gilrs reports LeftTrigger2 / RightTrigger2 as axes in
|
|
// range [0.0, 1.0]. Map to u8 0..=255.
|
|
let trig = |axis: Axis| -> u8 {
|
|
pad.axis_data(axis)
|
|
.map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
|
|
.unwrap_or(0)
|
|
};
|
|
state.left_trigger = pad
|
|
.button_data(Button::LeftTrigger2)
|
|
.map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
|
|
.unwrap_or_else(|| trig(Axis::LeftZ));
|
|
state.right_trigger = pad
|
|
.button_data(Button::RightTrigger2)
|
|
.map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
|
|
.unwrap_or_else(|| trig(Axis::RightZ));
|
|
|
|
// Sticks: gilrs reports [-1.0, 1.0]. Xbox uses i16 [-32768, 32767].
|
|
let stick = |axis: Axis| -> i16 {
|
|
let v = pad
|
|
.axis_data(axis)
|
|
.map(|d| d.value().clamp(-1.0, 1.0))
|
|
.unwrap_or(0.0);
|
|
(v * 32767.0) as i16
|
|
};
|
|
state.left_stick_x = stick(Axis::LeftStickX);
|
|
state.left_stick_y = stick(Axis::LeftStickY);
|
|
state.right_stick_x = stick(Axis::RightStickX);
|
|
state.right_stick_y = stick(Axis::RightStickY);
|
|
|
|
state
|
|
}
|
|
}
|