DolphinQt/InputCommon: Move some calibration logic to InputCommon and make the "Calibrate" button also map inputs.

This commit is contained in:
Jordan Woyak
2024-10-31 18:26:19 -05:00
parent 0780458069
commit 83beebaf8b
7 changed files with 521 additions and 145 deletions

View File

@ -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

View File

@ -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