#include "config.h"
#include "HIDGamepad.h"
#if ENABLE(GAMEPAD) && PLATFORM(MAC)
#include <IOKit/hid/IOHIDElement.h>
#include <IOKit/hid/IOHIDUsageTables.h>
#include <IOKit/hid/IOHIDValue.h>
#include <wtf/CurrentTime.h>
#include <wtf/cf/TypeCastsCF.h>
#include <wtf/text/CString.h>
#include <wtf/text/WTFString.h>
WTF_DECLARE_CF_TYPE_TRAIT(IOHIDElement);
namespace WebCore {
HIDGamepad::HIDGamepad(IOHIDDeviceRef hidDevice, unsigned index)
: PlatformGamepad(index)
, m_hidDevice(hidDevice)
{
m_connectTime = m_lastUpdateTime = monotonicallyIncreasingTime();
CFNumberRef cfVendorID = (CFNumberRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));
CFNumberRef cfProductID = (CFNumberRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));
int vendorID, productID;
CFNumberGetValue(cfVendorID, kCFNumberIntType, &vendorID);
CFNumberGetValue(cfProductID, kCFNumberIntType, &productID);
CFStringRef cfProductName = (CFStringRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));
String productName(cfProductName);
m_id = String::format("%x-%x-%s", vendorID, productID, productName.utf8().data());
initElements();
}
void HIDGamepad::getCurrentValueForElement(const HIDGamepadElement& gamepadElement)
{
IOHIDElementRef element = gamepadElement.iohidElement.get();
IOHIDValueRef value;
if (IOHIDDeviceGetValue(IOHIDElementGetDevice(element), element, &value) == kIOReturnSuccess)
valueChanged(value);
}
void HIDGamepad::initElements()
{
RetainPtr<CFArrayRef> elements = adoptCF(IOHIDDeviceCopyMatchingElements(m_hidDevice.get(), NULL, kIOHIDOptionsTypeNone));
initElementsFromArray(elements.get());
std::sort(m_buttons.begin(), m_buttons.end(), [](auto& a, auto& b) {
return a->priority < b->priority;
});
m_axisValues.resize(m_axes.size());
m_buttonValues.resize(m_buttons.size() + (m_dPads.size() * 4));
for (auto& button : m_buttons)
getCurrentValueForElement(button.get());
for (auto& dPad : m_dPads)
getCurrentValueForElement(dPad.get());
for (auto& axis : m_axes)
getCurrentValueForElement(axis.get());
}
void HIDGamepad::initElementsFromArray(CFArrayRef elements)
{
for (CFIndex i = 0, count = CFArrayGetCount(elements); i < count; ++i) {
IOHIDElementRef element = checked_cf_cast<IOHIDElementRef>(CFArrayGetValueAtIndex(elements, i));
if (CFGetTypeID(element) != IOHIDElementGetTypeID())
continue;
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
if (m_elementMap.contains(cookie))
continue;
IOHIDElementType type = IOHIDElementGetType(element);
if ((type == kIOHIDElementTypeInput_Misc || type == kIOHIDElementTypeInput_Button)) {
if (maybeAddButton(element))
continue;
if (maybeAddDPad(element))
continue;
}
if ((type == kIOHIDElementTypeInput_Misc || type == kIOHIDElementTypeInput_Axis) && maybeAddAxis(element))
continue;
if (type == kIOHIDElementTypeCollection)
initElementsFromArray(IOHIDElementGetChildren(element));
}
}
bool HIDGamepad::maybeAddButton(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_Button && usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
if (usagePage == kHIDPage_GenericDesktop) {
if (usage < kHIDUsage_GD_DPadUp || usage > kHIDUsage_GD_DPadLeft)
return false;
usage = std::numeric_limits<uint32_t>::max();
} else if (!usage)
return false;
CFIndex min = IOHIDElementGetLogicalMin(element);
CFIndex max = IOHIDElementGetLogicalMax(element);
m_buttons.append(makeUniqueRef<HIDGamepadButton>(usage, min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_buttons.last().get());
return true;
}
bool HIDGamepad::maybeAddDPad(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
if (!usage || usage != kHIDUsage_GD_Hatswitch)
return false;
CFIndex min = IOHIDElementGetLogicalMin(element);
CFIndex max = IOHIDElementGetLogicalMax(element);
m_dPads.append(makeUniqueRef<HIDGamepadDPad>(min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_dPads.last().get());
return true;
}
bool HIDGamepad::maybeAddAxis(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
if (usage < kHIDUsage_GD_X || usage > kHIDUsage_GD_Rz)
return false;
CFIndex min = IOHIDElementGetPhysicalMin(element);
CFIndex max = IOHIDElementGetPhysicalMax(element);
m_axes.append(makeUniqueRef<HIDGamepadAxis>(min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_axes.last().get());
return true;
}
static void fillInButtonValues(int value, double& button0, double& button1, double& button2, double& button3)
{
if (value < 0 || value > 7) {
button0 = 0.0;
button1 = 0.0;
button2 = 0.0;
button3 = 0.0;
return;
}
button0 = value < 2 || value == 7 ? 1.0 : 0.0;
button1 = value > 2 && value < 6 ? 1.0 : 0.0;
button2 = value > 4 ? 1.0 : 0.0;
button3 = value > 0 && value < 4 ? 1.0 : 0.0;
}
HIDInputType HIDGamepad::valueChanged(IOHIDValueRef value)
{
IOHIDElementCookie cookie = IOHIDElementGetCookie(IOHIDValueGetElement(value));
HIDGamepadElement* element = m_elementMap.get(cookie);
if (!element)
return HIDInputType::NotAButtonPress;
element->rawValue = IOHIDValueGetScaledValue(value, kIOHIDValueScaleTypePhysical);
if (element->isButton()) {
for (unsigned i = 0; i < m_buttons.size(); ++i) {
if (&m_buttons[i].get() == element) {
m_buttonValues[i] = element->normalizedValue();
break;
}
}
} else if (element->isAxis()) {
for (unsigned i = 0; i < m_axes.size(); ++i) {
if (&m_axes[i].get() == element) {
m_axisValues[i] = element->normalizedValue();
break;
}
}
} else if (element->isDPad()) {
int intValue = IOHIDValueGetIntegerValue(value) - element->min;
for (unsigned i = 0; i < m_dPads.size(); ++i) {
if (&m_dPads[i].get() != element)
continue;
unsigned firstButtonValue = m_buttons.size() + i * 4;
ASSERT(m_buttonValues.size() > firstButtonValue + 3);
fillInButtonValues(intValue, m_buttonValues[firstButtonValue], m_buttonValues[firstButtonValue + 1], m_buttonValues[firstButtonValue + 2], m_buttonValues[firstButtonValue + 3]);
}
} else
ASSERT_NOT_REACHED();
m_lastUpdateTime = monotonicallyIncreasingTime();
return element->isButton() ? HIDInputType::ButtonPress : HIDInputType::NotAButtonPress;
}
}
#endif // ENABLE(GAMEPAD) && PLATFORM(MAC)