From f1ff6e073fa23fb1d275c835442734ca84822685 Mon Sep 17 00:00:00 2001
From: Naxdy <naxdy@naxdy.org>
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 efc6830..6ae9db6 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,
@@ -544,6 +545,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 {
@@ -553,8 +566,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")]
@@ -567,7 +580,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(),
@@ -1645,7 +1658,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 {
@@ -1665,8 +1682,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,
@@ -1784,7 +1802,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<CriticalSectionRawMutex, bool> = Signal::new();
 
@@ -31,8 +31,10 @@ static SIGNAL_RUMBLE: Signal<CriticalSectionRawMutex, bool> = Signal::new();
 pub static SIGNAL_CHANGE_RUMBLE_STRENGTH: Signal<CriticalSectionRawMutex, u8> = Signal::new();
 
 /// Only dispatched ONCE after powerup, to determine how to advertise itself via USB.
-pub static SIGNAL_INPUT_CONSISTENCY_MODE_STATUS: Signal<CriticalSectionRawMutex, bool> =
-    Signal::new();
+pub static MUTEX_INPUT_CONSISTENCY_MODE: Mutex<
+    CriticalSectionRawMutex,
+    Option<InputConsistencyMode>,
+> = 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),