raze/source/common/platform/posix/cocoa/i_joystick.cpp
2023-04-05 07:39:31 +10:00

1242 lines
29 KiB
C++

/*
** i_joystick.cpp
**
**---------------------------------------------------------------------------
** Copyright 2012-2015 Alexey Lysiuk
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOMessage.h>
#include <IOKit/hid/IOHIDLib.h>
#include "d_eventbase.h"
#include "i_system.h"
#include "m_argv.h"
#include "m_joy.h"
#include "v_text.h"
#include "printf.h"
#include "keydef.h"
EXTERN_CVAR(Bool, joy_axespolling)
namespace
{
FString ToFString(const CFStringRef string)
{
if (NULL == string)
{
return FString();
}
const CFIndex stringLength = CFStringGetLength(string);
if (0 == stringLength)
{
return FString();
}
const size_t bufferSize = CFStringGetMaximumSizeForEncoding(stringLength, kCFStringEncodingUTF8) + 1;
char buffer[bufferSize];
memset(buffer, 0, bufferSize);
CFStringGetCString(string, buffer, bufferSize, kCFStringEncodingUTF8);
return FString(buffer);
}
// ---------------------------------------------------------------------------
class IOKitJoystick : public IJoystickConfig
{
public:
explicit IOKitJoystick(io_object_t device);
virtual ~IOKitJoystick();
virtual FString GetName();
virtual float GetSensitivity();
virtual void SetSensitivity(float scale);
virtual int GetNumAxes();
virtual float GetAxisDeadZone(int axis);
virtual EJoyAxis GetAxisMap(int axis);
virtual const char* GetAxisName(int axis);
virtual float GetAxisScale(int axis);
virtual void SetAxisDeadZone(int axis, float deadZone);
virtual void SetAxisMap(int axis, EJoyAxis gameAxis);
virtual void SetAxisScale(int axis, float scale);
virtual bool IsSensitivityDefault();
virtual bool IsAxisDeadZoneDefault(int axis);
virtual bool IsAxisMapDefault(int axis);
virtual bool IsAxisScaleDefault(int axis);
virtual void SetDefaultConfig();
virtual FString GetIdentifier();
void AddAxes(float axes[NUM_JOYAXIS]) const;
void Update();
void UseAxesPolling(bool axesPolling);
io_object_t* GetNotificationPtr();
private:
IOHIDDeviceInterface** m_interface;
IOHIDQueueInterface** m_queue;
FString m_name;
FString m_identifier;
float m_sensitivity;
struct AnalogAxis
{
IOHIDElementCookie cookie;
char name[64];
float value;
int32_t minValue;
int32_t maxValue;
float deadZone;
float defaultDeadZone;
float sensitivity;
float defaultSensitivity;
EJoyAxis gameAxis;
EJoyAxis defaultGameAxis;
AnalogAxis()
{
memset(this, 0, sizeof *this);
}
};
TArray<AnalogAxis> m_axes;
struct DigitalButton
{
IOHIDElementCookie cookie;
int32_t value;
explicit DigitalButton(const IOHIDElementCookie cookie)
: cookie(cookie)
, value(0)
{ }
};
TArray<DigitalButton> m_buttons;
TArray<DigitalButton> m_POVs;
bool m_useAxesPolling;
io_object_t m_notification;
static const float DEFAULT_DEADZONE;
static const float DEFAULT_SENSITIVITY;
void ProcessAxes();
bool ProcessAxis (const IOHIDEventStruct& event);
bool ProcessButton(const IOHIDEventStruct& event);
bool ProcessPOV (const IOHIDEventStruct& event);
void GatherDeviceInfo(io_object_t device, CFDictionaryRef properties);
static void GatherElementsHandler(const void* value, void* parameter);
void GatherCollectionElements(CFDictionaryRef properties);
void AddAxis(CFDictionaryRef element);
void AddButton(CFDictionaryRef element);
void AddPOV(CFDictionaryRef element);
void AddToQueue(IOHIDElementCookie cookie);
void RemoveFromQueue(IOHIDElementCookie cookie);
};
const float IOKitJoystick::DEFAULT_DEADZONE = 0.25f;
const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f;
IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device)
{
IOCFPlugInInterface** plugInInterface = NULL;
SInt32 score = 0;
const kern_return_t pluginResult = IOCreatePlugInInterfaceForService(device,
kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score);
IOHIDDeviceInterface** interface = NULL;
if (KERN_SUCCESS == pluginResult)
{
// Call a method of the intermediate plug-in to create the device interface
const HRESULT queryResult = (*plugInInterface)->QueryInterface(plugInInterface,
CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), reinterpret_cast<LPVOID*>(&interface));
IODestroyPlugInInterface(plugInInterface); // [?] or maybe (*plugInInterface)->Release(plugInInterface);
if (S_OK == queryResult)
{
const IOReturn openResult = (*interface)->open(interface, 0);
if (kIOReturnSuccess != openResult)
{
(*interface)->Release(interface);
Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::open() failed with code 0x%08X\n", openResult);
return NULL;
}
}
else
{
Printf(TEXTCOLOR_RED "IOCFPlugInInterface::QueryInterface() failed with code 0x%08X\n",
static_cast<int>(queryResult));
return NULL;
}
}
else
{
Printf(TEXTCOLOR_RED "IOCreatePlugInInterfaceForService() failed with code %i\n", pluginResult);
return NULL;
}
return interface;
}
IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface)
{
if (NULL == interface)
{
return NULL;
}
IOHIDQueueInterface** queue = (*interface)->allocQueue(interface);
if (NULL == queue)
{
Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::allocQueue() failed\n");
return NULL;
}
static const uint32_t QUEUE_FLAGS = 0;
static const uint32_t QUEUE_DEPTH = 0;
const IOReturn queueResult = (*queue)->create(queue, QUEUE_FLAGS, QUEUE_DEPTH);
if (kIOReturnSuccess != queueResult)
{
(*queue)->Release(queue);
Printf(TEXTCOLOR_RED "IOHIDQueueInterface::create() failed with code 0x%08X\n", queueResult);
return NULL;
}
return queue;
}
IOKitJoystick::IOKitJoystick(const io_object_t device)
: m_interface(CreateDeviceInterface(device))
, m_queue(CreateDeviceQueue(m_interface))
, m_sensitivity(DEFAULT_SENSITIVITY)
, m_useAxesPolling(true)
, m_notification(0)
{
if (NULL == m_interface || NULL == m_queue)
{
return;
}
CFMutableDictionaryRef properties = NULL;
const kern_return_t propertiesResult =
IORegistryEntryCreateCFProperties(device, &properties, kCFAllocatorDefault, kNilOptions);
if (KERN_SUCCESS != propertiesResult || NULL == properties)
{
Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", propertiesResult);
return;
}
GatherDeviceInfo(device, properties);
GatherCollectionElements(properties);
CFRelease(properties);
UseAxesPolling(joy_axespolling);
(*m_queue)->start(m_queue);
SetDefaultConfig();
}
IOKitJoystick::~IOKitJoystick()
{
M_SaveJoystickConfig(this);
if (0 != m_notification)
{
IOObjectRelease(m_notification);
}
if (NULL != m_queue)
{
(*m_queue)->stop(m_queue);
(*m_queue)->dispose(m_queue);
(*m_queue)->Release(m_queue);
}
if (NULL != m_interface)
{
(*m_interface)->close(m_interface);
(*m_interface)->Release(m_interface);
}
}
FString IOKitJoystick::GetName()
{
return m_name;
}
float IOKitJoystick::GetSensitivity()
{
return m_sensitivity;
}
void IOKitJoystick::SetSensitivity(float scale)
{
m_sensitivity = scale;
}
int IOKitJoystick::GetNumAxes()
{
return static_cast<int>(m_axes.Size());
}
#define IS_AXIS_VALID (static_cast<unsigned int>(axis) < m_axes.Size())
float IOKitJoystick::GetAxisDeadZone(int axis)
{
return IS_AXIS_VALID ? m_axes[axis].deadZone : 0.0f;
}
EJoyAxis IOKitJoystick::GetAxisMap(int axis)
{
return IS_AXIS_VALID ? m_axes[axis].gameAxis : JOYAXIS_None;
}
const char* IOKitJoystick::GetAxisName(int axis)
{
return IS_AXIS_VALID ? m_axes[axis].name : "Invalid";
}
float IOKitJoystick::GetAxisScale(int axis)
{
return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f;
}
void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone)
{
if (IS_AXIS_VALID)
{
m_axes[axis].deadZone = clamp(deadZone, 0.0f, 1.0f);
}
}
void IOKitJoystick::SetAxisMap(int axis, EJoyAxis gameAxis)
{
if (IS_AXIS_VALID)
{
m_axes[axis].gameAxis = (gameAxis> JOYAXIS_None && gameAxis <NUM_JOYAXIS)
? gameAxis
: JOYAXIS_None;
}
}
void IOKitJoystick::SetAxisScale(int axis, float scale)
{
if (IS_AXIS_VALID)
{
m_axes[axis].sensitivity = scale;
}
}
bool IOKitJoystick::IsSensitivityDefault()
{
return DEFAULT_SENSITIVITY == m_sensitivity;
}
bool IOKitJoystick::IsAxisDeadZoneDefault(int axis)
{
return IS_AXIS_VALID
? (m_axes[axis].deadZone == m_axes[axis].defaultDeadZone)
: true;
}
bool IOKitJoystick::IsAxisMapDefault(int axis)
{
return IS_AXIS_VALID
? (m_axes[axis].gameAxis == m_axes[axis].defaultGameAxis)
: true;
}
bool IOKitJoystick::IsAxisScaleDefault(int axis)
{
return IS_AXIS_VALID
? (m_axes[axis].sensitivity == m_axes[axis].defaultSensitivity)
: true;
}
#undef IS_AXIS_VALID
void IOKitJoystick::SetDefaultConfig()
{
m_sensitivity = DEFAULT_SENSITIVITY;
const size_t axisCount = m_axes.Size();
for (size_t i = 0; i < axisCount; ++i)
{
m_axes[i].deadZone = DEFAULT_DEADZONE;
m_axes[i].sensitivity = DEFAULT_SENSITIVITY;
m_axes[i].gameAxis = JOYAXIS_None;
}
// Two axes? Horizontal is yaw and vertical is forward.
if (2 == axisCount)
{
m_axes[0].gameAxis = JOYAXIS_Yaw;
m_axes[1].gameAxis = JOYAXIS_Forward;
}
// Three axes? First two are movement, third is yaw.
else if (axisCount >= 3)
{
m_axes[0].gameAxis = JOYAXIS_Side;
m_axes[1].gameAxis = JOYAXIS_Forward;
m_axes[2].gameAxis = JOYAXIS_Yaw;
// Four axes? First two are movement, last two are looking around.
if (axisCount >= 4)
{
m_axes[3].gameAxis = JOYAXIS_Pitch;
// ??? m_axes[3].sensitivity = 0.75f;
// Five axes? Use the fifth one for moving up and down.
if (axisCount >= 5)
{
m_axes[4].gameAxis = JOYAXIS_Up;
}
}
}
// If there is only one axis, then we make no assumptions about how
// the user might want to use it.
// Preserve defaults for config saving.
for (size_t i = 0; i < axisCount; ++i)
{
m_axes[i].defaultDeadZone = m_axes[i].deadZone;
m_axes[i].defaultSensitivity = m_axes[i].sensitivity;
m_axes[i].defaultGameAxis = m_axes[i].gameAxis;
}
}
FString IOKitJoystick::GetIdentifier()
{
return m_identifier;
}
void IOKitJoystick::AddAxes(float axes[NUM_JOYAXIS]) const
{
for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
{
const EJoyAxis axis = m_axes[i].gameAxis;
if (JOYAXIS_None == axis)
{
continue;
}
axes[axis] -= m_axes[i].value;
}
}
void IOKitJoystick::UseAxesPolling(const bool axesPolling)
{
m_useAxesPolling = axesPolling;
for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
{
AnalogAxis& axis = m_axes[i];
if (m_useAxesPolling)
{
RemoveFromQueue(axis.cookie);
}
else
{
AddToQueue(axis.cookie);
}
}
}
void IOKitJoystick::Update()
{
if (NULL == m_queue)
{
return;
}
IOHIDEventStruct event = { };
AbsoluteTime zeroTime = { };
const IOReturn eventResult = (*m_queue)->getNextEvent(m_queue, &event, zeroTime, 0);
if (kIOReturnSuccess == eventResult)
{
if (use_joystick)
{
ProcessAxis(event) || ProcessButton(event) || ProcessPOV(event);
}
}
else if (kIOReturnUnderrun != eventResult)
{
Printf(TEXTCOLOR_RED "IOHIDQueueInterface::getNextEvent() failed with code 0x%08X\n", eventResult);
}
ProcessAxes();
}
void IOKitJoystick::ProcessAxes()
{
if (NULL == m_interface || !m_useAxesPolling)
{
return;
}
for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
{
AnalogAxis& axis = m_axes[i];
static const double scaledMin = -1;
static const double scaledMax = 1;
IOHIDEventStruct event;
if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis.cookie, &event))
{
const double scaledValue = scaledMin +
(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);
}
else
{
axis.value = 0.0f;
}
}
}
bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event)
{
if (m_useAxesPolling)
{
return false;
}
for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
{
if (event.elementCookie != m_axes[i].cookie)
{
continue;
}
AnalogAxis& axis = m_axes[i];
static const double scaledMin = -1;
static const double scaledMax = 1;
const double scaledValue = scaledMin +
(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);
return true;
}
return false;
}
bool IOKitJoystick::ProcessButton(const IOHIDEventStruct& event)
{
for (size_t i = 0, count = m_buttons.Size(); i < count; ++i)
{
if (event.elementCookie != m_buttons[i].cookie)
{
continue;
}
int32_t& current = m_buttons[i].value;
const int32_t previous = current;
current = event.value;
Joy_GenerateButtonEvents(previous, current, 1, static_cast<int>(KEY_FIRSTJOYBUTTON + i));
return true;
}
return false;
}
bool IOKitJoystick::ProcessPOV(const IOHIDEventStruct& event)
{
for (size_t i = 0, count = m_POVs.Size(); i <count; ++i)
{
if (event.elementCookie != m_POVs[i].cookie)
{
continue;
}
int32_t& current = m_buttons[i].value;
const int32_t previous = current;
current = event.value;
static const int POV_BUTTONS[] =
{
0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09, 0x00
};
Joy_GenerateButtonEvents(POV_BUTTONS[previous], POV_BUTTONS[current], 4, KEY_JOYPOV1_UP + i * 4);
return true;
}
return false;
}
void IOKitJoystick::GatherDeviceInfo(const io_object_t device, const CFDictionaryRef properties)
{
assert(NULL != properties);
CFStringRef vendorRef = static_cast<CFStringRef>(
CFDictionaryGetValue(properties, CFSTR(kIOHIDManufacturerKey)));
CFStringRef productRef = static_cast<CFStringRef>(
CFDictionaryGetValue(properties, CFSTR(kIOHIDProductKey)));
CFNumberRef vendorIDRef = static_cast<CFNumberRef>(
CFDictionaryGetValue(properties, CFSTR(kIOHIDVendorIDKey)));
CFNumberRef productIDRef = static_cast<CFNumberRef>(
CFDictionaryGetValue(properties, CFSTR(kIOHIDProductIDKey)));
CFMutableDictionaryRef usbProperties = NULL;
if ( NULL == vendorRef || NULL == productRef
|| NULL == vendorIDRef || NULL == productIDRef)
{
// OS X is not mirroring all USB properties to HID page, so need to look at USB device page also
// Step up two levels and get dictionary of USB properties
io_registry_entry_t parent1;
kern_return_t ioResult = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent1);
if (KERN_SUCCESS == ioResult)
{
io_registry_entry_t parent2;
ioResult = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent2);
if (KERN_SUCCESS == ioResult)
{
ioResult = IORegistryEntryCreateCFProperties(parent2, &usbProperties, kCFAllocatorDefault, kNilOptions);
if (KERN_SUCCESS != ioResult)
{
Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", ioResult);
}
IOObjectRelease(parent2);
}
else
{
Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(2) failed with code %i\n", ioResult);
}
IOObjectRelease(parent1);
}
else
{
Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(1) failed with code %i\n", ioResult);
}
}
if (NULL != usbProperties)
{
if (NULL == vendorRef)
{
vendorRef = static_cast<CFStringRef>(
CFDictionaryGetValue(usbProperties, CFSTR("USB Vendor Name")));
}
if (NULL == productRef)
{
productRef = static_cast<CFStringRef>(
CFDictionaryGetValue(usbProperties, CFSTR("USB Product Name")));
}
if (NULL == vendorIDRef)
{
vendorIDRef = static_cast<CFNumberRef>(
CFDictionaryGetValue(usbProperties, CFSTR("idVendor")));
}
if (NULL == productIDRef)
{
productIDRef = static_cast<CFNumberRef>(
CFDictionaryGetValue(usbProperties, CFSTR("idProduct")));
}
}
m_name += ToFString(vendorRef);
m_name += " ";
m_name += ToFString(productRef);
int vendorID = 0, productID = 0;
if (NULL != vendorIDRef)
{
CFNumberGetValue(vendorIDRef, kCFNumberIntType, &vendorID);
}
if (NULL != productIDRef)
{
CFNumberGetValue(productIDRef, kCFNumberIntType, &productID);
}
m_identifier.AppendFormat("VID_%04x_PID_%04x", vendorID, productID);
if (NULL != usbProperties)
{
CFRelease(usbProperties);
}
}
long GetElementValue(const CFDictionaryRef element, const CFStringRef key)
{
const CFNumberRef number =
static_cast<CFNumberRef>(CFDictionaryGetValue(element, key));
long result = 0;
if (NULL != number && CFGetTypeID(number) == CFNumberGetTypeID())
{
CFNumberGetValue(number, kCFNumberLongType, &result);
}
return result;
}
void IOKitJoystick::GatherElementsHandler(const void* value, void* parameter)
{
assert(NULL != value);
assert(NULL != parameter);
const CFDictionaryRef element = static_cast<CFDictionaryRef>(value);
IOKitJoystick* thisPtr = static_cast<IOKitJoystick*>(parameter);
if (CFGetTypeID(element) != CFDictionaryGetTypeID())
{
Printf(TEXTCOLOR_RED "IOKitJoystick: Encountered wrong element type\n");
return;
}
const long type = GetElementValue(element, CFSTR(kIOHIDElementTypeKey));
if (kIOHIDElementTypeCollection == type)
{
thisPtr->GatherCollectionElements(element);
}
else if (0 != type)
{
const long usagePage = GetElementValue(element, CFSTR(kIOHIDElementUsagePageKey));
if (kHIDPage_GenericDesktop == usagePage)
{
const long usage = GetElementValue(element, CFSTR(kIOHIDElementUsageKey));
if ( kHIDUsage_GD_Slider == usage
|| kHIDUsage_GD_X == usage || kHIDUsage_GD_Y == usage || kHIDUsage_GD_Z == usage
|| kHIDUsage_GD_Rx == usage || kHIDUsage_GD_Ry == usage || kHIDUsage_GD_Rz == usage)
{
thisPtr->AddAxis(element);
}
else if (kHIDUsage_GD_Hatswitch == usage && thisPtr->m_POVs.Size() < 4)
{
thisPtr->AddPOV(element);
}
}
else if (kHIDPage_Button == usagePage)
{
thisPtr->AddButton(element);
}
}
}
void IOKitJoystick::GatherCollectionElements(const CFDictionaryRef properties)
{
const CFArrayRef topElement = static_cast<CFArrayRef>(
CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey)));
if (NULL == topElement || CFGetTypeID(topElement) != CFArrayGetTypeID())
{
Printf(TEXTCOLOR_RED "GatherCollectionElements: invalid properties dictionary\n");
return;
}
const CFRange range = { 0, CFArrayGetCount(topElement) };
CFArrayApplyFunction(topElement, range, GatherElementsHandler, this);
}
IOHIDElementCookie GetElementCookie(const CFDictionaryRef element)
{
// Use C-style cast to avoid 32/64-bit IOHIDElementCookie type issue
return (IOHIDElementCookie)GetElementValue(element, CFSTR(kIOHIDElementCookieKey));
}
void IOKitJoystick::AddAxis(const CFDictionaryRef element)
{
AnalogAxis axis;
axis.cookie = GetElementCookie(element);
axis.minValue = GetElementValue(element, CFSTR(kIOHIDElementMinKey));
axis.maxValue = GetElementValue(element, CFSTR(kIOHIDElementMaxKey));
const CFStringRef nameRef = static_cast<CFStringRef>(
CFDictionaryGetValue(element, CFSTR(kIOHIDElementNameKey)));
if (NULL != nameRef && CFStringGetTypeID() == CFGetTypeID(nameRef))
{
CFStringGetCString(nameRef, axis.name, sizeof(axis.name) - 1, kCFStringEncodingUTF8);
}
else
{
snprintf(axis.name, sizeof(axis.name), "Axis %i", m_axes.Size() + 1);
}
m_axes.Push(axis);
}
void IOKitJoystick::AddButton(CFDictionaryRef element)
{
const DigitalButton button(GetElementCookie(element));
m_buttons.Push(button);
AddToQueue(button.cookie);
}
void IOKitJoystick::AddPOV(CFDictionaryRef element)
{
const DigitalButton pov(GetElementCookie(element));
m_POVs.Push(pov);
AddToQueue(pov.cookie);
}
void IOKitJoystick::AddToQueue(const IOHIDElementCookie cookie)
{
if (NULL == m_queue)
{
return;
}
if (!(*m_queue)->hasElement(m_queue, cookie))
{
(*m_queue)->addElement(m_queue, cookie, 0);
}
}
void IOKitJoystick::RemoveFromQueue(const IOHIDElementCookie cookie)
{
if (NULL == m_queue)
{
return;
}
if ((*m_queue)->hasElement(m_queue, cookie))
{
(*m_queue)->removeElement(m_queue, cookie);
}
}
io_object_t* IOKitJoystick::GetNotificationPtr()
{
return &m_notification;
}
// ---------------------------------------------------------------------------
class IOKitJoystickManager
{
public:
IOKitJoystickManager();
~IOKitJoystickManager();
void GetJoysticks(TArray<IJoystickConfig*>& joysticks) const;
void AddAxes(float axes[NUM_JOYAXIS]) const;
// Updates axes/buttons states
void Update();
void UseAxesPolling(bool axesPolling);
private:
typedef TDeletingArray<IOKitJoystick*> JoystickList;
JoystickList m_joysticks;
static const size_t NOTIFICATION_PORT_COUNT = 2;
IONotificationPortRef m_notificationPorts[NOTIFICATION_PORT_COUNT];
io_iterator_t m_notifications [NOTIFICATION_PORT_COUNT];
// Rebuilds device list
void Rescan(int usagePage, int usage, size_t notificationPortIndex);
void AddDevices(IONotificationPortRef notificationPort, const io_iterator_t iterator);
static void OnDeviceAttached(void* refcon, io_iterator_t iterator);
static void OnDeviceRemoved(void* refcon, io_service_t service,
natural_t messageType, void* messageArgument);
};
IOKitJoystickManager* s_joystickManager;
IOKitJoystickManager::IOKitJoystickManager()
{
memset(m_notifications, 0, sizeof m_notifications);
for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i)
{
m_notificationPorts[i] = IONotificationPortCreate(kIOMasterPortDefault);
if (NULL == m_notificationPorts[i])
{
Printf(TEXTCOLOR_RED "IONotificationPortCreate(%zu) failed\n", i);
return;
}
CFRunLoopAddSource(CFRunLoopGetCurrent(),
IONotificationPortGetRunLoopSource(m_notificationPorts[i]), kCFRunLoopDefaultMode);
}
Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, 0);
Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, 1);
}
IOKitJoystickManager::~IOKitJoystickManager()
{
for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i)
{
IONotificationPortRef& port = m_notificationPorts[i];
if (NULL != port)
{
CFRunLoopRemoveSource(CFRunLoopGetCurrent(),
IONotificationPortGetRunLoopSource(port), kCFRunLoopDefaultMode);
IONotificationPortDestroy(port);
port = NULL;
}
io_iterator_t& notification = m_notifications[i];
if (0 != notification)
{
IOObjectRelease(notification);
notification = 0;
}
}
}
void IOKitJoystickManager::GetJoysticks(TArray<IJoystickConfig*>& joysticks) const
{
const size_t joystickCount = m_joysticks.Size();
joysticks.Resize(joystickCount);
for (size_t i = 0; i < joystickCount; ++i)
{
M_LoadJoystickConfig(m_joysticks[i]);
joysticks[i] = m_joysticks[i];
}
}
void IOKitJoystickManager::AddAxes(float axes[NUM_JOYAXIS]) const
{
for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
{
m_joysticks[i]->AddAxes(axes);
}
}
void IOKitJoystickManager::Update()
{
for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
{
m_joysticks[i]->Update();
}
}
void IOKitJoystickManager::UseAxesPolling(const bool axesPolling)
{
for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
{
m_joysticks[i]->UseAxesPolling(axesPolling);
}
}
void PostDeviceChangeEvent()
{
event_t event = { EV_DeviceChange };
D_PostEvent(&event);
}
void IOKitJoystickManager::Rescan(const int usagePage, const int usage, const size_t notificationPortIndex)
{
CFMutableDictionaryRef deviceMatching = IOServiceMatching(kIOHIDDeviceKey);
if (NULL == deviceMatching)
{
Printf(TEXTCOLOR_RED "IOServiceMatching() returned NULL\n");
return;
}
const CFNumberRef usagePageRef =
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usagePage);
CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsagePageKey), usagePageRef);
const CFNumberRef usageRef =
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsageKey), usageRef);
assert(notificationPortIndex < NOTIFICATION_PORT_COUNT);
io_iterator_t* iteratorPtr = &m_notifications[notificationPortIndex];
const IONotificationPortRef notificationPort = m_notificationPorts[notificationPortIndex];
assert(NULL != notificationPort);
const kern_return_t notificationResult = IOServiceAddMatchingNotification(notificationPort,
kIOFirstMatchNotification, deviceMatching, OnDeviceAttached, notificationPort, iteratorPtr);
// IOServiceAddMatchingNotification() consumes one reference of matching dictionary
// Thus CFRelease(deviceMatching) is not needed
CFRelease(usageRef);
CFRelease(usagePageRef);
if (KERN_SUCCESS != notificationResult)
{
Printf(TEXTCOLOR_RED "IOServiceAddMatchingNotification() failed with code %i\n", notificationResult);
}
AddDevices(notificationPort, *iteratorPtr);
}
void IOKitJoystickManager::AddDevices(const IONotificationPortRef notificationPort, const io_iterator_t iterator)
{
while (io_object_t device = IOIteratorNext(iterator))
{
IOKitJoystick* joystick = new IOKitJoystick(device);
m_joysticks.Push(joystick);
const kern_return_t notificationResult = IOServiceAddInterestNotification(notificationPort,
device, kIOGeneralInterest, OnDeviceRemoved, joystick, joystick->GetNotificationPtr());
if (KERN_SUCCESS != notificationResult)
{
Printf(TEXTCOLOR_RED "IOServiceAddInterestNotification() failed with code %i\n", notificationResult);
}
IOObjectRelease(device);
PostDeviceChangeEvent();
}
}
void IOKitJoystickManager::OnDeviceAttached(void* const refcon, const io_iterator_t iterator)
{
assert(NULL != refcon);
const IONotificationPortRef notificationPort = static_cast<IONotificationPortRef>(refcon);
assert(NULL != s_joystickManager);
s_joystickManager->AddDevices(notificationPort, iterator);
}
void IOKitJoystickManager::OnDeviceRemoved(void* const refcon, io_service_t, const natural_t messageType, void*)
{
if (messageType != kIOMessageServiceIsTerminated)
{
return;
}
assert(NULL != refcon);
IOKitJoystick* const joystick = static_cast<IOKitJoystick*>(refcon);
assert(NULL != s_joystickManager);
JoystickList& joysticks = s_joystickManager->m_joysticks;
for (unsigned int i = 0, count = joysticks.Size(); i < count; ++i)
{
if (joystick == joysticks[i])
{
joysticks.Delete(i);
break;
}
}
delete joystick;
PostDeviceChangeEvent();
}
} // unnamed namespace
// ---------------------------------------------------------------------------
void I_ShutdownInput()
{
delete s_joystickManager;
s_joystickManager = NULL;
}
void I_GetJoysticks(TArray<IJoystickConfig*>& sticks)
{
// Instances of IOKitJoystick depend on GameConfig object.
// M_SaveDefaultsFinal() must be called after destruction of IOKitJoystickManager.
// To ensure this, its initialization is moved here.
// As M_LoadDefaults() was already called at this moment,
// the order of atterm's functions will be correct
if (NULL == s_joystickManager && !Args->CheckParm("-nojoy"))
{
s_joystickManager = new IOKitJoystickManager;
}
if (NULL != s_joystickManager)
{
s_joystickManager->GetJoysticks(sticks);
}
}
void I_GetAxes(float axes[NUM_JOYAXIS])
{
memset(axes, 0, sizeof(float) * NUM_JOYAXIS);
if (use_joystick && NULL != s_joystickManager)
{
s_joystickManager->AddAxes(axes);
}
}
IJoystickConfig* I_UpdateDeviceList()
{
// Does nothing, device list is always kept up-to-date
return NULL;
}
// ---------------------------------------------------------------------------
void I_ProcessJoysticks()
{
if (NULL != s_joystickManager)
{
s_joystickManager->Update();
}
}
// ---------------------------------------------------------------------------
CUSTOM_CVAR(Bool, joy_axespolling, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL)
{
if (NULL != s_joystickManager)
{
s_joystickManager->UseAxesPolling(self);
}
}