rawinput: reimplement analog relative mode and delay (#674)

## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Implement analog relative mode and delay option using a new
millisecond-based algorithm.

## Testing
WIP
This commit is contained in:
bicarus
2026-05-04 22:50:11 -07:00
committed by GitHub
parent 5f94ef4478
commit 96950b6b4e
13 changed files with 250 additions and 132 deletions
+74 -2
View File
@@ -237,7 +237,8 @@ float Analog::applyDeadzone(float raw_value) {
deadzone = -deadzone;
}
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive) {
// relative mode assumes that user is using a stick, so center is neutral regardless of analog type
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) {
// calculate values
const auto delta = value - 0.5f;
@@ -290,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
}
}
return value;
}
}
float Analog::getRelativeModeValue(float raw_value) {
const auto now = get_performance_seconds();
auto delta_time = now - this->rel_mode_last_read_time_s;
if (this->rel_mode_last_read_time_s != 0.f) {
// some heuristics to prevent huge jumps:
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
if (delta_time < 0.f || 0.25f < delta_time) {
delta_time = 0.f;
} else if (delta_time > 0.1f) {
delta_time = 0.1f;
}
// scale [0, 1] to [-1, 1] to simplify calculations
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
// target is one revolution per second at max speed at 1.0 sensitivity
auto adjusted_delta_value = delta_raw_value * delta_time;
// multiplier / divisor
if (this->getMultiplier() > 1) {
adjusted_delta_value *= this->getMultiplier();
} else if (this->getMultiplier() < -1) {
adjusted_delta_value /= -this->getMultiplier();
}
// sensitivity
if (this->isSensitivitySet()) {
adjusted_delta_value *= this->getSensitivity();
}
// calculate the new absolute value
this->rel_mode_absolute_value += adjusted_delta_value;
}
// update for next poll
this->rel_mode_last_read_time_s = now;
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
return this->rel_mode_absolute_value;
}
float Analog::getDelayedValue(float raw_value) {
const double delay_ms = static_cast<double>(this->getDelayMs());
if (delay_ms == 0.0) {
return raw_value;
}
// always push a new value
const auto now = get_performance_milliseconds();
this->delayed_inputs.emplace(now, raw_value);
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz which is overkill
// (UI only allows for 250ms of delay)
while (this->delayed_inputs.size() > 1024) {
this->delayed_inputs.pop();
}
// pop until we find the oldest value still inside the delay window
while (this->delayed_inputs.size() > 1) {
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
if (delta_t <= delay_ms) {
break;
}
this->delayed_inputs.pop();
}
return this->delayed_inputs.front().value;
}
+40
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <cmath>
@@ -34,6 +35,11 @@ struct AnalogMovingAverage {
float cosine;
};
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog {
private:
std::string name;
@@ -63,6 +69,15 @@ private:
float divisor_previous_value = 0.5f;
unsigned short divisor_region = 0;
// relative input mode
float rel_mode_absolute_value = 0.5f;
float rel_mode_last_read_time_s = 0.f;
bool relative_mode = false;
// delay
uint32_t delay_ms = 0;
std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads);
float normalizeAnalogValue(float value);
@@ -83,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() {
if (this->override_enabled) {
@@ -98,7 +115,9 @@ public:
smoothing = false;
deadzone_mirror = false;
setMultiplier(1);
setRelativeMode(false);
setLastState(0.5f);
setDelayMs(0);
}
inline void clearBindings() {
@@ -195,6 +214,19 @@ public:
this->last_state = last_state;
}
inline bool isRelativeMode() const {
return this->relative_mode;
}
inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
this->relative_mode = relative_mode;
this->rel_mode_absolute_value = 0.5f;
this->rel_mode_last_read_time_s = 0.f;
}
inline GameAPI::Analogs::AnalogType getType() const {
return this->type;
}
@@ -202,4 +234,12 @@ public:
inline void setType(GameAPI::Analogs::AnalogType type) {
this->type = type;
}
inline uint32_t getDelayMs() const {
return this->delay_ms;
}
inline void setDelayMs(uint32_t delay_ms) {
this->delay_ms = delay_ms;
}
};
+34 -21
View File
@@ -679,38 +679,45 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
}
// deadzone
// do not apply deadzone to circular analogs since it doesn't make sense
if (analog.isDeadzoneSet() && analog.getType() != AnalogType::Circular) {
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
if (analog.isDeadzoneSet() &&
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
value = analog.applyDeadzone(value);
}
if (analog.getType() == AnalogType::Circular) {
// integer multiplier
value = analog.applyMultiplier(value);
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
if (analog.isRelativeMode()) {
value = analog.getRelativeModeValue(value);
// smoothing
if (analog.getSmoothing()) {
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// smoothing
if (analog.getSmoothing()) {
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
}
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
// apply to value
value = rads * (float) M_1_TAU;
}
// apply to value
value = rads * (float) M_1_TAU;
}
} else {
// sensitivity
@@ -760,6 +767,12 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
value = std::clamp(value, 0.f, 1.f);
}
}
// delay
if (analog.getDelayMs() > 0) {
value = analog.getDelayedValue(value);
}
break;
}
case rawinput::MIDI: {
+22
View File
@@ -252,6 +252,8 @@ bool Config::addGame(Game &game) {
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -259,6 +261,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("invert", &invert);
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -273,6 +277,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else {
it.setIndex(static_cast<unsigned short int>(index));
@@ -283,6 +289,8 @@ bool Config::addGame(Game &game) {
it.setInvert(invert);
it.setSmoothing(smoothing);
it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode);
it.setDelayMs(delay);
}
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -294,6 +302,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -447,6 +457,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -679,6 +691,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("invert", analog.getInvert());
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -689,6 +703,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("invert", analog.getInvert());
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -1125,6 +1141,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1132,6 +1150,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("invert", &invert);
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list
@@ -1143,6 +1163,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setInvert(invert);
analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode);
analog.setDelayMs(delay);
if (devid) {
analog.setDeviceIdentifier(devid);
}
+4
View File
@@ -69,6 +69,8 @@ namespace overlay::windows {
el->SetAttribute("invert", analog.invert);
el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el);
}
@@ -90,6 +92,8 @@ namespace overlay::windows {
el->QueryBoolAttribute("invert", &a.invert);
el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a;
}