diff options
| author | Jordan Woyak <jordan.woyak@gmail.com> | 2025-06-15 15:45:04 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-15 15:45:04 -0500 |
| commit | 28a6eb26cc2c98726c59f254e8962eb1209f5578 (patch) | |
| tree | a60a3e16e01077cfdaf80c55a41c530b1884d663 /Source/Core/InputCommon/ControllerInterface | |
| parent | 3a94289ad6e27011fe0ca3ab0fb0fee771fb9fa8 (diff) | |
| parent | 83beebaf8b074f1c7161134065f19baea08f78a6 (diff) | |
Merge pull request #13703 from jordan-woyak/map-and-calibrate
DolphinQt/InputCommon: Make the "Calibrate" button also map inputs.
Diffstat (limited to 'Source/Core/InputCommon/ControllerInterface')
4 files changed, 212 insertions, 3 deletions
diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index 5a370cadc8..41ef16c330 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -391,7 +391,7 @@ InputDetector::InputDetector() : m_start_time{}, m_state{} } void InputDetector::Start(const DeviceContainer& container, - const std::vector<std::string>& device_strings) + std::span<const std::string> device_strings) { m_start_time = Clock::now(); diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index 95f9f5b88e..c8be0aeb65 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -8,6 +8,7 @@ #include <memory> #include <mutex> #include <optional> +#include <span> #include <string> #include <string_view> #include <vector> @@ -217,6 +218,8 @@ public: Clock::time_point press_time; std::optional<Clock::time_point> release_time; ControlState smoothness = 0; + + bool IsAnalogPress() const { return smoothness > 1.00001; } }; Device::Input* FindInput(std::string_view name, const Device* def_dev) const; @@ -253,7 +256,7 @@ public: InputDetector(); ~InputDetector(); - void Start(const DeviceContainer& container, const std::vector<std::string>& device_strings); + void Start(const DeviceContainer& container, std::span<const std::string> device_strings); void Update(std::chrono::milliseconds initial_wait, std::chrono::milliseconds confirmation_wait, std::chrono::milliseconds maximum_wait); bool IsComplete() const; diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp index 9660d9ede8..2852291021 100644 --- a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp @@ -5,14 +5,21 @@ #include <algorithm> #include <chrono> +#include <ranges> #include <string> #include <vector> #include <fmt/format.h> #include <fmt/ranges.h> +#include "Common/MathUtil.h" #include "Common/StringUtil.h" + +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ControllerEmu/StickGate.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/CoreDevice.h" +#include "InputCommon/InputConfig.h" namespace ciface::MappingCommon { @@ -139,7 +146,7 @@ void RemoveSpuriousTriggerCombinations(Core::InputDetector::Results* detections) const auto is_spurious = [&](const auto& detection) { return std::ranges::any_of(*detections, [&](const auto& d) { // This is a spurious digital detection if a "smooth" (analog) detection is temporally near. - return &d != &detection && d.smoothness > 1 && d.smoothness > detection.smoothness && + return &d != &detection && d.IsAnalogPress() && !detection.IsAnalogPress() && abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD; }); }; @@ -163,4 +170,127 @@ bool ContainsCompleteDetection(const Core::InputDetector::Results& results) }); } +ReshapableInputMapper::ReshapableInputMapper(const Core::DeviceContainer& container, + std::span<const std::string> device_strings) +{ + m_input_detector.Start(container, device_strings); +} + +bool ReshapableInputMapper::Update() +{ + const auto prev_size = m_input_detector.GetResults().size(); + + constexpr auto wait_time = std::chrono::seconds{4}; + m_input_detector.Update(wait_time, wait_time, wait_time * REQUIRED_INPUT_COUNT); + + return m_input_detector.GetResults().size() != prev_size; +} + +float ReshapableInputMapper::GetCurrentAngle() const +{ + constexpr auto quarter_circle = float(MathUtil::TAU) * 0.25f; + return quarter_circle - (float(m_input_detector.GetResults().size()) * quarter_circle); +} + +bool ReshapableInputMapper::IsComplete() const +{ + return m_input_detector.GetResults().size() >= REQUIRED_INPUT_COUNT || + m_input_detector.IsComplete(); +} + +bool ReshapableInputMapper::IsCalibrationNeeded() const +{ + return std::ranges::any_of(m_input_detector.GetResults() | std::views::take(REQUIRED_INPUT_COUNT), + &ciface::Core::InputDetector::Detection::IsAnalogPress); +} + +bool ReshapableInputMapper::ApplyResults(ControllerEmu::EmulatedController* controller, + ControllerEmu::ReshapableInput* stick) +{ + auto const detections = m_input_detector.TakeResults(); + + if (detections.size() < REQUIRED_INPUT_COUNT) + return false; + + // Transpose URDL to UDLR. + const std::array results{detections[0], detections[2], detections[3], detections[1]}; + + const auto default_device = controller->GetDefaultDevice(); + + for (std::size_t i = 0; i != results.size(); ++i) + { + ciface::Core::DeviceQualifier device_qualifier; + device_qualifier.FromDevice(results[i].device.get()); + + stick->controls[i]->control_ref->SetExpression(ciface::MappingCommon::GetExpressionForControl( + results[i].input->GetName(), device_qualifier, default_device, + ciface::MappingCommon::Quote::On)); + + controller->UpdateSingleControlReference(g_controller_interface, + stick->controls[i]->control_ref.get()); + } + + controller->GetConfig()->GenerateControllerTextures(); + + return true; +} + +CalibrationBuilder::CalibrationBuilder(std::optional<Common::DVec2> center) + : m_calibration_data(ControllerEmu::ReshapableInput::CALIBRATION_SAMPLE_COUNT, 0.0), + m_center{center} +{ +} + +void CalibrationBuilder::Update(Common::DVec2 point) +{ + if (!m_center.has_value()) + m_center = point; + + const auto new_point = point - *m_center; + ControllerEmu::ReshapableInput::UpdateCalibrationData(m_calibration_data, m_prev_point, + new_point); + m_prev_point = new_point; +} + +bool CalibrationBuilder::IsCalibrationDataSensible() const +{ + // Even the GC controller's small range would pass this test. + constexpr double REASONABLE_AVERAGE_RADIUS = 0.6; + + // Test that the average input radius is not below a threshold. + // This will make sure the user has actually moved their stick from neutral. + + MathUtil::RunningVariance<ControlState> stats; + + for (const auto x : m_calibration_data) + stats.Push(x); + + if (stats.Mean() < REASONABLE_AVERAGE_RADIUS) + return false; + + // Test that the standard deviation is below a threshold. + // This will make sure the user has not just filled in one side of their input. + + // Approx. deviation of a square input gate, anything much more than that would be unusual. + constexpr double REASONABLE_DEVIATION = 0.14; + + return stats.StandardDeviation() < REASONABLE_DEVIATION; +} + +ControlState CalibrationBuilder::GetCalibrationRadiusAtAngle(double angle) const +{ + return ControllerEmu::ReshapableInput::GetCalibrationDataRadiusAtAngle(m_calibration_data, angle); +} + +void CalibrationBuilder::ApplyResults(ControllerEmu::ReshapableInput* stick) +{ + stick->SetCenter(GetCenter()); + stick->SetCalibrationData(std::move(m_calibration_data)); +} + +Common::DVec2 CalibrationBuilder::GetCenter() const +{ + return m_center.value_or(Common::DVec2{}); +} + } // namespace ciface::MappingCommon diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.h b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h index 62c1a8f281..a72549c458 100644 --- a/Source/Core/InputCommon/ControllerInterface/MappingCommon.h +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h @@ -5,8 +5,15 @@ #include <string> +#include "Common/Matrix.h" +#include "InputCommon/ControllerEmu/StickGate.h" #include "InputCommon/ControllerInterface/CoreDevice.h" +namespace ControllerEmu +{ +class EmulatedController; +} // namespace ControllerEmu + namespace ciface::MappingCommon { enum class Quote @@ -27,4 +34,73 @@ void RemoveSpuriousTriggerCombinations(Core::InputDetector::Results*); void RemoveDetectionsAfterTimePoint(Core::InputDetector::Results*, Clock::time_point after); bool ContainsCompleteDetection(const Core::InputDetector::Results&); +// class for detecting four directional input mappings in sequence. +class ReshapableInputMapper +{ +public: + // Four cardinal directions. + static constexpr std::size_t REQUIRED_INPUT_COUNT = 4; + + // Caller should hold the "StateLock". + ReshapableInputMapper(const Core::DeviceContainer& container, + std::span<const std::string> device_strings); + + // Reads inputs and updates internal state. + // Returns true if an input was detected in this call. + // (useful for UI animation) + // Caller should hold the "StateLock". + bool Update(); + + // A counter-clockwise angle in radians for the currently desired input direction. + // Used for a graphical indicator in the UI. + // 0 == East + float GetCurrentAngle() const; + + // True if all four directions have been detected or the timer expired. + bool IsComplete() const; + + // Returns true if "analog" inputs were detected and calibration should be performed. + // Must use *before* ApplyResults. + bool IsCalibrationNeeded() const; + + // Use when IsComplete returns true. + // Updates the mappings on the provided ReshapableInput. + // Caller should hold the "StateLock". + bool ApplyResults(ControllerEmu::EmulatedController*, ControllerEmu::ReshapableInput* stick); + +private: + Core::InputDetector m_input_detector; +}; + +class CalibrationBuilder +{ +public: + // Provide nullopt if you want to calibrate the center on first Update. + explicit CalibrationBuilder(std::optional<Common::DVec2> center = Common::DVec2{}); + + // Updates the calibration data using the provided point and the previous point. + void Update(Common::DVec2 point); + + // Returns true when the calibration data seems to be reasonably filled in. + // Used to update the UI to encourage the user to click the "Finish" button. + bool IsCalibrationDataSensible() const; + + // Grabs the calibration value at the provided angle. + // Used to render the calibration in the UI while it's in progress. + ControlState GetCalibrationRadiusAtAngle(double angle) const; + + // Sets the calibration data of the provided ReshapableInput. + // Caller should hold the "StateLock". + void ApplyResults(ControllerEmu::ReshapableInput* stick); + + Common::DVec2 GetCenter() const; + +private: + ControllerEmu::ReshapableInput::CalibrationData m_calibration_data; + + std::optional<Common::DVec2> m_center = std::nullopt; + + Common::DVec2 m_prev_point{}; +}; + } // namespace ciface::MappingCommon |
