input: new saturation setting for full analog axes. Gamepad settings UI

Saturation reduces or increases the range of analog axes.
Issue #675
Axes values are re-scaled with dead zone to allow small values.
New Gamepad Settings dialog box to set up dead zone, saturation and
rumble/haptic power.
This commit is contained in:
Flyinghead 2023-12-04 11:57:49 +01:00
parent d7c28a4805
commit 4aecdbbb25
5 changed files with 119 additions and 31 deletions

View File

@ -179,6 +179,26 @@ bool GamepadDevice::gamepad_btn_input(u32 code, bool pressed)
return rc;
}
static DreamcastKey getOppositeAxis(DreamcastKey key)
{
switch (key)
{
case DC_AXIS_RIGHT: return DC_AXIS_LEFT;
case DC_AXIS_LEFT: return DC_AXIS_RIGHT;
case DC_AXIS_UP: return DC_AXIS_DOWN;
case DC_AXIS_DOWN: return DC_AXIS_UP;
case DC_AXIS2_RIGHT: return DC_AXIS2_LEFT;
case DC_AXIS2_LEFT: return DC_AXIS2_RIGHT;
case DC_AXIS2_UP: return DC_AXIS2_DOWN;
case DC_AXIS2_DOWN: return DC_AXIS2_UP;
case DC_AXIS3_RIGHT: return DC_AXIS3_LEFT;
case DC_AXIS3_LEFT: return DC_AXIS3_RIGHT;
case DC_AXIS3_UP: return DC_AXIS3_DOWN;
case DC_AXIS3_DOWN: return DC_AXIS3_UP;
default: return key;
}
}
//
// value must be >= -32768 and <= 32767 for full axes
// and 0 to 32767 for half axes/triggers
@ -216,7 +236,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
{
//printf("AXIS %d Mapped to %d -> %d\n", key, value, v);
s16 *this_axis;
s16 *other_axis;
int otherAxisValue;
int axisDirection = -1;
switch (key)
{
@ -225,7 +245,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS_LEFT:
this_axis = &joyx[port];
other_axis = &joyy[port];
otherAxisValue = lastAxisValue[port][DC_AXIS_UP];
break;
case DC_AXIS_DOWN:
@ -233,7 +253,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS_UP:
this_axis = &joyy[port];
other_axis = &joyx[port];
otherAxisValue = lastAxisValue[port][DC_AXIS_LEFT];
break;
case DC_AXIS2_RIGHT:
@ -241,7 +261,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS2_LEFT:
this_axis = &joyrx[port];
other_axis = &joyry[port];
otherAxisValue = lastAxisValue[port][DC_AXIS2_UP];
break;
case DC_AXIS2_DOWN:
@ -249,7 +269,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS2_UP:
this_axis = &joyry[port];
other_axis = &joyrx[port];
otherAxisValue = lastAxisValue[port][DC_AXIS2_LEFT];
break;
case DC_AXIS3_RIGHT:
@ -257,7 +277,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS3_LEFT:
this_axis = &joy3x[port];
other_axis = &joy3y[port];
otherAxisValue = lastAxisValue[port][DC_AXIS3_UP];
break;
case DC_AXIS3_DOWN:
@ -265,17 +285,18 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
[[fallthrough]];
case DC_AXIS3_UP:
this_axis = &joy3y[port];
other_axis = &joy3x[port];
otherAxisValue = lastAxisValue[port][DC_AXIS3_LEFT];
break;
default:
return false;
}
// Lightgun with left analog stick
int& lastValue = lastAxisValue[port][key];
if (lastValue != v)
int& lastOpValue = lastAxisValue[port][getOppositeAxis(key)];
if (lastValue != v || lastOpValue != v)
{
lastValue = v;
lastValue = lastOpValue = v;
// Lightgun with left analog stick
if (key == DC_AXIS_RIGHT || key == DC_AXIS_LEFT)
mo_x_abs[port] = (std::abs(v) * axisDirection + 32768) * 639 / 65535;
else if (key == DC_AXIS_UP || key == DC_AXIS_DOWN)
@ -283,14 +304,19 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value)
}
// Radial dead zone
// FIXME compute both axes at the same time
v = std::min(32767, std::abs(v));
if ((float)(v * v + *other_axis * *other_axis) < input_mapper->dead_zone * input_mapper->dead_zone * 32768.f * 32768.f)
const float nv = std::abs(v) / 32768.f;
const float r2 = nv * nv + otherAxisValue * otherAxisValue / 32768.f / 32768.f;
if (r2 < input_mapper->dead_zone * input_mapper->dead_zone || r2 == 0.f)
{
*this_axis = 0;
*other_axis = 0;
}
else
*this_axis = v * axisDirection;
{
float pdz = nv * input_mapper->dead_zone / std::sqrt(r2);
// there's a dead angular zone at 45° with saturation > 1 (both axes are saturated)
v = std::round((nv - pdz) / (1 - pdz) * 32768.f * input_mapper->saturation);
*this_axis = std::clamp(v * axisDirection, -32768, 32767);
}
}
else if (key != EMU_BTN_NONE && key <= DC_BTN_BITMAPPED_LAST) // Map triggers to digital buttons
{

View File

@ -80,6 +80,16 @@ public:
save_mapping();
}
}
float get_saturation() const { return input_mapper->saturation; }
void set_saturation(float saturation)
{
if (saturation != input_mapper->saturation)
{
input_mapper->saturation = saturation;
input_mapper->set_dirty();
save_mapping();
}
}
static void Register(const std::shared_ptr<GamepadDevice>& gamepad);

