229 lines
7.9 KiB
Rust
229 lines
7.9 KiB
Rust
use std::panic::{self, AssertUnwindSafe};
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread::JoinHandle;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use bevy::prelude::*;
|
|
use gc_adapter_neo::{Controller, GcAdapter};
|
|
#[derive(Clone, Copy, PartialEq, Default)]
|
|
pub struct GcPortState {
|
|
pub connected: bool,
|
|
pub a: bool,
|
|
pub b: bool,
|
|
pub x: bool,
|
|
pub y: bool,
|
|
pub z: bool,
|
|
pub start: bool,
|
|
pub dpad_up: bool,
|
|
pub dpad_down: bool,
|
|
pub dpad_left: bool,
|
|
pub dpad_right: bool,
|
|
pub left_trigger_digital: bool,
|
|
pub right_trigger_digital: bool,
|
|
pub left_stick: (f32, f32),
|
|
pub right_stick: (f32, f32),
|
|
/// Raw trigger bytes, 0 is released, 255 is fully pressed.
|
|
pub triggers: (u8, u8),
|
|
}
|
|
|
|
impl GcPortState {
|
|
fn make_port_state(c: &Controller, cal: &gc_adapter_neo::PortCalibration) -> Self {
|
|
if !c.connected() {
|
|
return Self::default();
|
|
}
|
|
let b = &c.buttons;
|
|
Self {
|
|
connected: true,
|
|
a: b.a(),
|
|
b: b.b(),
|
|
x: b.x(),
|
|
y: b.y(),
|
|
z: b.z(),
|
|
start: b.start(),
|
|
dpad_up: b.dpad_up(),
|
|
dpad_down: b.dpad_down(),
|
|
dpad_left: b.dpad_left(),
|
|
dpad_right: b.dpad_right(),
|
|
left_trigger_digital: b.left_trigger(),
|
|
right_trigger_digital: b.right_trigger(),
|
|
left_stick: cal.left_stick_coords(&c),
|
|
right_stick: cal.right_stick_coords(&c),
|
|
triggers: (c.triggers.left.raw(), c.triggers.right.raw()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shared, `Send + Sync` handle to the latest snapshot from the poll
|
|
/// thread. Safe to store as an ordinary [`Resource`] (no `NonSend` needed)
|
|
/// because the `GcAdapter` itself never leaves its thread.
|
|
#[derive(Resource)]
|
|
pub struct GcAdapterSnapshot {
|
|
ports: Arc<Mutex<[GcPortState; 4]>>,
|
|
shutdown: Arc<AtomicBool>,
|
|
poll_thread: Option<JoinHandle<()>>,
|
|
}
|
|
|
|
impl GcAdapterSnapshot {
|
|
pub fn read(&self) -> [GcPortState; 4] {
|
|
*self.ports.lock().unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
}
|
|
|
|
impl Drop for GcAdapterSnapshot {
|
|
fn drop(&mut self) {
|
|
self.shutdown.store(true, Ordering::Release);
|
|
|
|
if let Some(poll_thread) = self.poll_thread.take() {
|
|
if let Err(payload) = poll_thread.join() {
|
|
error!(
|
|
"GameCube Controller Adapter poll thread panicked during shutdown: {payload:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const INITIAL_ERROR_BACKOFF: Duration = Duration::from_millis(1);
|
|
const MAX_ERROR_BACKOFF: Duration = Duration::from_millis(100);
|
|
|
|
fn next_error_backoff(backoff: Duration) -> Duration {
|
|
backoff.saturating_mul(2).min(MAX_ERROR_BACKOFF)
|
|
}
|
|
|
|
/// Wait for a retry while still allowing the owner to shut the poll thread down promptly.
|
|
fn wait_for_retry(shutdown: &AtomicBool, delay: Duration) -> bool {
|
|
let deadline = Instant::now() + delay;
|
|
while !shutdown.load(Ordering::Acquire) {
|
|
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
if remaining.is_zero() {
|
|
return true;
|
|
}
|
|
std::thread::sleep(remaining.min(Duration::from_millis(10)));
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Connects to the first GameCube adapter found on the USB bus and starts
|
|
/// polling it on a dedicated thread. Returns `None` (after logging a
|
|
/// warning) if no adapter is plugged in
|
|
pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError> {
|
|
let mut adapter = match GcAdapter::from_usb()? {
|
|
Some(adapter) => adapter,
|
|
None => {
|
|
warn!("No GameCube Controller Adapter found on USB; gamepad input disabled.");
|
|
return Err(gc_adapter_neo::UsbError::Disconnected);
|
|
}
|
|
};
|
|
|
|
let ports = Arc::new(Mutex::new([GcPortState::default(); 4]));
|
|
let shutdown = Arc::new(AtomicBool::new(false));
|
|
|
|
let thread_ports = ports.clone();
|
|
let thread_shutdown = shutdown.clone();
|
|
let poll_thread = std::thread::Builder::new()
|
|
.name("gc-adapter-poll".into())
|
|
.spawn(move || {
|
|
// Flush whatever the adapter had buffered before we started
|
|
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| adapter.refresh_inputs()))
|
|
{
|
|
error!("GameCube Controller Adapter USB refresh panicked on startup: {payload:?}");
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
let mut rate_window_start = std::time::Instant::now();
|
|
#[cfg(debug_assertions)]
|
|
let mut rate_window_count: u32 = 0;
|
|
let mut error_backoff = INITIAL_ERROR_BACKOFF;
|
|
|
|
while !thread_shutdown.load(Ordering::Acquire) {
|
|
let result = panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
|
|
|
let controllers = match result {
|
|
Ok(Ok(controllers)) => {
|
|
error_backoff = INITIAL_ERROR_BACKOFF;
|
|
controllers
|
|
}
|
|
// Ignore this, for some reason we get a bunch of these when we start reading
|
|
Ok(Err(
|
|
gc_adapter_neo::AdapterError::Parse(_)
|
|
| gc_adapter_neo::AdapterError::ShortRead(_),
|
|
)) => {
|
|
if !wait_for_retry(&thread_shutdown, error_backoff) {
|
|
break;
|
|
}
|
|
error_backoff = next_error_backoff(error_backoff);
|
|
continue;
|
|
}
|
|
Ok(Err(gc_adapter_neo::AdapterError::Usb(err))) => {
|
|
error!(
|
|
"GameCube Controller Adapter USB read failed ({err:?}); stopping \
|
|
polling. Gamepad input will stop updating until the game is \
|
|
restarted."
|
|
);
|
|
break;
|
|
}
|
|
Err(payload) => {
|
|
error!(
|
|
"GameCube Controller Adapter USB read panicked: {payload:?}; \
|
|
stopping polling. Gamepad input will stop updating until the \
|
|
game is restarted."
|
|
);
|
|
break;
|
|
}
|
|
};
|
|
|
|
let mut states = [GcPortState::default(); 4];
|
|
for i in 0..4 {
|
|
states[i] =
|
|
GcPortState::make_port_state(&controllers[i], adapter.calibration(i));
|
|
}
|
|
*thread_ports.lock().unwrap_or_else(|e| e.into_inner()) = states;
|
|
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
rate_window_count += 1;
|
|
let elapsed = rate_window_start.elapsed();
|
|
if elapsed >= std::time::Duration::from_secs(1) {
|
|
debug!(
|
|
"GameCube adapter snapshot publish rate: {:.1} Hz",
|
|
rate_window_count as f64 / elapsed.as_secs_f64()
|
|
);
|
|
rate_window_count = 0;
|
|
rate_window_start = std::time::Instant::now();
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.expect("failed to spawn gc-adapter poll thread");
|
|
|
|
Ok(GcAdapterSnapshot {
|
|
ports,
|
|
shutdown,
|
|
poll_thread: Some(poll_thread),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn error_backoff_is_capped() {
|
|
let mut backoff = INITIAL_ERROR_BACKOFF;
|
|
for _ in 0..16 {
|
|
backoff = next_error_backoff(backoff);
|
|
}
|
|
|
|
assert_eq!(backoff, MAX_ERROR_BACKOFF);
|
|
assert_eq!(next_error_backoff(MAX_ERROR_BACKOFF), MAX_ERROR_BACKOFF);
|
|
}
|
|
|
|
#[test]
|
|
fn retry_wait_stops_when_shutdown_is_requested() {
|
|
let shutdown = AtomicBool::new(true);
|
|
|
|
assert!(!wait_for_retry(&shutdown, Duration::from_secs(1)));
|
|
}
|
|
}
|