mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-25 00:04:49 +02:00
531 lines
14 KiB
C++
531 lines
14 KiB
C++
// This file in particular along with its header is public domain, use it for whatever you want.
|
||
|
||
#include <windows.h>
|
||
#include <hidsdi.h>
|
||
#include <setupapi.h>
|
||
#include <initguid.h>
|
||
#include <vector>
|
||
|
||
#include "Windows/HidInputDevice.h"
|
||
#include "Common/TimeUtil.h"
|
||
#include "Common/Log.h"
|
||
#include "Common/Input/InputState.h"
|
||
#include "Common/Common.h"
|
||
#include "Common/System/NativeApp.h"
|
||
#include "Common/System/OSD.h"
|
||
|
||
enum PSButton : u32 {
|
||
PS_DPAD_UP = 1, // These dpad ones are not real, we convert from hat switch format.
|
||
PS_DPAD_DOWN = 2,
|
||
PS_DPAD_LEFT = 4,
|
||
PS_DPAD_RIGHT = 8,
|
||
PS_BTN_SQUARE = 16,
|
||
PS_BTN_CROSS = 32,
|
||
PS_BTN_TRIANGLE = 64,
|
||
PS_BTN_CIRCLE = 128,
|
||
|
||
PS_BTN_L1 = (1 << 8),
|
||
PS_BTN_R1 = (1 << 9),
|
||
PS_BTN_L2 = (1 << 10),
|
||
PS_BTN_R2 = (1 << 11),
|
||
PS_BTN_SHARE = (1 << 12),
|
||
PS_BTN_OPTIONS = (1 << 13),
|
||
PS_BTN_L3 = (1 << 14),
|
||
PS_BTN_R3 = (1 << 15),
|
||
PS_BTN_PS_BUTTON = (1 << 16),
|
||
PS_BTN_TOUCHPAD = (1 << 17),
|
||
};
|
||
|
||
struct PSInputMapping {
|
||
PSButton button;
|
||
InputKeyCode keyCode;
|
||
};
|
||
|
||
static const PSInputMapping g_psInputMappings[] = {
|
||
{PS_DPAD_UP, NKCODE_DPAD_UP},
|
||
{PS_DPAD_DOWN, NKCODE_DPAD_DOWN},
|
||
{PS_DPAD_LEFT, NKCODE_DPAD_LEFT},
|
||
{PS_DPAD_RIGHT, NKCODE_DPAD_RIGHT},
|
||
{PS_BTN_SQUARE, NKCODE_BUTTON_4},
|
||
{PS_BTN_TRIANGLE, NKCODE_BUTTON_3},
|
||
{PS_BTN_CIRCLE, NKCODE_BUTTON_1},
|
||
{PS_BTN_CROSS, NKCODE_BUTTON_2},
|
||
{PS_BTN_PS_BUTTON, NKCODE_HOME},
|
||
{PS_BTN_SHARE, NKCODE_BUTTON_9},
|
||
{PS_BTN_OPTIONS, NKCODE_BUTTON_10},
|
||
{PS_BTN_L1, NKCODE_BUTTON_7},
|
||
{PS_BTN_R1, NKCODE_BUTTON_8},
|
||
// {PS_BTN_L2, NKCODE_BUTTON_L2}, // These are done by the analog triggers.
|
||
// {PS_BTN_R2, NKCODE_BUTTON_R2},
|
||
{PS_BTN_L3, NKCODE_BUTTON_THUMBL},
|
||
{PS_BTN_R3, NKCODE_BUTTON_THUMBR},
|
||
};
|
||
|
||
enum PSStickAxis : u32 {
|
||
PS_STICK_LX = 0,
|
||
PS_STICK_LY = 1,
|
||
PS_STICK_RX = 2,
|
||
PS_STICK_RY = 3,
|
||
};
|
||
|
||
struct PSStickMapping {
|
||
PSStickAxis stickAxis;
|
||
InputAxis inputAxis;
|
||
};
|
||
|
||
// This is the same mapping as DInput etc.
|
||
static const PSStickMapping g_psStickMappings[] = {
|
||
{PS_STICK_LX, JOYSTICK_AXIS_X},
|
||
{PS_STICK_LY, JOYSTICK_AXIS_Y},
|
||
{PS_STICK_RX, JOYSTICK_AXIS_Z},
|
||
{PS_STICK_RY, JOYSTICK_AXIS_RX},
|
||
};
|
||
|
||
enum PSTriggerAxis : u32 {
|
||
PS_TRIGGER_L2 = 0,
|
||
PS_TRIGGER_R2 = 1,
|
||
};
|
||
|
||
struct PSTriggerMapping {
|
||
PSTriggerAxis triggerAxis;
|
||
InputAxis inputAxis;
|
||
};
|
||
|
||
static const PSTriggerMapping g_psTriggerMappings[] = {
|
||
{PS_TRIGGER_L2, JOYSTICK_AXIS_LTRIGGER},
|
||
{PS_TRIGGER_R2, JOYSTICK_AXIS_RTRIGGER},
|
||
};
|
||
|
||
struct PSControllerInfo {
|
||
u16 vendorId;
|
||
u16 productId;
|
||
PSSubType type;
|
||
const char *name;
|
||
};
|
||
|
||
constexpr u16 SONY_VID = 0x054C;
|
||
|
||
constexpr u16 DS4_WIRELESS = 0x0BA0;
|
||
constexpr u16 PS_CLASSIC = 0x0CDA;
|
||
|
||
// We pick a few ones from here to support, let's add more later.
|
||
// https://github.com/ds4windowsapp/DS4Windows/blob/65609b470f53a4f832fb07ac24085d3e28ec15bd/DS4Windows/DS4Library/DS4Devices.cs#L126
|
||
static const PSControllerInfo g_psInfos[] = {
|
||
{SONY_VID, 0x05C4, PSSubType::DS4, "DS4 v.1"},
|
||
{SONY_VID, 0x09CC, PSSubType::DS4, "DS4 v.2"},
|
||
{SONY_VID, 0x0CE6, PSSubType::DS5, "DualSense"},
|
||
{SONY_VID, PS_CLASSIC, PSSubType::DS4, "PS Classic"},
|
||
// {PSSubType::DS4, DS4_WIRELESS},
|
||
// {PSSubType::DS5, DUALSENSE_WIRELESS},
|
||
// {PSSubType::DS5, DUALSENSE_EDGE_WIRELESS},
|
||
};
|
||
|
||
static bool IsSupportedGamepad(HANDLE handle, USHORT *pidOut, PSSubType *subType) {
|
||
HIDD_ATTRIBUTES attr{sizeof(HIDD_ATTRIBUTES)};
|
||
if (!HidD_GetAttributes(handle, &attr)) {
|
||
return false;
|
||
}
|
||
for (const auto &info : g_psInfos) {
|
||
if (attr.VendorID == info.vendorId && attr.ProductID == info.productId) {
|
||
*pidOut = attr.ProductID;
|
||
*subType = info.type;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
struct DualSenseOutputReport{
|
||
uint8_t reportId;
|
||
uint8_t flags1;
|
||
uint8_t flags2;
|
||
uint8_t headphone[4];
|
||
uint8_t muteLED;
|
||
uint8_t micMute;
|
||
};
|
||
|
||
// https://github.com/ds4windowsapp/DS4Windows/blob/65609b470f53a4f832fb07ac24085d3e28ec15bd/DS4Windows/DS4Library/InputDevices/DualSenseDevice.cs#L905
|
||
|
||
// Sends initialization packet to DualSense
|
||
static bool InitializeDualSense(HANDLE handle, int outReportSize) {
|
||
if (outReportSize != 48) {
|
||
return false;
|
||
}
|
||
|
||
// Output report (0x05) – sets lightbar, enables full report mode
|
||
uint8_t reportData[48] = {0};
|
||
|
||
DualSenseOutputReport report;
|
||
report.reportId = 2;
|
||
report.flags1 = 0x0C;
|
||
report.flags2 = 0x15;
|
||
report.muteLED = 1;
|
||
|
||
memcpy(reportData, &report, sizeof(report));
|
||
|
||
DWORD written;
|
||
bool result = WriteFile(handle, reportData, sizeof(reportData), &written, NULL);
|
||
if (!result) {
|
||
u32 errorCode = GetLastError();
|
||
|
||
if (errorCode == ERROR_INVALID_PARAMETER) {
|
||
if (!HidD_SetOutputReport(handle, reportData, outReportSize)) {
|
||
errorCode = GetLastError();
|
||
}
|
||
}
|
||
|
||
WARN_LOG(Log::UI, "Failed initializing: %08x", errorCode);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
enum class DS4FeatureBits : u8 {
|
||
VOL_L = 0x10,
|
||
VOL_R = 0x20,
|
||
MIC_VOL = 0x40,
|
||
SPEAKER_VOL = 0x80,
|
||
RUMBLE = 0x1,
|
||
LIGHTBAR = 0x2,
|
||
FLASH = 0x4,
|
||
};
|
||
ENUM_CLASS_BITOPS(DS4FeatureBits);
|
||
|
||
static bool InitializeDualShock(HANDLE handle, int outReportSize) {
|
||
if (outReportSize != 32) {
|
||
WARN_LOG(Log::UI, "DS4 unexpected report size %d", outReportSize);
|
||
return false;
|
||
}
|
||
|
||
// DS4 USB output report (report ID 0x05)
|
||
// Total size: 32 bytes (must match device buffer size)
|
||
uint8_t report[32] = {0};
|
||
|
||
report[0] = 0x05; // Report ID (DS4 output)
|
||
report[1] = (u8)(DS4FeatureBits::RUMBLE | DS4FeatureBits::LIGHTBAR | DS4FeatureBits::FLASH); // Flags: enable lightbar, rumble, etc.
|
||
report[2] = 0x02;
|
||
// Rumble
|
||
report[4] = 0x00; // Right (weak)
|
||
report[5] = 0x00; // Left (strong)
|
||
|
||
// Lightbar (RGB)
|
||
report[6] = 0x00; // Red
|
||
report[7] = 0x10; // Green
|
||
report[8] = 0x40; // Blue (dim blue)
|
||
|
||
DWORD written;
|
||
return WriteFile(handle, report, sizeof(report), &written, NULL);
|
||
}
|
||
|
||
HANDLE OpenFirstDualShockOrSense(PSSubType *subType, int *reportSize) {
|
||
GUID hidGuid;
|
||
HidD_GetHidGuid(&hidGuid);
|
||
|
||
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&hidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
||
if (deviceInfoSet == INVALID_HANDLE_VALUE)
|
||
return nullptr;
|
||
|
||
SP_DEVICE_INTERFACE_DATA interfaceData;
|
||
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||
|
||
for (DWORD i = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, nullptr, &hidGuid, i, &interfaceData); ++i) {
|
||
DWORD requiredSize = 0;
|
||
SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &interfaceData, nullptr, 0, &requiredSize, nullptr);
|
||
|
||
std::vector<BYTE> buffer(requiredSize);
|
||
auto* detailData = reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(buffer.data());
|
||
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
|
||
|
||
if (SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &interfaceData, detailData, requiredSize, nullptr, nullptr)) {
|
||
HANDLE handle = CreateFile(detailData->DevicePath, GENERIC_READ | GENERIC_WRITE,
|
||
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||
if (handle != INVALID_HANDLE_VALUE) {
|
||
USHORT pid;
|
||
if (IsSupportedGamepad(handle, &pid, subType)) {
|
||
INFO_LOG(Log::UI, "Found supported gamepad. PID: %04x", pid);
|
||
HIDP_CAPS caps;
|
||
PHIDP_PREPARSED_DATA preparsedData;
|
||
|
||
HidD_GetPreparsedData(handle, &preparsedData);
|
||
HidP_GetCaps(preparsedData, &caps);
|
||
HidD_FreePreparsedData(preparsedData);
|
||
|
||
*reportSize = caps.InputReportByteLength;
|
||
int outReportSize = caps.OutputReportByteLength;
|
||
|
||
INFO_LOG(Log::UI, "Initializing gamepad. out report size=%d", outReportSize);
|
||
bool result;
|
||
if (*subType == PSSubType::DS5) {
|
||
result = InitializeDualSense(handle, outReportSize);
|
||
} else {
|
||
result = InitializeDualShock(handle, outReportSize);
|
||
}
|
||
|
||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||
|
||
return handle;
|
||
} else {
|
||
DEBUG_LOG(Log::UI, "Skipping HID device: pid=%02x", pid);
|
||
}
|
||
CloseHandle(handle);
|
||
}
|
||
}
|
||
}
|
||
|
||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||
return nullptr;
|
||
}
|
||
|
||
void HidInputDevice::AddSupportedDevices(std::set<u32> *deviceVIDPIDs) {
|
||
for (const auto &info : g_psInfos) {
|
||
const u32 vidpid = MAKELONG(SONY_VID, info.productId);
|
||
deviceVIDPIDs->insert(vidpid);
|
||
}
|
||
}
|
||
|
||
static u32 DecodeHatSwitch(u8 dpad) {
|
||
u32 buttons = 0;
|
||
if (dpad == 0 || dpad == 1 || dpad == 7) {
|
||
buttons |= PS_DPAD_UP;
|
||
}
|
||
if (dpad == 1 || dpad == 2 || dpad == 3) {
|
||
buttons |= PS_DPAD_RIGHT;
|
||
}
|
||
if (dpad == 3 || dpad == 4 || dpad == 5) {
|
||
buttons |= PS_DPAD_DOWN;
|
||
}
|
||
if (dpad == 5 || dpad == 6 || dpad == 7) {
|
||
buttons |= PS_DPAD_LEFT;
|
||
}
|
||
return buttons;
|
||
}
|
||
|
||
struct DualShockInputReport {
|
||
u8 lx;
|
||
u8 ly;
|
||
u8 rx;
|
||
u8 ry;
|
||
u8 buttons[3]; // note, starts at 5 so not aligned
|
||
u8 l2_analog;
|
||
u8 r2_analog;
|
||
u8 pad[2];
|
||
u8 battery;
|
||
// Then there's motion and all kinds of stuff.
|
||
};
|
||
|
||
bool HidInputDevice::ReadDualShockInput(HANDLE handle, PSControllerState *state) {
|
||
BYTE inputReport[64]{}; // 64-byte input report for DS4
|
||
DWORD bytesRead = 0;
|
||
if (ReadFile(handle, inputReport, sizeof(inputReport), &bytesRead, nullptr)) {
|
||
DualShockInputReport report{};
|
||
static_assert(sizeof(report) < sizeof(inputReport));
|
||
if (bytesRead < 14) {
|
||
return false;
|
||
}
|
||
|
||
// OK, check the first byte to figure out what we're dealing with here.
|
||
int offset = 1;
|
||
int reportId;
|
||
if (inputReport[0] == 0xA1) {
|
||
// 2-byte bluetooth frame
|
||
offset = 2;
|
||
reportId = inputReport[1];
|
||
} else {
|
||
offset = 1;
|
||
reportId = inputReport[0];
|
||
}
|
||
// const bool isBluetooth = (reportId == 0x11 || reportId == 0x31);
|
||
|
||
memcpy(&report, inputReport + offset, sizeof(report));
|
||
|
||
// Center the sticks.
|
||
state->stickAxes[PS_STICK_LX] = report.lx - 128;
|
||
state->stickAxes[PS_STICK_LY] = report.ly - 128;
|
||
state->stickAxes[PS_STICK_RX] = report.rx - 128;
|
||
state->stickAxes[PS_STICK_RY] = report.ry - 128;
|
||
|
||
// Copy over the triggers.
|
||
state->triggerAxes[PS_TRIGGER_L2] = report.l2_analog;
|
||
state->triggerAxes[PS_TRIGGER_R2] = report.r2_analog;
|
||
|
||
u32 buttons{};
|
||
int frameCounter = report.buttons[2] >> 2;
|
||
report.buttons[2] &= 3;
|
||
memcpy(&buttons, &report.buttons[0], 3);
|
||
|
||
// Clear out and re-fill the DPAD, it works differently somehow
|
||
buttons &= ~0xF;
|
||
buttons |= DecodeHatSwitch(report.buttons[0] & 0xF);
|
||
|
||
state->buttons = buttons;
|
||
return true;
|
||
} else {
|
||
u32 error = GetLastError();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// So strange that this is different!
|
||
struct DualSenseInputReport {
|
||
u8 lx;
|
||
u8 ly;
|
||
u8 rx;
|
||
u8 ry;
|
||
|
||
u8 l2_analog;
|
||
u8 r2_analog;
|
||
|
||
u8 frameCounter;
|
||
|
||
u8 buttons[3];
|
||
|
||
// TODO: More stuff (battery, tilt, etc).
|
||
};
|
||
|
||
bool HidInputDevice::ReadDualSenseInput(HANDLE handle, PSControllerState *state) {
|
||
BYTE inputReport[64]{}; // 64-byte input report for DS4
|
||
DWORD bytesRead = 0;
|
||
if (ReadFile(handle, inputReport, sizeof(inputReport), &bytesRead, nullptr)) {
|
||
DualSenseInputReport report{};
|
||
static_assert(sizeof(report) < sizeof(inputReport));
|
||
if (bytesRead < 14) {
|
||
return false;
|
||
}
|
||
|
||
// OK, check the first byte to figure out what we're dealing with here.
|
||
int offset = 1;
|
||
if (inputReport[0] != 1) {
|
||
// Wrong data
|
||
return false;
|
||
}
|
||
// const bool isBluetooth = (reportId == 0x11 || reportId == 0x31);
|
||
|
||
memcpy(&report, inputReport + offset, sizeof(report));
|
||
|
||
// Center the sticks.
|
||
state->stickAxes[PS_STICK_LX] = report.lx - 128;
|
||
state->stickAxes[PS_STICK_LY] = report.ly - 128;
|
||
state->stickAxes[PS_STICK_RX] = report.rx - 128;
|
||
state->stickAxes[PS_STICK_RY] = report.ry - 128;
|
||
|
||
// Copy over the triggers.
|
||
state->triggerAxes[PS_TRIGGER_L2] = report.l2_analog;
|
||
state->triggerAxes[PS_TRIGGER_R2] = report.r2_analog;
|
||
|
||
u32 buttons{};
|
||
report.buttons[2] &= 3; // Remove noise
|
||
memcpy(&buttons, &report.buttons[0], 3);
|
||
|
||
// Clear out and re-fill the DPAD, it works differently somehow
|
||
buttons &= ~0xF;
|
||
buttons |= DecodeHatSwitch(report.buttons[0] & 0xF);
|
||
|
||
state->buttons = buttons;
|
||
return true;
|
||
} else {
|
||
u32 error = GetLastError();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void HidInputDevice::Init() {}
|
||
void HidInputDevice::Shutdown() {
|
||
if (controller_) {
|
||
CloseHandle(controller_);
|
||
controller_ = nullptr;
|
||
}
|
||
}
|
||
|
||
void HidInputDevice::ReleaseAllKeys() {
|
||
for (const auto &mapping : g_psInputMappings) {
|
||
KeyInput key;
|
||
key.deviceId = DEVICE_ID_XINPUT_0 + pad_;
|
||
key.flags = KEY_UP;
|
||
key.keyCode = mapping.keyCode;
|
||
NativeKey(key);
|
||
}
|
||
}
|
||
|
||
InputDeviceID HidInputDevice::DeviceID(int pad) {
|
||
return DEVICE_ID_PAD_0 + pad;
|
||
}
|
||
|
||
int HidInputDevice::UpdateState() {
|
||
if (!controller_) {
|
||
// Poll for controllers from time to time.
|
||
if (pollCount_ == 0) {
|
||
pollCount_ = POLL_FREQ;
|
||
HANDLE newController = OpenFirstDualShockOrSense(&subType_, &reportSize_);
|
||
if (newController) {
|
||
controller_ = newController;
|
||
}
|
||
} else {
|
||
pollCount_--;
|
||
}
|
||
}
|
||
|
||
if (controller_) {
|
||
PSControllerState state{};
|
||
bool result;
|
||
if (subType_ == PSSubType::DS4) {
|
||
result = ReadDualShockInput(controller_, &state);
|
||
} else if (subType_ == PSSubType::DS5) {
|
||
result = ReadDualSenseInput(controller_, &state);
|
||
}
|
||
|
||
if (result) {
|
||
const InputDeviceID deviceID = DeviceID(pad_);
|
||
// Process the input and generate input events.
|
||
const u32 downMask = state.buttons & (~prevState_.buttons);
|
||
const u32 upMask = (~state.buttons) & prevState_.buttons;
|
||
|
||
for (const auto &mapping : g_psInputMappings) {
|
||
if (downMask & mapping.button) {
|
||
KeyInput key;
|
||
key.deviceId = deviceID;
|
||
key.flags = KEY_DOWN;
|
||
key.keyCode = mapping.keyCode;
|
||
NativeKey(key);
|
||
}
|
||
if (upMask & mapping.button) {
|
||
KeyInput key;
|
||
key.deviceId = deviceID;
|
||
key.flags = KEY_UP;
|
||
key.keyCode = mapping.keyCode;
|
||
NativeKey(key);
|
||
}
|
||
}
|
||
|
||
for (const auto &mapping : g_psStickMappings) {
|
||
if (state.stickAxes[mapping.stickAxis] != prevState_.stickAxes[mapping.stickAxis]) {
|
||
AxisInput axis;
|
||
axis.deviceId = deviceID;
|
||
axis.axisId = mapping.inputAxis;
|
||
axis.value = (float)state.stickAxes[mapping.stickAxis] * (1.0f / 128.0f);
|
||
NativeAxis(&axis, 1);
|
||
}
|
||
}
|
||
|
||
for (const auto &mapping : g_psTriggerMappings) {
|
||
if (state.triggerAxes[mapping.triggerAxis] != prevState_.triggerAxes[mapping.triggerAxis]) {
|
||
AxisInput axis;
|
||
axis.deviceId = deviceID;
|
||
axis.axisId = mapping.inputAxis;
|
||
axis.value = (float)state.triggerAxes[mapping.triggerAxis] * (1.0f / 255.0f);
|
||
NativeAxis(&axis, 1);
|
||
}
|
||
}
|
||
|
||
prevState_ = state;
|
||
return UPDATESTATE_NO_SLEEP; // The ReadFile sleeps for us, effectively.
|
||
} else {
|
||
// might have been disconnected. retry later.
|
||
ReleaseAllKeys();
|
||
CloseHandle(controller_);
|
||
controller_ = NULL;
|
||
pollCount_ = POLL_FREQ;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|