2020-10-20 16:30:15 +00:00
|
|
|
// Copyright 2020 Dolphin Emulator Project
|
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
#include "InputCommon/ControllerInterface/WGInput/WGInput.h"
|
|
|
|
|
|
|
|
#include <array>
|
2022-08-08 09:17:48 +00:00
|
|
|
#include <map>
|
2020-10-20 16:30:15 +00:00
|
|
|
#include <string_view>
|
2024-01-17 11:39:45 +00:00
|
|
|
#include <utility>
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
// For CoGetApartmentType
|
|
|
|
#include <objbase.h>
|
|
|
|
#pragma comment(lib, "ole32")
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
// NOTE: winrt translates com failures to c++ exceptions, so we must use try/catch in this file to
|
|
|
|
// prevent possible errors from escaping and terminating Dolphin.
|
|
|
|
#include <winrt/base.h>
|
2022-08-08 09:17:48 +00:00
|
|
|
#include <winrt/windows.devices.haptics.h>
|
|
|
|
#include <winrt/windows.devices.power.h>
|
|
|
|
#include <winrt/windows.foundation.collections.h>
|
|
|
|
#include <winrt/windows.gaming.input.h>
|
|
|
|
#include <winrt/windows.system.power.h>
|
|
|
|
#pragma comment(lib, "windowsapp")
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
#include <fmt/format.h>
|
|
|
|
|
2023-06-12 02:51:49 +00:00
|
|
|
#include "Common/HRWrap.h"
|
2020-10-20 16:30:15 +00:00
|
|
|
#include "Common/Logging/Log.h"
|
2024-01-17 11:39:45 +00:00
|
|
|
#include "Common/ScopeGuard.h"
|
2020-10-20 16:30:15 +00:00
|
|
|
#include "Common/StringUtil.h"
|
2024-01-17 11:39:45 +00:00
|
|
|
#include "Common/WorkQueueThread.h"
|
2020-10-20 16:30:15 +00:00
|
|
|
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
namespace WGI = winrt::Windows::Gaming::Input;
|
|
|
|
namespace Haptics = winrt::Windows::Devices::Haptics;
|
|
|
|
using winrt::Windows::Foundation::Collections::IVectorView;
|
2020-10-20 16:30:15 +00:00
|
|
|
|
|
|
|
namespace ciface::WGInput
|
|
|
|
{
|
|
|
|
static constexpr std::string_view SOURCE_NAME = "WGInput";
|
|
|
|
|
|
|
|
// These names correspond to the values of the GameControllerButtonLabel enum.
|
|
|
|
// "None" is not used.
|
|
|
|
// There are some overlapping names assuming no device exposes both
|
2022-08-08 09:17:48 +00:00
|
|
|
// GameControllerButtonLabel::XboxLeftBumper and GameControllerButtonLabel::LeftBumper.
|
2020-10-20 16:30:15 +00:00
|
|
|
// If needed we can prepend "Xbox" to relevant input names on conflict in the future.
|
|
|
|
static constexpr std::array wgi_button_names = {
|
|
|
|
"None", "Back", "Start", "Menu", "View", "Pad N",
|
|
|
|
"Pad S", "Pad W", "Pad E", "Button A", "Button B", "Button X",
|
|
|
|
"Button Y", "Bumper L", "Trigger L", "Thumb L", "Bumper R", "Trigger R",
|
|
|
|
"Thumb R", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4", "Mode",
|
|
|
|
"Select", "Menu", "View", "Back", "Start", "Options",
|
|
|
|
"Share", "Pad N", "Pad S", "Pad W", "Pad E", "Letter A",
|
|
|
|
"Letter B", "Letter C", "Letter L", "Letter R", "Letter X", "Letter Y",
|
|
|
|
"Letter Z", "Cross", "Circle", "Square", "Triangle", "Bumper L",
|
|
|
|
"Trigger L", "Thumb L", "Left 1", "Left 2", "Left 3", "Bumper R",
|
|
|
|
"Trigger R", "Thumb R", "Right 1", "Right 2", "Right 3", "Paddle 1",
|
|
|
|
"Paddle 2", "Paddle 3", "Paddle 4", "Plus", "Minus", "Down Left Arrow",
|
|
|
|
"Dial L", "Dial R", "Suspension",
|
|
|
|
};
|
|
|
|
|
|
|
|
template <typename T, typename M>
|
|
|
|
struct MemberName
|
|
|
|
{
|
|
|
|
M T::*ptr;
|
|
|
|
const char* name;
|
|
|
|
};
|
|
|
|
|
|
|
|
static constexpr MemberName<WGI::GamepadReading, double> gamepad_trigger_names[] = {
|
|
|
|
{&WGI::GamepadReading::LeftTrigger, "Trigger L"},
|
|
|
|
{&WGI::GamepadReading::RightTrigger, "Trigger R"}};
|
|
|
|
|
|
|
|
static constexpr MemberName<WGI::GamepadReading, double> gamepad_axis_names[] = {
|
|
|
|
{&WGI::GamepadReading::LeftThumbstickX, "Left X"},
|
|
|
|
{&WGI::GamepadReading::LeftThumbstickY, "Left Y"},
|
|
|
|
{&WGI::GamepadReading::RightThumbstickX, "Right X"},
|
|
|
|
{&WGI::GamepadReading::RightThumbstickY, "Right Y"}};
|
|
|
|
|
|
|
|
static constexpr MemberName<WGI::GamepadVibration, double> gamepad_motor_names[] = {
|
|
|
|
{&WGI::GamepadVibration::LeftMotor, "Motor L"},
|
|
|
|
{&WGI::GamepadVibration::RightMotor, "Motor R"},
|
|
|
|
{&WGI::GamepadVibration::LeftTrigger, "Trigger L"},
|
|
|
|
{&WGI::GamepadVibration::RightTrigger, "Trigger R"}};
|
|
|
|
|
|
|
|
class Device : public Core::Device
|
|
|
|
{
|
|
|
|
public:
|
2022-08-08 09:17:48 +00:00
|
|
|
Device(std::string name, WGI::RawGameController raw_controller, WGI::Gamepad gamepad)
|
2020-10-20 16:30:15 +00:00
|
|
|
: m_name(std::move(name)), m_raw_controller(raw_controller), m_gamepad(gamepad)
|
|
|
|
{
|
|
|
|
// Buttons:
|
|
|
|
PopulateButtons();
|
|
|
|
|
|
|
|
// Add inputs for IGamepad interface if available.
|
|
|
|
if (m_gamepad)
|
|
|
|
{
|
|
|
|
// Axes:
|
|
|
|
for (auto& axis : gamepad_axis_names)
|
|
|
|
{
|
|
|
|
AddInput(new NamedAxis(&(m_gamepad_reading.*axis.ptr), 0.0, -1.0, axis.name));
|
|
|
|
AddInput(new NamedAxis(&(m_gamepad_reading.*axis.ptr), 0.0, +1.0, axis.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Triggers:
|
|
|
|
for (auto& trigger : gamepad_trigger_names)
|
|
|
|
AddInput(new NamedTrigger(&(m_gamepad_reading.*trigger.ptr), trigger.name));
|
|
|
|
|
|
|
|
// Motors:
|
|
|
|
for (auto& motor : gamepad_motor_names)
|
|
|
|
AddOutput(new NamedMotor(&(m_state_out.*motor.ptr), motor.name, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add IRawGameController's axes if IGamepad is not available.
|
|
|
|
// This may need to change if some devices expose additional axes.
|
|
|
|
// Maybe we can determine which additional axes are not in IGamepad's collection.
|
|
|
|
const bool use_raw_controller_axes = !m_gamepad;
|
|
|
|
|
|
|
|
if (use_raw_controller_axes)
|
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
// Axes:
|
|
|
|
m_axes.resize(m_raw_controller.AxisCount());
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
u32 i = 0;
|
|
|
|
for (auto& axis : m_axes)
|
|
|
|
{
|
|
|
|
// AddAnalogInputs adds additional "FullAnalogSurface" Inputs.
|
|
|
|
AddAnalogInputs(new IndexedAxis(&axis, 0.5, +0.5, i),
|
|
|
|
new IndexedAxis(&axis, 0.5, -0.5, i));
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
// If AxisCount failed, m_axes will remain zero-sized; nothing to do.
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating axes");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apparently some devices (e.g. DS4) provide the IGameController interface
|
|
|
|
// but expose the dpad only on a switch (IRawGameController interface).
|
|
|
|
// We'll need to add switches regardless of available interfaces.
|
|
|
|
constexpr bool use_raw_controller_switches = true;
|
|
|
|
|
|
|
|
// Switches (Hats):
|
|
|
|
if (use_raw_controller_switches)
|
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
m_switches.resize(m_raw_controller.SwitchCount());
|
|
|
|
// Accumulate switch kinds first, to ensure no inputs are added if there is any error.
|
|
|
|
std::vector<WGI::GameControllerSwitchKind> switch_kinds;
|
|
|
|
for (u32 i = 0; i < m_switches.size(); i++)
|
|
|
|
switch_kinds.push_back(m_raw_controller.GetSwitchKind(i));
|
|
|
|
|
|
|
|
u32 i = 0;
|
|
|
|
for (auto& swtch : m_switches)
|
|
|
|
{
|
|
|
|
using gcsp = WGI::GameControllerSwitchPosition;
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
AddInput(new IndexedSwitch(&swtch, i, gcsp::Up));
|
|
|
|
AddInput(new IndexedSwitch(&swtch, i, gcsp::Down));
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
if (switch_kinds[i] != WGI::GameControllerSwitchKind::TwoWay)
|
|
|
|
{
|
|
|
|
// If it's not a "two-way" switch (up/down only) then add the left/right inputs.
|
|
|
|
AddInput(new IndexedSwitch(&swtch, i, gcsp::Left));
|
|
|
|
AddInput(new IndexedSwitch(&swtch, i, gcsp::Right));
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
++i;
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
2022-08-27 09:11:27 +00:00
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
// Safe as no inputs will have been added.
|
|
|
|
m_switches.clear();
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating switches");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Haptics:
|
|
|
|
PopulateHaptics();
|
|
|
|
|
|
|
|
// Battery:
|
2022-08-08 09:17:48 +00:00
|
|
|
if (UpdateBatteryLevel())
|
|
|
|
AddInput(new Battery(&m_battery_level));
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
|
2022-03-02 21:40:20 +00:00
|
|
|
int GetSortPriority() const override { return -1; }
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
const WGI::RawGameController GetRawGameController() const { return m_raw_controller; }
|
|
|
|
|
2020-10-20 16:30:15 +00:00
|
|
|
private:
|
2022-08-08 09:17:48 +00:00
|
|
|
using ButtonValueType = u8;
|
2020-10-20 16:30:15 +00:00
|
|
|
|
|
|
|
class Button : public Input
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
explicit Button(const ButtonValueType* button) : m_button(*button) {}
|
|
|
|
ControlState GetState() const override { return ControlState(m_button != 0); }
|
|
|
|
|
|
|
|
private:
|
|
|
|
const ButtonValueType& m_button;
|
|
|
|
};
|
|
|
|
|
|
|
|
// A button with one of the "labels" that WGI provides.
|
|
|
|
class NamedButton final : public Button
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
NamedButton(const ButtonValueType* button, std::string_view name) : Button(button), m_name(name)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override { return std::string(m_name); }
|
|
|
|
|
|
|
|
private:
|
|
|
|
const std::string_view m_name;
|
|
|
|
};
|
|
|
|
|
|
|
|
// A button with no label so we name it by its index.
|
|
|
|
class IndexedButton final : public Button
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
IndexedButton(const ButtonValueType* button, u32 index) : Button(button), m_index(index) {}
|
|
|
|
std::string GetName() const override { return fmt::format("Button {}", m_index); }
|
|
|
|
|
|
|
|
private:
|
|
|
|
u32 m_index;
|
|
|
|
};
|
|
|
|
|
|
|
|
class Axis : public Input
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
Axis(const double* axis, double base, double range)
|
|
|
|
: m_base(base), m_range(range), m_axis(*axis)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ControlState GetState() const override { return ControlState(m_axis - m_base) / m_range; }
|
|
|
|
|
|
|
|
protected:
|
|
|
|
const double m_base;
|
|
|
|
const double m_range;
|
|
|
|
|
|
|
|
private:
|
|
|
|
const double& m_axis;
|
|
|
|
};
|
|
|
|
|
|
|
|
class NamedAxis final : public Axis
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
NamedAxis(const double* axis, double base, double range, std::string_view name)
|
|
|
|
: Axis(axis, base, range), m_name(name)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override
|
|
|
|
{
|
|
|
|
return fmt::format("{}{}", m_name, m_range < 0 ? '-' : '+');
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const std::string_view m_name;
|
|
|
|
};
|
|
|
|
|
|
|
|
class NamedTrigger final : public Axis
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
NamedTrigger(const double* axis, std::string_view name) : Axis(axis, 0.0, 1.0), m_name(name) {}
|
|
|
|
std::string GetName() const override { return std::string(m_name); }
|
|
|
|
|
|
|
|
private:
|
|
|
|
const std::string_view m_name;
|
|
|
|
};
|
|
|
|
|
|
|
|
class NamedMotor final : public Output
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
NamedMotor(double* motor, std::string_view name, Device* parent)
|
|
|
|
: m_motor(*motor), m_name(name), m_parent(*parent)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override { return std::string(m_name); }
|
|
|
|
void SetState(ControlState state) override
|
|
|
|
{
|
|
|
|
if (m_motor == state)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_motor = state;
|
|
|
|
m_parent.UpdateMotors();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
double& m_motor;
|
|
|
|
const std::string_view m_name;
|
|
|
|
Device& m_parent;
|
|
|
|
};
|
|
|
|
|
|
|
|
class IndexedAxis final : public Axis
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
IndexedAxis(const double* axis, double base, double range, u32 index)
|
|
|
|
: Axis(axis, base, range), m_index(index)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override
|
|
|
|
{
|
|
|
|
return fmt::format("Axis {}{}", m_index, m_range < 0 ? '-' : '+');
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const u32 m_index;
|
|
|
|
};
|
|
|
|
|
|
|
|
class IndexedSwitch final : public Input
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
IndexedSwitch(const WGI::GameControllerSwitchPosition* swtch, u32 index,
|
|
|
|
WGI::GameControllerSwitchPosition direction)
|
2022-08-08 09:17:48 +00:00
|
|
|
: m_switch(*swtch), m_index(index), m_direction(static_cast<int32_t>(direction))
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override
|
|
|
|
{
|
|
|
|
return fmt::format("Switch {} {}", m_index, "NESW"[m_direction / 2]);
|
|
|
|
}
|
|
|
|
ControlState GetState() const override
|
|
|
|
{
|
2022-08-08 09:17:48 +00:00
|
|
|
if (m_switch == WGI::GameControllerSwitchPosition::Center)
|
2020-10-20 16:30:15 +00:00
|
|
|
return 0.0;
|
|
|
|
|
|
|
|
// All of the "inbetween" states (e.g. Up-Right) are one-off from the four cardinal
|
|
|
|
// directions. This tests that the current switch state value is within 1 of the desired
|
|
|
|
// state.
|
2022-08-08 09:17:48 +00:00
|
|
|
const auto direction_diff = std::abs(static_cast<int32_t>(m_switch) - m_direction);
|
2020-10-20 16:30:15 +00:00
|
|
|
return ControlState(direction_diff <= 1 || direction_diff == 7);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const WGI::GameControllerSwitchPosition& m_switch;
|
|
|
|
const u32 m_index;
|
2022-08-08 09:17:48 +00:00
|
|
|
const int32_t m_direction;
|
2020-10-20 16:30:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
class Battery : public Input
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
Battery(const double* level) : m_level(*level) {}
|
|
|
|
|
|
|
|
bool IsDetectable() const override { return false; }
|
|
|
|
|
|
|
|
ControlState GetState() const override { return m_level; }
|
|
|
|
|
|
|
|
std::string GetName() const override { return "Battery"; }
|
|
|
|
|
|
|
|
private:
|
|
|
|
const double& m_level;
|
|
|
|
};
|
|
|
|
|
|
|
|
class SimpleHaptics : public Output
|
|
|
|
{
|
|
|
|
public:
|
2022-08-08 09:17:48 +00:00
|
|
|
SimpleHaptics(Haptics::SimpleHapticsController haptics,
|
|
|
|
Haptics::SimpleHapticsControllerFeedback feedback, u32 haptics_index)
|
2020-10-20 16:30:15 +00:00
|
|
|
: m_haptics(haptics), m_feedback(feedback), m_haptics_index(haptics_index)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetState(ControlState state) override
|
|
|
|
{
|
|
|
|
if (m_current_state == state)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_current_state = state;
|
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
if (state)
|
|
|
|
m_haptics.SendHapticFeedback(m_feedback, state);
|
|
|
|
else
|
|
|
|
m_haptics.StopFeedback();
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
// Ignore
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
u32 GetHapticsIndex() const { return m_haptics_index; }
|
|
|
|
|
|
|
|
private:
|
2022-08-08 09:17:48 +00:00
|
|
|
Haptics::SimpleHapticsController m_haptics;
|
|
|
|
Haptics::SimpleHapticsControllerFeedback m_feedback;
|
2020-10-20 16:30:15 +00:00
|
|
|
const u32 m_haptics_index;
|
|
|
|
ControlState m_current_state = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
class NamedFeedback final : public SimpleHaptics
|
|
|
|
{
|
|
|
|
public:
|
2022-08-08 09:17:48 +00:00
|
|
|
NamedFeedback(Haptics::SimpleHapticsController haptics,
|
|
|
|
Haptics::SimpleHapticsControllerFeedback feedback, u32 haptics_index,
|
|
|
|
std::string_view feedback_name)
|
2020-10-20 16:30:15 +00:00
|
|
|
: SimpleHaptics(haptics, feedback, haptics_index), m_feedback_name(feedback_name)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
std::string GetName() const override
|
|
|
|
{
|
|
|
|
return fmt::format("{} {}", m_feedback_name, GetHapticsIndex());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const std::string_view m_feedback_name;
|
|
|
|
};
|
|
|
|
|
|
|
|
void PopulateButtons()
|
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
// Using RawGameController for buttons because it gives us a nice array instead of a bitmask.
|
|
|
|
m_buttons.resize(m_raw_controller.ButtonCount());
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
u32 i = 0;
|
|
|
|
for (const auto& button : m_buttons)
|
|
|
|
{
|
|
|
|
WGI::GameControllerButtonLabel lbl{WGI::GameControllerButtonLabel::None};
|
|
|
|
try
|
|
|
|
{
|
|
|
|
lbl = m_raw_controller.GetButtonLabel(i);
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
lbl = WGI::GameControllerButtonLabel::None;
|
|
|
|
}
|
|
|
|
|
|
|
|
const int32_t button_name_idx = static_cast<int32_t>(lbl);
|
|
|
|
if (lbl != WGI::GameControllerButtonLabel::None &&
|
|
|
|
button_name_idx < wgi_button_names.size())
|
|
|
|
AddInput(new NamedButton(&button, wgi_button_names[button_name_idx]));
|
|
|
|
else
|
|
|
|
AddInput(new IndexedButton(&button, i));
|
|
|
|
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
// If ButtonCount failed, m_buttons will be zero-sized.
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating buttons");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void PopulateHaptics()
|
|
|
|
{
|
2022-08-08 09:17:48 +00:00
|
|
|
static const std::map<uint16_t, std::string> waveform_name_map{
|
|
|
|
{Haptics::KnownSimpleHapticsControllerWaveforms::Click(), "Click"},
|
|
|
|
{Haptics::KnownSimpleHapticsControllerWaveforms::BuzzContinuous(), "Buzz"},
|
|
|
|
{Haptics::KnownSimpleHapticsControllerWaveforms::RumbleContinuous(), "Rumble"},
|
|
|
|
};
|
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
// SimpleHapticsControllers is available from Windows 1709.
|
|
|
|
u32 haptics_index = 0;
|
|
|
|
for (auto haptics_controller : m_raw_controller.SimpleHapticsControllers())
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
for (Haptics::SimpleHapticsControllerFeedback feedback :
|
|
|
|
haptics_controller.SupportedFeedback())
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
const uint16_t waveform = feedback.Waveform();
|
|
|
|
auto waveform_name_it = waveform_name_map.find(waveform);
|
|
|
|
if (waveform_name_it == waveform_name_map.end())
|
|
|
|
{
|
|
|
|
WARN_LOG_FMT(CONTROLLERINTERFACE,
|
|
|
|
"WGInput: Unhandled haptics feedback waveform: 0x{:04x}.", waveform);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
AddOutput(new NamedFeedback(haptics_controller, feedback, haptics_index,
|
|
|
|
waveform_name_it->second));
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
2022-08-27 09:11:27 +00:00
|
|
|
++haptics_index;
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
2022-08-27 09:11:27 +00:00
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
// Don't need to cleanup any state. It's OK if some outputs were added.
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating haptics");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string GetName() const override { return m_name; }
|
|
|
|
|
|
|
|
std::string GetSource() const override { return std::string(SOURCE_NAME); }
|
|
|
|
|
2023-05-24 19:58:30 +00:00
|
|
|
Core::DeviceRemoval UpdateInput() override
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
|
|
|
// IRawGameController:
|
2022-08-08 09:17:48 +00:00
|
|
|
static_assert(sizeof(bool) == sizeof(ButtonValueType));
|
|
|
|
// This cludge is needed to workaround GetCurrentReading wanting array_view<bool>, while
|
|
|
|
// using std::vector<bool> would create a bit-packed array, which isn't wanted. So, we keep
|
|
|
|
// vector<u8> and view it as array<bool>.
|
|
|
|
auto buttons =
|
|
|
|
winrt::array_view<bool>(reinterpret_cast<winrt::array_view<bool>::pointer>(&m_buttons[0]),
|
|
|
|
static_cast<winrt::array_view<bool>::size_type>(m_buttons.size()));
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
m_raw_controller.GetCurrentReading(buttons, m_switches, m_axes);
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error error)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE,
|
2023-06-12 02:51:49 +00:00
|
|
|
"WGInput: IRawGameController::GetCurrentReading failed: {}", error.code());
|
2022-08-27 09:11:27 +00:00
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
|
|
|
|
// IGamepad:
|
2022-05-19 18:33:29 +00:00
|
|
|
if (m_gamepad)
|
2022-08-27 09:11:27 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
m_gamepad_reading = m_gamepad.GetCurrentReading();
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error error)
|
|
|
|
{
|
2023-06-12 02:51:49 +00:00
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: IGamepad::GetCurrentReading failed: {}",
|
2022-08-27 09:11:27 +00:00
|
|
|
error.code());
|
|
|
|
}
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
|
|
|
|
// IGameControllerBatteryInfo:
|
2022-08-08 09:17:48 +00:00
|
|
|
if (!UpdateBatteryLevel())
|
2022-05-19 18:33:29 +00:00
|
|
|
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "WGInput: UpdateBatteryLevel failed.");
|
2023-05-24 19:58:30 +00:00
|
|
|
|
|
|
|
return Core::DeviceRemoval::Keep;
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
void UpdateMotors()
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
m_gamepad.Vibration(m_state_out);
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
// Ignore
|
|
|
|
}
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
|
|
|
|
bool UpdateBatteryLevel()
|
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
2022-09-07 19:06:26 +00:00
|
|
|
// Workaround for Steam. If Steam's GameOverlayRenderer64.dll is loaded, battery_info is null.
|
|
|
|
auto battery_info = m_raw_controller.try_as<WGI::IGameControllerBatteryInfo>();
|
|
|
|
if (!battery_info)
|
|
|
|
return false;
|
2022-08-27 09:11:27 +00:00
|
|
|
const winrt::Windows::Devices::Power::BatteryReport report =
|
2022-09-07 19:06:26 +00:00
|
|
|
battery_info.TryGetBatteryReport();
|
2022-08-27 09:11:27 +00:00
|
|
|
if (!report)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// TryGetBatteryReport causes a memleak of 0x58 bytes each time it is called, up to at
|
|
|
|
// least Windows 21H2. A hack to fix the memleak is recorded here for posterity.
|
|
|
|
// auto report_ = *(uintptr_t***)&report;
|
|
|
|
// auto rc = &report_[0x40 / 8][0x30 / 8];
|
|
|
|
// if (*rc == 2)
|
|
|
|
// (*rc)--;
|
|
|
|
|
|
|
|
using winrt::Windows::System::Power::BatteryStatus;
|
|
|
|
const BatteryStatus status = report.Status();
|
|
|
|
switch (status)
|
|
|
|
{
|
|
|
|
case BatteryStatus::NotPresent:
|
|
|
|
m_battery_level = 0;
|
|
|
|
return true;
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
case BatteryStatus::Idle:
|
|
|
|
case BatteryStatus::Charging:
|
|
|
|
m_battery_level = BATTERY_INPUT_MAX_VALUE;
|
|
|
|
return true;
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
const int32_t full_value = report.FullChargeCapacityInMilliwattHours().GetInt32();
|
|
|
|
const int32_t remaining_value = report.RemainingCapacityInMilliwattHours().GetInt32();
|
|
|
|
m_battery_level = BATTERY_INPUT_MAX_VALUE * remaining_value / full_value;
|
2020-10-20 16:30:15 +00:00
|
|
|
return true;
|
|
|
|
}
|
2022-08-27 09:11:27 +00:00
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const std::string m_name;
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
const WGI::RawGameController m_raw_controller;
|
2020-10-20 16:30:15 +00:00
|
|
|
std::vector<ButtonValueType> m_buttons;
|
|
|
|
std::vector<WGI::GameControllerSwitchPosition> m_switches;
|
|
|
|
std::vector<double> m_axes;
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
WGI::Gamepad m_gamepad = nullptr;
|
2020-10-20 16:30:15 +00:00
|
|
|
WGI::GamepadReading m_gamepad_reading{};
|
|
|
|
WGI::GamepadVibration m_state_out{};
|
|
|
|
|
|
|
|
ControlState m_battery_level = 0;
|
|
|
|
};
|
|
|
|
|
2024-01-17 11:39:45 +00:00
|
|
|
enum class AddRemoveEventType
|
|
|
|
{
|
|
|
|
AddOrReplace,
|
|
|
|
Remove,
|
|
|
|
};
|
|
|
|
|
|
|
|
struct AddRemoveEvent
|
|
|
|
{
|
|
|
|
AddRemoveEventType type;
|
|
|
|
WGI::RawGameController raw_game_controller;
|
|
|
|
};
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
static thread_local bool s_initialized_winrt;
|
|
|
|
static winrt::event_token s_event_added, s_event_removed;
|
2024-01-17 11:39:45 +00:00
|
|
|
static Common::WorkQueueThread<AddRemoveEvent> s_device_add_remove_queue;
|
2022-08-08 09:17:48 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
static bool COMIsInitialized()
|
|
|
|
{
|
|
|
|
APTTYPE apt_type{};
|
|
|
|
APTTYPEQUALIFIER apt_qualifier{};
|
|
|
|
return CoGetApartmentType(&apt_type, &apt_qualifier) == S_OK;
|
|
|
|
}
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
static void AddDevice(const WGI::RawGameController& raw_game_controller)
|
2022-04-07 04:34:39 +00:00
|
|
|
{
|
2022-08-08 09:17:48 +00:00
|
|
|
// Get user-facing device name if available. Otherwise generate a name from vid/pid.
|
2022-08-27 09:11:27 +00:00
|
|
|
std::string device_name;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
device_name = StripWhitespace(WStringToUTF8(raw_game_controller.DisplayName().c_str()));
|
|
|
|
if (device_name.empty())
|
|
|
|
{
|
|
|
|
const u16 vid = raw_game_controller.HardwareVendorId();
|
|
|
|
const u16 pid = raw_game_controller.HardwareProductId();
|
|
|
|
device_name = fmt::format("Device_{:04x}:{:04x}", vid, pid);
|
|
|
|
INFO_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Empty device name, using {}", device_name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
2022-04-07 04:34:39 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
device_name = "Device";
|
2022-08-08 09:17:48 +00:00
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to get device name, using {}", device_name);
|
2022-04-07 04:34:39 +00:00
|
|
|
}
|
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
WGI::Gamepad gamepad = WGI::Gamepad::FromGameController(raw_game_controller);
|
|
|
|
// Note that gamepad may be nullptr here. The Device class will deal with this.
|
|
|
|
auto dev = std::make_shared<Device>(std::move(device_name), raw_game_controller, gamepad);
|
2022-04-07 04:34:39 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
// Only add if it has some inputs/outputs.
|
|
|
|
if (dev->Inputs().size() || dev->Outputs().size())
|
|
|
|
g_controller_interface.AddDevice(std::move(dev));
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to add device {}", device_name);
|
|
|
|
}
|
2022-08-08 09:17:48 +00:00
|
|
|
}
|
2022-04-07 04:34:39 +00:00
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
static void RemoveDevice(const WGI::RawGameController& raw_game_controller)
|
|
|
|
{
|
|
|
|
g_controller_interface.RemoveDevice([&](const auto* dev) {
|
|
|
|
if (dev->GetSource() != SOURCE_NAME)
|
|
|
|
return false;
|
|
|
|
return static_cast<const Device*>(dev)->GetRawGameController() == raw_game_controller;
|
|
|
|
});
|
2022-04-07 04:34:39 +00:00
|
|
|
}
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
#pragma warning(push)
|
|
|
|
// 'winrt::impl::implements_delegate<winrt::Windows::Foundation::EventHandler<winrt::Windows::Gaming::Input::RawGameController>,H>':
|
|
|
|
// class has virtual functions, but its non-trivial destructor is not virtual; instances of this
|
|
|
|
// class may not be destructed correctly
|
|
|
|
// (H is the lambda)
|
|
|
|
#pragma warning(disable : 4265)
|
|
|
|
|
2024-01-17 11:39:45 +00:00
|
|
|
static void HandleAddRemoveEvent(AddRemoveEvent evt)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
winrt::init_apartment();
|
|
|
|
}
|
|
|
|
catch (const winrt::hresult_error& ex)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE,
|
|
|
|
"WGInput: Failed to CoInitialize for add/remove controller event: {}",
|
|
|
|
WStringToUTF8(ex.message()));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Common::ScopeGuard coinit_guard([] { winrt::uninit_apartment(); });
|
|
|
|
|
|
|
|
switch (evt.type)
|
|
|
|
{
|
|
|
|
case AddRemoveEventType::AddOrReplace:
|
|
|
|
RemoveDevice(evt.raw_game_controller);
|
|
|
|
AddDevice(evt.raw_game_controller);
|
|
|
|
break;
|
|
|
|
case AddRemoveEventType::Remove:
|
|
|
|
RemoveDevice(evt.raw_game_controller);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Invalid add/remove controller event: {}",
|
|
|
|
std::to_underlying(evt.type));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-20 16:30:15 +00:00
|
|
|
void Init()
|
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
if (!COMIsInitialized())
|
|
|
|
{
|
|
|
|
// NOTE: Devices in g_controller_interface should only be accessed by threads that have had
|
|
|
|
// winrt (com) initialized.
|
|
|
|
winrt::init_apartment();
|
|
|
|
s_initialized_winrt = true;
|
|
|
|
}
|
|
|
|
|
2024-01-17 11:39:45 +00:00
|
|
|
s_device_add_remove_queue.Reset("WGInput Add/Remove Device Thread", HandleAddRemoveEvent);
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
try
|
2022-04-07 04:34:39 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
// These events will be invoked from WGI-managed threadpool.
|
|
|
|
s_event_added = WGI::RawGameController::RawGameControllerAdded(
|
2024-01-17 11:39:45 +00:00
|
|
|
[](auto&&, WGI::RawGameController raw_game_controller) {
|
|
|
|
s_device_add_remove_queue.EmplaceItem(
|
|
|
|
AddRemoveEvent{AddRemoveEventType::AddOrReplace, std::move(raw_game_controller)});
|
2022-08-27 09:11:27 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
s_event_removed = WGI::RawGameController::RawGameControllerRemoved(
|
2024-01-17 11:39:45 +00:00
|
|
|
[](auto&&, WGI::RawGameController raw_game_controller) {
|
|
|
|
s_device_add_remove_queue.EmplaceItem(
|
|
|
|
AddRemoveEvent{AddRemoveEventType::Remove, std::move(raw_game_controller)});
|
2022-08-27 09:11:27 +00:00
|
|
|
});
|
2022-04-07 04:34:39 +00:00
|
|
|
}
|
2022-08-27 09:11:27 +00:00
|
|
|
catch (winrt::hresult_error)
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to register event handlers");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
#pragma warning(pop)
|
|
|
|
|
2020-10-20 16:30:15 +00:00
|
|
|
void DeInit()
|
|
|
|
{
|
2024-01-17 11:39:45 +00:00
|
|
|
s_device_add_remove_queue.Shutdown();
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
WGI::RawGameController::RawGameControllerAdded(s_event_added);
|
|
|
|
WGI::RawGameController::RawGameControllerRemoved(s_event_removed);
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
if (s_initialized_winrt)
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-08 09:17:48 +00:00
|
|
|
winrt::uninit_apartment();
|
|
|
|
s_initialized_winrt = false;
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
2022-04-07 04:34:39 +00:00
|
|
|
}
|
|
|
|
|
2020-10-20 16:30:15 +00:00
|
|
|
void PopulateDevices()
|
|
|
|
{
|
|
|
|
// WGI Interfaces to potentially use:
|
|
|
|
// Gamepad: Buttons, 2x Sticks and 2x Triggers, 4x Vibration Motors
|
|
|
|
// RawGameController: Buttons, Switches (Hats), Axes, Haptics
|
|
|
|
// The following are not implemented:
|
|
|
|
// ArcadeStick: Buttons (probably no need to specialize, literally just buttons)
|
|
|
|
// FlightStick: Buttons, HatSwitch, Pitch, Roll, Throttle, Yaw
|
|
|
|
// RacingWheel: Buttons, Clutch, Handbrake, PatternShifterGear, Throttle, Wheel, WheelMotor
|
|
|
|
// UINavigationController: Directions, Scrolling, etc.
|
|
|
|
|
2022-08-08 09:17:48 +00:00
|
|
|
// RawGameController available from Windows 15063.
|
|
|
|
// properties added in 1709: DisplayName, NonRoamableId, SimpleHapticsControllers
|
2020-10-20 16:30:15 +00:00
|
|
|
|
2022-08-27 09:11:27 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
for (const WGI::RawGameController& raw_game_controller :
|
|
|
|
WGI::RawGameController::RawGameControllers())
|
|
|
|
{
|
|
|
|
RemoveDevice(raw_game_controller);
|
|
|
|
AddDevice(raw_game_controller);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (winrt::hresult_error)
|
2020-10-20 16:30:15 +00:00
|
|
|
{
|
2022-08-27 09:11:27 +00:00
|
|
|
// Only reach here if RawGameControllers() failed
|
|
|
|
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: PopulateDevices failed");
|
2020-10-20 16:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace ciface::WGInput
|