Last active
June 5, 2025 17:33
-
-
Save kylawl/470e86a0ac0c3da8f598948b1930db4e to your computer and use it in GitHub Desktop.
DS4U - Playstation DualShock 4 Windows Driver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Luminawesome Games Ltd. 2015 | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software | |
// and associated documentation files (the “Software”), to deal in the Software without | |
// restriction, including without limitation the rights to use, copy, modify, merge, publish, | |
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the | |
// Software is furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all copies or | |
// substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
// OTHER DEALINGS IN THE SOFTWARE. | |
#include <new> | |
#include <memory.h> | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <assert.h> | |
#include <thread> | |
#include <mutex> | |
#include <atomic> | |
#include "DS4U.hpp" | |
// http://eleccelerator.com/wiki/index.php?title=DualShock_4 | |
// http://www.psdevwiki.com/ps4/DualShock_4 | |
// https://gist.github.com/johndrinkwater/7708901 | |
// https://github.com/ehd/node-ds4 | |
// http://forums.pcsx2.net/Thread-DS4-To-XInput-Wrapper | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// Forward Decl // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
static void DS4UInitPlat(DS4UContext* ds4u); | |
static void DS4UClosePlat(DS4UContext* ds4u); | |
static void DS4WriteOutputStatePlat(uint64_t device_id, uint8_t* packet, size_t packet_length, bool is_bluetooth); | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// DS4UInternal // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
struct DS4UGamepadDevice | |
{ | |
bool mIsUSBDevice; | |
uint32_t mPacketID; | |
DS4UGamepadState mGamepadState; | |
}; | |
struct DS4UOuputState | |
{ | |
bool mIsUSB; | |
uint64_t mDeviceID; | |
uint8_t mRed; | |
uint8_t mGreen; | |
uint8_t mBlue; | |
uint8_t mLeftMotor; | |
uint8_t mRightMotor; | |
}; | |
struct DS4UContext | |
{ | |
DS4UAllocFunc mAlloc; | |
DS4UFreeFunc mFree; | |
DS4UInputEventHandlerFunc mInputEventHandler; | |
void* mInputEventContext; | |
uint64_t* mDeviceIDs; | |
DS4UGamepadDevice* mDevices; | |
DS4UOuputState* mOutputStates[2]; | |
int32_t mOutputStatesIdx; | |
int32_t mMaxGamepadCount; | |
void* mHIDInterfaceHandle; | |
std::atomic<int32_t> mRunning; | |
std::thread mOutputReportThread; | |
std::mutex mOutputStatesMutex; | |
DS4UContext(); | |
~DS4UContext(); | |
int32_t GetGamepadIndex (uint64_t device_id); | |
int32_t RegisterGamepad (uint64_t device_id); | |
void UnregisterGamepad (uint64_t device_id); | |
uint64_t GetDeviceID (int32_t gamepad_index); | |
void DecodeInputReport (uint8_t* report, uint64_t device_id, bool usb_report); | |
bool IsGamepadConnected (int32_t gamepad_index); | |
void StartOutputReportThread (); | |
void StopOutputReportThread (); | |
static void DecodeTouch (DS4UTouchState* dest, uint8_t* buffer); | |
static void SetButton (DS4UGamepadState* state, DS4UButton button, bool set); | |
static void SendOutputReport (DS4UContext* ds4u_internal); | |
}; | |
DS4UContext::DS4UContext() | |
{ | |
mAlloc = nullptr; | |
mFree = nullptr; | |
mInputEventHandler = nullptr; | |
mInputEventContext = nullptr; | |
mDevices = nullptr; | |
mOutputStates[0] = nullptr; | |
mOutputStates[1] = nullptr; | |
mMaxGamepadCount = 0; | |
mOutputStatesIdx = 0; | |
mHIDInterfaceHandle = nullptr; | |
} | |
DS4UContext::~DS4UContext() | |
{ | |
mFree(mOutputStates[0]); | |
mFree(mOutputStates[1]); | |
mFree(mDeviceIDs); | |
mFree(mDevices); | |
} | |
bool DS4UContext::IsGamepadConnected(int32_t gamepad_index) | |
{ | |
if( ((gamepad_index < 0) | (gamepad_index >= mMaxGamepadCount)) ) | |
return false; | |
return mDeviceIDs[gamepad_index] != 0; | |
} | |
void DS4UContext::StartOutputReportThread() | |
{ | |
mRunning = 1; | |
mOutputReportThread = std::thread(&DS4UContext::SendOutputReport, this); | |
} | |
void DS4UContext::StopOutputReportThread() | |
{ | |
mRunning--; | |
mOutputReportThread.join(); | |
} | |
uint64_t DS4UContext::GetDeviceID(int32_t gamepad_index) | |
{ | |
if( ((gamepad_index < 0) | (gamepad_index >= mMaxGamepadCount)) ) | |
return 0; | |
return mDeviceIDs[gamepad_index]; | |
} | |
int32_t DS4UContext::GetGamepadIndex(uint64_t device_id) | |
{ | |
for( int8_t i = 0; i < mMaxGamepadCount; ++i ) | |
{ | |
if( mDeviceIDs[i] == device_id ) | |
{ | |
return i; | |
} | |
} | |
return -1; | |
} | |
int32_t DS4UContext::RegisterGamepad(uint64_t device_id) | |
{ | |
// Validate we don't already have a gamepad with this id | |
int32_t gamepad_index = GetGamepadIndex(device_id); | |
assert(gamepad_index == -1); | |
gamepad_index = GetGamepadIndex(0); | |
if( gamepad_index > -1 ) | |
{ | |
printf("Registering device: %llu with index: %d\n", device_id, gamepad_index); | |
mDeviceIDs[gamepad_index] = device_id; | |
const uint32_t gamepad_colors[4] = | |
{ | |
DS4UGamepadColor_Gamepad0, | |
DS4UGamepadColor_Gamepad1, | |
DS4UGamepadColor_Gamepad2, | |
DS4UGamepadColor_Gamepad3 | |
}; | |
DS4USetGamepadState( this, gamepad_index, 0, 0, gamepad_colors[gamepad_index % 4] ); | |
if( mInputEventHandler != nullptr ) | |
{ | |
mInputEventHandler( gamepad_index, DS4UEventID_Connected, nullptr, mInputEventContext ); | |
} | |
} | |
else | |
{ | |
printf("Exceeded device limit\n"); | |
} | |
return gamepad_index; | |
} | |
void DS4UContext::UnregisterGamepad(uint64_t device_id) | |
{ | |
int32_t gamepad_index = GetGamepadIndex(device_id); | |
if( gamepad_index > -1 ) | |
{ | |
mDeviceIDs[gamepad_index] = 0; | |
if( mInputEventHandler != nullptr ) | |
{ | |
mInputEventHandler( gamepad_index, DS4UEventID_Disconnected, nullptr, mInputEventContext ); | |
} | |
printf("Unregistering device: %llu at index: %d\n", device_id, gamepad_index); | |
} | |
} | |
void DS4UContext::SetButton(DS4UGamepadState* state, DS4UButton button, bool set) | |
{ | |
if( set ) | |
{ | |
state->mButtons = state->mButtons | uint32_t(button); | |
} | |
else | |
{ | |
state->mButtons = state->mButtons & (~uint32_t(button)); | |
} | |
} | |
void DS4UContext::SendOutputReport(DS4UContext* ds4u_internal) | |
{ | |
while( ds4u_internal->mRunning > 0 ) | |
{ | |
int32_t read_index = 0; | |
{ | |
std::lock_guard<std::mutex> output_guard(ds4u_internal->mOutputStatesMutex); | |
read_index = ds4u_internal->mOutputStatesIdx; | |
ds4u_internal->mOutputStatesIdx = (read_index + 1) % 2; | |
} | |
DS4UOuputState* output_states = ds4u_internal->mOutputStates[ds4u_internal->mOutputStatesIdx]; | |
for( int32_t i = 0; i < ds4u_internal->mMaxGamepadCount; ++i ) | |
{ | |
if( output_states[i].mDeviceID == 0 ) | |
continue; | |
DS4UOuputState* output_state = &output_states[i]; | |
if( output_state->mIsUSB ) | |
{ | |
uint8_t packet[32] ={ 0 }; | |
packet[0] = 0x05; | |
packet[1] = 0xff; | |
packet[4] = output_state->mRightMotor; // right_motor_strength | |
packet[5] = output_state->mLeftMotor; // left_motor_strength | |
packet[6] = output_state->mRed; // led_red_level | |
packet[7] = output_state->mGreen; // led_green_level | |
packet[8] = output_state->mBlue; // led_blue_level | |
packet[9] = 255; // Flash Duration On | |
packet[10] = 0; // Flash Duration Off | |
DS4WriteOutputStatePlat(output_state->mDeviceID, packet, sizeof(packet), true); | |
} | |
else | |
{ | |
uint8_t packet[78] ={ 0 }; | |
packet[0] = 0x11; | |
packet[1] = 0x80; | |
packet[3] = 0xff; // 0xf0 disables the rumble motors, 0xf3 enables them | |
packet[6] = output_state->mRightMotor; // right_motor_strength | |
packet[7] = output_state->mLeftMotor; // left_motor_strength | |
packet[8] = output_state->mRed; // led_red_level | |
packet[9] = output_state->mGreen; // led_green_level | |
packet[10] = output_state->mBlue; // led_blue_level | |
packet[11] = 255; // Flash Duration On | |
packet[12] = 0; // Flash Duration Off | |
DS4WriteOutputStatePlat(output_state->mDeviceID, packet, sizeof(packet), false); | |
} | |
} | |
memset(output_states, 0, sizeof(DS4UOuputState) * ds4u_internal->mMaxGamepadCount); | |
std::this_thread::sleep_for( std::chrono::milliseconds(34) ); | |
} | |
} | |
void DS4UContext::DecodeTouch(DS4UTouchState* dest, uint8_t* buffer) | |
{ | |
dest->mId = ( buffer[0] & 0x7f); | |
dest->mActive = ( buffer[0] >> 7) == 0; | |
dest->mX = ((buffer[2] & 0x0f) << 8 ) | ( buffer[1] ); | |
dest->mY = ( buffer[3] << 4 ) | ((buffer[2] & 0xf0) >> 4); | |
} | |
void DS4UContext::DecodeInputReport(uint8_t* report, uint64_t device_id, bool is_usb_report) | |
{ | |
int32_t gamepad_index = GetGamepadIndex(device_id); | |
// There can be more gamepads plugged in that we care about | |
if( gamepad_index == -1 ) | |
return; | |
// | |
// Decode | |
// | |
// uint8_t controller_id = report[0]; | |
int8_t left_stick_x = report[1] - 128; | |
int8_t left_stick_y = report[2] - 128; | |
int8_t right_stick_x = report[3] - 128; | |
int8_t right_stick_y = report[4] - 128; | |
uint8_t dpad = report[5] & 15; | |
bool dpad_up = dpad == 0 || dpad == 1 || dpad == 7; | |
bool dpad_right = dpad == 1 || dpad == 2 || dpad == 3; | |
bool dpad_down = dpad == 3 || dpad == 4 || dpad == 5; | |
bool dpad_left = dpad == 5 || dpad == 6 || dpad == 7; | |
bool square = (report[5] & 16) != 0; | |
bool cross = (report[5] & 32) != 0; | |
bool circle = (report[5] & 64) != 0; | |
bool triangle = (report[5] & 128) != 0; | |
bool l1 = (report[6] & 0x01) != 0; | |
bool l2 = (report[6] & 0x04) != 0; | |
bool r1 = (report[6] & 0x02) != 0; | |
bool r2 = (report[6] & 0x08) != 0; | |
bool l3 = (report[6] & 0x40) != 0; | |
bool r3 = (report[6] & 0x80) != 0; | |
bool share = (report[6] & 0x10) != 0; | |
bool options = (report[6] & 0x20) != 0; | |
bool tpad_button = (report[7] & 2) != 0; | |
bool ps_button = (report[7] & 1) != 0; | |
// uint8_t event_id = (report[7] >> 2); // Incriments per report | |
uint8_t l2_analog = report[8]; | |
uint8_t r2_analog = report[9]; | |
// Seems to be a timestamp. A common increment value between two reports is 188 | |
// (at full rate the report period is 1.25ms). | |
// This timestamp is used by the PS4 to process acceleration and gyroscope data. | |
// uint8_t unknown10 = report[10]; // seems to count downwards, non-random pattern | |
// uint8_t unknown11 = report[11]; // seems to count upwards by 3, but by 2 when [10] underflows | |
uint8_t battery_level = report[12]; | |
// int16_t motionY = (*(int16_t*)(&report[13])); | |
// int16_t motionX = -(*(int16_t*)(&report[15])); | |
// int16_t motionZ = -(*(int16_t*)(&report[17])); | |
// int16_t orientationRoll = -(*(int16_t*)(&report[19])); | |
// int16_t orientationYaw = (*(int16_t*)(&report[21])); | |
// int16_t orientationPitch = (*(int16_t*)(&report[23])); | |
// Unknown (seems to be always 0x00) | |
// uint8_t unknown25 = report[25]; | |
// uint8_t unknown26 = report[26]; | |
// uint8_t unknown27 = report[27]; | |
// uint8_t unknown28 = report[28]; | |
// uint8_t unknown29 = report[29]; | |
// bool unknown_flag30 = (report[30] & 0xf0) != 0; | |
// bool phones_connected = (report[30] & 0x40) != 0; | |
// bool mic_connected = (report[30] & 0x20) != 0; | |
// bool usb_connected = (report[30] & 0x10) != 0; | |
// uint8_t battery_level2 = (report[30] & 0x0f) != 0; // Why are there 2 battery levels? | |
// uint8_t unknown31 = report[31]; // Unknown (seems to be always 0x00) | |
// uint8_t unknown32 = report[32]; // Unknown (seems to be always 0x00) | |
// uint8_t unknown33 = report[33]; // number of trackpad packets (0x00 to 0x04) | |
// uint8_t packet_counter0 = report[34]; // packet counter | |
uint32_t touch0 = *((uint32_t*)(&report[35])); // 4 Bytes | |
uint32_t touch1 = *((uint32_t*)(&report[39])); // 4 Bytes | |
// This stuff is no good | |
// uint8_t packet_counter1 = report[43]; // packet counter | |
// DS4UTouch prev_touch0 = DS4UDecodeTouch( report + 44 ); // Reads 4 bytes | |
// DS4UTouch prev_touch1 = DS4UDecodeTouch( report + 48 ); // Reads 4 | |
// | |
// Fill out input state | |
// | |
DS4UGamepadState gamepad_state = {0}; | |
SetButton( &gamepad_state, DS4UButton_DPadLeft , dpad_left ); | |
SetButton( &gamepad_state, DS4UButton_DPadDown , dpad_down ); | |
SetButton( &gamepad_state, DS4UButton_DPadRight , dpad_right ); | |
SetButton( &gamepad_state, DS4UButton_DPadUp , dpad_up ); | |
SetButton( &gamepad_state, DS4UButton_Square , square ); | |
SetButton( &gamepad_state, DS4UButton_Cross , cross ); | |
SetButton( &gamepad_state, DS4UButton_Circle , circle ); | |
SetButton( &gamepad_state, DS4UButton_Triangle , triangle ); | |
SetButton( &gamepad_state, DS4UButton_L1 , l1 ); | |
SetButton( &gamepad_state, DS4UButton_L2 , l2 ); | |
SetButton( &gamepad_state, DS4UButton_R1 , r1 ); | |
SetButton( &gamepad_state, DS4UButton_R2 , r2 ); | |
SetButton( &gamepad_state, DS4UButton_L3 , l3 ); | |
SetButton( &gamepad_state, DS4UButton_R3 , r3 ); | |
SetButton( &gamepad_state, DS4UButton_Share , share ); | |
SetButton( &gamepad_state, DS4UButton_Options , options ); | |
SetButton( &gamepad_state, DS4UButton_TouchPad , tpad_button ); | |
SetButton( &gamepad_state, DS4UButton_Playstation , ps_button ); | |
gamepad_state.mStickLeftX = left_stick_x; | |
gamepad_state.mStickLeftY = left_stick_y; | |
gamepad_state.mStickRightX = right_stick_x; | |
gamepad_state.mStickRightY = right_stick_y; | |
gamepad_state.mTriggerL2 = l2_analog; | |
gamepad_state.mTriggerR2 = r2_analog; | |
gamepad_state.mBatteryLevel = battery_level; | |
DecodeTouch( &gamepad_state.mTouchStates[0], (uint8_t*)&touch0 ); | |
DecodeTouch( &gamepad_state.mTouchStates[1], (uint8_t*)&touch1 ); | |
DS4UGamepadDevice* device = &mDevices[gamepad_index]; | |
device->mIsUSBDevice = is_usb_report; | |
// Set the previous packet index so that the mem-compare is correct | |
gamepad_state.mPacketID = device->mPacketID; | |
device->mPacketID += 1; | |
if( memcmp(&gamepad_state, &device->mGamepadState, sizeof(DS4UGamepadState)) != 0 ) | |
{ | |
gamepad_state.mPacketID = device->mPacketID; | |
device->mGamepadState = gamepad_state; | |
if( mInputEventHandler != nullptr ) | |
{ | |
mInputEventHandler( gamepad_index, DS4UEventID_GamepadState, &gamepad_state, mInputEventContext ); | |
} | |
} | |
#if defined(DEBUG_DECODING) | |
printf("\033[2J"); | |
printf("ControllerID: %d\n", controller_id); | |
printf("LeftStickX: %d\n", left_stick_x); | |
printf("LeftStickY: %d\n", left_stick_y); | |
printf("RightStickX: %d\n", right_stick_x); | |
printf("RightStickY: %d\n", right_stick_y); | |
printf("DPad: %s %s %s %s\n", dpad_up ? "up" : "--", dpad_right ? "right" : "-----", dpad_down ? "down" : "----", dpad_left ? "left" : "----" ); | |
printf("FaceButtons: %s %s %s %s\n", cross ? "cross" : "-----", circle ? "circle" : "------", square ? "square" : "------", triangle ? "triangle" : "--------" ); | |
printf("LButtons: %s %s %s %s %s %s\n", l1 ? "l1" : "--", l2 ? "l2" : "--", l3 ? "l3" : "--", r1 ? "r1" : "--", r2 ? "r2" : "--", r3 ? "r3" : "--" ); | |
printf("Share: %s\n", share ? "true" : "false"); | |
printf("Options: %s\n", options ? "true" : "false"); | |
printf("TrackPadButton: %s\n", tpad_button ? "true" : "false"); | |
printf("PSButton: %s\n", ps_button ? "true" : "false"); | |
printf("EventID: %d\n", event_id); | |
printf("L2Analog: %d\n", l2_analog); | |
printf("R2Analog: %d\n", r2_analog); | |
printf("Unknown10: %d\n", unknown10); | |
printf("Unknown11: %d\n", unknown11); | |
printf("BatteryLevel: %d\n", battery_level); | |
printf("AccelX: %d\n", motionX); | |
printf("AccelY: %d\n", motionY); | |
printf("AccelZ: %d\n", motionZ); | |
printf("GyroRoll: %d\n", orientationRoll); | |
printf("GyroYaw: %d\n", orientationYaw); | |
printf("GyroPitch: %d\n", orientationPitch); | |
printf("Unknown25: %d\n", unknown25); | |
printf("Unknown26: %d\n", unknown26); | |
printf("Unknown27: %d\n", unknown27); | |
printf("Unknown28: %d\n", unknown28); | |
printf("Unknown29: %d\n", unknown29); | |
printf("unknown_flag30: %s\n", unknown_flag30 ? "true" : "false"); | |
printf("HeadphonesConnected: %s\n", phones_connected ? "true" : "false"); | |
printf("MicrophoneConnected: %s\n", mic_connected ? "true" : "false"); | |
printf("UsbConnected: %s\n", usb_connected ? "true" : "false"); | |
printf("BatteryLevel2: %d\n", battery_level2); | |
printf("Unknown31: %d\n", unknown31); | |
printf("Unknown32: %d\n", unknown32); | |
printf("Unknown33: %d\n", unknown33); | |
printf("packet_counter0: %d\n", packet_counter0); | |
printf("TrackPadTouch0ID: %d\n", touch0.mId); | |
printf("TrackPadTouch0Active: %s\n", touch0.mActive ? "true" : "false" ); | |
printf("TrackPadTouch0X: %d\n", touch0.mX); | |
printf("TrackPadTouch0Y: %d\n", touch0.mY); | |
printf("TrackPadTouch1ID: %d\n", touch1.mId); | |
printf("TrackPadTouch1Active: %s\n", touch1.mActive ? "true" : "false" ); | |
printf("TrackPadTouch1X: %d\n", touch1.mX); | |
printf("TrackPadTouch1Y: %d\n", touch1.mY); | |
#endif | |
} | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// API Implementation // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
DS4UContext* DS4UInit(int32_t max_gamepad_count, DS4UInputEventHandlerFunc input_event_handler, void* context) | |
{ | |
max_gamepad_count = max_gamepad_count >= 0 ? max_gamepad_count : 0; | |
DS4UContext* ds4u = new (malloc(sizeof(DS4UContext))) DS4UContext(); | |
ds4u->mAlloc = &malloc; | |
ds4u->mFree = &free; | |
ds4u->mMaxGamepadCount = max_gamepad_count; | |
ds4u->mInputEventHandler = input_event_handler; | |
ds4u->mInputEventContext = context; | |
size_t input_states_size = sizeof(DS4UGamepadDevice) * max_gamepad_count; | |
size_t device_ids_size = sizeof(uint64_t) * max_gamepad_count; | |
size_t output_states_size = sizeof(DS4UOuputState) * max_gamepad_count; | |
ds4u->mDevices = (DS4UGamepadDevice*) ds4u->mAlloc( input_states_size ); | |
ds4u->mDeviceIDs = (uint64_t*) ds4u->mAlloc( device_ids_size ); | |
ds4u->mOutputStates[0] = (DS4UOuputState*) ds4u->mAlloc( output_states_size ); | |
ds4u->mOutputStates[1] = (DS4UOuputState*) ds4u->mAlloc( output_states_size ); | |
memset( ds4u->mDevices, 0, input_states_size ); | |
memset( ds4u->mDeviceIDs, 0, device_ids_size ); | |
memset( ds4u->mOutputStates[0], 0, output_states_size ); | |
memset( ds4u->mOutputStates[1], 0, output_states_size ); | |
DS4UInitPlat(ds4u); | |
ds4u->StartOutputReportThread(); | |
return ds4u; | |
} | |
void DS4UClose(DS4UContext* ds4u) | |
{ | |
ds4u->StopOutputReportThread(); | |
// We don't want to dispatch any messages that might be generated during shutdown | |
ds4u->mInputEventHandler = nullptr; | |
DS4UClosePlat(ds4u); | |
DS4UFreeFunc ds4u_free = ds4u->mFree; | |
ds4u->~DS4UContext(); | |
ds4u_free(ds4u); | |
ds4u = nullptr; | |
} | |
bool DS4UIsGamepadConnected(DS4UContext* ds4u, int32_t gamepad_index) | |
{ | |
return ds4u->IsGamepadConnected( gamepad_index ); | |
} | |
void DS4UGetGamepadState(DS4UContext* ds4u, int32_t gamepad_index, DS4UGamepadState& out_state) | |
{ | |
if( ds4u->IsGamepadConnected(gamepad_index) ) | |
{ | |
out_state = ds4u->mDevices[gamepad_index].mGamepadState; | |
} | |
else | |
{ | |
out_state = DS4UGamepadState { 0 }; | |
} | |
} | |
void DS4USetGamepadState(DS4UContext* ds4u, int32_t gamepad_index, uint8_t motor_speed_left, uint8_t motor_speed_right, uint32_t lightbar_rgb) | |
{ | |
std::lock_guard<std::mutex> output_mutex( ds4u->mOutputStatesMutex ); | |
uint64_t device_id = ds4u->GetDeviceID(gamepad_index); | |
if( device_id == 0 ) | |
return; | |
DS4UOuputState* output_states = ds4u->mOutputStates[ds4u->mOutputStatesIdx]; | |
DS4UGamepadDevice* device = &ds4u->mDevices[gamepad_index]; | |
output_states->mIsUSB = device->mIsUSBDevice; | |
output_states->mDeviceID = device_id; | |
output_states->mRightMotor = motor_speed_right; | |
output_states->mLeftMotor = motor_speed_left; | |
output_states->mRed = uint8_t(lightbar_rgb >> 16 ); | |
output_states->mGreen = uint8_t(lightbar_rgb >> 8 ); | |
output_states->mBlue = uint8_t(lightbar_rgb ); | |
} | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// Platform Implementations // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
#define HID_VENDOR_ID_SONY uint16_t(0x054c) | |
#define HID_PRODUCT_ID_DS4 uint16_t(0x05c4) | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// Windows // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
#if defined(WIN32) || defined(_WIN32) | |
#include <windows.h> | |
// #include <winuser.h> | |
#include <hidsdi.h> | |
#include <winusb.h> | |
#include <Strsafe.h> | |
// Size of raw input message, we can safely reject any messages with a different size | |
#define RAW_INPUT_BUFF_SIZE 640 | |
#define RAW_INPUT_BT_MESSAGE_SIZE 579 | |
#define RAW_INPUT_USB_MESSAGE_SIZE 96 | |
bool DS4UIsDS4(HANDLE hDevice) | |
{ | |
RID_DEVICE_INFO info = { 0 }; | |
UINT info_size = sizeof(RID_DEVICE_INFO); | |
if( SUCCEEDED(GetRawInputDeviceInfo(hDevice, RIDI_DEVICEINFO, &info, &info_size)) ) | |
{ | |
return (info.hid.dwVendorId == HID_VENDOR_ID_SONY) & (info.hid.dwProductId == HID_PRODUCT_ID_DS4); | |
} | |
return false; | |
} | |
void DS4WriteOutputStatePlat( uint64_t device_id, uint8_t* packet, size_t packet_length, bool is_usb ) | |
{ | |
TCHAR device_name[512]; | |
UINT dwSize = 512; | |
GetRawInputDeviceInfo( (HANDLE)device_id, RIDI_DEVICENAME, &device_name, &dwSize); | |
HANDLE file_handle = CreateFile( device_name, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr ); | |
HRESULT result = E_FAIL; | |
if( is_usb ) | |
{ | |
result = WriteFile( file_handle, packet, (DWORD)packet_length, nullptr, nullptr ); | |
} | |
else | |
{ | |
result = HidD_SetOutputReport( file_handle, packet, UINT(packet_length) ); | |
} | |
CloseHandle( file_handle ); | |
} | |
LRESULT DS4UWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) | |
{ | |
DS4UContext* ds4u = (DS4UContext*)GetWindowLongPtr(hwnd, GWLP_USERDATA); | |
if( uMsg == WM_INPUT ) | |
{ | |
// Get the raw input data's size | |
UINT raw_input_size = 0; | |
GetRawInputData( (HRAWINPUT)lParam, RID_INPUT, nullptr, &raw_input_size, sizeof(RAWINPUTHEADER) ); | |
if( (raw_input_size > RAW_INPUT_BUFF_SIZE) ) | |
{ | |
return 0; | |
} | |
// Get the actual raw input data data | |
uint8_t raw_input_buffer[RAW_INPUT_BUFF_SIZE]; | |
RAWINPUT* raw_input = (RAWINPUT*)raw_input_buffer; | |
GetRawInputData( (HRAWINPUT)lParam, RID_INPUT, raw_input, &raw_input_size, sizeof(RAWINPUTHEADER) ); | |
if( (DS4UIsDS4(raw_input->header.hDevice) == false) ) | |
{ | |
return 0; | |
} | |
uint8_t* report_start = raw_input->data.hid.bRawData; | |
if( (raw_input_size == RAW_INPUT_BT_MESSAGE_SIZE) ) | |
{ | |
if( report_start[0] != 0x11 ) | |
{ | |
return 0; | |
} | |
report_start += 2; // in blutooth mode, jump forward 2 bytes to find the report | |
} | |
// We found a Dual Shock 4! | |
ds4u->DecodeInputReport( report_start, uint64_t(raw_input->header.hDevice), raw_input_size != RAW_INPUT_BT_MESSAGE_SIZE ); | |
return 0; | |
} | |
else if( uMsg == WM_INPUT_DEVICE_CHANGE ) | |
{ | |
HANDLE hDevice = (HANDLE)lParam; | |
if( wParam == GIDC_ARRIVAL && DS4UIsDS4(hDevice) ) | |
{ | |
// GetRawInputData() | |
ds4u->RegisterGamepad((uint64_t)hDevice); | |
} | |
else if( wParam == GIDC_REMOVAL ) | |
{ | |
ds4u->UnregisterGamepad((uint64_t)hDevice); | |
} | |
return 0; | |
} | |
return DefWindowProc( hwnd, uMsg, wParam, lParam ); | |
} | |
void DS4UInitPlat(DS4UContext* ds4u) | |
{ | |
// | |
// Setup Window Class | |
// | |
WNDCLASSEX wcx; | |
memset( &wcx, 0, sizeof(WNDCLASSEX) ); | |
wcx.cbSize = sizeof(WNDCLASSEX); // size of structure | |
wcx.lpfnWndProc = &DS4UWindowProc; // points to window procedure | |
wcx.hInstance = GetModuleHandle(nullptr); // handle to instance | |
wcx.lpszClassName = TEXT("DS4UMessageHandlerWindowClass"); // name of window class | |
RegisterClassEx( &wcx ); | |
// Create the window | |
HWND hWnd = CreateWindowEx(0, | |
TEXT("DS4UMessageHandlerWindowClass"), | |
TEXT("DS4UMessageHandlerWindow"), | |
0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr); | |
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)ds4u); | |
ds4u->mHIDInterfaceHandle = hWnd; | |
// Register rawinput device | |
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms645565%28v=vs.85%29.aspx | |
// | |
// RIDEV_INPUTSINK - Input occurred while the application was not in the foreground. The application must call DefWindowProc so the system can perform the cleanup | |
// RIDEV_NOHOTKEYS - Disables windows key. Not documented but works. See http://the-witness.net/news/2012/12/finding-and-fixing-a-five-second-stall/ | |
// | |
RAWINPUTDEVICE device; | |
device.usUsagePage = HID_USAGE_PAGE_GENERIC; | |
device.usUsage = HID_USAGE_GENERIC_GAMEPAD; | |
device.dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;// | RIDEV_NOLEGACY; | |
device.hwndTarget = hWnd; | |
RegisterRawInputDevices( &device, 1, sizeof(RAWINPUTDEVICE) ); | |
} | |
void ErrorExit(LPTSTR lpszFunction) | |
{ | |
// Retrieve the system error message for the last-error code | |
LPVOID lpMsgBuf; | |
LPVOID lpDisplayBuf; | |
DWORD dw = GetLastError(); | |
FormatMessage( | |
FORMAT_MESSAGE_ALLOCATE_BUFFER | | |
FORMAT_MESSAGE_FROM_SYSTEM | | |
FORMAT_MESSAGE_IGNORE_INSERTS, | |
NULL, | |
dw, | |
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), | |
(LPTSTR) &lpMsgBuf, | |
0, NULL ); | |
// Display the error message and exit the process | |
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, | |
(lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR)); | |
StringCchPrintf((LPTSTR)lpDisplayBuf, | |
LocalSize(lpDisplayBuf) / sizeof(TCHAR), | |
TEXT("%s failed with error %d: %s"), | |
lpszFunction, dw, lpMsgBuf); | |
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK); | |
LocalFree(lpMsgBuf); | |
LocalFree(lpDisplayBuf); | |
// ExitProcess(dw); | |
} | |
void DS4UClosePlat(DS4UContext* ds4u) | |
{ | |
HWND hWnd = (HWND)ds4u->mHIDInterfaceHandle; | |
DestroyWindow( hWnd ); | |
UnregisterClass( TEXT("DS4UMessageHandlerWindowClass"), GetModuleHandle(nullptr) ); | |
} | |
#endif // Windows | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// OSX // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
#if defined(__APPLE__) | |
#include <CoreFoundation/CFBase.h> | |
#include <CoreFoundation/CFNumber.h> | |
#include <CoreFoundation/CFDictionary.h> | |
#include <IOKit/hid/IOHIDManager.h> | |
#include <IOKit/hid/IOHIDKeys.h> | |
// this will be called when the HID Manager matches a new (hot plugged) HID device | |
static void Handle_DeviceMatchingCallback( | |
void * inContext, // context from IOHIDManagerRegisterDeviceMatchingCallback | |
IOReturn inResult, // the result of the matching operation | |
void * inSender, // the IOHIDManagerRef for the new device | |
IOHIDDeviceRef inIOHIDDeviceRef // the new HID device | |
) | |
{ | |
ds4u->RegisterGamepad((uint64_t)inIOHIDDeviceRef); | |
} | |
// this will be called when a HID device is removed (unplugged) | |
static void Handle_RemovalCallback( | |
void * inContext, // context from IOHIDManagerRegisterDeviceMatchingCallback | |
IOReturn inResult, // the result of the removing operation | |
void * inSender, // the IOHIDManagerRef for the device being removed | |
IOHIDDeviceRef inIOHIDDeviceRef) // the removed HID device | |
{ | |
ds4u->UnregisterGamepad((uint64_t)inIOHIDDeviceRef); | |
} | |
static void Handle_IOHIDDeviceIOHIDReportCallback( | |
void * inContext, // context from IOHIDDeviceRegisterInputReportCallback | |
IOReturn inResult, // completion result for the input report operation | |
void * inSender, // IOHIDDeviceRef of the device this report is from | |
IOHIDReportType inType, // the report type | |
uint32_t inReportID, // the report ID | |
uint8_t * inReport, // pointer to the report data | |
CFIndex inReportLength) // the actual size of the input report | |
{ | |
ds4u->DecodeInputReport(inReport, (uint64_t)inSender); | |
} | |
void DS4WriteOutputStatePlat(uint64_t device_id, uint8_t* packet, size_t packet_length) | |
{ | |
IOHIDDeviceSetReport((IOHIDDeviceRef)device_id, kIOHIDReportTypeOutput, 0, packet, packet_length); | |
} | |
void DS4UInitPlat() | |
{ | |
IOHIDManagerRef hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); | |
ds4u->mHIDInterfaceHandle = hid_manager; | |
const CFStringRef keys[] | |
{ | |
CFStringCreateWithCString(kCFAllocatorDefault, kIOHIDVendorIDKey, kCFStringEncodingASCII), | |
CFStringCreateWithCString(kCFAllocatorDefault, kIOHIDProductIDKey, kCFStringEncodingASCII), | |
CFStringCreateWithCString(kCFAllocatorDefault, kIOHIDPrimaryUsageKey, kCFStringEncodingASCII), | |
CFStringCreateWithCString(kCFAllocatorDefault, kIOHIDPrimaryUsagePageKey, kCFStringEncodingASCII), | |
}; | |
UInt32 vendor_id = HID_VENDOR_ID_SONY; | |
UInt32 product_id = HID_PRODUCT_ID_DS4; | |
UInt32 usage_id = 0x00000005; // GamePad | |
UInt32 usage_page = 0x00000001; // Generic | |
const CFNumberRef values[] | |
{ | |
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &vendor_id), | |
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &product_id), | |
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_id), | |
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_page) | |
}; | |
CFDictionaryRef device_match_dictionary = CFDictionaryCreate(kCFAllocatorDefault, (const void**)keys, (const void**)values, 4, nullptr, nullptr); | |
IOHIDManagerRegisterDeviceMatchingCallback (hid_manager, &Handle_DeviceMatchingCallback, nullptr); | |
IOHIDManagerRegisterDeviceRemovalCallback (hid_manager, &Handle_RemovalCallback, nullptr); | |
IOHIDManagerSetDeviceMatching (hid_manager, device_match_dictionary); | |
IOHIDManagerScheduleWithRunLoop (hid_manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); | |
IOHIDManagerOpen (hid_manager, kIOHIDOptionsTypeNone); | |
IOHIDManagerRegisterInputReportCallback (hid_manager, Handle_IOHIDDeviceIOHIDReportCallback, nullptr); | |
// Release Resources | |
CFRelease( device_match_dictionary ); | |
for( CFNumberRef number : values ) | |
{ | |
CFRelease( number ); | |
} | |
} | |
void DS4UClosePlat() | |
{ | |
IOHIDManagerRef hid_manager = (IOHIDManagerRef)ds4u->mHIDInterfaceHandle; | |
IOHIDManagerRegisterInputReportCallback (hid_manager, nullptr, nullptr); | |
IOHIDManagerClose (hid_manager, kIOHIDOptionsTypeNone); | |
IOHIDManagerUnscheduleFromRunLoop (hid_manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); | |
IOHIDManagerRegisterDeviceMatchingCallback (hid_manager, nullptr, nullptr); | |
IOHIDManagerRegisterDeviceRemovalCallback (hid_manager, nullptr, nullptr); | |
CFRelease(hid_manager); | |
} | |
#endif // OSX |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Luminawesome Games Ltd. 2015 | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software | |
// and associated documentation files (the “Software”), to deal in the Software without | |
// restriction, including without limitation the rights to use, copy, modify, merge, publish, | |
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the | |
// Software is furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all copies or | |
// substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
// OTHER DEALINGS IN THE SOFTWARE. | |
#pragma once | |
#include <cstddef> | |
#include <cstdint> | |
enum DS4UButton | |
{ | |
DS4UButton_None = 0, | |
DS4UButton_Square = 1 << 0, | |
DS4UButton_Cross = 1 << 1, | |
DS4UButton_Circle = 1 << 2, | |
DS4UButton_Triangle = 1 << 3, | |
DS4UButton_L1 = 1 << 4, | |
DS4UButton_R1 = 1 << 5, | |
DS4UButton_L2 = 1 << 6, | |
DS4UButton_R2 = 1 << 7, | |
DS4UButton_Options = 1 << 8, | |
DS4UButton_Share = 1 << 9, | |
DS4UButton_L3 = 1 << 10, | |
DS4UButton_R3 = 1 << 11, | |
DS4UButton_Playstation = 1 << 12, | |
DS4UButton_TouchPad = 1 << 13, | |
DS4UButton_DPadLeft = 1 << 14, | |
DS4UButton_DPadDown = 1 << 15, | |
DS4UButton_DPadRight = 1 << 16, | |
DS4UButton_DPadUp = 1 << 17, | |
}; | |
enum DS4UEventID | |
{ | |
DS4UEventID_Connected, | |
DS4UEventID_Disconnected, | |
DS4UEventID_GamepadState | |
}; | |
enum DS4UGamepadColor | |
{ | |
DS4UGamepadColor_Gamepad0 = 0x000000ff, | |
DS4UGamepadColor_Gamepad1 = 0x00ff0000, | |
DS4UGamepadColor_Gamepad2 = 0x0000ff00, | |
DS4UGamepadColor_Gamepad3 = 0x00ff69b4, | |
}; | |
struct DS4UTouchState | |
{ | |
uint8_t mId; | |
bool mActive; | |
uint16_t mX; | |
uint16_t mY; | |
}; | |
struct DS4UGamepadState | |
{ | |
uint32_t mButtons; | |
int8_t mStickLeftX; | |
int8_t mStickLeftY; | |
int8_t mStickRightX; | |
int8_t mStickRightY; | |
uint8_t mTriggerL2; | |
uint8_t mTriggerR2; | |
uint8_t mBatteryLevel; | |
DS4UTouchState mTouchStates[2]; | |
uint32_t mPacketID; | |
}; | |
struct DS4UContext; | |
typedef void* (*DS4UAllocFunc) (size_t size); | |
typedef void (*DS4UFreeFunc) (void* ptr); | |
typedef void (*DS4UInputEventHandlerFunc) (int32_t gamepad_index, DS4UEventID event_id, const DS4UGamepadState* gamepad_state, void* context); | |
extern "C" DS4UContext* DS4UInit (int32_t max_gamepad_count, DS4UInputEventHandlerFunc input_event_handler, void* context); | |
extern "C" void DS4UClose (DS4UContext* ds4u); | |
extern "C" bool DS4UIsGamepadConnected (DS4UContext* ds4u, int32_t gamepad_index); | |
extern "C" void DS4UGetGamepadState (DS4UContext* ds4u, int32_t gamepad_index, DS4UGamepadState& out_state); | |
extern "C" void DS4USetGamepadState (DS4UContext* ds4u, int32_t gamepad_index, uint8_t motor_speed_left, uint8_t motor_speed_right, uint32_t lightbar_rgb); | |
////////////////////////////////////////////////////////////////////////////////////////// | |
// // | |
// Inline // | |
// // | |
////////////////////////////////////////////////////////////////////////////////////////// | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Luminawesome Games Ltd. 2015 | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software | |
// and associated documentation files (the “Software”), to deal in the Software without | |
// restriction, including without limitation the rights to use, copy, modify, merge, publish, | |
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the | |
// Software is furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all copies or | |
// substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
// OTHER DEALINGS IN THE SOFTWARE. | |
#include <iostream> | |
#include <stdio.h> | |
#if defined(WIN32) | |
#include <windows.h> | |
#elif defined(__APPLE__) | |
#include <CoreFoundation/CFRunLoop.h> | |
#endif | |
#include "DS4U.hpp" | |
void HandleInputEvent(int32_t gamepad_index, DS4UEventID event_id, const DS4UGamepadState* input_state, void* context) | |
{ | |
// printf("-------------------------------\n"); | |
// printf("Controller : %d\n", input_state->mIndex); | |
// printf("LeftStickX : %d\n", input_state->mStickLeftX); | |
// printf("LeftStickY : %d\n", input_state->mStickLeftY); | |
// printf("RightStickX : %d\n", input_state->mStickRightX); | |
// printf("RightStickY : %d\n", input_state->mStickRightY); | |
// printf("TriggerL2 : %d\n", input_state->mTriggerL2); | |
// printf("TriggerR2 : %d\n", input_state->mTriggerR2); | |
// printf("TrackPadTouch0ID : %d\n", input_state->mTouchStates[0].mId); | |
// printf("TrackPadTouch0Active : %s\n", input_state->mTouchStates[0].mActive ? "true" : "false" ); | |
// printf("TrackPadTouch0X : %d\n", input_state->mTouchStates[0].mX); | |
// printf("TrackPadTouch0Y : %d\n", input_state->mTouchStates[0].mY); | |
// printf("TrackPadTouch1ID : %d\n", input_state->mTouchStates[1].mId); | |
// printf("TrackPadTouch1Active : %s\n", input_state->mTouchStates[1].mActive ? "true" : "false" ); | |
// printf("TrackPadTouch1X : %d\n", input_state->mTouchStates[1].mX); | |
// printf("TrackPadTouch1Y : %d\n", input_state->mTouchStates[1].mY); | |
// Only send a controller state every so often because it's damn slow | |
static int32_t counter = 0; | |
if( (counter++ % 30) == 0) | |
{ | |
DS4USetGamepadState( | |
gamepad_index, | |
input_state->mTriggerL2, | |
input_state->mTriggerR2, | |
input_state->mStickRightX, | |
input_state->mStickRightY, | |
input_state->mStickRightX); | |
} | |
} | |
int main(int argc, const char* args[]) | |
{ | |
DS4UInit(2, &HandleInputEvent, nullptr); | |
#if defined(WIN32) | |
// | |
// Message Loop | |
// | |
MSG msg = {0}; | |
while( WM_QUIT != msg.message ) | |
{ | |
if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ) | |
{ | |
TranslateMessage(&msg); | |
DispatchMessage(&msg); | |
} | |
} | |
#elif defined(__APPLE__) | |
CFRunLoopRun(); | |
#endif | |
DS4UClose(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment