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>, shutdown: Arc, poll_thread: Option>, } 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 { 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))); } }