Compare commits
1
Commits
master
...
819c680a79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
819c680a79 |
+74
-10
@@ -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<Mutex<[GcPortState; 4]>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
poll_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
|
||||
let thread_ports = ports.clone();
|
||||
let thread_shutdown = shutdown.clone();
|
||||
std::thread::Builder::new()
|
||||
let poll_thread = std::thread::Builder::new()
|
||||
.name("gc-adapter-poll".into())
|
||||
.spawn(move || {
|
||||
// Flush whatever the adapter had buffered before we started
|
||||
@@ -107,17 +134,27 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
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::Relaxed) {
|
||||
while !thread_shutdown.load(Ordering::Acquire) {
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
||||
|
||||
let controllers = match result {
|
||||
Ok(Ok(controllers)) => 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<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
})
|
||||
.expect("failed to spawn gc-adapter poll thread");
|
||||
|
||||
Ok(GcAdapterSnapshot { ports, shutdown })
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user