// Copyright 2013 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "InputCommon/ControllerInterface/OSX/OSXJoystick.h" #include #include #include #include #include "Common/Logging/Log.h" #include "Common/StringUtil.h" namespace ciface::OSX { void Joystick::AddElements(CFArrayRef elements, std::set& cookies) { for (int i = 0; i < CFArrayGetCount(elements); i++) { IOHIDElementRef e = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i); const uint32_t type = IOHIDElementGetType(e); switch (type) { case kIOHIDElementTypeCollection: AddElements(IOHIDElementGetChildren(e), cookies); continue; case kIOHIDElementTypeOutput: continue; } IOHIDElementCookie cookie = IOHIDElementGetCookie(e); // Check for any existing elements with the same cookie if (cookies.count(cookie) > 0) continue; cookies.insert(cookie); const uint32_t usage = IOHIDElementGetUsage(e); switch (usage) { // Axis case kHIDUsage_GD_X: case kHIDUsage_GD_Y: case kHIDUsage_GD_Z: case kHIDUsage_GD_Rx: case kHIDUsage_GD_Ry: case kHIDUsage_GD_Rz: case kHIDUsage_GD_Slider: case kHIDUsage_GD_Dial: case kHIDUsage_GD_Wheel: case kHIDUsage_GD_Hatswitch: // Simulator case kHIDUsage_Sim_Accelerator: case kHIDUsage_Sim_Brake: case kHIDUsage_Sim_Rudder: case kHIDUsage_Sim_Throttle: { if (usage == kHIDUsage_GD_Hatswitch) { AddInput(new Hat(e, m_device, Hat::up)); AddInput(new Hat(e, m_device, Hat::right)); AddInput(new Hat(e, m_device, Hat::down)); AddInput(new Hat(e, m_device, Hat::left)); } else { AddAnalogInputs(new Axis(e, m_device, Axis::negative), new Axis(e, m_device, Axis::positive)); } break; } // Buttons case kHIDUsage_GD_DPadUp: case kHIDUsage_GD_DPadDown: case kHIDUsage_GD_DPadRight: case kHIDUsage_GD_DPadLeft: case kHIDUsage_GD_Start: case kHIDUsage_GD_Select: case kHIDUsage_GD_SystemMainMenu: AddInput(new Button(e, m_device)); break; default: // Catch any easily identifiable axis and buttons that slipped through if (type == kIOHIDElementTypeInput_Button) { AddInput(new Button(e, m_device)); break; } const uint32_t usage_page = IOHIDElementGetUsagePage(e); if (usage_page == kHIDPage_Button || usage_page == kHIDPage_Consumer) { AddInput(new Button(e, m_device)); break; } if (type == kIOHIDElementTypeInput_Axis) { AddAnalogInputs(new Axis(e, m_device, Axis::negative), new Axis(e, m_device, Axis::positive)); break; } NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Unknown IOHIDElement, ignoring (Usage: {:x}, Type: {:x})", usage, IOHIDElementGetType(e)); break; } } } Joystick::Joystick(IOHIDDeviceRef device, std::string name) : m_device(device), m_device_name(name), m_ff_device(nullptr) { CFArrayRef elements = IOHIDDeviceCopyMatchingElements(m_device, nullptr, kIOHIDOptionsTypeNone); std::set known_cookies; AddElements(elements, known_cookies); // Force Feedback FFCAPABILITIES ff_caps; if (SUCCEEDED( ForceFeedback::FFDeviceAdapter::Create(IOHIDDeviceGetService(m_device), &m_ff_device)) && SUCCEEDED(FFDeviceGetForceFeedbackCapabilities(m_ff_device->m_device, &ff_caps))) { InitForceFeedback(m_ff_device, ff_caps.numFfAxes); } } Joystick::~Joystick() { DeInitForceFeedback(); if (m_ff_device) m_ff_device->Release(); } std::string Joystick::GetName() const { return m_device_name; } std::string Joystick::GetSource() const { return "Input"; } ControlState Joystick::Button::GetState() const { IOHIDValueRef value; if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess) return IOHIDValueGetIntegerValue(value); else return 0; } std::string Joystick::Button::GetName() const { std::ostringstream s; s << IOHIDElementGetUsage(m_element); return std::string("Button ").append(StripSpaces(s.str())); } Joystick::Axis::Axis(IOHIDElementRef element, IOHIDDeviceRef device, direction dir) : m_element(element), m_device(device), m_direction(dir) { // Need to parse the element a bit first std::string description("unk"); int const usage = IOHIDElementGetUsage(m_element); switch (usage) { case kHIDUsage_GD_X: description = "X"; break; case kHIDUsage_GD_Y: description = "Y"; break; case kHIDUsage_GD_Z: description = "Z"; break; case kHIDUsage_GD_Rx: description = "Rx"; break; case kHIDUsage_GD_Ry: description = "Ry"; break; case kHIDUsage_GD_Rz: description = "Rz"; break; case kHIDUsage_GD_Wheel: description = "Wheel"; break; case kHIDUsage_Csmr_ACPan: description = "Pan"; break; default: { IOHIDElementCookie elementCookie = IOHIDElementGetCookie(m_element); // This axis isn't a 'well-known' one so cook a descriptive and uniquely // identifiable name. macOS provides a 'cookie' for each element that // will persist between sessions and identify the same physical controller // element so we can use that as a component of the axis name std::ostringstream s; s << "CK-"; s << elementCookie; description = StripSpaces(s.str()); break; } } m_name = std::string("Axis ") + description; m_name.append((m_direction == positive) ? "+" : "-"); m_neutral = (IOHIDElementGetLogicalMax(m_element) + IOHIDElementGetLogicalMin(m_element)) / 2.; m_scale = 1 / fabs(IOHIDElementGetLogicalMax(m_element) - m_neutral); } ControlState Joystick::Axis::GetState() const { IOHIDValueRef value; if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess) { // IOHIDValueGetIntegerValue() crashes when trying // to convert unusually large element values. if (IOHIDValueGetLength(value) > 2) return 0; float position = IOHIDValueGetIntegerValue(value); if (m_direction == positive && position > m_neutral) return (position - m_neutral) * m_scale; if (m_direction == negative && position < m_neutral) return (m_neutral - position) * m_scale; } return 0; } std::string Joystick::Axis::GetName() const { return m_name; } Joystick::Hat::Hat(IOHIDElementRef element, IOHIDDeviceRef device, direction dir) : m_element(element), m_device(device), m_direction(dir) { switch (dir) { case up: m_name = "Up"; break; case right: m_name = "Right"; break; case down: m_name = "Down"; break; case left: m_name = "Left"; break; default: m_name = "unk"; } } ControlState Joystick::Hat::GetState() const { IOHIDValueRef value; if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess) { int position = IOHIDValueGetIntegerValue(value); int min = IOHIDElementGetLogicalMin(m_element); int max = IOHIDElementGetLogicalMax(m_element); // if the position is outside the min or max, don't register it as a valid button press if (position < min || position > max) { return 0; } // normalize the position so that its lowest value is 0 position -= min; switch (position) { case 0: if (m_direction == up) return 1; break; case 1: if (m_direction == up || m_direction == right) return 1; break; case 2: if (m_direction == right) return 1; break; case 3: if (m_direction == right || m_direction == down) return 1; break; case 4: if (m_direction == down) return 1; break; case 5: if (m_direction == down || m_direction == left) return 1; break; case 6: if (m_direction == left) return 1; break; case 7: if (m_direction == left || m_direction == up) return 1; break; }; } return 0; } std::string Joystick::Hat::GetName() const { return m_name; } bool Joystick::IsSameDevice(const IOHIDDeviceRef other_device) const { return m_device == other_device; } } // namespace ciface::OSX