diff options
| author | Jordan Woyak <jordan.woyak@gmail.com> | 2024-10-31 18:26:19 -0500 |
|---|---|---|
| committer | Jordan Woyak <jordan.woyak@gmail.com> | 2025-06-14 16:29:25 -0500 |
| commit | 83beebaf8b074f1c7161134065f19baea08f78a6 (patch) | |
| tree | 20ee967641ef2855dbafee633158e78d1c159474 /Source/Core/InputCommon/ControllerInterface | |
| parent | 0780458069abacf9ca1b33c2f4c523f0a8d8bde9 (diff) | |
DolphinQt/InputCommon: Move some calibration logic to InputCommon and make the "Calibrate" button also map inputs.
Diffstat (limited to 'Source/Core/InputCommon/ControllerInterface')
| -rw-r--r-- | Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp | 130 | ||||
| -rw-r--r-- | Source/Core/InputCommon/ControllerInterface/MappingCommon.h | 76 |
2 files changed, 206 insertions, 0 deletions
diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp index 505fd0260c..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 { @@ -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 |
