InputManager: Refactor and simplify vibration mapping

Now multiple devices can be bound if anyone wants to do that for some
reason...

Current strength will also synchronize on binding reload instead of
getting lost.
This commit is contained in:
Stenzek
2025-10-04 22:53:13 +10:00
parent f57eeb349c
commit 1665cb6953
8 changed files with 223 additions and 198 deletions

View File

@@ -271,18 +271,18 @@ void InputBindingWidget::reloadBinding()
void InputBindingWidget::onClicked()
{
if (m_bindings.size() > 1)
{
openDialog();
return;
}
if (InputBindingInfo::IsEffectType(m_bind_type))
{
showEffectBindingDialog();
}
else
{
if (m_bindings.size() > 1)
{
openDialog();
return;
}
if (isListeningForInput())
stopListeningForInput();
@@ -419,13 +419,47 @@ void InputBindingWidget::openDialog()
void InputBindingWidget::showEffectBindingDialog()
{
std::vector<InputBindingKey> options;
QStringList option_names;
QString current;
if (!g_emu_thread->getInputDeviceListModel()->hasEffectsOfType(m_bind_type))
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Error"),
(m_bind_type == InputBindingInfo::Type::Motor) ?
tr("No devices with vibration motors were detected.") :
tr("No devices with LEDs were detected."));
return;
}
const QString full_key(QString::fromStdString(fmt::format("{}/{}", m_section_name, m_key_name)));
QDialog dlg(this);
dlg.setWindowTitle(full_key);
dlg.setFixedWidth(450);
dlg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
QVBoxLayout* const main_layout = new QVBoxLayout(&dlg);
QHBoxLayout* const heading_layout = new QHBoxLayout();
QLabel* const icon = new QLabel(&dlg);
icon->setPixmap(QIcon::fromTheme(QStringLiteral("pushpin-line")).pixmap(32, 32));
QLabel* const heading =
new QLabel(tr("<strong>%1</strong><br>Select the device and effect to map this bind to.").arg(full_key), &dlg);
heading->setWordWrap(true);
heading_layout->addWidget(icon, 0, Qt::AlignTop | Qt::AlignLeft);
heading_layout->addWidget(heading, 1);
main_layout->addLayout(heading_layout);
QListWidget* const list = new QListWidget(&dlg);
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
// hook up selection to alter check state
connect(list, &QListWidget::itemSelectionChanged, [list]() {
const int count = list->count();
for (int i = 0; i < count; i++)
list->item(i)->setCheckState(Qt::Unchecked);
for (QListWidgetItem* item : list->selectedItems())
item->setCheckState(item->isSelected() ? Qt::Checked : Qt::Unchecked);
});
const InputDeviceListModel::EffectList& all_options = g_emu_thread->getInputDeviceListModel()->getEffectList();
options.reserve(all_options.size());
option_names.reserve(all_options.size());
for (const auto& [type, key] : g_emu_thread->getInputDeviceListModel()->getEffectList())
{
if (type != m_bind_type)
@@ -435,47 +469,57 @@ void InputBindingWidget::showEffectBindingDialog()
if (name.empty())
continue;
QString qname = QtUtils::StringViewToQString(name);
if (!m_bindings.empty() && name == m_bindings.front())
current = qname;
const bool is_bound =
std::ranges::any_of(m_bindings, [&name](const std::string& other_name) { return (other_name == name.view()); });
options.push_back(key);
option_names.push_back(std::move(qname));
QListWidgetItem* const item = new QListWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(is_bound ? Qt::Checked : Qt::Unchecked);
item->setText(QStringLiteral("%1\n%2")
.arg(QtUtils::StringViewToQString(name))
.arg(g_emu_thread->getInputDeviceListModel()->getDeviceName(key)));
item->setData(Qt::UserRole, QtUtils::StringViewToQString(name));
item->setIcon(InputDeviceListModel::getIconForKey(key));
list->addItem(item);
item->setSelected(is_bound);
}
if (options.empty())
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Error"),
(m_bind_type == InputBindingInfo::Type::Motor) ?
tr("No devices with vibration motors were detected.") :
tr("No devices with LEDs were detected."));
return;
}
main_layout->addWidget(list);
// TODO: Multiple options? needs a custom dialog
const QString full_key(
QStringLiteral("%1/%2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name)));
QInputDialog input_dialog(this);
input_dialog.setWindowTitle(full_key);
input_dialog.setLabelText(tr("Select device and effect for %1.").arg(full_key));
input_dialog.setInputMode(QInputDialog::TextInput);
input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems);
input_dialog.setComboBoxEditable(false);
input_dialog.setComboBoxItems(option_names);
input_dialog.setTextValue(current);
if (input_dialog.exec() == QDialog::Rejected)
QDialogButtonBox* const bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
connect(bbox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(bbox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
main_layout->addWidget(bbox);
if (dlg.exec() != QDialog::Accepted)
return;
const QString new_value = input_dialog.textValue();
for (qsizetype i = 0; i < option_names.size(); i++)
m_bindings.clear();
const int count = list->count();
for (int i = 0; i < count; i++)
{
if (new_value == option_names[i])
{
m_new_bindings.clear();
m_new_bindings.push_back(options[i]);
setNewBinding();
reloadBinding();
return;
}
const QListWidgetItem* const item = list->item(i);
if (item->checkState() == Qt::Checked)
m_bindings.push_back(item->data(Qt::UserRole).toString().toStdString());
}
if (m_sif)
{
m_sif->SetStringList(m_section_name.c_str(), m_key_name.c_str(), m_bindings);
QtHost::SaveGameSettings(m_sif, false);
g_emu_thread->reloadGameSettings();
}
else
{
Host::SetBaseStringListSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_bindings);
Host::CommitBaseSettingChanges();
if (m_bind_type == InputBindingInfo::Type::Pointer)
g_emu_thread->updateControllerSettings();
g_emu_thread->reloadInputBindings();
}
setNewBinding();
reloadBinding();
}

View File

@@ -2543,6 +2543,26 @@ QIcon InputDeviceListModel::getIconForKey(const InputBindingKey& key)
return QIcon::fromTheme(QStringLiteral("controller-line"));
}
QString InputDeviceListModel::getDeviceName(const InputBindingKey& key)
{
QString ret;
for (const InputDeviceListModel::Device& device : m_devices)
{
if (device.key.source_type == key.source_type && device.key.source_index == key.source_index)
{
ret = device.display_name;
break;
}
}
return ret;
}
bool InputDeviceListModel::hasEffectsOfType(InputBindingInfo::Type type)
{
return std::ranges::any_of(m_effects, [type](const auto& eff) { return eff.first == type; });
}
int InputDeviceListModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const
{
return m_devices.size();

View File

@@ -287,6 +287,12 @@ public:
ALWAYS_INLINE const DeviceList& getDeviceList() const { return m_devices; }
ALWAYS_INLINE const EffectList& getEffectList() const { return m_effects; }
/// Returns the device name for the specified key, or an empty string if not found.
QString getDeviceName(const InputBindingKey& key);
/// Returns whether any effects are available for the specified type.
bool hasEffectsOfType(InputBindingInfo::Type type);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;

View File

@@ -98,6 +98,7 @@
<file>icons/black/svg/play-line.svg</file>
<file>icons/black/svg/play-list-2-line.svg</file>
<file>icons/black/svg/price-tag-3-line.svg</file>
<file>icons/black/svg/pushpin-line.svg</file>
<file>icons/black/svg/refresh-line.svg</file>
<file>icons/black/svg/restart-line.svg</file>
<file>icons/black/svg/save-3-line.svg</file>
@@ -320,6 +321,7 @@
<file>icons/white/svg/play-line.svg</file>
<file>icons/white/svg/play-list-2-line.svg</file>
<file>icons/white/svg/price-tag-3-line.svg</file>
<file>icons/white/svg/pushpin-line.svg</file>
<file>icons/white/svg/refresh-line.svg</file>
<file>icons/white/svg/restart-line.svg</file>
<file>icons/white/svg/save-3-line.svg</file>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M13.8273 1.69L22.3126 10.1753L20.8984 11.5895L20.1913 10.8824L15.9486 15.125L15.2415 18.6606L13.8273 20.0748L9.58466 15.8321L4.63492 20.7819L3.2207 19.3677L8.17045 14.4179L3.92781 10.1753L5.34202 8.76107L8.87756 8.05396L13.1202 3.81132L12.4131 3.10422L13.8273 1.69ZM14.5344 5.22554L9.86358 9.89637L7.0417 10.4607L13.5418 16.9609L14.1062 14.139L18.7771 9.46818L14.5344 5.22554Z"></path></svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13.8273 1.69L22.3126 10.1753L20.8984 11.5895L20.1913 10.8824L15.9486 15.125L15.2415 18.6606L13.8273 20.0748L9.58466 15.8321L4.63492 20.7819L3.2207 19.3677L8.17045 14.4179L3.92781 10.1753L5.34202 8.76107L8.87756 8.05396L13.1202 3.81132L12.4131 3.10422L13.8273 1.69ZM14.5344 5.22554L9.86358 9.89637L7.0417 10.4607L13.5418 16.9609L14.1062 14.139L18.7771 9.46818L14.5344 5.22554Z"></path></svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -68,34 +68,19 @@ struct InputBinding
struct PadVibrationBinding
{
struct Motor
{
InputBindingKey binding;
Timer::Value last_update_time;
InputSource* source;
u32 bind_index;
float last_intensity;
};
u32 pad_index = 0;
Motor motors[MAX_MOTORS_PER_PAD] = {};
/// Returns true if the two motors are bound to the same host motor.
ALWAYS_INLINE bool AreMotorsCombined() const { return motors[0].binding == motors[1].binding; }
/// Returns the intensity when both motors are combined.
ALWAYS_INLINE float GetCombinedIntensity() const
{
return std::max(motors[0].last_intensity, motors[1].last_intensity);
}
u64 pad_and_bind_index; ///< Combined pad index and bind index for quick lookup.
InputBindingKey binding; ///< Binding key for this motor.
Timer::Value last_update_time; ///< Last time this motor was updated.
InputSource* source; ///< Input source for this motor.
float last_intensity; ///< Last intensity we sent to the motor.
};
struct PadLEDBinding
{
InputBindingKey binding;
InputSource* source;
float last_intensity;
u32 pad_index;
InputBindingKey binding; ///< Binding key for this LED.
InputSource* source; ///< Input source for this LED.
float last_intensity; ///< Last intensity we sent to the LED.
u32 pad_index; ///< Pad index this LED is for.
};
struct MacroButton
@@ -164,6 +149,11 @@ static void UpdateInputSourceState(const SettingsInterface& si, std::unique_lock
static const KeyCodeData* FindKeyCodeData(u32 usb_code);
ALWAYS_INLINE static u64 PackPadAndBindIndex(u32 pad_index, u32 bind_index)
{
return (static_cast<u64>(pad_index) << 32) | static_cast<u64>(bind_index);
}
// ------------------------------------------------------------------------
// Tracking host mouse movement and turning into relative events
// 4 axes: pointer left/right, wheel vertical/horizontal. Last/Next/Normalized.
@@ -623,23 +613,15 @@ void InputManager::AddBinding(std::string_view binding, const InputEventHandler&
s_state.binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
}
void InputManager::AddVibrationBinding(u32 pad_index, const InputBindingKey* motor_0_binding,
InputSource* motor_0_source, const InputBindingKey* motor_1_binding,
InputSource* motor_1_source)
void InputManager::AddVibrationBinding(u32 pad_index, u32 bind_index, const InputBindingKey& binding,
InputSource* source)
{
PadVibrationBinding vib;
vib.pad_index = pad_index;
if (motor_0_binding)
{
vib.motors[0].binding = *motor_0_binding;
vib.motors[0].source = motor_0_source;
}
if (motor_1_binding)
{
vib.motors[1].binding = *motor_1_binding;
vib.motors[1].source = motor_1_source;
}
s_state.pad_vibration_array.push_back(std::move(vib));
s_state.pad_vibration_array.push_back(
PadVibrationBinding{.pad_and_bind_index = PackPadAndBindIndex(pad_index, bind_index),
.binding = binding,
.last_update_time = 0,
.source = source,
.last_intensity = 0.0f});
}
// ------------------------------------------------------------------------
@@ -965,12 +947,6 @@ void InputManager::AddHotkeyBindings(const SettingsInterface& si)
void InputManager::AddPadBindings(const SettingsInterface& si, const std::string& section, u32 pad_index,
const Controller::ControllerInfo& cinfo)
{
bool vibration_binding_valid = false;
PadVibrationBinding vibration_binding = {};
vibration_binding.pad_index = pad_index;
PadLEDBinding led_binding = {};
led_binding.pad_index = pad_index;
for (const Controller::ControllerBindingInfo& bi : cinfo.bindings)
{
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bi.name));
@@ -1032,20 +1008,25 @@ void InputManager::AddPadBindings(const SettingsInterface& si, const std::string
case InputBindingInfo::Type::Motor:
{
DebugAssert(bi.generic_mapping == GenericInputBinding::LargeMotor ||
bi.generic_mapping == GenericInputBinding::SmallMotor);
if (bindings.empty())
continue;
if (bindings.size() > 1)
WARNING_LOG("More than one vibration motor binding for {}:{}", pad_index, bi.name);
PadVibrationBinding::Motor& motor =
vibration_binding.motors[BoolToUInt32(bi.generic_mapping == GenericInputBinding::SmallMotor)];
if (ParseBindingAndGetSource(bindings.front(), &motor.binding, &motor.source))
for (const std::string& binding : bindings)
{
motor.bind_index = bi.bind_index;
vibration_binding_valid = true;
PadVibrationBinding vib_binding;
if (ParseBindingAndGetSource(binding, &vib_binding.binding, &vib_binding.source))
{
vib_binding.pad_and_bind_index = PackPadAndBindIndex(pad_index, bi.bind_index);
vib_binding.last_update_time = 0;
// If we're reloading bindings due to e.g. device connection, sync the vibration state.
if (Controller* controller = System::GetController(pad_index))
vib_binding.last_intensity = controller->GetBindState(bi.bind_index);
else
vib_binding.last_intensity = 0.0f;
s_state.pad_vibration_array.push_back(vib_binding);
}
}
}
break;
@@ -1055,16 +1036,24 @@ void InputManager::AddPadBindings(const SettingsInterface& si, const std::string
if (bindings.empty())
continue;
if (ParseBindingAndGetSource(bindings.front(), &led_binding.binding, &led_binding.source))
for (const std::string& binding : bindings)
{
// If we're reloading bindings due to e.g. device connection, sync the LED state.
if (Controller* controller = System::GetController(pad_index))
led_binding.last_intensity = controller->GetBindState(bi.bind_index);
PadLEDBinding led_binding;
if (ParseBindingAndGetSource(binding, &led_binding.binding, &led_binding.source))
{
led_binding.pad_index = pad_index;
// Need to pass it through unconditionally, otherwise if the LED was on it'll stay on.
led_binding.source->UpdateLEDState(led_binding.binding, led_binding.last_intensity);
// If we're reloading bindings due to e.g. device connection, sync the LED state.
if (Controller* controller = System::GetController(pad_index))
led_binding.last_intensity = controller->GetBindState(bi.bind_index);
else
led_binding.last_intensity = 0.0f;
s_state.pad_led_array.push_back(std::move(led_binding));
// Need to pass it through unconditionally, otherwise if the LED was on it'll stay on.
led_binding.source->UpdateLEDState(led_binding.binding, led_binding.last_intensity);
s_state.pad_led_array.push_back(led_binding);
}
}
}
break;
@@ -1079,9 +1068,6 @@ void InputManager::AddPadBindings(const SettingsInterface& si, const std::string
break;
}
}
if (vibration_binding_valid)
s_state.pad_vibration_array.push_back(std::move(vibration_binding));
}
// ------------------------------------------------------------------------
@@ -1833,25 +1819,14 @@ std::unique_ptr<ForceFeedbackDevice> InputManager::CreateForceFeedbackDevice(con
void InputManager::SetPadVibrationIntensity(u32 pad_index, u32 bind_index, float intensity)
{
for (PadVibrationBinding& pad : s_state.pad_vibration_array)
const u64 pad_and_bind_index = PackPadAndBindIndex(pad_index, bind_index);
for (PadVibrationBinding& vib : s_state.pad_vibration_array)
{
if (pad.pad_index != pad_index)
continue;
PadVibrationBinding::Motor* motor;
if (bind_index == pad.motors[0].bind_index)
motor = &pad.motors[0];
else if (bind_index == pad.motors[1].bind_index)
motor = &pad.motors[1];
else
break;
if (motor->last_intensity == intensity)
break;
motor->last_intensity = intensity;
motor->last_update_time = 0; // force update at end of frame
break;
if (vib.pad_and_bind_index == pad_and_bind_index && vib.last_intensity != intensity)
{
vib.last_intensity = intensity;
vib.last_update_time = 0; // force update at end of frame
}
}
}
@@ -1859,16 +1834,9 @@ void InputManager::PauseVibration()
{
for (PadVibrationBinding& binding : s_state.pad_vibration_array)
{
for (u32 motor_index = 0; motor_index < MAX_MOTORS_PER_PAD; motor_index++)
{
PadVibrationBinding::Motor& motor = binding.motors[motor_index];
if (!motor.source || motor.last_intensity == 0.0f)
continue;
// we deliberately don't zero the intensity here, so it can resume later
motor.last_update_time = 0;
motor.source->UpdateMotorState(motor.binding, 0.0f);
}
// we deliberately don't zero the intensity here, so it can resume later
binding.last_update_time = 0;
binding.source->UpdateMotorState(binding.binding, 0.0f);
}
}
@@ -1876,66 +1844,50 @@ void InputManager::UpdateContinuedVibration()
{
// update vibration intensities, so if the game does a long effect, it continues
const u64 current_time = Timer::GetCurrentValue();
for (PadVibrationBinding& pad : s_state.pad_vibration_array)
for (PadVibrationBinding& binding : s_state.pad_vibration_array)
{
if (pad.AreMotorsCombined())
// skip if motor is off and wasn't just changed
if (binding.last_update_time > 0)
{
// motors are combined
PadVibrationBinding::Motor& large_motor = pad.motors[0];
PadVibrationBinding::Motor& small_motor = pad.motors[1];
// skip if both motors are off and this wasn't just changed
const Timer::Value min_update_time = std::min(large_motor.last_update_time, small_motor.last_update_time);
const double dt = Timer::ConvertValueToSeconds(current_time - min_update_time);
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
continue;
// but take max of both motors for the intensity
const float intensity = pad.GetCombinedIntensity();
if (intensity == 0.0f && min_update_time > 0)
continue;
large_motor.last_update_time = current_time;
large_motor.source->UpdateMotorState(large_motor.binding, intensity);
}
else if (pad.motors[0].source && pad.motors[0].source == pad.motors[1].source)
{
// motors are independent, but share the same source. do a combined update
PadVibrationBinding::Motor& large_motor = pad.motors[0];
PadVibrationBinding::Motor& small_motor = pad.motors[1];
const Timer::Value min_update_time = std::min(large_motor.last_update_time, small_motor.last_update_time);
// skip if both motors are off and this wasn't just changed
if (std::max(large_motor.last_intensity, small_motor.last_intensity) == 0.0f && min_update_time > 0)
continue;
const double dt = Timer::ConvertValueToSeconds(current_time - min_update_time);
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
continue;
large_motor.last_update_time = current_time;
small_motor.last_update_time = current_time;
large_motor.source->UpdateMotorState(large_motor.binding, small_motor.binding, large_motor.last_intensity,
small_motor.last_intensity);
}
else
{
// independent motor control
for (u32 i = 0; i < MAX_MOTORS_PER_PAD; i++)
if (binding.last_intensity == 0.0f ||
Timer::ConvertValueToSeconds(current_time - binding.last_update_time) < VIBRATION_UPDATE_INTERVAL_SECONDS)
{
PadVibrationBinding::Motor& motor = pad.motors[i];
if (!motor.source || (motor.last_intensity == 0.0f && motor.last_update_time > 0))
continue;
const double dt = Timer::ConvertValueToSeconds(current_time - motor.last_update_time);
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
continue;
// re-notify the source of the continued effect
motor.last_update_time = current_time;
motor.source->UpdateMotorState(motor.binding, motor.last_intensity);
continue;
}
}
// figure out the intensity, we need to search all bindings since it may be combined
// merge into a single update where possible
std::array<InputBindingKey, 2> motor_keys = {};
std::array<float, 2> motor_intensities = {0.0f, 0.0f};
u32 motor_intensities_mask = 0;
for (PadVibrationBinding& other_binding : s_state.pad_vibration_array)
{
// only try to merge devices of the same source/index
if (other_binding.source != binding.source || other_binding.binding.source_index != binding.binding.source_index)
continue;
// data should probably never be more than 1, but just in case
if (other_binding.binding.data > 1 && binding.binding.data != other_binding.binding.data)
continue;
const u32 motor_index = std::min<u32>(other_binding.binding.data, 1u);
other_binding.last_update_time = current_time;
motor_keys[motor_index] = other_binding.binding;
motor_intensities[motor_index] = std::max(motor_intensities[motor_index], other_binding.last_intensity);
motor_intensities_mask |= (1u << motor_index);
}
// can we send it as a single update?
DEV_COLOR_LOG(StrongOrange, "Sending vibration update to device {}: mask 0x{:02X}, intensities {{{}, {}}}",
static_cast<u32>(binding.binding.source_index), motor_intensities_mask, motor_intensities[0],
motor_intensities[1]);
if (motor_intensities_mask == 0b11)
binding.source->UpdateMotorState(motor_keys[0], motor_keys[1], motor_intensities[0], motor_intensities[1]);
else if (motor_intensities_mask == 0b01)
binding.source->UpdateMotorState(motor_keys[0], motor_intensities[0]);
else // if (motor_intensities_mask == 0b10)
binding.source->UpdateMotorState(motor_keys[1], motor_intensities[1]);
}
}

View File

@@ -321,8 +321,7 @@ bool ParseBindingAndGetSource(std::string_view binding, InputBindingKey* key, In
void AddBinding(std::string_view binding, const InputEventHandler& handler);
/// Adds an external vibration binding.
void AddVibrationBinding(u32 pad_index, const InputBindingKey* motor_0_binding, InputSource* motor_0_source,
const InputBindingKey* motor_1_binding, InputSource* motor_1_source);
void AddVibrationBinding(u32 pad_index, u32 bind_index, const InputBindingKey& binding, InputSource* source);
/// Updates internal state for any binds for this key, and fires callbacks as needed.
/// Returns true if anything was bound to this key, otherwise false.