diff --git a/README.md b/README.md index 493e3eb..759d9cf 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,18 @@ The more general function, which is relevant with a set input offset, is: $$(dx_f, dy_f) = (dx_0, dy_0) * (1 + a * (V - offset_in)^2 / V)$$ +## Classic Acceleration Function + +Classic mode generalizes the linear formula with a configurable exponent: + +$$(dx_f, dy_f) = (dx_0, dy_0) * (1 + a * (V - offset_in)^e / V)$$ + +The default exponent is `2`, matching the existing linear behavior. Values above +`2` ramp up more aggressively at higher input speeds. + ## Other Curves +- [x] **Classic** - [x] **Natural** ![image](https://github.com/user-attachments/assets/d14d0fa3-f762-4ad6-911c-cf564227d1ac) diff --git a/README_NIXOS.md b/README_NIXOS.md index 330c226..c5068d7 100644 --- a/README_NIXOS.md +++ b/README_NIXOS.md @@ -43,6 +43,9 @@ Create your `maccel.nix` module: offset = 2.0; outputCap = 2.0; + # Classic mode + exponent = 2.5; + # Natural mode decayRate = 0.1; offset = 2.0; diff --git a/cli/src/main.rs b/cli/src/main.rs index 695aaf9..ad9b153 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,8 +4,8 @@ use maccel_core::{ fixedptc::Fpt, persist::{ParamStore, SysFsStore}, subcommads::*, - AccelMode, NoAccelParamArgs, Param, ALL_COMMON_PARAMS, ALL_LINEAR_PARAMS, ALL_NATURAL_PARAMS, - ALL_SYNCHRONOUS_PARAMS, + AccelMode, NoAccelParamArgs, Param, ALL_CLASSIC_PARAMS, ALL_COMMON_PARAMS, ALL_LINEAR_PARAMS, + ALL_NATURAL_PARAMS, ALL_SYNCHRONOUS_PARAMS, }; use maccel_tui::run_tui; @@ -78,6 +78,9 @@ fn main() -> anyhow::Result<()> { SetParamByModesSubcommands::Synchronous(param_args) => { param_store.set_all_synchronous(param_args)? } + SetParamByModesSubcommands::Classic(param_args) => { + param_store.set_all_classic(param_args)? + } SetParamByModesSubcommands::NoAccel(NoAccelParamArgs {}) => { eprintln!( "NOTE: There are no parameters specific here except for the common ones." @@ -110,6 +113,9 @@ fn main() -> anyhow::Result<()> { GetParamsByModesSubcommands::Synchronous => { print_all_params(ALL_SYNCHRONOUS_PARAMS.iter(), oneline, quiet)?; } + GetParamsByModesSubcommands::Classic => { + print_all_params(ALL_CLASSIC_PARAMS.iter(), oneline, quiet)?; + } GetParamsByModesSubcommands::NoAccel => { eprintln!( "NOTE: There are no parameters specific here except for the common ones." @@ -131,6 +137,9 @@ fn main() -> anyhow::Result<()> { AccelMode::Synchronous => { print_all_params(ALL_SYNCHRONOUS_PARAMS.iter(), false, false)?; } + AccelMode::Classic => { + print_all_params(ALL_CLASSIC_PARAMS.iter(), false, false)?; + } AccelMode::NoAccel => { eprintln!( "NOTE: There are no parameters specific here except for the common ones." diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index d3282af..a3226ea 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -96,6 +96,10 @@ impl TuiContext { offset_linear: get!(OffsetLinear), offset_natural: get!(OffsetNatural), output_cap: get!(OutputCap), + classic_accel: get!(ClassicAccel), + offset_classic: get!(OffsetClassic), + classic_output_cap: get!(ClassicOutputCap), + classic_exponent: get!(ClassicExponent), decay_rate: get!(DecayRate), limit: get!(Limit), gamma: get!(Gamma), diff --git a/crates/core/src/params.rs b/crates/core/src/params.rs index 10215d3..14aed4e 100644 --- a/crates/core/src/params.rs +++ b/crates/core/src/params.rs @@ -193,6 +193,12 @@ declare_params!( SyncSpeed, }, NoAccel {}, + Classic { + ClassicAccel, + OffsetClassic, + ClassicOutputCap, + ClassicExponent, + }, ); impl AccelMode { @@ -202,6 +208,7 @@ impl AccelMode { AccelMode::Natural => "Natural (w/ Gain)", AccelMode::Synchronous => "Synchronous", AccelMode::NoAccel => "No Acceleration", + AccelMode::Classic => "Classic Acceleration", } } } @@ -226,6 +233,10 @@ impl Param { Param::OffsetLinear => "OFFSET", Param::OffsetNatural => "OFFSET", Param::OutputCap => "OUTPUT_CAP", + Param::ClassicAccel => "ACCEL", + Param::OffsetClassic => "OFFSET", + Param::ClassicOutputCap => "OUTPUT_CAP", + Param::ClassicExponent => "EXPONENT", Param::DecayRate => "DECAY_RATE", Param::Limit => "LIMIT", Param::Gamma => "GAMMA", @@ -244,6 +255,10 @@ impl Param { Param::OffsetLinear => "Offset", Param::OffsetNatural => "Offset", Param::OutputCap => "Output-Cap", + Param::ClassicAccel => "Accel", + Param::OffsetClassic => "Offset", + Param::ClassicOutputCap => "Output-Cap", + Param::ClassicExponent => "Exponent", Param::YxRatio => "Y/x Ratio", Param::DecayRate => "Decay-Rate", Param::Limit => "Limit", @@ -268,6 +283,16 @@ impl Param { Param::Accel => "Acceleration strength. Higher values = faster cursor at high speeds.", Param::OffsetLinear => "Speed threshold (counts/ms) before acceleration begins.", Param::OutputCap => "Maximum sensitivity multiplier cap. Prevents excessive speed.", + Param::ClassicAccel => { + "Acceleration strength for the Classic curve. Higher values = faster cursor at high speeds." + } + Param::OffsetClassic => "Speed threshold (counts/ms) before Classic acceleration begins.", + Param::ClassicOutputCap => { + "Maximum sensitivity multiplier cap for the Classic curve." + } + Param::ClassicExponent => { + "Exponent controlling the Classic curve shape. Values above 2 ramp up more aggressively." + } Param::DecayRate => "How quickly acceleration decays. Higher = faster decay.", Param::OffsetNatural => "Speed threshold (counts/ms) for natural curve activation.", Param::Limit => "Maximum gain limit for natural acceleration.", @@ -316,13 +341,18 @@ pub(crate) fn validate_param_value(param_tag: Param, value: f64) -> anyhow::Resu } } Param::AngleRotation => {} - Param::Accel => {} - Param::OutputCap => {} - Param::OffsetLinear | Param::OffsetNatural => { + Param::Accel | Param::ClassicAccel => {} + Param::OutputCap | Param::ClassicOutputCap => {} + Param::OffsetLinear | Param::OffsetNatural | Param::OffsetClassic => { if value < 0.0 { anyhow::bail!("offset cannot be less than 0"); } } + Param::ClassicExponent => { + if value <= 0.0 { + anyhow::bail!("Classic exponent must be positive"); + } + } Param::DecayRate => { if value <= 0.0 { anyhow::bail!("decay rate must be positive"); diff --git a/crates/core/src/persist.rs b/crates/core/src/persist.rs index 2173492..c9618dc 100644 --- a/crates/core/src/persist.rs +++ b/crates/core/src/persist.rs @@ -10,8 +10,8 @@ use anyhow::{Context, anyhow}; use crate::{ fixedptc::Fpt, params::{ - ALL_MODES, AccelMode, CommonParamArgs, LinearParamArgs, NaturalParamArgs, Param, - SynchronousParamArgs, format_param_value, validate_param_value, + ALL_MODES, AccelMode, ClassicParamArgs, CommonParamArgs, LinearParamArgs, + NaturalParamArgs, Param, SynchronousParamArgs, format_param_value, validate_param_value, }, }; @@ -131,6 +131,22 @@ impl SysFsStore { Ok(()) } + + pub fn set_all_classic(&mut self, args: ClassicParamArgs) -> anyhow::Result<()> { + let ClassicParamArgs { + classic_accel, + offset_classic, + classic_output_cap, + classic_exponent, + } = args; + + self.set(Param::ClassicAccel, classic_accel)?; + self.set(Param::OffsetClassic, offset_classic)?; + self.set(Param::ClassicOutputCap, classic_output_cap)?; + self.set(Param::ClassicExponent, classic_exponent)?; + + Ok(()) + } } fn parameter_path(name: &'static str) -> anyhow::Result { diff --git a/crates/core/src/sens_fns.rs b/crates/core/src/sens_fns.rs index 76b0b82..b9171aa 100644 --- a/crates/core/src/sens_fns.rs +++ b/crates/core/src/sens_fns.rs @@ -1,5 +1,6 @@ use crate::{ - AccelParams, AccelParamsByMode, LinearCurveParams, NaturalCurveParams, SynchronousCurveParams, + AccelParams, AccelParamsByMode, ClassicCurveParams, LinearCurveParams, NaturalCurveParams, + SynchronousCurveParams, libmaccel::{self, fixedptc::Fpt}, params::AllParamArgs, }; @@ -28,6 +29,12 @@ impl AllParamArgs { AccelMode::NoAccel => { AccelParamsByMode::NoAccel(crate::params::NoAccelCurveParams { _ffi_guard: [] }) } + AccelMode::Classic => AccelParamsByMode::Classic(ClassicCurveParams { + classic_accel: self.classic_accel, + offset_classic: self.offset_classic, + classic_output_cap: self.classic_output_cap, + classic_exponent: self.classic_exponent, + }), }; AccelParams { diff --git a/driver/accel.h b/driver/accel.h index 43e24dc..a67d19c 100644 --- a/driver/accel.h +++ b/driver/accel.h @@ -1,6 +1,7 @@ #ifndef _ACCEL_H_ #define _ACCEL_H_ +#include "accel/classic.h" #include "accel/linear.h" #include "accel/mode.h" #include "accel/natural.h" @@ -17,6 +18,7 @@ union __accel_args { struct linear_curve_args linear; struct synchronous_curve_args synchronous; struct no_accel_curve_args no_accel; + struct classic_curve_args classic; }; struct accel_args { @@ -25,7 +27,7 @@ struct accel_args { fpt input_dpi; fpt angle_rotation_deg; - enum accel_mode tag; + unsigned char tag; union __accel_args args; }; @@ -57,6 +59,10 @@ static inline struct vector sensitivity(fpt input_speed, dbg("accel mode %d: no_accel", args.tag); sens = FIXEDPT_ONE; break; + case classic: + dbg("accel mode %d: classic", args.tag); + sens = __classic_sens_fun(input_speed, args.args.classic); + break; default: sens = FIXEDPT_ONE; } diff --git a/driver/accel/classic.h b/driver/accel/classic.h new file mode 100644 index 0000000..817d070 --- /dev/null +++ b/driver/accel/classic.h @@ -0,0 +1,61 @@ +#ifndef __ACCEL_CLASSIC_H_ +#define __ACCEL_CLASSIC_H_ + +#include "../dbg.h" +#include "../fixedptc.h" +#include "../math.h" + +struct classic_curve_args { + fpt accel; + fpt offset; + fpt output_cap; + fpt exponent; +}; + +static inline fpt classic_power(fpt base, fpt exponent) { + if (exponent == FIXEDPT_TWO) { + return fpt_mul(base, base); + } + + return fpt_pow(base, exponent); +} + +static inline fpt classic_base_fn(fpt x, fpt accel, fpt input_offset, + fpt exponent) { + fpt _x = x - input_offset; + fpt powered_x = classic_power(_x, exponent); + return fpt_mul(accel, fpt_div(powered_x, x)); +} + +/** + * Sensitivity Function for Classic Acceleration + */ +static inline fpt __classic_sens_fun(fpt input_speed, + struct classic_curve_args args) { + dbg("classic: accel %s", fptoa(args.accel)); + dbg("classic: offset %s", fptoa(args.offset)); + dbg("classic: output_cap %s", fptoa(args.output_cap)); + dbg("classic: exponent %s", fptoa(args.exponent)); + + if (input_speed == 0 || input_speed <= args.offset || args.exponent <= 0) { + return FIXEDPT_ONE; + } + + fpt sens = + classic_base_fn(input_speed, args.accel, args.offset, args.exponent); + dbg("classic: base_fn sens %s", fptoa(sens)); + + fpt sign = FIXEDPT_ONE; + if (args.output_cap > 0) { + fpt cap = fpt_sub(args.output_cap, FIXEDPT_ONE); + if (cap < 0) { + cap = -cap; + sign = -sign; + } + sens = minsd(sens, cap); + } + + return fpt_add(FIXEDPT_ONE, fpt_mul(sign, sens)); +} + +#endif // !__ACCEL_CLASSIC_H_ diff --git a/driver/accel/mode.h b/driver/accel/mode.h index 4aa36d2..a90bbf0 100644 --- a/driver/accel/mode.h +++ b/driver/accel/mode.h @@ -1,6 +1,6 @@ #ifndef __ACCEL_MODE_H #define __ACCEL_MODE_H -enum accel_mode : unsigned char { linear, natural, synchronous, no_accel }; +enum accel_mode { linear, natural, synchronous, no_accel, classic }; #endif // !__ACCEL_MODE_H diff --git a/driver/accel_k.h b/driver/accel_k.h index 029b6c9..fb08b2f 100644 --- a/driver/accel_k.h +++ b/driver/accel_k.h @@ -2,6 +2,7 @@ #define _ACCELK_H_ #include "accel.h" +#include "accel/classic.h" #include "accel/linear.h" #include "accel/mode.h" #include "fixedptc.h" @@ -40,6 +41,13 @@ static struct accel_args collect_args(void) { accel.args.linear.output_cap = atofp(PARAM_OUTPUT_CAP); break; } + case classic: { + accel.args.classic.accel = atofp(PARAM_ACCEL); + accel.args.classic.offset = atofp(PARAM_OFFSET); + accel.args.classic.output_cap = atofp(PARAM_OUTPUT_CAP); + accel.args.classic.exponent = atofp(PARAM_EXPONENT); + break; + } case no_accel: default: { } diff --git a/driver/params.h b/driver/params.h index 277a27d..3ef6f09 100644 --- a/driver/params.h +++ b/driver/params.h @@ -41,6 +41,16 @@ PARAM(ACCEL, 0, "Control the sensitivity calculation."); PARAM(OFFSET, 0, "Input speed threshold (counts/ms) before acceleration begins."); PARAM(OUTPUT_CAP, 0, "Control the maximum sensitivity."); +// For Classic Mode + +#if FIXEDPT_BITS == 64 +PARAM(EXPONENT, 8589934592, // 2 << 32 + "Exponent of the Classic acceleration curve."); +#else +PARAM(EXPONENT, 131072, // 2 << 16 + "Exponent of the Classic acceleration curve."); +#endif + // For Natural Mode #if FIXEDPT_BITS == 64 diff --git a/driver/tests/accel.test.c b/driver/tests/accel.test.c index 861f656..ec79a45 100644 --- a/driver/tests/accel.test.c +++ b/driver/tests/accel.test.c @@ -106,6 +106,50 @@ static int test_no_accel_acceleration(const char *filename, fpt param_sens_mult, return test_acceleration(filename, args); } +static int test_classic_sensitivity(void) { + struct linear_curve_args linear_args = (struct linear_curve_args){ + .accel = fpt_rconst(0.3), + .offset = fpt_fromint(1), + .output_cap = 0, + }; + struct classic_curve_args classic_args = (struct classic_curve_args){ + .accel = fpt_rconst(0.3), + .offset = fpt_fromint(1), + .output_cap = 0, + .exponent = fpt_fromint(2), + }; + + int speeds[] = {0, 1, 2, 4, 8}; + int speed_count = (int)(sizeof(speeds) / sizeof(speeds[0])); + int accelerating_speeds[] = {2, 4, 8}; + int accelerating_speed_count = + (int)(sizeof(accelerating_speeds) / sizeof(accelerating_speeds[0])); + + for (int i = 0; i < speed_count; i++) { + fpt speed = fpt_fromint(speeds[i]); + fpt linear_sens = __linear_sens_fun(speed, linear_args); + fpt classic_sens = __classic_sens_fun(speed, classic_args); + + assert(linear_sens == classic_sens); + } + + classic_args.offset = -FIXEDPT_ONE; + assert(__classic_sens_fun(0, classic_args) == FIXEDPT_ONE); + classic_args.offset = fpt_fromint(1); + + for (int i = 0; i < accelerating_speed_count; i++) { + fpt speed = fpt_fromint(accelerating_speeds[i]); + classic_args.exponent = fpt_fromint(2); + fpt baseline_classic_sens = __classic_sens_fun(speed, classic_args); + classic_args.exponent = fpt_fromint(3); + fpt steeper_classic_sens = __classic_sens_fun(speed, classic_args); + + assert(steeper_classic_sens > baseline_classic_sens); + } + + return 0; +} + static int test_rotation_no_accel(const char *filename, fpt param_sens_mult, fpt param_angle_deg) { struct no_accel_curve_args _args = (struct no_accel_curve_args){}; @@ -177,6 +221,8 @@ int main(void) { test_no_accel(1, 1); test_no_accel(0.5, 1.5); + assert(test_classic_sensitivity() == 0); + /* Rotation tests: verify cross-axis output when one axis is 0. * At 45 degrees, (10, 0) should produce roughly (7, 7) - not (10, 0). * At 90 degrees, (10, 0) should produce roughly (0, 10). */ diff --git a/module.nix b/module.nix index 1efbd29..75acab5 100644 --- a/module.nix +++ b/module.nix @@ -26,6 +26,7 @@ with lib; let natural = 1; synchronous = 2; no_accel = 3; + classic = 4; }; # Parameter mapping (from driver/params.h) @@ -42,6 +43,9 @@ with lib; let OFFSET = cfg.parameters.offset; OUTPUT_CAP = cfg.parameters.outputCap; + # Classic mode parameters + EXPONENT = cfg.parameters.exponent; + # Natural mode parameters DECAY_RATE = cfg.parameters.decayRate; LIMIT = cfg.parameters.limit; @@ -163,7 +167,7 @@ in { }; mode = mkOption { - type = types.nullOr (types.enum ["linear" "natural" "synchronous" "no_accel"]); + type = types.nullOr (types.enum ["linear" "natural" "synchronous" "no_accel" "classic"]); default = null; description = "Acceleration mode."; }; @@ -189,6 +193,14 @@ in { description = "Maximum sensitivity multiplier cap."; }; + exponent = mkOption { + type = + types.nullOr (types.addCheck types.float (x: x > 0.0) + // {description = "positive float";}); + default = null; + description = "Exponent of the Classic acceleration curve. Values above 2 ramp up more aggressively."; + }; + # Natural mode parameters decayRate = mkOption { type = diff --git a/tui/src/app.rs b/tui/src/app.rs index 296617d..1b7a586 100644 --- a/tui/src/app.rs +++ b/tui/src/app.rs @@ -1,4 +1,5 @@ use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; +use maccel_core::ALL_CLASSIC_PARAMS; use maccel_core::ALL_COMMON_PARAMS; use maccel_core::ALL_LINEAR_PARAMS; use maccel_core::ALL_MODES; @@ -109,6 +110,21 @@ impl App { }), ), ), + Screen::new( + AccelMode::Classic, + collect_inputs_for_params(ALL_CLASSIC_PARAMS, context.clone()), + Box::new( + SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| { + // Appropriate dynamic bounds for the Classic sens graph + let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult)) + * f64::from(get_param_value_from_ctx!(ctx, ClassicOutputCap)) + .max(1.0) + * 2.0; + + [0.0, upper_bound] + }), + ), + ), ], screen_idx: CyclingIdx::new_starting_at( ALL_MODES.len(),