From 3f47f5bdb0a246b69ad6ca55f8479bab2ac1d789 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Thu, 4 Apr 2024 18:29:13 +0200 Subject: [PATCH] feat(input): implement "SuperHack" mode --- src/config.rs | 44 +++++++++++++++++++------- src/gcc_hid.rs | 84 ++++++++++++++++++++++++++++++++------------------ src/input.rs | 35 ++++++++++++++++++--- src/main.rs | 4 +-- 4 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/config.rs b/src/config.rs index f087179..449ab32 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,12 +10,13 @@ use embassy_rp::{ flash::{Async, Flash, ERASE_SIZE}, peripherals::FLASH, }; -use packed_struct::{derive::PackedStruct, PackedStruct}; +use packed_struct::{ + derive::{PackedStruct, PrimitiveEnum_u8}, + PackedStruct, +}; use crate::{ - gcc_hid::{ - Buttons1, Buttons2, SIGNAL_CHANGE_RUMBLE_STRENGTH, SIGNAL_INPUT_CONSISTENCY_MODE_STATUS, - }, + gcc_hid::{Buttons1, Buttons2, MUTEX_INPUT_CONSISTENCY_MODE, SIGNAL_CHANGE_RUMBLE_STRENGTH}, helpers::{PackedFloat, ToPackedFloatArray, ToRegularArray, XyValuePair}, input::{ read_ext_adc, Stick, StickAxis, FLOAT_ORIGIN, SPI_ACS_SHARED, SPI_CCS_SHARED, SPI_SHARED, @@ -523,6 +524,18 @@ impl Default for StickConfig { } } +#[derive(Debug, Clone, Copy, Format, PrimitiveEnum_u8, PartialEq, Eq)] +pub enum InputConsistencyMode { + /// Transmit inputs every 8ms, same as the original GCC adapter (and any other). + Original = 0, + /// Forcibly delay transmissions to be 8.33ms apart, to better align with the game's frame rate. + ConsistencyHack = 1, + /// Transmit inputs _at most_ every 8.33ms, but don't transmit anything at all if the controller state doesn't change. + /// This has the potential to drastically improve latency in certain situations, such as when you are waiting to react + /// to something your opponent does. + SuperHack = 2, +} + #[derive(Debug, Clone, Format, PackedStruct)] #[packed_struct(endian = "msb")] pub struct ControllerConfig { @@ -532,8 +545,8 @@ pub struct ControllerConfig { /// will trick the Switch into updating the state every 8.33ms /// instead of every 8ms. The tradeoff is a slight increase in /// input lag. - #[packed_field(size_bits = "8")] - pub input_consistency_mode: bool, + #[packed_field(size_bits = "8", ty = "enum")] + pub input_consistency_mode: InputConsistencyMode, #[packed_field(size_bits = "8")] pub rumble_strength: u8, #[packed_field(size_bytes = "328")] @@ -546,7 +559,7 @@ impl Default for ControllerConfig { fn default() -> Self { Self { config_revision: CONTROLLER_CONFIG_REVISION, - input_consistency_mode: true, + input_consistency_mode: InputConsistencyMode::ConsistencyHack, astick_config: StickConfig::default(), rumble_strength: 9, cstick_config: StickConfig::default(), @@ -1621,7 +1634,11 @@ async fn configuration_main_loop< } // input consistency toggle 37 => { - final_config.input_consistency_mode = !final_config.input_consistency_mode; + final_config.input_consistency_mode = match final_config.input_consistency_mode { + InputConsistencyMode::Original => InputConsistencyMode::ConsistencyHack, + InputConsistencyMode::ConsistencyHack => InputConsistencyMode::SuperHack, + InputConsistencyMode::SuperHack => InputConsistencyMode::Original, + }; override_gcc_state_and_wait(&OverrideGcReportInstruction { report: GcReport { @@ -1641,8 +1658,9 @@ async fn configuration_main_loop< stick_x: 127, stick_y: (127_i8 + match final_config.input_consistency_mode { - true => 69, - false => -69, + InputConsistencyMode::Original => -69, + InputConsistencyMode::ConsistencyHack => 42, + InputConsistencyMode::SuperHack => 69, }) as u8, cstick_x: 127, cstick_y: 127, @@ -1682,7 +1700,11 @@ pub async fn config_task(mut flash: Flash<'static, FLASH, Async, FLASH_SIZE>) { let mut current_config = ControllerConfig::from_flash_memory(&mut flash).unwrap(); - SIGNAL_INPUT_CONSISTENCY_MODE_STATUS.signal(current_config.input_consistency_mode); + { + let mut m_input_consistency = MUTEX_INPUT_CONSISTENCY_MODE.lock().await; + *m_input_consistency = Some(current_config.input_consistency_mode); + } + SIGNAL_CHANGE_RUMBLE_STRENGTH.signal(current_config.rumble_strength); SIGNAL_CONFIG_CHANGE.signal(current_config.clone()); diff --git a/src/gcc_hid.rs b/src/gcc_hid.rs index 170fd3f..47e0563 100644 --- a/src/gcc_hid.rs +++ b/src/gcc_hid.rs @@ -12,8 +12,8 @@ use embassy_rp::{ usb::Driver, }; -use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; -use embassy_time::{Duration, Instant, Ticker}; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal}; +use embassy_time::{Duration, Instant, Ticker, Timer}; use embassy_usb::{ class::hid::{HidReaderWriter, ReportId, RequestHandler, State}, control::OutResponse, @@ -22,7 +22,7 @@ use embassy_usb::{ use libm::powf; use packed_struct::{derive::PackedStruct, PackedStruct}; -use crate::input::CHANNEL_GCC_STATE; +use crate::{config::InputConsistencyMode, input::CHANNEL_GCC_STATE}; static SIGNAL_RUMBLE: Signal = Signal::new(); @@ -31,8 +31,10 @@ static SIGNAL_RUMBLE: Signal = Signal::new(); pub static SIGNAL_CHANGE_RUMBLE_STRENGTH: Signal = Signal::new(); /// Only dispatched ONCE after powerup, to determine how to advertise itself via USB. -pub static SIGNAL_INPUT_CONSISTENCY_MODE_STATUS: Signal = - Signal::new(); +pub static MUTEX_INPUT_CONSISTENCY_MODE: Mutex< + CriticalSectionRawMutex, + Option, +> = Mutex::new(None); #[rustfmt::skip] pub const GCC_REPORT_DESCRIPTOR: &[u8] = &[ @@ -266,7 +268,12 @@ impl Handler for MyDeviceHandler { #[embassy_executor::task] pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>) { - let input_consistency_mode = SIGNAL_INPUT_CONSISTENCY_MODE_STATUS.wait().await; + let input_consistency_mode = { + while MUTEX_INPUT_CONSISTENCY_MODE.lock().await.is_none() { + Timer::after(Duration::from_millis(100)).await; + } + MUTEX_INPUT_CONSISTENCY_MODE.lock().await.unwrap() + }; let mut serial_buffer = [0u8; 64]; @@ -291,10 +298,10 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB> trace!("Start of config"); let mut usb_config = embassy_usb::Config::new(0x057e, 0x0337); usb_config.manufacturer = Some("Naxdy"); - usb_config.product = Some(if input_consistency_mode { - "NaxGCC (Consistency Mode)" - } else { - "NaxGCC (OG Mode)" + usb_config.product = Some(match input_consistency_mode { + InputConsistencyMode::Original => "NaxGCC (OG Mode)", + InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)", + InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)", }); usb_config.serial_number = Some(serial); usb_config.max_power = 200; @@ -331,7 +338,7 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB> let hid_config = embassy_usb::class::hid::Config { report_descriptor: GCC_REPORT_DESCRIPTOR, request_handler: Some(&request_handler), - poll_ms: if input_consistency_mode { 4 } else { 8 }, + poll_ms: 8, max_packet_size_in: 37, max_packet_size_out: 5, }; @@ -350,25 +357,40 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB> let (mut reader, mut writer) = hid.split(); - let mut lasttime = Instant::now(); - let in_fut = async { let mut gcc_subscriber = CHANNEL_GCC_STATE.subscriber().unwrap(); + let mut last_report_time = Instant::now(); + let mut last_loop_time = Instant::now(); let mut ticker = Ticker::every(Duration::from_micros(8333)); loop { - if input_consistency_mode { - // This is what we like to call a "hack". - // It forces reports to be sent every 8.33ms instead of every 8ms. - // 8.33ms is a multiple of the game's frame interval (16.66ms), so if we - // send a report every 8.33ms, it should (in theory) ensure (close to) - // 100% input accuracy. - // - // From the console's perspective, we are basically a laggy adapter, taking - // a minimum of 333 extra us to send a report every time it's polled, but it - // works to our advantage. - ticker.next().await; + // This is what we like to call a "hack". + // It forces reports to be sent at least every 8.33ms instead of every 8ms. + // 8.33ms is a multiple of the game's frame interval (16.66ms), so if we + // send a report every 8.33ms, it should (in theory) ensure (close to) + // 100% input accuracy. + // + // From the console's perspective, we are basically a laggy adapter, taking + // a minimum of 333 extra us to send a report every time it's polled, but it + // works to our advantage. + match input_consistency_mode { + InputConsistencyMode::SuperHack => { + // In SuperHack mode, we send reports only if the state changes, but + // in order to not mess up very fast inputs (like sticks travelling, for example), + // we still need a delay that is higher than the polling rate, but ideally also + // a multiple/divisor of the game's frame rate. + // This doesn't quite hit the 8.33ms every time though, so inputs during lots of + // stick movement might still be a bit off. + Timer::at(last_loop_time + Duration::from_micros(8333)).await; + last_loop_time = Instant::now(); + } + InputConsistencyMode::ConsistencyHack => { + // Ticker better maintains a consistent interval than Timer, so + // we prefer it for consistency mode, where we send reports regularly. + ticker.next().await; + } + InputConsistencyMode::Original => {} } match writer @@ -382,15 +404,17 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB> { Ok(()) => { let currtime = Instant::now(); - let polltime = currtime.duration_since(lasttime); + let polltime = currtime.duration_since(last_report_time); let micros = polltime.as_micros(); - trace!("Report written in {}us", micros); - // If we're sending reports too fast, reset the ticker. + debug!("Report written in {}us", micros); + // If we're sending reports too fast in regular consistency mode, reset the ticker. // This might happen right after plug-in, or after suspend. - if micros < 8150 { - ticker.reset(); + if input_consistency_mode == InputConsistencyMode::ConsistencyHack + && micros < 8150 + { + ticker.reset() } - lasttime = currtime; + last_report_time = currtime; } Err(e) => warn!("Failed to send report: {:?}", e), } diff --git a/src/input.rs b/src/input.rs index 17e5e46..05484d0 100644 --- a/src/input.rs +++ b/src/input.rs @@ -16,11 +16,12 @@ use libm::{fmaxf, fminf}; use crate::{ config::{ - ControllerConfig, OverrideGcReportInstruction, OverrideStickState, SIGNAL_CONFIG_CHANGE, - SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE, SIGNAL_OVERRIDE_STICK_STATE, + ControllerConfig, InputConsistencyMode, OverrideGcReportInstruction, OverrideStickState, + SIGNAL_CONFIG_CHANGE, SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE, + SIGNAL_OVERRIDE_STICK_STATE, }, filter::{run_waveshaping, FilterGains, KalmanState, WaveshapingValues, FILTER_GAINS}, - gcc_hid::GcReport, + gcc_hid::{GcReport, MUTEX_INPUT_CONSISTENCY_MODE}, helpers::XyValuePair, input_filter::{DummyFilter, InputFilter}, stick::{linearize, notch_remap, StickParams}, @@ -439,6 +440,15 @@ pub async fn update_button_state_task( loop {} } + let input_consistency_mode = { + while MUTEX_INPUT_CONSISTENCY_MODE.lock().await.is_none() { + Timer::after(Duration::from_millis(100)).await; + } + MUTEX_INPUT_CONSISTENCY_MODE.lock().await.unwrap() + }; + + let mut previous_state = GcReport::default(); + let mut gcc_state = GcReport::default(); let gcc_publisher = CHANNEL_GCC_STATE.publisher().unwrap(); @@ -483,7 +493,15 @@ pub async fn update_button_state_task( trace!("Overridden gcc state: {:?}", override_gcc_state.report); let end_time = Instant::now() + Duration::from_millis(override_gcc_state.duration_ms); while Instant::now() < end_time { - gcc_publisher.publish_immediate(override_gcc_state.report); + if input_consistency_mode == InputConsistencyMode::SuperHack { + if override_gcc_state.report != previous_state { + gcc_publisher.publish_immediate(override_gcc_state.report); + previous_state = override_gcc_state.report; + } + } else { + gcc_publisher.publish_immediate(override_gcc_state.report); + } + yield_now().await; } }; @@ -503,7 +521,14 @@ pub async fn update_button_state_task( gcc_publisher.publish_immediate(overriden_gcc_state); } else { input_filter.apply_filter(&mut gcc_state); - gcc_publisher.publish_immediate(gcc_state); + if input_consistency_mode == InputConsistencyMode::SuperHack { + if gcc_state != previous_state { + gcc_publisher.publish_immediate(gcc_state); + previous_state = gcc_state.clone(); + } + } else { + gcc_publisher.publish_immediate(gcc_state); + } } // give other tasks a chance to do something diff --git a/src/main.rs b/src/main.rs index 719cf41..c4cff7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,8 +30,8 @@ use gpio::{Level, Output}; use input::{update_button_state_task, update_stick_states_task}; use static_cell::StaticCell; -use crate::config::enter_config_mode_task; use crate::gcc_hid::rumble_task; +use crate::{config::enter_config_mode_task, input::input_integrity_benchmark}; use {defmt_rtt as _, panic_probe as _}; @@ -84,7 +84,7 @@ fn main() -> ! { spawner .spawn(rumble_task(p.PIN_25, p.PIN_29, p.PWM_CH4, p.PWM_CH6)) .unwrap(); - // spawner.spawn(input_integrity_benchmark()).unwrap(); + spawner.spawn(input_integrity_benchmark()).unwrap(); spawner .spawn(update_button_state_task( Input::new(AnyPin::from(p.PIN_20), gpio::Pull::Up),