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:
MechaCat02
2026-05-01 16:26:48 +02:00
parent f166d061be
commit e2b8860e10
13 changed files with 7534 additions and 42 deletions

View 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
}
}