mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
Make DualSense over Bluetooth work on Windows
This commit is contained in:
+79
-75
@@ -1,33 +1,18 @@
|
||||
// Some info from
|
||||
// https://controllers.fandom.com/wiki/Sony_DualSense
|
||||
// https://github.com/ds4windowsapp/DS4Windows/blob/65609b470f53a4f832fb07ac24085d3e28ec15bd/DS4Windows/DS4Library/InputDevices/DualSenseDevice.cs#L905
|
||||
|
||||
// Bluetooth information
|
||||
// When connecting via bluetooth, outReportSize is 547. However, you can send a smaller 78-byte format instead,
|
||||
// and if you do, you'll get smaller reports back. They are organized more like the USB messages.
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "Windows/Hid/DualShock.h"
|
||||
#include "Windows/Hid/DualSense.h"
|
||||
#include "Windows/Hid/HidInputDevice.h"
|
||||
#include "Windows/Hid/HidCommon.h"
|
||||
|
||||
#pragma pack(push,1)
|
||||
struct DualSenseOutputReport {
|
||||
u8 reportId;
|
||||
u8 flags1;
|
||||
u8 flags2;
|
||||
u8 rumbleRight;
|
||||
u8 rumbleLeft;
|
||||
u8 pad[2];
|
||||
u8 muteLED;
|
||||
u8 micMute; // 10
|
||||
u8 other[32];
|
||||
u8 enableBrightness;
|
||||
u8 fade;
|
||||
u8 brightness;
|
||||
u8 playerLights;
|
||||
u8 lightbarRed;
|
||||
u8 lightbarGreen;
|
||||
u8 lightbarBlue;
|
||||
};
|
||||
static_assert(sizeof(DualSenseOutputReport) == 48);
|
||||
|
||||
struct DualSenseInputReportUSB {
|
||||
u8 reportId; // Must be 1
|
||||
|
||||
@@ -78,9 +63,29 @@ struct DualSenseInputReportBT {
|
||||
|
||||
// There's more stuff after here.
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// https://github.com/ds4windowsapp/DS4Windows/blob/65609b470f53a4f832fb07ac24085d3e28ec15bd/DS4Windows/DS4Library/InputDevices/DualSenseDevice.cs#L905
|
||||
#pragma pack(push,1)
|
||||
struct DualSenseOutputReport {
|
||||
u8 reportId;
|
||||
u8 flags1;
|
||||
u8 flags2;
|
||||
u8 rumbleRight;
|
||||
u8 rumbleLeft;
|
||||
u8 pad[2];
|
||||
u8 muteLED;
|
||||
u8 micMute; // 10
|
||||
u8 other[32];
|
||||
u8 enableLightbar;
|
||||
u8 fade;
|
||||
u8 brightness;
|
||||
u8 playerLights;
|
||||
u8 lightbarRed;
|
||||
u8 lightbarGreen;
|
||||
u8 lightbarBlue;
|
||||
};
|
||||
static_assert(sizeof(DualSenseOutputReport) == 48);
|
||||
|
||||
static void FillDualSenseOutputReport(DualSenseOutputReport *report, bool lightsOn) {
|
||||
report->reportId = 2;
|
||||
@@ -88,8 +93,8 @@ static void FillDualSenseOutputReport(DualSenseOutputReport *report, bool lights
|
||||
report->flags2 = 0x15;
|
||||
report->muteLED = 1;
|
||||
// Turn on the lights.
|
||||
report->playerLights = 1;
|
||||
report->enableBrightness = 1;
|
||||
report->playerLights = lightsOn ? 1 : 0;
|
||||
report->enableLightbar = 1;
|
||||
report->brightness = lightsOn ? 0 : 2; // 0 = high, 1 = medium, 2 = low
|
||||
report->lightbarRed = lightsOn ? LED_R : 0;
|
||||
report->lightbarGreen = lightsOn ? LED_G : 0;
|
||||
@@ -101,8 +106,10 @@ static bool SendReport(HANDLE handle, const DualSenseOutputReport &report, int o
|
||||
// USB case: Just write the plain struct.
|
||||
return WriteReport(handle, report);
|
||||
} else if (outReportSize >= 547) {
|
||||
// BT case. Not as fun!
|
||||
std::vector<uint8_t> buffer(outReportSize, 0);
|
||||
_dbg_assert_(outReportSize == 547);
|
||||
// BT case. Not as fun! NOTE: We use the small size method.
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.resize(78);
|
||||
|
||||
// 1. Report ID 0x31 is required for Extended Features (Rumble/LED) over BT
|
||||
buffer[0] = 0x31;
|
||||
@@ -112,19 +119,14 @@ static bool SendReport(HANDLE handle, const DualSenseOutputReport &report, int o
|
||||
// We skip report.reportId because buffer[0] is already 0x31
|
||||
// Copy everything after reportId into buffer starting at index 2
|
||||
memcpy(&buffer[2], (uint8_t*)&report + 1, sizeof(DualSenseOutputReport) - 1);
|
||||
buffer[3] = 0x15;
|
||||
|
||||
// 4. Calculate CRC32
|
||||
// The DualSense expects a CRC of the Report ID (0x31) + the entire data payload.
|
||||
// For a 547 byte report, the CRC is placed at index 543 (the last 4 bytes).
|
||||
uint32_t crc = ComputePSControllerCRC(buffer.data(), 543);
|
||||
// Calculate CRC over the first 74 bytes (0 to 73)
|
||||
// This function includes the 0xA2 "hidden" seed internally
|
||||
uint32_t crc = ComputeDualSenseBTCRC(buffer.data(), 74);
|
||||
|
||||
// Append CRC in Little Endian
|
||||
// memcpy(buffer.data() + 543, &crc, 4);
|
||||
buffer[543] = (uint8_t)(crc & 0xFF);
|
||||
buffer[544] = (uint8_t)((crc >> 8) & 0xFF);
|
||||
buffer[545] = (uint8_t)((crc >> 16) & 0xFF);
|
||||
buffer[546] = (uint8_t)((crc >> 24) & 0xFF);
|
||||
|
||||
memcpy(buffer.data() + 74, &crc, 4);
|
||||
return WriteReport(handle, buffer.data(), buffer.size());
|
||||
} else {
|
||||
ERROR_LOG(Log::System, "SendReport: Unexpected outReportSize: %d", outReportSize);
|
||||
@@ -148,6 +150,29 @@ bool ShutdownDualsense(HANDLE handle, int outReportSize) {
|
||||
return SendReport(handle, report, outReportSize);
|
||||
}
|
||||
|
||||
// Templated to handle different DualSense struct layouts.
|
||||
template<class T>
|
||||
static void ReadReport(HIDControllerState* state, u32 *buttons, const T& report) {
|
||||
// Center the sticks.
|
||||
state->stickAxes[HID_STICK_LX] = report.lx - 128;
|
||||
state->stickAxes[HID_STICK_LY] = report.ly - 128;
|
||||
state->stickAxes[HID_STICK_RX] = report.rx - 128;
|
||||
state->stickAxes[HID_STICK_RY] = report.ry - 128;
|
||||
|
||||
// Copy over the triggers.
|
||||
state->triggerAxes[HID_TRIGGER_L2] = report.l2_analog;
|
||||
state->triggerAxes[HID_TRIGGER_R2] = report.r2_analog;
|
||||
|
||||
const float accelScale = (1.0f / 8192.0f) * 9.81f;
|
||||
// We need to remap the axes a bit.
|
||||
state->accValid = true;
|
||||
state->accelerometer[0] = -report.accelerometer[2] * accelScale;
|
||||
state->accelerometer[1] = -report.accelerometer[0] * accelScale;
|
||||
state->accelerometer[2] = report.accelerometer[1] * accelScale;
|
||||
*buttons = 0;
|
||||
memcpy(buttons, &report.buttons[0], 3);
|
||||
}
|
||||
|
||||
bool ReadDualSenseInput(HANDLE handle, HIDControllerState *state, int inReportSize) {
|
||||
if (inReportSize > 1024) {
|
||||
return false;
|
||||
@@ -171,54 +196,33 @@ bool ReadDualSenseInput(HANDLE handle, HIDControllerState *state, int inReportSi
|
||||
// A valid USB packet.
|
||||
DualSenseInputReportUSB report;
|
||||
memcpy(&report, inputReport, sizeof(report));
|
||||
|
||||
// Center the sticks.
|
||||
state->stickAxes[HID_STICK_LX] = report.lx - 128;
|
||||
state->stickAxes[HID_STICK_LY] = report.ly - 128;
|
||||
state->stickAxes[HID_STICK_RX] = report.rx - 128;
|
||||
state->stickAxes[HID_STICK_RY] = report.ry - 128;
|
||||
|
||||
// Copy over the triggers.
|
||||
state->triggerAxes[HID_TRIGGER_L2] = report.l2_analog;
|
||||
state->triggerAxes[HID_TRIGGER_R2] = report.r2_analog;
|
||||
|
||||
const float accelScale = (1.0f / 8192.0f) * 9.81f;
|
||||
// We need to remap the axes a bit.
|
||||
state->accValid = true;
|
||||
state->accelerometer[0] = -report.accelerometer[2] * accelScale;
|
||||
state->accelerometer[1] = -report.accelerometer[0] * accelScale;
|
||||
state->accelerometer[2] = report.accelerometer[1] * accelScale;
|
||||
memcpy(&buttons, &report.buttons[0], 3);
|
||||
|
||||
ReadReport(state, &buttons, report);
|
||||
// Fall through to button processing
|
||||
} else if (inputReport[0] == 0x31 && inReportSize >= 547) {
|
||||
// A valid bluetooth packet. The layout is a bit different!
|
||||
// A valid bluetooth packet, large format. Probably we can delete this, see the note
|
||||
// at the top of thie file. The layout is a bit different!
|
||||
// A valid USB packet.
|
||||
DualSenseInputReportUSB report;
|
||||
DualSenseInputReportBT report;
|
||||
memcpy(&report, inputReport, sizeof(report));
|
||||
ReadReport(state, &buttons, report);
|
||||
|
||||
// Center the sticks.
|
||||
state->stickAxes[HID_STICK_LX] = report.lx - 128;
|
||||
state->stickAxes[HID_STICK_LY] = report.ly - 128;
|
||||
state->stickAxes[HID_STICK_RX] = report.rx - 128;
|
||||
state->stickAxes[HID_STICK_RY] = report.ry - 128;
|
||||
|
||||
// Copy over the triggers.
|
||||
state->triggerAxes[HID_TRIGGER_L2] = report.l2_analog;
|
||||
state->triggerAxes[HID_TRIGGER_R2] = report.r2_analog;
|
||||
|
||||
const float accelScale = (1.0f / 8192.0f) * 9.81f;
|
||||
// We need to remap the axes a bit.
|
||||
state->accValid = true;
|
||||
state->accelerometer[0] = -report.accelerometer[2] * accelScale;
|
||||
state->accelerometer[1] = -report.accelerometer[0] * accelScale;
|
||||
state->accelerometer[2] = report.accelerometer[1] * accelScale;
|
||||
memcpy(&buttons, &report.buttons[0], 3);
|
||||
// Fall through to button processing
|
||||
} else if (inputReport[0] == 0x31 && inReportSize == 78) {
|
||||
// Bluetooth packet, short and fast format.
|
||||
DualSenseInputReportUSB report;
|
||||
// Note: These bluetooth packets are offset from the USB packets by 1.
|
||||
memcpy(((uint8_t *)&report) + 1, inputReport + 2, sizeof(report) - 1);
|
||||
ReadReport(state, &buttons, report);
|
||||
|
||||
// Fall through to button processing
|
||||
} else if (inputReport[0] == 0x1 && inReportSize == 78) {
|
||||
// Simple BT packet. This shouldn't happen if we correctly initialize the gamepad.
|
||||
buttons = 0;
|
||||
// Fall through to button processing
|
||||
} else {
|
||||
// Unknown packet (simple BT?). Ignore.
|
||||
return false;
|
||||
WARN_LOG(Log::System, "Unexpected: %02x type, %d size", inputReport[0], (int)inReportSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shared button handling.
|
||||
|
||||
@@ -22,7 +22,7 @@ static const ButtonInputMapping g_psInputMappings[] = {
|
||||
{PS_BTN_R3, NKCODE_BUTTON_THUMBR},
|
||||
};
|
||||
|
||||
void GetPSInputMappings(const ButtonInputMapping **mappings, size_t *size) {
|
||||
void GetPSButtonInputMappings(const ButtonInputMapping **mappings, size_t *size) {
|
||||
*mappings = g_psInputMappings;
|
||||
*size = ARRAY_SIZE(g_psInputMappings);
|
||||
}
|
||||
@@ -166,15 +166,3 @@ bool ReadDualShockInput(HANDLE handle, HIDControllerState *state, int inReportSi
|
||||
state->buttons = buttons;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard CRC32 used by PlayStation controllers
|
||||
uint32_t ComputePSControllerCRC(uint8_t* data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320 & (-(int32_t)(crc & 1)));
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
@@ -48,5 +48,4 @@ inline u32 DecodePSHatSwitch(u8 dpad) {
|
||||
bool InitializeDualShock(HANDLE handle, int outReportSize);
|
||||
bool ShutdownDualShock(HANDLE handle, int outReportSize);
|
||||
bool ReadDualShockInput(HANDLE handle, HIDControllerState *state, int inReportSize);
|
||||
void GetPSInputMappings(const ButtonInputMapping **mappings, size_t *size);
|
||||
uint32_t ComputePSControllerCRC(uint8_t* data, size_t len);
|
||||
void GetPSButtonInputMappings(const ButtonInputMapping **mappings, size_t *size);
|
||||
|
||||
@@ -25,3 +25,28 @@ template<class T>
|
||||
inline bool WriteReport(HANDLE handle, const T &report) {
|
||||
return WriteReport(handle, (const u8 *)&report, sizeof(T));
|
||||
}
|
||||
|
||||
// Standard CRC32 Update function. Used for DualSense bluetooth messages.
|
||||
constexpr inline uint32_t UpdateCRC32(uint32_t crc, const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320 & (-(int32_t)(crc & 1)));
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// The specific "Sony Bluetooth" CRC32
|
||||
constexpr inline uint32_t ComputeDualSenseBTCRC(const uint8_t* buffer, size_t len) {
|
||||
constexpr uint8_t btHeader = 0xA2;
|
||||
|
||||
// Step 1: Initialize with 0xFFFFFFFF and process the "Hidden" BT header
|
||||
constexpr uint32_t headerCRC = UpdateCRC32(0xFFFFFFFF, &btHeader, 1);
|
||||
|
||||
// Step 2: Continue the calculation with the actual Report ID and Data
|
||||
const uint32_t crc = UpdateCRC32(headerCRC, buffer, len);
|
||||
|
||||
// Step 3: Final XOR (the ~ in C#)
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ 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 HIDControllerInfo g_psInfos[] = {
|
||||
{SONY_VID, 0x05C4, HIDControllerType::DS4, "DS4 v.1"},
|
||||
{SONY_VID, 0x09CC, HIDControllerType::DS4, "DS4 v.2"},
|
||||
{SONY_VID, 0x0CE6, HIDControllerType::DS5, "DualSense"},
|
||||
{SONY_VID, PS_CLASSIC, HIDControllerType::DS4, "PS Classic"},
|
||||
{SONY_VID, 0x05C4, HIDControllerType::DualShock, "DS4 v.1"},
|
||||
{SONY_VID, 0x09CC, HIDControllerType::DualShock, "DS4 v.2"},
|
||||
{SONY_VID, 0x0CE6, HIDControllerType::DualSense, "DualSense"},
|
||||
{SONY_VID, PS_CLASSIC, HIDControllerType::DualShock, "PS Classic"},
|
||||
{NINTENDO_VID, SWITCH_PRO_PID, HIDControllerType::SwitchPro, "Switch Pro"},
|
||||
// {PSSubType::DS4, DS4_WIRELESS},
|
||||
// {PSSubType::DS5, DUALSENSE_WIRELESS},
|
||||
@@ -124,10 +124,10 @@ static HANDLE OpenFirstHIDController(HIDControllerType *subType, int *reportSize
|
||||
INFO_LOG(Log::UI, "Initializing gamepad. out report size=%d", *outReportSize);
|
||||
bool result;
|
||||
switch (*subType) {
|
||||
case HIDControllerType::DS5:
|
||||
case HIDControllerType::DualSense:
|
||||
result = InitializeDualSense(handle, *outReportSize);
|
||||
break;
|
||||
case HIDControllerType::DS4:
|
||||
case HIDControllerType::DualShock:
|
||||
result = InitializeDualShock(handle, *outReportSize);
|
||||
break;
|
||||
case HIDControllerType::SwitchPro:
|
||||
@@ -162,10 +162,10 @@ void HidInputDevice::Init() {}
|
||||
void HidInputDevice::Shutdown() {
|
||||
if (controller_) {
|
||||
switch (subType_) {
|
||||
case HIDControllerType::DS4:
|
||||
case HIDControllerType::DualShock:
|
||||
ShutdownDualShock(controller_, outReportSize_);
|
||||
break;
|
||||
case HIDControllerType::DS5:
|
||||
case HIDControllerType::DualSense:
|
||||
ShutdownDualsense(controller_, outReportSize_);
|
||||
break;
|
||||
}
|
||||
@@ -232,15 +232,15 @@ int HidInputDevice::UpdateState() {
|
||||
bool result = false;
|
||||
const ButtonInputMapping *buttonMappings = nullptr;
|
||||
size_t buttonMappingsSize = 0;
|
||||
if (subType_ == HIDControllerType::DS4) {
|
||||
if (subType_ == HIDControllerType::DualShock) {
|
||||
result = ReadDualShockInput(controller_, &state, inReportSize_);
|
||||
GetPSInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
} else if (subType_ == HIDControllerType::DS5) {
|
||||
GetPSButtonInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
} else if (subType_ == HIDControllerType::DualSense) {
|
||||
result = ReadDualSenseInput(controller_, &state, inReportSize_);
|
||||
GetPSInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
GetPSButtonInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
} else if (subType_ == HIDControllerType::SwitchPro) {
|
||||
result = ReadSwitchProInput(controller_, &state);
|
||||
GetSwitchInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
GetSwitchButtonInputMappings(&buttonMappings, &buttonMappingsSize);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
@@ -293,6 +293,7 @@ int HidInputDevice::UpdateState() {
|
||||
prevState_ = state;
|
||||
return UPDATESTATE_NO_SLEEP; // The ReadFile sleeps for us, effectively.
|
||||
} else {
|
||||
WARN_LOG(Log::System, "Failed to read controller - assuming disconnected.");
|
||||
// might have been disconnected. retry later.
|
||||
KeyMap::NotifyPadDisconnected(deviceID);
|
||||
ReleaseAllKeys(buttonMappings, (int)buttonMappingsSize);
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
#include "Common/CommonWindows.h"
|
||||
|
||||
enum class HIDControllerType {
|
||||
DS4,
|
||||
DS5,
|
||||
DualShock,
|
||||
DualSense,
|
||||
SwitchPro,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ public:
|
||||
|
||||
static void AddSupportedDevices(std::set<u32> *deviceVIDPIDs);
|
||||
bool HasAccelerometer() const override {
|
||||
return subType_ == HIDControllerType::DS5;
|
||||
return subType_ == HIDControllerType::DualSense;
|
||||
}
|
||||
private:
|
||||
void ReleaseAllKeys(const ButtonInputMapping *buttonMappings, int count);
|
||||
|
||||
@@ -44,7 +44,7 @@ static const ButtonInputMapping g_switchProInputMappings[] = {
|
||||
{SWITCH_PRO_BTN_R3, NKCODE_BUTTON_THUMBR},
|
||||
};
|
||||
|
||||
void GetSwitchInputMappings(const ButtonInputMapping **mappings, size_t *size) {
|
||||
void GetSwitchButtonInputMappings(const ButtonInputMapping **mappings, size_t *size) {
|
||||
*mappings = g_switchProInputMappings;
|
||||
*size = ARRAY_SIZE(g_switchProInputMappings);
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
#include "Windows/Hid/HidCommon.h"
|
||||
|
||||
bool InitializeSwitchPro(HANDLE handle);
|
||||
void GetSwitchInputMappings(const ButtonInputMapping **mappings, size_t *size);
|
||||
void GetSwitchButtonInputMappings(const ButtonInputMapping **mappings, size_t *size);
|
||||
bool ReadSwitchProInput(HANDLE handle, HIDControllerState *state);
|
||||
|
||||
Reference in New Issue
Block a user