feat(input): implement "SuperHack" mode

This commit is contained in:
Naxdy 2024-04-04 18:29:13 +02:00
parent f4de1326a5
commit 3f47f5bdb0
4 changed files with 119 additions and 48 deletions

View file

@ -10,12 +10,13 @@ use embassy_rp::{
flash::{Async, Flash, ERASE_SIZE}, flash::{Async, Flash, ERASE_SIZE},
peripherals::FLASH, peripherals::FLASH,
}; };
use packed_struct::{derive::PackedStruct, PackedStruct}; use packed_struct::{
derive::{PackedStruct, PrimitiveEnum_u8},
PackedStruct,
};
use crate::{ use crate::{
gcc_hid::{ gcc_hid::{Buttons1, Buttons2, MUTEX_INPUT_CONSISTENCY_MODE, SIGNAL_CHANGE_RUMBLE_STRENGTH},
Buttons1, Buttons2, SIGNAL_CHANGE_RUMBLE_STRENGTH, SIGNAL_INPUT_CONSISTENCY_MODE_STATUS,
},
helpers::{PackedFloat, ToPackedFloatArray, ToRegularArray, XyValuePair}, helpers::{PackedFloat, ToPackedFloatArray, ToRegularArray, XyValuePair},
input::{ input::{
read_ext_adc, Stick, StickAxis, FLOAT_ORIGIN, SPI_ACS_SHARED, SPI_CCS_SHARED, SPI_SHARED, 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)] #[derive(Debug, Clone, Format, PackedStruct)]
#[packed_struct(endian = "msb")] #[packed_struct(endian = "msb")]
pub struct ControllerConfig { pub struct ControllerConfig {
@ -532,8 +545,8 @@ pub struct ControllerConfig {
/// will trick the Switch into updating the state every 8.33ms /// will trick the Switch into updating the state every 8.33ms
/// instead of every 8ms. The tradeoff is a slight increase in /// instead of every 8ms. The tradeoff is a slight increase in
/// input lag. /// input lag.
#[packed_field(size_bits = "8")] #[packed_field(size_bits = "8", ty = "enum")]
pub input_consistency_mode: bool, pub input_consistency_mode: InputConsistencyMode,
#[packed_field(size_bits = "8")] #[packed_field(size_bits = "8")]
pub rumble_strength: u8, pub rumble_strength: u8,
#[packed_field(size_bytes = "328")] #[packed_field(size_bytes = "328")]
@ -546,7 +559,7 @@ impl Default for ControllerConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
config_revision: CONTROLLER_CONFIG_REVISION, config_revision: CONTROLLER_CONFIG_REVISION,
input_consistency_mode: true, input_consistency_mode: InputConsistencyMode::ConsistencyHack,
astick_config: StickConfig::default(), astick_config: StickConfig::default(),
rumble_strength: 9, rumble_strength: 9,
cstick_config: StickConfig::default(), cstick_config: StickConfig::default(),
@ -1621,7 +1634,11 @@ async fn configuration_main_loop<
} }
// input consistency toggle // input consistency toggle
37 => { 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 { override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport { report: GcReport {
@ -1641,8 +1658,9 @@ async fn configuration_main_loop<
stick_x: 127, stick_x: 127,
stick_y: (127_i8 stick_y: (127_i8
+ match final_config.input_consistency_mode { + match final_config.input_consistency_mode {
true => 69, InputConsistencyMode::Original => -69,
false => -69, InputConsistencyMode::ConsistencyHack => 42,
InputConsistencyMode::SuperHack => 69,
}) as u8, }) as u8,
cstick_x: 127, cstick_x: 127,
cstick_y: 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(); 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_CHANGE_RUMBLE_STRENGTH.signal(current_config.rumble_strength);
SIGNAL_CONFIG_CHANGE.signal(current_config.clone()); SIGNAL_CONFIG_CHANGE.signal(current_config.clone());

View file

@ -12,8 +12,8 @@ use embassy_rp::{
usb::Driver, usb::Driver,
}; };
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal};
use embassy_time::{Duration, Instant, Ticker}; use embassy_time::{Duration, Instant, Ticker, Timer};
use embassy_usb::{ use embassy_usb::{
class::hid::{HidReaderWriter, ReportId, RequestHandler, State}, class::hid::{HidReaderWriter, ReportId, RequestHandler, State},
control::OutResponse, control::OutResponse,
@ -22,7 +22,7 @@ use embassy_usb::{
use libm::powf; use libm::powf;
use packed_struct::{derive::PackedStruct, PackedStruct}; 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(); 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(); pub static SIGNAL_CHANGE_RUMBLE_STRENGTH: Signal<CriticalSectionRawMutex, u8> = Signal::new();
/// Only dispatched ONCE after powerup, to determine how to advertise itself via USB. /// Only dispatched ONCE after powerup, to determine how to advertise itself via USB.
pub static SIGNAL_INPUT_CONSISTENCY_MODE_STATUS: Signal<CriticalSectionRawMutex, bool> = pub static MUTEX_INPUT_CONSISTENCY_MODE: Mutex<
Signal::new(); CriticalSectionRawMutex,
Option<InputConsistencyMode>,
> = Mutex::new(None);
#[rustfmt::skip] #[rustfmt::skip]
pub const GCC_REPORT_DESCRIPTOR: &[u8] = &[ pub const GCC_REPORT_DESCRIPTOR: &[u8] = &[
@ -266,7 +268,12 @@ impl Handler for MyDeviceHandler {
#[embassy_executor::task] #[embassy_executor::task]
pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>) { 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]; 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"); trace!("Start of config");
let mut usb_config = embassy_usb::Config::new(0x057e, 0x0337); let mut usb_config = embassy_usb::Config::new(0x057e, 0x0337);
usb_config.manufacturer = Some("Naxdy"); usb_config.manufacturer = Some("Naxdy");
usb_config.product = Some(if input_consistency_mode { usb_config.product = Some(match input_consistency_mode {
"NaxGCC (Consistency Mode)" InputConsistencyMode::Original => "NaxGCC (OG Mode)",
} else { InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)",
"NaxGCC (OG Mode)" InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)",
}); });
usb_config.serial_number = Some(serial); usb_config.serial_number = Some(serial);
usb_config.max_power = 200; 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 { let hid_config = embassy_usb::class::hid::Config {
report_descriptor: GCC_REPORT_DESCRIPTOR, report_descriptor: GCC_REPORT_DESCRIPTOR,
request_handler: Some(&request_handler), 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_in: 37,
max_packet_size_out: 5, max_packet_size_out: 5,
}; };
@ -350,17 +357,16 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>
let (mut reader, mut writer) = hid.split(); let (mut reader, mut writer) = hid.split();
let mut lasttime = Instant::now();
let in_fut = async { let in_fut = async {
let mut gcc_subscriber = CHANNEL_GCC_STATE.subscriber().unwrap(); 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)); let mut ticker = Ticker::every(Duration::from_micros(8333));
loop { loop {
if input_consistency_mode {
// This is what we like to call a "hack". // This is what we like to call a "hack".
// It forces reports to be sent every 8.33ms instead of every 8ms. // 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 // 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) // send a report every 8.33ms, it should (in theory) ensure (close to)
// 100% input accuracy. // 100% input accuracy.
@ -368,8 +374,24 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>
// From the console's perspective, we are basically a laggy adapter, taking // 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 // a minimum of 333 extra us to send a report every time it's polled, but it
// works to our advantage. // 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; ticker.next().await;
} }
InputConsistencyMode::Original => {}
}
match writer match writer
.write(&{ .write(&{
@ -382,15 +404,17 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>
{ {
Ok(()) => { Ok(()) => {
let currtime = Instant::now(); let currtime = Instant::now();
let polltime = currtime.duration_since(lasttime); let polltime = currtime.duration_since(last_report_time);
let micros = polltime.as_micros(); let micros = polltime.as_micros();
trace!("Report written in {}us", micros); debug!("Report written in {}us", micros);
// If we're sending reports too fast, reset the ticker. // If we're sending reports too fast in regular consistency mode, reset the ticker.
// This might happen right after plug-in, or after suspend. // This might happen right after plug-in, or after suspend.
if micros < 8150 { if input_consistency_mode == InputConsistencyMode::ConsistencyHack
ticker.reset(); && micros < 8150
{
ticker.reset()
} }
lasttime = currtime; last_report_time = currtime;
} }
Err(e) => warn!("Failed to send report: {:?}", e), Err(e) => warn!("Failed to send report: {:?}", e),
} }

View file

@ -16,11 +16,12 @@ use libm::{fmaxf, fminf};
use crate::{ use crate::{
config::{ config::{
ControllerConfig, OverrideGcReportInstruction, OverrideStickState, SIGNAL_CONFIG_CHANGE, ControllerConfig, InputConsistencyMode, OverrideGcReportInstruction, OverrideStickState,
SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE, SIGNAL_OVERRIDE_STICK_STATE, SIGNAL_CONFIG_CHANGE, SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE,
SIGNAL_OVERRIDE_STICK_STATE,
}, },
filter::{run_waveshaping, FilterGains, KalmanState, WaveshapingValues, FILTER_GAINS}, filter::{run_waveshaping, FilterGains, KalmanState, WaveshapingValues, FILTER_GAINS},
gcc_hid::GcReport, gcc_hid::{GcReport, MUTEX_INPUT_CONSISTENCY_MODE},
helpers::XyValuePair, helpers::XyValuePair,
input_filter::{DummyFilter, InputFilter}, input_filter::{DummyFilter, InputFilter},
stick::{linearize, notch_remap, StickParams}, stick::{linearize, notch_remap, StickParams},
@ -439,6 +440,15 @@ pub async fn update_button_state_task(
loop {} 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 mut gcc_state = GcReport::default();
let gcc_publisher = CHANNEL_GCC_STATE.publisher().unwrap(); 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); trace!("Overridden gcc state: {:?}", override_gcc_state.report);
let end_time = Instant::now() + Duration::from_millis(override_gcc_state.duration_ms); let end_time = Instant::now() + Duration::from_millis(override_gcc_state.duration_ms);
while Instant::now() < end_time { while Instant::now() < end_time {
if input_consistency_mode == InputConsistencyMode::SuperHack {
if override_gcc_state.report != previous_state {
gcc_publisher.publish_immediate(override_gcc_state.report); 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; yield_now().await;
} }
}; };
@ -503,7 +521,14 @@ pub async fn update_button_state_task(
gcc_publisher.publish_immediate(overriden_gcc_state); gcc_publisher.publish_immediate(overriden_gcc_state);
} else { } else {
input_filter.apply_filter(&mut gcc_state); input_filter.apply_filter(&mut gcc_state);
if input_consistency_mode == InputConsistencyMode::SuperHack {
if gcc_state != previous_state {
gcc_publisher.publish_immediate(gcc_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 // give other tasks a chance to do something

View file

@ -30,8 +30,8 @@ use gpio::{Level, Output};
use input::{update_button_state_task, update_stick_states_task}; use input::{update_button_state_task, update_stick_states_task};
use static_cell::StaticCell; use static_cell::StaticCell;
use crate::config::enter_config_mode_task;
use crate::gcc_hid::rumble_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 _}; use {defmt_rtt as _, panic_probe as _};
@ -84,7 +84,7 @@ fn main() -> ! {
spawner spawner
.spawn(rumble_task(p.PIN_25, p.PIN_29, p.PWM_CH4, p.PWM_CH6)) .spawn(rumble_task(p.PIN_25, p.PIN_29, p.PWM_CH4, p.PWM_CH6))
.unwrap(); .unwrap();
// spawner.spawn(input_integrity_benchmark()).unwrap(); spawner.spawn(input_integrity_benchmark()).unwrap();
spawner spawner
.spawn(update_button_state_task( .spawn(update_button_state_task(
Input::new(AnyPin::from(p.PIN_20), gpio::Pull::Up), Input::new(AnyPin::from(p.PIN_20), gpio::Pull::Up),