The Shadow PC app uses SDL2 for gamepad recognition. This has the advantage that you can change the button mapping of recognized controllers. You can add a custom configuration for each SDL2 application using the SDL_GAMECONTROLLERCONFIG
environment variable. The format is relatively simple:
GUID,Name,Mapping
Currently, I am having issues with Xbox 360 controllers (a Xim Matrix which is using the PC XInput mode) on macOS 15, as they are recognized twice in the Shadow PC app with SDL version 2.24.2 (as of Nov 23, 2024). The problem is described here: GitHub Issue #11002. Since I do not want to wait too long for an update, I use the following mapping to "break" the 360 controller, so no duplicate inputs are sent to Shadow PC:
030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X
To get the correct GUID of the duplicate controllers so that I can disable the faulty one, I wrote my own tool. For this, the correct SDL2 framework must be installed on macOS. I followed this guide: https://github.com/rosejoshua/QuickSDL2Mac
With the help of ChatGPT, I built a test controller app that also reads the mapping config on Shadow. This allowed me to test how SDL2 applications respond and whether my mapping works in the end:
#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>
#include <string>
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS) != 0) {
std::cerr << "SDL could not be initialized: " << SDL_GetError() << std::endl;
return 1;
}
// Load the gamecontroller mapping file
std::string mappingFilePath = "/Applications/Shadow PC.app/Contents/Resources/app.asar.unpacked/release/native/ShadowPCDisplay.app/Contents/Resources/gamecontrollerdb.txt";
int mappingsAdded = SDL_GameControllerAddMappingsFromFile(mappingFilePath.c_str());
if (mappingsAdded == -1) {
std::cerr << "Error loading gamecontroller mapping file: " << SDL_GetError() << std::endl;
} else {
std::cout << "Gamecontroller mapping file successfully loaded. Number of mappings added: " << mappingsAdded << std::endl;
}
int numJoysticks = SDL_NumJoysticks();
for (int i = 0; i < numJoysticks; ++i) {
if (SDL_IsGameController(i)) {
SDL_GameController* controller = SDL_GameControllerOpen(i);
if (controller) {
SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
char guidStr[33];
SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
std::cout << "Controller " << i << " connected: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << std::endl;
if (SDL_GameControllerMapping(controller)) {
std::cout << " Mapping: " << SDL_GameControllerMapping(controller) << std::endl;
} else {
std::cout << " No valid mapping information available." << std::endl;
}
}
}
}
bool running = true;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
running = false;
break;
case SDL_CONTROLLERDEVICEADDED: {
SDL_GameController* controller = SDL_GameControllerOpen(event.cdevice.which);
if (controller) {
SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
char guidStr[33];
SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
std::cout << "New controller connected: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << std::endl;
if (SDL_GameControllerMapping(controller)) {
std::cout << " Mapping: " << SDL_GameControllerMapping(controller) << std::endl;
} else {
std::cout << " No valid mapping information available." << std::endl;
}
}
break;
}
case SDL_CONTROLLERDEVICEREMOVED:
std::cout << "Controller removed: Instance ID " << event.cdevice.which << std::endl;
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP: {
SDL_GameController* controller = SDL_GameControllerFromInstanceID(event.cbutton.which);
if (controller) {
SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
char guidStr[33];
SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
std::cout << "Controller Button Event: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << " Button: " << static_cast<int>(event.cbutton.button) << " Status: " << (event.type == SDL_CONTROLLERBUTTONDOWN ? "pressed" : "released") << std::endl;
}
break;
}
case SDL_CONTROLLERAXISMOTION: {
SDL_GameController* controller = SDL_GameControllerFromInstanceID(event.caxis.which);
if (controller) {
SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
char guidStr[33];
SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
const char* axisName;
switch (event.caxis.axis) {
case SDL_CONTROLLER_AXIS_LEFTX:
axisName = "Left Stick X";
break;
case SDL_CONTROLLER_AXIS_LEFTY:
axisName = "Left Stick Y";
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
axisName = "Right Stick X";
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
axisName = "Right Stick Y";
break;
default:
axisName = "Unknown Axis";
break;
}
std::cout << "Controller Axis Event: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << " " << axisName << " Value: " << event.caxis.value << std::endl;
}
break;
}
}
}
}
SDL_Quit();
return 0;
}
To ensure that every macOS application and Terminal has this environment variable set, you need to create a .plist
file in ~/Library/LaunchAgents/
. I named it com.user.sdlcontroller.plist
.
The content of the .plist
file is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.sdlcontroller</string>
<key>ProgramArguments</key>
<array>
<string>launchctl</string>
<string>setenv</string>
<string>SDL_GAMECONTROLLERCONFIG</string>
<string>030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Then, the .plist
file needs to be loaded once using launchctl
. This can be done via Terminal:
launchctl load ~/Library/LaunchAgents/com.user.sdlcontroller.plist
Now, a restart is enough, and the environment variable should always be set. You can verify it in Terminal using the command:
export
Now every SDL2 application should load and process this additional configuration. This helps me currently, as the controller is still recognized twice, but only one of the controllers responds directly when pressing buttons on Shadow PC.
Here is an example of a mapping string that you can adjust:
030000005e0400008e02000014010000,Xbox One Controller,a:b1,b:b2,x:b3,y:b4,back:b6,start:b7,leftstick:b8,rightstick:b9,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5
This string defines the button and axis assignments for an Xbox One controller. You can adjust the values to achieve the desired button mapping.
Another example is for disabling the duplicate Xbox 360 controller for a Xim Matrix in PC XInput mode on macOS 15 Sequoia:
030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X
With these steps, you can adjust the button mapping of your gamepad for the Shadow PC app and other SDL2 Apps and ensure that everything works as desired. It requires some experimentation, but by using SDL2, you have full control over the configuration of your gamepad.
Good luck and have fun gaming!