View File

@ -196,6 +196,8 @@ void InputMapping::load(FILE* fp)
dz = std::min(dz, 100);
dz = std::max(dz, 0);
this->dead_zone = (float)dz / 100.f;
int sat = std::clamp(mf.get_int("emulator", "saturation", 100), 50, 200);
this->saturation = (float)sat / 100.f;
this->rumblePower = mf.get_int("emulator", "rumble_power", this->rumblePower);
version = mf.get_int("emulator", "version", 1);
@ -428,6 +430,7 @@ bool InputMapping::save(const std::string& name)
mf.set("emulator", "mapping_name", this->name);
mf.set_int("emulator", "dead_zone", (int)std::round(this->dead_zone * 100.f));
mf.set_int("emulator", "saturation", (int)std::round(this->saturation * 100.f));
mf.set_int("emulator", "rumble_power", this->rumblePower);
mf.set_int("emulator", "version", 3);

View File

@ -35,6 +35,7 @@ public:
InputMapping(const InputMapping& other) {
name = other.name;
dead_zone = other.dead_zone;
saturation = other.saturation;
for (int port = 0; port < 4; port++)
{
buttons[port] = other.buttons[port];
@ -44,6 +45,7 @@ public:
std::string name;
float dead_zone = 0.1f;
float saturation = 1.0f;
int rumblePower = 100;
int version = 3;

View File

@ -1257,6 +1257,61 @@ static void controller_mapping_popup(const std::shared_ptr<GamepadDevice>& gamep
ImGui::PopStyleVar();
}
static void gamepadSettingsPopup(const std::shared_ptr<GamepadDevice>& gamepad)
{
centerNextWindow();
ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f)));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
if (ImGui::Button("Done", ScaledVec2(100, 30)))
{
ImGui::CloseCurrentPopup();
gamepad->save_mapping(map_system);
ImGui::EndPopup();
ImGui::PopStyleVar();
return;
}
ImGui::NewLine();
if (gamepad->is_virtual_gamepad())
{
header("Haptic");
OptionSlider("Power", config::VirtualGamepadVibration, 0, 60, "Haptic feedback power");
}
else if (gamepad->is_rumble_enabled())
{
header("Rumble");
int power = gamepad->get_rumble_power();
ImGui::SetNextItemWidth(300 * settings.display.uiScale);
if (ImGui::SliderInt("Power", &power, 0, 100, "%d%%"))
gamepad->set_rumble_power(power);
ImGui::SameLine();
ShowHelpMarker("Rumble power");
}
if (gamepad->has_analog_stick())
{
header("Thumbsticks");
int deadzone = std::round(gamepad->get_dead_zone() * 100.f);
ImGui::SetNextItemWidth(300 * settings.display.uiScale);
if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%"))
gamepad->set_dead_zone(deadzone / 100.f);
ImGui::SameLine();
ShowHelpMarker("Minimum deflection to register as input");
int saturation = std::round(gamepad->get_saturation() * 100.f);
ImGui::SetNextItemWidth(300 * settings.display.uiScale);
if (ImGui::SliderInt("Saturation", &saturation, 50, 200, "%d%%"))
gamepad->set_saturation(saturation / 100.f);
ImGui::SameLine();
ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. "
"Values greater than 100% will saturate before full deflection of the thumbstick.");
}
ImGui::EndPopup();
}
ImGui::PopStyleVar();
}
void error_popup()
{
if (!error_msg_shown && !error_msg.empty())
@ -1690,31 +1745,23 @@ static void gui_display_settings()
#ifdef __ANDROID__
if (gamepad->is_virtual_gamepad())
{
if (ImGui::Button("Edit"))
if (ImGui::Button("Edit Layout"))
{
vjoy_start_editing();
gui_setState(GuiState::VJoyEdit);
}
ImGui::SameLine();
OptionSlider("Haptic", config::VirtualGamepadVibration, 0, 60);
}
else
#endif
if (gamepad->is_rumble_enabled())
if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick()
#ifdef __ANDROID__
|| gamepad->is_virtual_gamepad()
#endif
)
{
ImGui::SameLine(0, 16 * settings.display.uiScale);
int power = gamepad->get_rumble_power();
ImGui::SetNextItemWidth(150 * settings.display.uiScale);
if (ImGui::SliderInt("Rumble", &power, 0, 100, "%d%%"))
gamepad->set_rumble_power(power);
}
if (gamepad->has_analog_stick())
{
ImGui::SameLine(0, 16 * settings.display.uiScale);
int deadzone = std::round(gamepad->get_dead_zone() * 100.f);
ImGui::SetNextItemWidth(150 * settings.display.uiScale);
if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%"))
gamepad->set_dead_zone(deadzone / 100.f);
if (ImGui::Button("Settings"))
ImGui::OpenPopup("Gamepad Settings");
gamepadSettingsPopup(gamepad);
}
ImGui::NextColumn();
ImGui::PopID();