diff --git a/src/input/gamecube/poll.rs b/src/input/gamecube/poll.rs index f286c19..e5b6e8a 100644 --- a/src/input/gamecube/poll.rs +++ b/src/input/gamecube/poll.rs @@ -1,6 +1,8 @@ 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}; @@ -55,10 +57,11 @@ impl GcPortState { /// 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, Clone)] +#[derive(Resource)] pub struct GcAdapterSnapshot { ports: Arc>, shutdown: Arc, + poll_thread: Option>, } impl GcAdapterSnapshot { @@ -69,14 +72,38 @@ impl GcAdapterSnapshot { impl Drop for GcAdapterSnapshot { fn drop(&mut self) { - // Only actually signals shutdown once the last clone (this resource - // is never cloned in practice, but be defensive) is dropped. - if Arc::strong_count(&self.shutdown) <= 1 { - self.shutdown.store(true, Ordering::Relaxed); + 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 @@ -94,7 +121,7 @@ pub fn spawn_poll_thread() -> Result Result controllers, + 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(_), - )) => continue, + )) => { + 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 \ @@ -160,5 +197,32 @@ pub fn spawn_poll_thread() -> Result