Add xenia-ui crate; switch analysis store to DuckDB
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>
This commit is contained in:
159
crates/xenia-ui/src/input.rs
Normal file
159
crates/xenia-ui/src/input.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user