summaryrefslogtreecommitdiff
path: root/Source/Core/InputCommon/ControllerInterface
diff options
context:
space:
mode:
authorJordan Woyak <jordan.woyak@gmail.com>2024-10-31 18:26:19 -0500
committerJordan Woyak <jordan.woyak@gmail.com>2025-06-14 16:29:25 -0500
commit83beebaf8b074f1c7161134065f19baea08f78a6 (patch)
tree20ee967641ef2855dbafee633158e78d1c159474 /Source/Core/InputCommon/ControllerInterface
parent0780458069abacf9ca1b33c2f4c523f0a8d8bde9 (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.cpp130
-rw-r--r--Source/Core/InputCommon/ControllerInterface/MappingCommon.h76
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