From 1bcdb35e42b5b54b661ea8e3a48ef8950940a5ec Mon Sep 17 00:00:00 2001 From: Nicholas F <51171750+rluxv@users.noreply.github.com> Date: Thu, 11 May 2023 01:19:44 -0400 Subject: [PATCH] Add GameMode support for Linux build (#796) --- CMakeLists.txt | 11 + dependencies/gamemode/CMakeLists.txt | 4 + dependencies/gamemode/lib/client_loader.c | 35 ++ dependencies/gamemode/lib/gamemode_client.h | 376 ++++++++++++++++++++ src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 1 + src/gui/CMakeLists.txt | 5 + src/gui/GeneralSettings2.cpp | 11 +- src/gui/GeneralSettings2.h | 1 + src/gui/MainWindow.cpp | 22 ++ 10 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 dependencies/gamemode/CMakeLists.txt create mode 100644 dependencies/gamemode/lib/client_loader.c create mode 100644 dependencies/gamemode/lib/gamemode_client.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a556ff79..2ce81a04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,12 +76,14 @@ endif() if (UNIX AND NOT APPLE) option(ENABLE_WAYLAND "Build with Wayland support" ON) + option(ENABLE_FERAL_GAMEMODE "Enables Feral Interactive GameMode Support" ON) endif() option(ENABLE_OPENGL "Enables the OpenGL backend" ON) option(ENABLE_VULKAN "Enables the Vulkan backend" ON) option(ENABLE_DISCORD_RPC "Enables the Discord Rich Presence feature" ON) + # input backends if (WIN32) option(ENABLE_XINPUT "Enables the usage of XInput" ON) @@ -127,6 +129,7 @@ if (UNIX AND NOT APPLE) add_compile_definitions(HAS_WAYLAND) endif() find_package(GTK3 REQUIRED) + endif() if (ENABLE_VULKAN) @@ -143,6 +146,14 @@ if (ENABLE_DISCORD_RPC) target_include_directories(discord-rpc INTERFACE ./dependencies/discord-rpc/include) endif() +if(UNIX AND NOT APPLE) + if(ENABLE_FERAL_GAMEMODE) + add_compile_definitions(ENABLE_FERAL_GAMEMODE) + add_subdirectory(dependencies/gamemode EXCLUDE_FROM_ALL) + target_include_directories(gamemode INTERFACE ./dependencies/gamemode/lib) + endif() +endif() + if (ENABLE_WXWIDGETS) find_package(wxWidgets 3.2 REQUIRED COMPONENTS base core gl propgrid xrc) endif() diff --git a/dependencies/gamemode/CMakeLists.txt b/dependencies/gamemode/CMakeLists.txt new file mode 100644 index 00000000..78275174 --- /dev/null +++ b/dependencies/gamemode/CMakeLists.txt @@ -0,0 +1,4 @@ +project( gamemode LANGUAGES C ) +add_library (gamemode + "lib/gamemode_client.h" + "lib/client_loader.c") \ No newline at end of file diff --git a/dependencies/gamemode/lib/client_loader.c b/dependencies/gamemode/lib/client_loader.c new file mode 100644 index 00000000..08c86d06 --- /dev/null +++ b/dependencies/gamemode/lib/client_loader.c @@ -0,0 +1,35 @@ +/* + +Copyright (c) 2017-2019, Feral Interactive +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + */ + +// Simply include the header with GAMEMODE_AUTO set +// This will ensure it calls the functions when it's loaded +#define GAMEMODE_AUTO +#include "gamemode_client.h" diff --git a/dependencies/gamemode/lib/gamemode_client.h b/dependencies/gamemode/lib/gamemode_client.h new file mode 100644 index 00000000..b9f64fe4 --- /dev/null +++ b/dependencies/gamemode/lib/gamemode_client.h @@ -0,0 +1,376 @@ +/* + +Copyright (c) 2017-2019, Feral Interactive +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + */ +#ifndef CLIENT_GAMEMODE_H +#define CLIENT_GAMEMODE_H +/* + * GameMode supports the following client functions + * Requests are refcounted in the daemon + * + * int gamemode_request_start() - Request gamemode starts + * 0 if the request was sent successfully + * -1 if the request failed + * + * int gamemode_request_end() - Request gamemode ends + * 0 if the request was sent successfully + * -1 if the request failed + * + * GAMEMODE_AUTO can be defined to make the above two functions apply during static init and + * destruction, as appropriate. In this configuration, errors will be printed to stderr + * + * int gamemode_query_status() - Query the current status of gamemode + * 0 if gamemode is inactive + * 1 if gamemode is active + * 2 if gamemode is active and this client is registered + * -1 if the query failed + * + * int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process + * 0 if the request was sent successfully + * -1 if the request failed + * -2 if the request was rejected + * + * int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process + * 0 if the request was sent successfully + * -1 if the request failed + * -2 if the request was rejected + * + * int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process + * 0 if gamemode is inactive + * 1 if gamemode is active + * 2 if gamemode is active and this client is registered + * -1 if the query failed + * + * const char* gamemode_error_string() - Get an error string + * returns a string describing any of the above errors + * + * Note: All the above requests can be blocking - dbus requests can and will block while the daemon + * handles the request. It is not recommended to make these calls in performance critical code + */ + +#include +#include + +#include +#include + +#include + +#include + +static char internal_gamemode_client_error_string[512] = { 0 }; + +/** + * Load libgamemode dynamically to dislodge us from most dependencies. + * This allows clients to link and/or use this regardless of runtime. + * See SDL2 for an example of the reasoning behind this in terms of + * dynamic versioning as well. + */ +static volatile int internal_libgamemode_loaded = 1; + +/* Typedefs for the functions to load */ +typedef int (*api_call_return_int)(void); +typedef const char *(*api_call_return_cstring)(void); +typedef int (*api_call_pid_return_int)(pid_t); + +/* Storage for functors */ +static api_call_return_int REAL_internal_gamemode_request_start = NULL; +static api_call_return_int REAL_internal_gamemode_request_end = NULL; +static api_call_return_int REAL_internal_gamemode_query_status = NULL; +static api_call_return_cstring REAL_internal_gamemode_error_string = NULL; +static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL; +static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL; +static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL; + +/** + * Internal helper to perform the symbol binding safely. + * + * Returns 0 on success and -1 on failure + */ +__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol( + void *handle, const char *name, void **out_func, size_t func_size, bool required) +{ + void *symbol_lookup = NULL; + char *dl_error = NULL; + + /* Safely look up the symbol */ + symbol_lookup = dlsym(handle, name); + dl_error = dlerror(); + if (required && (dl_error || !symbol_lookup)) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "dlsym failed - %s", + dl_error); + return -1; + } + + /* Have the symbol correctly, copy it to make it usable */ + memcpy(out_func, &symbol_lookup, func_size); + return 0; +} + +/** + * Loads libgamemode and needed functions + * + * Returns 0 on success and -1 on failure + */ +__attribute__((always_inline)) static inline int internal_load_libgamemode(void) +{ + /* We start at 1, 0 is a success and -1 is a fail */ + if (internal_libgamemode_loaded != 1) { + return internal_libgamemode_loaded; + } + + /* Anonymous struct type to define our bindings */ + struct binding { + const char *name; + void **functor; + size_t func_size; + bool required; + } bindings[] = { + { "real_gamemode_request_start", + (void **)&REAL_internal_gamemode_request_start, + sizeof(REAL_internal_gamemode_request_start), + true }, + { "real_gamemode_request_end", + (void **)&REAL_internal_gamemode_request_end, + sizeof(REAL_internal_gamemode_request_end), + true }, + { "real_gamemode_query_status", + (void **)&REAL_internal_gamemode_query_status, + sizeof(REAL_internal_gamemode_query_status), + false }, + { "real_gamemode_error_string", + (void **)&REAL_internal_gamemode_error_string, + sizeof(REAL_internal_gamemode_error_string), + true }, + { "real_gamemode_request_start_for", + (void **)&REAL_internal_gamemode_request_start_for, + sizeof(REAL_internal_gamemode_request_start_for), + false }, + { "real_gamemode_request_end_for", + (void **)&REAL_internal_gamemode_request_end_for, + sizeof(REAL_internal_gamemode_request_end_for), + false }, + { "real_gamemode_query_status_for", + (void **)&REAL_internal_gamemode_query_status_for, + sizeof(REAL_internal_gamemode_query_status_for), + false }, + }; + + void *libgamemode = NULL; + + /* Try and load libgamemode */ + libgamemode = dlopen("libgamemode.so.0", RTLD_NOW); + if (!libgamemode) { + /* Attempt to load unversioned library for compatibility with older + * versions (as of writing, there are no ABI changes between the two - + * this may need to change if ever ABI-breaking changes are made) */ + libgamemode = dlopen("libgamemode.so", RTLD_NOW); + if (!libgamemode) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "dlopen failed - %s", + dlerror()); + internal_libgamemode_loaded = -1; + return -1; + } + } + + /* Attempt to bind all symbols */ + for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { + struct binding *binder = &bindings[i]; + + if (internal_bind_libgamemode_symbol(libgamemode, + binder->name, + binder->functor, + binder->func_size, + binder->required)) { + internal_libgamemode_loaded = -1; + return -1; + }; + } + + /* Success */ + internal_libgamemode_loaded = 0; + return 0; +} + +/** + * Redirect to the real libgamemode + */ +__attribute__((always_inline)) static inline const char *gamemode_error_string(void) +{ + /* If we fail to load the system gamemode, or we have an error string already, return our error + * string instead of diverting to the system version */ + if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') { + return internal_gamemode_client_error_string; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_error_string != NULL); + + return REAL_internal_gamemode_error_string(); +} + +/** + * Redirect to the real libgamemode + * Allow automatically requesting game mode + * Also prints errors as they happen. + */ +#ifdef GAMEMODE_AUTO +__attribute__((constructor)) +#else +__attribute__((always_inline)) static inline +#endif +int gamemode_request_start(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_request_start != NULL); + + if (REAL_internal_gamemode_request_start() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + return 0; +} + +/* Redirect to the real libgamemode */ +#ifdef GAMEMODE_AUTO +__attribute__((destructor)) +#else +__attribute__((always_inline)) static inline +#endif +int gamemode_request_end(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_request_end != NULL); + + if (REAL_internal_gamemode_request_end() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + return 0; +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_query_status(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_query_status == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_query_status missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_query_status(); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_request_start_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_request_start_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_request_start_for(pid); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_request_end_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_request_end_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_request_end_for(pid); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_query_status_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_query_status_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_query_status_for(pid); +} + +#endif // CLIENT_GAMEMODE_H diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index a4016ae4..5d99a23d 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -53,6 +53,7 @@ void CemuConfig::Load(XMLConfigParser& parser) language = parser.get("language", wxLANGUAGE_DEFAULT); use_discord_presence = parser.get("use_discord_presence", true); fullscreen_menubar = parser.get("fullscreen_menubar", false); + feral_gamemode = parser.get("feral_gamemode", false); check_update = parser.get("check_update", check_update); save_screenshot = parser.get("save_screenshot", save_screenshot); did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning); @@ -357,6 +358,7 @@ void CemuConfig::Save(XMLConfigParser& parser) config.set("language", language); config.set("use_discord_presence", use_discord_presence); config.set("fullscreen_menubar", fullscreen_menubar); + config.set("feral_gamemode", feral_gamemode); config.set("check_update", check_update); config.set("save_screenshot", save_screenshot); config.set("vk_warning", did_show_vulkan_warning); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index ad591ee8..e17a9344 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -366,6 +366,7 @@ struct CemuConfig ConfigValue mlc_path {}; ConfigValue fullscreen_menubar{ false }; ConfigValue fullscreen{ false }; + ConfigValue feral_gamemode{false}; ConfigValue proxy_server{}; // temporary workaround because feature crashes on macOS diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 30fb492a..74eaf7de 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -161,6 +161,11 @@ if(ENABLE_CUBEB) target_link_libraries(CemuGui PRIVATE cubeb::cubeb) endif() +if(UNIX AND NOT APPLE) + if(ENABLE_FERAL_GAMEMODE) + target_link_libraries(CemuGui PRIVATE gamemode) + endif() +endif() if (ENABLE_WXWIDGETS) # PUBLIC because wx/app.h is included in CemuApp.h target_link_libraries(CemuGui PUBLIC wx::base wx::core wx::gl wx::propgrid wx::xrc) diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index 34627737..697be0d7 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -179,6 +179,13 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) m_disable_screensaver->SetToolTip(_("Prevents the system from activating the screen saver or going to sleep while running a game.")); second_row->Add(m_disable_screensaver, 0, botflag, 5); + // Enable/disable feral interactive gamemode +#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) + m_feral_gamemode = new wxCheckBox(box, wxID_ANY, _("Enable Feral GameMode")); + m_feral_gamemode->SetToolTip(_("Use FeralInteractive GameMode if installed.")); + second_row->Add(m_feral_gamemode, 0, botflag, 5); +#endif + // temporary workaround because feature crashes on macOS #if BOOST_OS_MACOS m_disable_screensaver->Enable(false); @@ -868,7 +875,7 @@ void GeneralSettings2::StoreConfig() config.fullscreen_menubar = m_fullscreen_menubar->IsChecked(); config.check_update = m_auto_update->IsChecked(); config.save_screenshot = m_save_screenshot->IsChecked(); - + config.feral_gamemode = m_feral_gamemode->IsChecked(); const bool use_ps = m_permanent_storage->IsChecked(); if(use_ps) { @@ -1508,6 +1515,8 @@ void GeneralSettings2::ApplyConfig() m_permanent_storage->SetValue(config.permanent_storage); m_disable_screensaver->SetValue(config.disable_screensaver); + + m_feral_gamemode->SetValue(config.feral_gamemode); // temporary workaround because feature crashes on macOS #if BOOST_OS_MACOS m_disable_screensaver->SetValue(false); diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index 1bc086cc..eaacc3ca 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -43,6 +43,7 @@ private: wxCheckBox* m_auto_update, *m_save_screenshot; wxCheckBox* m_permanent_storage; wxCheckBox* m_disable_screensaver; + wxCheckBox* m_feral_gamemode; wxListBox* m_game_paths; wxTextCtrl* m_mlc_path; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d4db3232..98483a2d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -61,6 +61,11 @@ #include "gui/helpers/wxWayland.h" #endif +//GameMode support +#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) +#include "gamemode_client.h" +#endif + #include "Cafe/TitleList/TitleInfo.h" #include "Cafe/TitleList/TitleList.h" #include "wxHelper.h" @@ -581,6 +586,23 @@ bool MainWindow::FileLoad(std::wstring fileName, wxLaunchGameEvent::INITIATED_BY if (ActiveSettings::FullscreenEnabled()) SetFullScreen(true); + //GameMode support +#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) + if(GetConfig().feral_gamemode) + { + // attempt to start gamemode + if(gamemode_request_start() < 0) + { + // GameMode failed to start + cemuLog_log(LogType::Force, "Could not start GameMode"); + } + else + { + cemuLog_log(LogType::Force, "GameMode has been started."); + } + } +#endif + CreateCanvas(); CafeSystem::LaunchForegroundTitle(); RecreateMenu();