diff --git a/CMakeLists.txt b/CMakeLists.txt index e672839c482..f95ce678840 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -652,6 +652,7 @@ if(NOT DEFINED CMAKE_CUDA_STANDARD) endif() if(APPLE) + target_link_libraries(sunshine "-framework IOKit") target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${APPLE_PLIST_FILE}) # Tell linker to dynamically load these symbols at runtime, in case they're unavailable: target_link_options(sunshine PRIVATE -Wl,-U,_CGPreflightScreenCaptureAccess -Wl,-U,_CGRequestScreenCaptureAccess) diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst index dede1f22d0c..9cf81352eb3 100644 --- a/docs/source/about/installation.rst +++ b/docs/source/about/installation.rst @@ -188,7 +188,6 @@ Uninstall: macOS ----- -Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected. pkg ^^^ diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index db7af09ea7e..262385cd6b5 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -144,14 +144,14 @@ Sunshine needs access to `uinput` to create mouse and gamepad events. macOS ^^^^^ +For gamepad support, install `VirtualHID `_ + Sunshine can only access microphones on macOS due to system limitations. To stream system audio use `Soundflower `_ or `BlackHole `_. .. Note:: Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. -.. Caution:: Gamepads are not currently supported. - Configure autostart service **MacPorts** .. code-block:: bash diff --git a/docs/source/troubleshooting/macos.rst b/docs/source/troubleshooting/macos.rst index 551f6945f4a..8bfe17ce916 100644 --- a/docs/source/troubleshooting/macos.rst +++ b/docs/source/troubleshooting/macos.rst @@ -11,3 +11,14 @@ If you get this error: .. code-block:: bash launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist + +No gamepad detected +------------------- +#. Verify that you've installed `VirtualHID `_. + +Unable to create HID device +--------------------------- +If you get this error: + `Gamepad: Unable to create HID device. May be fine if created previously.` + + It's okay. It's just a warning. diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index fb5fb13a8f0..d34be7a919c 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -62,4 +62,5 @@ pre-build { notes-append "Run @PROJECT_NAME@ by executing 'sunshine ', e.g. 'sunshine ~/sunshine.conf' " notes-append "The config file will be created if it doesn't exist." notes-append "It is recommended to set a location for the apps file in the config." +notes-append "For gamepad support, install VirtualHID from 'https://github.com/kotleni/VirtualHID-macOS/releases/latest'." notes-append "See our documentation at 'https://docs.lizardbyte.dev/projects/sunshine/en/v@PROJECT_VERSION@/' for further info." diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index ff59f4fb293..5edc3f35e76 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -2,6 +2,10 @@ #include #include +// For gamepad emulation +#include +#include + #include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" @@ -10,6 +14,86 @@ // FIXME: we probably want to make this configurable #define MULTICLICK_DELAY_NS 500000000 +// For gamepad emulation +// https://github.com/kotleni/foohid +#define VIRTGAMEPAD_NAME "DualShock 4" // gamepad name +#define VIRTGAMEPAD_SN "CUH-ZCT1x" // serial number +#define VIRTGAMEPAD_INPUT_COUNT 8 // device arguments count +#define SERVICE_NAME "it_unbit_foohid" // virthid service id +#define FOOHID_CREATE 0 // create selector +#define FOOHID_SEND 2 // send selector + +// Gamepad buttons +#define GAMEPAD_ZEROBTNS 0 +#define GAMEPAD_BTN_A (1 << 0) +#define GAMEPAD_BTN_B (1 << 1) +#define GAMEPAD_BTN_X (1 << 2) +#define GAMEPAD_BTN_Y (1 << 3) +#define GAMEPAD_BTN_L (1 << 4) +#define GAMEPAD_BTN_R (1 << 5) +#define GAMEPAD_BTN_LT (1 << 6) +#define GAMEPAD_BTN_RT (1 << 7) +#define GAMEPAD_BTN_BACK (1 << 8) +#define GAMEPAD_BTN_START (1 << 9) +#define GAMEPAD_BTN_LS (1 << 10) +#define GAMEPAD_BTN_RS (1 << 11) +#define GAMEPAD_BTN_UP (1 << 12) +#define GAMEPAD_BTN_DOWN (1 << 13) +#define GAMEPAD_BTN_LEFT (1 << 14) +#define GAMEPAD_BTN_RIGHT (1 << 15) + +// http://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/ +// Used HIDTool to generate the data +unsigned char gamepad_report_descriptor[] = { + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x05, // USAGE (Game Pad) + 0xa1, 0x01, // COLLECTION (Application) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x33, // USAGE (Rx) + 0x09, 0x34, // USAGE (Ry) + 0x16, 0x00, 0x80, // LOGICAL_MINIMUM (-32768) + 0x26, 0xff, 0x7f, // LOGICAL_MAXIMUM (32767) + 0x75, 0x10, // REPORT_SIZE (16) + 0x95, 0x04, // REPORT_COUNT (4) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x32, // USAGE (Z) + 0x09, 0x35, // USAGE (Rz) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x46, 0xff, 0x00, // PHYSICAL_MAXIMUM (255) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x02, // REPORT_COUNT (2) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x10, // USAGE_MAXIMUM (Button 16) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x10, // REPORT_COUNT (16) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + 0xc0 // END_COLLECTION +}; + +struct gamepad_report_t { + int16_t x; + int16_t y; + int16_t rx; + int16_t ry; + uint8_t z; + uint8_t rz; + uint16_t buttons; +}; + +io_iterator_t virtgamepad_iterator; +io_service_t virtgamepad_service; +io_connect_t virtgamepad_connect; +uint64_t virtgamepad_input[VIRTGAMEPAD_INPUT_COUNT]; + namespace platf { using namespace std::literals; @@ -282,16 +366,99 @@ void unicode(input_t &input, char *utf8, int size) { } int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { - BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; - return -1; + // Get a reference to the IOService + kern_return_t ret = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(SERVICE_NAME), &virtgamepad_iterator); + + if(ret != KERN_SUCCESS) { + BOOST_LOG(info) << "Gamepad: Unable to access IOService."sv; + return 1; + } + + // Iterate till success + int found = 0; + while((virtgamepad_service = IOIteratorNext(virtgamepad_iterator)) != IO_OBJECT_NULL) { + ret = IOServiceOpen(virtgamepad_service, mach_task_self(), 0, &virtgamepad_connect); + if(ret == KERN_SUCCESS) { + found = 1; + break; + } + + IOObjectRelease(virtgamepad_service); + } + + IOObjectRelease(virtgamepad_iterator); + + if(!found) { + BOOST_LOG(info) << "Gamepad: Unable to open IOService."sv; + return 1; + } + + // Fill up the input arguments. + virtgamepad_input[0] = (uint64_t)strdup(VIRTGAMEPAD_NAME); // device name + virtgamepad_input[1] = strlen((char *)virtgamepad_input[0]); // name length + virtgamepad_input[2] = (uint64_t)gamepad_report_descriptor; // report descriptor + virtgamepad_input[3] = sizeof(gamepad_report_descriptor); // report descriptor len + virtgamepad_input[4] = (uint64_t)strdup(VIRTGAMEPAD_SN); // serial number + virtgamepad_input[5] = strlen((char *)virtgamepad_input[4]); // serial number len + virtgamepad_input[6] = (uint64_t)0x054c; // vendor ID + virtgamepad_input[7] = (uint64_t)0x05c4; // device ID + + ret = IOConnectCallScalarMethod(virtgamepad_connect, FOOHID_CREATE, virtgamepad_input, VIRTGAMEPAD_INPUT_COUNT, NULL, 0); + if(ret != KERN_SUCCESS) { + BOOST_LOG(info) << "Gamepad: Unable to create HID device. May be fine if created previously."sv; + } + return 0; } void free_gamepad(input_t &input, int nr) { - BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; + IOObjectRelease(virtgamepad_connect); } void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; + // Arguments to be passed through the HID message. + struct gamepad_report_t gamepad; + uint32_t send_count = 4; + uint64_t send[send_count]; + send[0] = (uint64_t)virtgamepad_input[0]; // device name + send[1] = strlen((char *)virtgamepad_input[0]); // name length + send[2] = (uint64_t)&gamepad; // mouse struct + send[3] = sizeof(struct gamepad_report_t); // mouse struct len + + auto flags = gamepad_state.buttonFlags; + + // reset buttons + gamepad.buttons = GAMEPAD_ZEROBTNS; + + // Map buttons + if(flags & LEFT_STICK) gamepad.buttons |= GAMEPAD_BTN_LS; + if(flags & RIGHT_STICK) gamepad.buttons |= GAMEPAD_BTN_RS; + if(flags & LEFT_BUTTON) gamepad.buttons |= GAMEPAD_BTN_L; + if(flags & RIGHT_BUTTON) gamepad.buttons |= GAMEPAD_BTN_R; + if(flags & START) gamepad.buttons |= GAMEPAD_BTN_START; + if(flags & BACK) gamepad.buttons |= GAMEPAD_BTN_BACK; + if(flags & A) gamepad.buttons |= GAMEPAD_BTN_A; + if(flags & B) gamepad.buttons |= GAMEPAD_BTN_B; + if(flags & X) gamepad.buttons |= GAMEPAD_BTN_X; + if(flags & Y) gamepad.buttons |= GAMEPAD_BTN_Y; + if(flags & DPAD_UP) gamepad.buttons |= GAMEPAD_BTN_UP; + if(flags & DPAD_DOWN) gamepad.buttons |= GAMEPAD_BTN_DOWN; + if(flags & DPAD_LEFT) gamepad.buttons |= GAMEPAD_BTN_LEFT; + if(flags & DPAD_RIGHT) gamepad.buttons |= GAMEPAD_BTN_RIGHT; + + // Map triggers + if(gamepad_state.lt > 0) gamepad.buttons |= GAMEPAD_BTN_LT; + if(gamepad_state.rt > 0) gamepad.buttons |= GAMEPAD_BTN_RT; + + // Map sticks + gamepad.x = gamepad_state.lsX; + gamepad.y = -gamepad_state.lsY; + gamepad.rx = gamepad_state.rsX; + gamepad.ry = -gamepad_state.rsY; + + kern_return_t ret = IOConnectCallScalarMethod(virtgamepad_connect, FOOHID_SEND, send, send_count, NULL, 0); + if(ret != KERN_SUCCESS) { + BOOST_LOG(info) << "Gamepad: Unable to send message to HID device."sv; + } } // returns current mouse location: