Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Apple's GameController.framework as an input device #9745

Closed
wants to merge 10 commits into from

Conversation

dhannema
Copy link

Add support for input devices to be detected from Apple's GameController.framework.

So far, I have only tested this with a DualShock 4 and an Xbox One S controller.

This will allow Mac users to directly use accelerometer and gyroscope motion input (if available) without need for external DSU applications:
image

And will also allow Mac users to use the touchpad of the DS4 controller:
image


for (GCControllerButtonInput* item in controller.physicalInputProfile.allButtons)
{
pairs.push_back(std::make_pair(std::string([item.aliases.allObjects[0] UTF8String]), item));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pairs.push_back(std::make_pair(std::string([item.aliases.allObjects[0] UTF8String]), item));
pairs.emplace_back(std::string([item.aliases.allObjects[0] UTF8String], item);

Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is item.aliases.allObjects[0] guaranteed to be valid?
Feels like it should probably be checked, unless it's a c array.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://developer.apple.com/documentation/gamecontroller/gccontrollerelement, the aliases object is always present, but I will add a check if the array contains more than 0 entries

pairs.push_back(std::make_pair(std::string([item.aliases.allObjects[0] UTF8String]), item));
}

sort(pairs.begin(), pairs.end(), [=](std::pair<std::string, GCControllerButtonInput*>& a, std::pair<std::string, GCControllerButtonInput*>& b)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sort(pairs.begin(), pairs.end(), [=](std::pair<std::string, GCControllerButtonInput*>& a, std::pair<std::string, GCControllerButtonInput*>& b)
std::sort(pairs.begin(), pairs.end(), [](const auto& lhs, const auto& rhs)

AddInput(new Button(pairs[i].second));
}

if (controller.motion)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (controller.motion)
if (!controller.motion)
return;

Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference I guess, but I'd use m_controller instead of controller inside this function.


class Controller : public Core::Device
{
private:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private interface should be below the public interface.

class Motion : public Input
{
public:
enum button
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum button
enum class ButtonAxis

INFO_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={}", vendorName, controller.motion.sensorsActive);
NOTICE_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={} and id=", vendorName, controller.motion.sensorsActive);

std::shared_ptr<ciface::GameController::Controller> j=std::make_shared<Controller>(controller);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just collapse into:

Suggested change
std::shared_ptr<ciface::GameController::Controller> j=std::make_shared<Controller>(controller);
g_controller_interface.AddDevice(std::make_shared<Controller>(controller));

{
controller.motion.sensorsActive = true;

std::string vendorName = std::string([controller.vendorName UTF8String]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::string vendorName = std::string([controller.vendorName UTF8String]);
std::string vendor_name = std::string([controller.vendorName UTF8String]);

Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't particularly care but I think this string could be const


void Init(void* window)
{
CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), s_observer, &onControllerConnect, CFSTR("GCControllerDidConnectNotification"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using nullptr over NULL. Ditto for other applicable cases elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this trigger the callback for devices that are connection on Init, or does it only call it for devices that will be connected after binding to it? I guess it's the second case, but please specify it in a comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there is a space between the two functions here, but there isn't in DeInit()


namespace ciface::GameController
{
using MathUtil::GRAVITY_ACCELERATION; // 9.80665
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this into the constructor, given it's only used in there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not like GRAVITY_ACCELERATION is likely to change, but why copy its value in the comment?

// I was unable to use [GCController controllers] in Objective-C++ and therefore needed to access this class method using objc_msgSend instead
typedef NSArray<GCController*>* (*send_type)(id, SEL);
send_type func = (send_type)objc_msgSend;
NSArray<GCController*>* controllers=(NSArray<GCController*>*)func(objc_getClass("GCController"), sel_getUid("controllers"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NSArray<GCController*>* controllers=(NSArray<GCController*>*)func(objc_getClass("GCController"), sel_getUid("controllers"));
auto* controllers = func(objc_getClass("GCController"), sel_getUid("controllers"));

namespace ciface::GameController
{
void Init(void* window);
void PopulateDevices(void* window);
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the window ptr is never used.
It would be better without it, especially after the changes I made to the controller interface in my pending PR:
#9702

}

void DeInit()
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some safety measure to make sure DeInit doesn't break if called when Init hasn't been called? Or is that the case already?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked this and the CFNotificationCenterRemoveObserver methods in DeInit() can already be called multiple times without any problems, so it looks like an explicit safety is not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, maybe specify that in a comment, it wouldn't hurt.

Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does s_observer do? Is it assigned in Init? Maybe you could set it to null after DeInit?
Edit: nevermind.


std::string Controller::GetSource() const
{
return "GCF";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr somewhere


namespace ciface::GameController
{
using MathUtil::GRAVITY_ACCELERATION; // 9.80665
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not like GRAVITY_ACCELERATION is likely to change, but why copy its value in the comment?

@@ -16,6 +16,7 @@
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
#endif
#ifdef CIFACE_USE_OSX
#include "InputCommon/ControllerInterface/GameController/GameController.h"
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this PR a Draft (work in progress)? Init/PopulateDevices/DeInit aren't plugged in in the ControllerInterface.
I don't understand how you even tested this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry for the confusion. I own an M1 Mac and had to test/develop in skylersaleh's fork as I am unable to build Dolphin on the main branch.
I forgot to manually copy the other changes I made in ControllerInterface.cpp


namespace ciface::GameController
{
static char *s_observer="";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why assigning a "" to a ptr?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to ensure s_observer is an actual pointer to something. Otherwise the pointer will always be 0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand.
I've never allocated a ptr like that.
Does that create a new object and assign it to the pointer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand what this is, you should probably assign "dolphin" to that char array.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this a constexpr or a const which you then pass as a reference to the callbacks bindings functions?

Copy link
Member

@JosJuice JosJuice May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this does is to ensure that there is an empty string in the constant data segment of the binary and make the pointer point to it. If this is to be done, the variable should be marked const char* instead of just char*, because writing to such a string is not allowed. Or you can use Filoppi's suggestion of defining some simple variable (like an int) and then taking a pointer to it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked the manual (https://developer.apple.com/documentation/corefoundation/1543316-cfnotificationcenteraddobserver?language=objc) and it looks like I can completely remove the s_observer variable.
I will remove it and pass nullptr to the callback functions!


std::string vendorName = std::string([controller.vendorName UTF8String]);
INFO_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={}", vendorName, controller.motion.sensorsActive);
NOTICE_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={} and id=", vendorName, controller.motion.sensorsActive);
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is CONTROLLERINTERFACE now, I've recently added a new log type. Same for any other log you might have here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID for the notice log doesn't seem to be specified?
Do we really need two different logs of different verbosity?


g_controller_interface.RemoveDevice([&inController](const auto* obj)
{
const Controller* controller = dynamic_cast<const Controller*>(obj);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought dynamic_cast was forbidden in dolphin, how does this even build?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I borrowed this code from the already existing OSX.mm, so I guess it is not a problem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know it's there as well. Not a problem to me, but on MSVC if you try to add a dynamic_cast it refused to build as it's been disabled.


sort(pairs.begin(), pairs.end(), [=](std::pair<std::string, GCControllerButtonInput*>& a, std::pair<std::string, GCControllerButtonInput*>& b)
{
return a.first.compare(b.first)<0;
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return a.first.compare(b.first) < 0;

AddInput(new Button(pairs[i].second));
}

if (controller.motion)
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference I guess, but I'd use m_controller instead of controller inside this function.


for (GCControllerButtonInput* item in controller.physicalInputProfile.allButtons)
{
pairs.push_back(std::make_pair(std::string([item.aliases.allObjects[0] UTF8String]), item));
Copy link
Contributor

@Filoppi Filoppi May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is item.aliases.allObjects[0] guaranteed to be valid?
Feels like it should probably be checked, unless it's a c array.

AddInput(new Motion(controller, "Gyro Yaw Left", Motion::gyro_z, 1));
AddInput(new Motion(controller, "Gyro Yaw Right", Motion::gyro_z, -1));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra space


void Init(void* window)
{
CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), s_observer, &onControllerConnect, CFSTR("GCControllerDidConnectNotification"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there is a space between the two functions here, but there isn't in DeInit()

// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <random>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used?


void addController(GCController* controller)
{
controller.motion.sensorsActive = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Why is it not "deactivated" on disconnection? Is it relevant? Persistent?


std::string vendorName = std::string([controller.vendorName UTF8String]);
INFO_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={}", vendorName, controller.motion.sensorsActive);
NOTICE_LOG_FMT(SERIALINTERFACE, "Controller '{}' connected with motion={} and id=", vendorName, controller.motion.sensorsActive);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID for the notice log doesn't seem to be specified?
Do we really need two different logs of different verbosity?


for (GCController* controller in controllers)
{
addController(controller);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should remove all devices which have the same source name as ours before re-adding them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Filoppi during my tests, it seems like this is working properly. When I click 'Refresh' in the Device popup screen the controllers are added properly again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does work properly in the way the controller interface is currently setup, meaning that it cleans all devices before calling PopulateDevices() on each input backend, but it doesn't hurt to also remove your devices manually, and some input backends do. It would avoid duplicates if the controller interface design ever changed.

@skylersaleh
Copy link
Contributor

skylersaleh commented Jun 2, 2021

@dhannema Could you please rebase your PR to the latest master to pick up the changes to support M1 builds, so that the osx-universal buildbot will be able to run?

{
static void AddController(GCController* controller)
{
if (!@available(macOS 11.0, *)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't negate an @available check. An alternative might be

if (@available(macOS 11.0, *))
{
}
else
{
    INFO_LOG_FMT(...);
    return;
}

But admittedly, it is a bit ugly.

@canesalato
Copy link

I tested this on my M1 MacBook and it seems to work well, both when running native (arm) and under Rosetta2

@adityajs12321
Copy link

Tested this on my m1 mac and a DualShock4, can confirm it works well

@ghost
Copy link

ghost commented May 17, 2022

Can confirm this works even with a DS5 (DualSense) controller!

@Rumi-Larry
Copy link

Protip: When making small changes, is always better to ammend the main commits than create a new one... Also hit the weak spot for massive damage!

@OatmealDome OatmealDome mentioned this pull request Jun 3, 2022
@jordan-woyak
Copy link
Member

SDL should now provide accelerometer/gyroscope input.
Does this "GameController Famework" provide any additional functionality on macOS over our SDL input backend?
Unfortunately macOS-specific code can tend to quickly become unmaintained in dolphin so if SDL does everything we need I think it might be best to just rely on SDL for macOS gamepad input.

@dhannema
Copy link
Author

Does this "GameController Famework" provide any additional functionality on macOS over our SDL input backend?

You are correct. We are 3 years later and indeed SDL also provides this functionality now.
This PR can be closed !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
10 participants