diff --git a/CMakeLists.txt b/CMakeLists.txt index 5de35ce02..f14a4db25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) project(duckstation C CXX) message("CMake Version: ${CMAKE_VERSION}") diff --git a/CMakeModules/FindWaylandProtocols.cmake b/CMakeModules/FindWaylandProtocols.cmake new file mode 100644 index 000000000..bfe7c9a21 --- /dev/null +++ b/CMakeModules/FindWaylandProtocols.cmake @@ -0,0 +1,29 @@ +# from https://github.com/glfw/glfw/blob/master/CMake/modules/FindWaylandProtocols.cmake + +find_package(PkgConfig) + +pkg_check_modules(WaylandProtocols QUIET wayland-protocols>=${WaylandProtocols_FIND_VERSION}) + +execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=pkgdatadir wayland-protocols + OUTPUT_VARIABLE WaylandProtocols_PKGDATADIR + RESULT_VARIABLE _pkgconfig_failed) +if (_pkgconfig_failed) + message(FATAL_ERROR "Missing wayland-protocols pkgdatadir") +endif() + +string(REGEX REPLACE "[\r\n]" "" WaylandProtocols_PKGDATADIR "${WaylandProtocols_PKGDATADIR}") + +find_package_handle_standard_args(WaylandProtocols + FOUND_VAR + WaylandProtocols_FOUND + REQUIRED_VARS + WaylandProtocols_PKGDATADIR + VERSION_VAR + WaylandProtocols_VERSION + HANDLE_COMPONENTS +) + +set(WAYLAND_PROTOCOLS_FOUND ${WaylandProtocols_FOUND}) +set(WAYLAND_PROTOCOLS_PKGDATADIR ${WaylandProtocols_PKGDATADIR}) +set(WAYLAND_PROTOCOLS_VERSION ${WaylandProtocols_VERSION}) + diff --git a/CMakeModules/FindXKBCommon.cmake b/CMakeModules/FindXKBCommon.cmake new file mode 100644 index 000000000..53ad025b1 --- /dev/null +++ b/CMakeModules/FindXKBCommon.cmake @@ -0,0 +1,38 @@ +# - Try to find XKBCommon +# Once done, this will define +# +# XKBCOMMON_FOUND - System has XKBCommon +# XKBCOMMON_INCLUDE_DIRS - The XKBCommon include directories +# XKBCOMMON_LIBRARIES - The libraries needed to use XKBCommon +# XKBCOMMON_DEFINITIONS - Compiler switches required for using XKBCommon + +find_package(PkgConfig) +pkg_check_modules(PC_XKBCOMMON QUIET xkbcommon) +set(XKBCOMMON_DEFINITIONS ${PC_XKBCOMMON_CFLAGS_OTHER}) + +find_path(XKBCOMMON_INCLUDE_DIR + NAMES xkbcommon/xkbcommon.h + HINTS ${PC_XKBCOMMON_INCLUDE_DIR} ${PC_XKBCOMMON_INCLUDE_DIRS} +) + +find_library(XKBCOMMON_LIBRARY + NAMES xkbcommon + HINTS ${PC_XKBCOMMON_LIBRARY} ${PC_XKBCOMMON_LIBRARY_DIRS} +) + +set(XKBCOMMON_LIBRARIES ${XKBCOMMON_LIBRARY}) +set(XKBCOMMON_LIBRARY_DIRS ${XKBCOMMON_LIBRARY_DIRS}) +set(XKBCOMMON_INCLUDE_DIRS ${XKBCOMMON_INCLUDE_DIR}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(XKBCommon DEFAULT_MSG + XKBCOMMON_LIBRARY + XKBCOMMON_INCLUDE_DIR +) + +mark_as_advanced(XKBCOMMON_LIBRARY XKBCOMMON_INCLUDE_DIR) + +if (XKBCOMMON_INCLUDE_DIR AND XKBCOMMON_LIBRARY AND NOT TARGET XKBCommon::XKBCommon) + add_library(XKBCommon::XKBCommon UNKNOWN IMPORTED) + set_target_properties(XKBCommon::XKBCommon PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${XKBCOMMON_INCLUDE_DIR}" IMPORTED_LOCATION "${XKBCOMMON_LIBRARY}") +endif() diff --git a/duckstation.sln b/duckstation.sln index 6f8103db5..47e27a548 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -783,6 +783,7 @@ Global {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.Debug|x86.Build.0 = Debug|Win32 {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugFast|ARM64.ActiveCfg = DebugFast|ARM64 {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugFast|x64.Build.0 = DebugFast|x64 {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugFast|x86.ActiveCfg = DebugFast|Win32 {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugFast|x86.Build.0 = DebugFast|Win32 {0A172B2E-DC67-49FC-A4C1-975F93C586C4}.DebugUWP|ARM64.ActiveCfg = DebugUWP|ARM64 diff --git a/src/duckstation-nogui/CMakeLists.txt b/src/duckstation-nogui/CMakeLists.txt index a51778ec3..173018495 100644 --- a/src/duckstation-nogui/CMakeLists.txt +++ b/src/duckstation-nogui/CMakeLists.txt @@ -1,39 +1,18 @@ add_executable(duckstation-nogui - main.cpp - nogui_host_interface.cpp - nogui_host_interface.h + nogui_host.cpp + nogui_host.h + nogui_platform.h ) -target_link_libraries(duckstation-nogui PRIVATE core common imgui glad frontend-common scmversion) - -if(USE_SDL2) - target_sources(duckstation-nogui PRIVATE - sdl_host_interface.cpp - sdl_host_interface.h - sdl_key_names.h - ) - target_include_directories(duckstation-nogui PRIVATE ${SDL2_INCLUDE_DIRS}) - target_link_libraries(duckstation-nogui PRIVATE ${SDL2_LIBRARIES}) -endif() - -if(USE_EVDEV) - target_sources(duckstation-nogui PRIVATE - vty_host_interface.cpp - vty_host_interface.h - ) - target_compile_definitions(duckstation-nogui PRIVATE "-DWITH_VTY=1") - target_compile_definitions(duckstation-nogui PRIVATE "-DUSE_LIBEVDEV=1") - target_include_directories(duckstation-nogui PRIVATE ${LIBEVDEV_INCLUDE_DIRS}) - target_link_libraries(duckstation-nogui PRIVATE ${LIBEVDEV_LIBRARIES}) -endif() - -if(USE_DRMKMS) - target_compile_definitions(duckstation-nogui PRIVATE "-DWITH_DRMKMS=1") -endif() +target_link_libraries(duckstation-nogui PRIVATE core util common imgui glad frontend-common scmversion) if(WIN32) + message(STATUS "Building Win32 NoGUI Platform.") target_sources(duckstation-nogui PRIVATE duckstation-nogui.manifest + resource.h + win32_nogui_platform.cpp + win32_nogui_platform.h ) # We want a Windows subsystem application not console. @@ -42,3 +21,57 @@ if(WIN32) DEBUG_POSTFIX "-debug") endif() +if(USE_X11) + message(STATUS "Building X11 NoGUI Platform.") + target_compile_definitions(duckstation-nogui PRIVATE "NOGUI_PLATFORM_X11=1") + target_sources(duckstation-nogui PRIVATE + x11_nogui_platform.cpp + x11_nogui_platform.h + ) + target_include_directories(duckstation-nogui PRIVATE "${X11_INCLUDE_DIR}" "${X11_Xrandr_INCLUDE_PATH}") + target_link_libraries(duckstation-nogui PRIVATE "${X11_LIBRARIES}" "${X11_Xrandr_LIB}") +endif() + +if(USE_WAYLAND) + message(STATUS "Building Wayland NoGUI Platform.") + find_package(Wayland REQUIRED Client) + find_package(WaylandScanner REQUIRED) + find_package(WaylandProtocols 1.15 REQUIRED) + find_package(XKBCommon REQUIRED) + + target_compile_definitions(duckstation-nogui PRIVATE "NOGUI_PLATFORM_WAYLAND=1") + target_sources(duckstation-nogui PRIVATE + wayland_nogui_platform.cpp + wayland_nogui_platform.h + ) + + # Generate the xdg-shell and xdg-decoration protocols at build-time. + # Because these are C, not C++, we have to put them in their own library, otherwise + # cmake tries to generate a C PCH as well as the C++ one... + ecm_add_wayland_client_protocol(WAYLAND_PLATFORM_SRCS + PROTOCOL "${WAYLAND_PROTOCOLS_PKGDATADIR}/stable/xdg-shell/xdg-shell.xml" + BASENAME xdg-shell) + ecm_add_wayland_client_protocol(WAYLAND_PLATFORM_SRCS + PROTOCOL "${WAYLAND_PROTOCOLS_PKGDATADIR}/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" + BASENAME xdg-decoration) + add_library(duckstation-nogui-wayland-protocols STATIC ${WAYLAND_PLATFORM_SRCS}) + target_include_directories(duckstation-nogui-wayland-protocols PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") + + target_link_libraries(duckstation-nogui PRIVATE + duckstation-nogui-wayland-protocols + Wayland::Client + XKBCommon::XKBCommon + ) +endif() + +if(USE_DRMKMS AND USE_EVDEV) + message(STATUS "Building VTY/DRM/KMS/EVDev NoGUI Platform.") + target_compile_definitions(duckstation-nogui PRIVATE "NOGUI_PLATFORM_VTY=1" "WITH_DRMKMS=1") + target_sources(duckstation-nogui PRIVATE + vty_key_names.h + vty_nogui_platform.cpp + vty_nogui_platform.h + ) + target_include_directories(duckstation-nogui PRIVATE ${LIBEVDEV_INCLUDE_DIRS}) + target_link_libraries(duckstation-nogui PRIVATE ${LIBEVDEV_LIBRARIES}) +endif() diff --git a/src/duckstation-nogui/duckstation-nogui.vcxproj b/src/duckstation-nogui/duckstation-nogui.vcxproj index 666d520eb..af36f8230 100644 --- a/src/duckstation-nogui/duckstation-nogui.vcxproj +++ b/src/duckstation-nogui/duckstation-nogui.vcxproj @@ -2,26 +2,29 @@ - + + true - - - - + + true + + - - true - - - true - - + + - - - + + true + + + true + + + true + + diff --git a/src/duckstation-nogui/duckstation-nogui.vcxproj.filters b/src/duckstation-nogui/duckstation-nogui.vcxproj.filters index b549da117..aa0b8fc40 100644 --- a/src/duckstation-nogui/duckstation-nogui.vcxproj.filters +++ b/src/duckstation-nogui/duckstation-nogui.vcxproj.filters @@ -1,20 +1,19 @@  - - - - - + + + + - - - - - - + + + + + + diff --git a/src/duckstation-nogui/main.cpp b/src/duckstation-nogui/main.cpp deleted file mode 100644 index 4f3fc15ba..000000000 --- a/src/duckstation-nogui/main.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "common/assert.h" -#include "common/file_system.h" -#include "common/log.h" -#include "common/string_util.h" -#include "core/system.h" -#include -#include -#include -#include - -#ifdef WITH_VTY -#include "vty_host_interface.h" -#endif - -#ifdef WITH_SDL2 -#include "sdl_host_interface.h" - -static bool IsSDLHostInterfaceAvailable() -{ -#if defined(__linux__) - // Only available if we have a X11 or Wayland display. - if (std::getenv("DISPLAY") || std::getenv("WAYLAND_DISPLAY")) - return true; - else - return false; -#else - // Always available on Windows/Apple. - return true; -#endif -} -#endif - -#ifdef _WIN32 -#include "common/windows_headers.h" -#include "win32_host_interface.h" -#include -#endif - -static std::unique_ptr CreateHostInterface() -{ - const char* platform = std::getenv("DUCKSTATION_NOGUI_PLATFORM"); - std::unique_ptr host_interface; - -#ifdef WITH_SDL2 - if (!host_interface && (!platform || StringUtil::Strcasecmp(platform, "sdl") == 0) && IsSDLHostInterfaceAvailable()) - host_interface = SDLHostInterface::Create(); -#endif - -#ifdef WITH_VTY - if (!host_interface && (!platform || StringUtil::Strcasecmp(platform, "vty") == 0)) - host_interface = VTYHostInterface::Create(); -#endif - -#ifdef _WIN32 - if (!host_interface && (!platform || StringUtil::Strcasecmp(platform, "win32") == 0)) - host_interface = Win32HostInterface::Create(); -#endif - - return host_interface; -} - -static int Run(std::unique_ptr host_interface, std::unique_ptr boot_params) -{ - if (!host_interface->Initialize()) - { - host_interface->Shutdown(); - return EXIT_FAILURE; - } - - if (boot_params) - host_interface->BootSystem(std::move(boot_params)); - - int result; - if (System::IsValid() || !host_interface->InBatchMode()) - { - host_interface->Run(); - result = EXIT_SUCCESS; - } - else - { - host_interface->ReportError("No file specified, and we're in batch mode. Exiting."); - result = EXIT_FAILURE; - } - - host_interface->Shutdown(); - return result; -} - -#ifdef _WIN32 - -int wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) -{ - std::unique_ptr host_interface = CreateHostInterface(); - std::unique_ptr boot_params; - - { - std::vector argc_strings; - argc_strings.reserve(1); - - // CommandLineToArgvW() only adds the program path if the command line is empty?! - argc_strings.push_back(FileSystem::GetProgramPath()); - - if (std::wcslen(lpCmdLine) > 0) - { - int argc; - LPWSTR* argv_wide = CommandLineToArgvW(lpCmdLine, &argc); - if (argv_wide) - { - for (int i = 0; i < argc; i++) - argc_strings.push_back(StringUtil::WideStringToUTF8String(argv_wide[i])); - - LocalFree(argv_wide); - } - } - - std::vector argc_pointers; - argc_pointers.reserve(argc_strings.size()); - for (std::string& arg : argc_strings) - argc_pointers.push_back(arg.data()); - - if (!host_interface->ParseCommandLineParameters(static_cast(argc_pointers.size()), argc_pointers.data(), - &boot_params)) - { - return EXIT_FAILURE; - } - } - - return Run(std::move(host_interface), std::move(boot_params)); -} - -#else - -int main(int argc, char* argv[]) -{ - std::unique_ptr host_interface = CreateHostInterface(); - std::unique_ptr boot_params; - if (!host_interface->ParseCommandLineParameters(argc, argv, &boot_params)) - return EXIT_FAILURE; - - return Run(std::move(host_interface), std::move(boot_params)); -} - -#endif \ No newline at end of file diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp new file mode 100644 index 000000000..77ecdde8d --- /dev/null +++ b/src/duckstation-nogui/nogui_host.cpp @@ -0,0 +1,1363 @@ +#include "nogui_host.h" +#include "common/assert.h" +#include "common/byte_stream.h" +#include "common/crash_handler.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/path.h" +#include "common/string_util.h" +#include "common/threading.h" +#include "core/controller.h" +#include "core/gpu.h" +#include "core/host.h" +#include "core/host_display.h" +#include "core/host_settings.h" +#include "core/settings.h" +#include "core/system.h" +#include "frontend-common/common_host.h" +#include "frontend-common/fullscreen_ui.h" +#include "frontend-common/game_list.h" +#include "frontend-common/icon.h" +#include "frontend-common/imgui_manager.h" +#include "frontend-common/imgui_overlays.h" +#include "frontend-common/input_manager.h" +#include "imgui.h" +#include "imgui_internal.h" +#include "imgui_stdlib.h" +#include "nogui_platform.h" +#include "scmversion/scmversion.h" +#include "util/ini_settings_interface.h" +#include +#include +#include +#include +#include +Log_SetChannel(NoGUIHost); + +#ifdef WITH_CHEEVOS +#include "frontend-common/achievements.h" +#endif + +#ifdef _WIN32 +#include "common/windows_headers.h" +#include +#include +#endif + +static constexpr u32 SETTINGS_VERSION = 3; +static constexpr auto CPU_THREAD_POLL_INTERVAL = + std::chrono::milliseconds(8); // how often we'll poll controllers when paused + +std::unique_ptr g_nogui_window; + +////////////////////////////////////////////////////////////////////////// +// Local function declarations +////////////////////////////////////////////////////////////////////////// +namespace NoGUIHost { +static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[], + std::optional& autoboot); +static void PrintCommandLineVersion(); +static void PrintCommandLineHelp(const char* progname); +static bool InitializeConfig(std::string settings_filename); +static void InitializeEarlyConsole(); +static void HookSignals(); +static bool ShouldUsePortableMode(); +static void SetAppRoot(); +static void SetResourcesDirectory(); +static void SetDataDirectory(); +static bool SetCriticalFolders(); +static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller); +static void StartCPUThread(); +static void StopCPUThread(); +static void ProcessCPUThreadEvents(bool block); +static void ProcessCPUThreadPlatformMessages(); +static void CPUThreadEntryPoint(); +static void CPUThreadMainLoop(); +static std::unique_ptr CreatePlatform(); +static std::string GetWindowTitle(const std::string& game_title); +static void UpdateWindowTitle(const std::string& game_title); +static void GameListRefreshThreadEntryPoint(bool invalidate_cache); +static bool AcquireHostDisplay(HostDisplay::RenderAPI api); +static void ReleaseHostDisplay(); +} // namespace NoGUIHost + +////////////////////////////////////////////////////////////////////////// +// Local variable declarations +////////////////////////////////////////////////////////////////////////// +static std::unique_ptr s_base_settings_interface; +static bool s_batch_mode = false; +static bool s_is_fullscreen = false; +static bool s_save_state_on_shutdown = false; +static bool s_was_paused_by_focus_loss = false; + +static Threading::Thread s_cpu_thread; +static Threading::KernelSemaphore s_host_display_created; +static std::atomic_bool s_running{false}; +static std::mutex s_cpu_thread_events_mutex; +static std::condition_variable s_cpu_thread_event_done; +static std::condition_variable s_cpu_thread_event_posted; +static std::deque, bool>> s_cpu_thread_events; +static u32 s_blocking_cpu_events_pending = 0; // TODO: Token system would work better here. + +static std::mutex s_game_list_refresh_lock; +static std::thread s_game_list_refresh_thread; +static FullscreenUI::ProgressCallback* s_game_list_refresh_progress = nullptr; + +////////////////////////////////////////////////////////////////////////// +// Initialization/Shutdown +////////////////////////////////////////////////////////////////////////// + +bool NoGUIHost::SetCriticalFolders() +{ + SetAppRoot(); + SetResourcesDirectory(); + SetDataDirectory(); + + // logging of directories in case something goes wrong super early + Log_DevPrintf("AppRoot Directory: %s", EmuFolders::AppRoot.c_str()); + Log_DevPrintf("DataRoot Directory: %s", EmuFolders::DataRoot.c_str()); + Log_DevPrintf("Resources Directory: %s", EmuFolders::Resources.c_str()); + + // Write crash dumps to the data directory, since that'll be accessible for certain. + CrashHandler::SetWriteDirectory(EmuFolders::DataRoot); + + // the resources directory should exist, bail out if not + if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str())) + { + g_nogui_window->ReportError("Error", "Resources directory is missing, your installation is incomplete."); + return false; + } + + return true; +} + +bool NoGUIHost::ShouldUsePortableMode() +{ + // Check whether portable.ini exists in the program directory. + return (FileSystem::FileExists(Path::Combine(EmuFolders::AppRoot, "portable.txt").c_str()) || + FileSystem::FileExists(Path::Combine(EmuFolders::AppRoot, "settings.ini").c_str())); +} + +void NoGUIHost::SetAppRoot() +{ + std::string program_path(FileSystem::GetProgramPath()); + Log_InfoPrintf("Program Path: %s", program_path.c_str()); + + EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path)); +} + +void NoGUIHost::SetResourcesDirectory() +{ +#ifndef __APPLE__ + // On Windows/Linux, these are in the binary directory. + EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources"); +#else + // On macOS, this is in the bundle resources directory. + EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources")); +#endif +} + +void NoGUIHost::SetDataDirectory() +{ + if (ShouldUsePortableMode()) + { + EmuFolders::DataRoot = EmuFolders::AppRoot; + return; + } + +#if defined(_WIN32) + // On Windows, use My Documents\DuckStation. + PWSTR documents_directory; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &documents_directory))) + { + if (std::wcslen(documents_directory) > 0) + EmuFolders::DataRoot = Path::Combine(StringUtil::WideStringToUTF8String(documents_directory), "DuckStation"); + CoTaskMemFree(documents_directory); + } +#elif defined(__linux__) + // Use $XDG_CONFIG_HOME/duckstation if it exists. + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + if (xdg_config_home && Path::IsAbsolute(xdg_config_home)) + { + EmuFolders::DataRoot = Path::Combine(xdg_config_home, "duckstation"); + } + else + { + // Use ~/.local/share/duckstation otherwise. + const char* home_dir = getenv("HOME"); + if (home_dir) + { + // ~/.local/share should exist, but just in case it doesn't and this is a fresh profile.. + const std::string local_dir(Path::Combine(home_dir, ".local")); + const std::string share_dir(Path::Combine(local_dir, "share")); + FileSystem::EnsureDirectoryExists(local_dir.c_str(), false); + FileSystem::EnsureDirectoryExists(share_dir.c_str(), false); + EmuFolders::DataRoot = Path::Combine(share_dir, "duckstation"); + } + } +#elif defined(__APPLE__) + static constexpr char MAC_DATA_DIR[] = "Library/Application Support/DuckStation"; + const char* home_dir = getenv("HOME"); + if (home_dir) + EmuFolders::DataRoot = Path::Combine(home_dir, MAC_DATA_DIR); +#endif + + // make sure it exists + if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str())) + { + // we're in trouble if we fail to create this directory... but try to hobble on with portable + if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false)) + EmuFolders::DataRoot.clear(); + } + + // couldn't determine the data directory? fallback to portable. + if (EmuFolders::DataRoot.empty()) + EmuFolders::DataRoot = EmuFolders::AppRoot; +} + +bool NoGUIHost::InitializeConfig(std::string settings_filename) +{ + if (!SetCriticalFolders()) + return false; + + if (settings_filename.empty()) + settings_filename = Path::Combine(EmuFolders::DataRoot, "settings.ini"); + + Log_InfoPrintf("Loading config from %s.", settings_filename.c_str()); + s_base_settings_interface = std::make_unique(std::move(settings_filename)); + Host::Internal::SetBaseSettingsLayer(s_base_settings_interface.get()); + + u32 settings_version; + if (!s_base_settings_interface->Load() || + !s_base_settings_interface->GetUIntValue("Main", "SettingsVersion", &settings_version) || + settings_version != SETTINGS_VERSION) + { + if (s_base_settings_interface->ContainsValue("Main", "SettingsVersion")) + { + // NOTE: No point translating this, because there's no config loaded, so no language loaded. + Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.", + settings_version, SETTINGS_VERSION)); + } + + s_base_settings_interface->SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION); + SetDefaultSettings(*s_base_settings_interface, true, true); + s_base_settings_interface->Save(); + } + + EmuFolders::LoadConfig(*s_base_settings_interface.get()); + EmuFolders::EnsureFoldersExist(); + + // We need to create the console window early, otherwise it appears behind the main window. + if (!Log::IsConsoleOutputEnabled() && + s_base_settings_interface->GetBoolValue("Logging", "LogToConsole", Settings::DEFAULT_LOG_TO_CONSOLE)) + { + Log::SetConsoleOutputParams(true, nullptr, LOGLEVEL_NONE); + } + + return true; +} + +void NoGUIHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller) +{ + if (system) + { + System::SetDefaultSettings(si); + CommonHost::SetDefaultSettings(si); + EmuFolders::SetDefaults(); + EmuFolders::Save(si); + } + + if (controller) + { + CommonHost::SetDefaultControllerSettings(si); + CommonHost::SetDefaultHotkeyBindings(si); + } + + g_nogui_window->SetDefaultConfig(si); +} + +void Host::ReportErrorAsync(const std::string_view& title, const std::string_view& message) +{ + if (!title.empty() && !message.empty()) + { + Log_ErrorPrintf("ReportErrorAsync: %.*s: %.*s", static_cast(title.size()), title.data(), + static_cast(message.size()), message.data()); + } + else if (!message.empty()) + { + Log_ErrorPrintf("ReportErrorAsync: %.*s", static_cast(message.size()), message.data()); + } + + g_nogui_window->ReportError(title, message); +} + +bool Host::ConfirmMessage(const std::string_view& title, const std::string_view& message) +{ + // TODO: Post to window + if (!title.empty() && !message.empty()) + { + Log_ErrorPrintf("ConfirmMessage: %.*s: %.*s", static_cast(title.size()), title.data(), + static_cast(message.size()), message.data()); + } + else if (!message.empty()) + { + Log_ErrorPrintf("ConfirmMessage: %.*s", static_cast(message.size()), message.data()); + } + + return true; +} + +void Host::ReportDebuggerMessage(const std::string_view& message) +{ + Log_ErrorPrintf("ReportDebuggerMessage: %.*s", static_cast(message.size()), message.data()); +} + +void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name) +{ + Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier), + fmt::format("Input device {0} ({1}) connected.", device_name, identifier), 10.0f); +} + +void Host::OnInputDeviceDisconnected(const std::string_view& identifier) +{ + Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier), + fmt::format("Input device {} disconnected.", identifier), 10.0f); +} + +TinyString Host::TranslateString(const char* context, const char* str, const char* disambiguation, int n) +{ + return str; +} + +std::string Host::TranslateStdString(const char* context, const char* str, const char* disambiguation, int n) +{ + return str; +} + +std::optional> Host::ReadResourceFile(const char* filename) +{ + const std::string path(Path::Combine(EmuFolders::Resources, filename)); + std::optional> ret(FileSystem::ReadBinaryFile(path.c_str())); + if (!ret.has_value()) + Log_ErrorPrintf("Failed to read resource file '%s'", filename); + return ret; +} + +std::optional Host::ReadResourceFileToString(const char* filename) +{ + const std::string path(Path::Combine(EmuFolders::Resources, filename)); + std::optional ret(FileSystem::ReadFileToString(path.c_str())); + if (!ret.has_value()) + Log_ErrorPrintf("Failed to read resource file to string '%s'", filename); + return ret; +} + +std::optional Host::GetResourceFileTimestamp(const char* filename) +{ + const std::string path(Path::Combine(EmuFolders::Resources, filename)); + FILESYSTEM_STAT_DATA sd; + if (!FileSystem::StatFile(path.c_str(), &sd)) + { + Log_ErrorPrintf("Failed to stat resource file '%s'", filename); + return std::nullopt; + } + + return sd.ModificationTime; +} + +void Host::LoadSettings(SettingsInterface& si, std::unique_lock& lock) +{ + CommonHost::LoadSettings(si, lock); +} + +void Host::CheckForSettingsChanges(const Settings& old_settings) +{ + CommonHost::CheckForSettingsChanges(old_settings); +} + +void Host::SetBaseBoolSettingValue(const char* section, const char* key, bool value) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetBoolValue(section, key, value); + NoGUIHost::SaveSettings(); +} + +void Host::SetBaseIntSettingValue(const char* section, const char* key, int value) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetIntValue(section, key, value); + NoGUIHost::SaveSettings(); +} + +void Host::SetBaseFloatSettingValue(const char* section, const char* key, float value) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetFloatValue(section, key, value); + NoGUIHost::SaveSettings(); +} + +void Host::SetBaseStringSettingValue(const char* section, const char* key, const char* value) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetStringValue(section, key, value); + NoGUIHost::SaveSettings(); +} + +void Host::SetBaseStringListSettingValue(const char* section, const char* key, const std::vector& values) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetStringList(section, key, values); + NoGUIHost::SaveSettings(); +} + +bool Host::AddValueToBaseStringListSetting(const char* section, const char* key, const char* value) +{ + auto lock = Host::GetSettingsLock(); + if (!s_base_settings_interface->AddToStringList(section, key, value)) + return false; + + NoGUIHost::SaveSettings(); + return true; +} + +bool Host::RemoveValueFromBaseStringListSetting(const char* section, const char* key, const char* value) +{ + auto lock = Host::GetSettingsLock(); + if (!s_base_settings_interface->RemoveFromStringList(section, key, value)) + return false; + + NoGUIHost::SaveSettings(); + return true; +} + +void Host::DeleteBaseSettingValue(const char* section, const char* key) +{ + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->DeleteValue(section, key); + NoGUIHost::SaveSettings(); +} + +void Host::CommitBaseSettingChanges() +{ + NoGUIHost::SaveSettings(); +} + +void NoGUIHost::SaveSettings() +{ + auto lock = Host::GetSettingsLock(); + if (!s_base_settings_interface->Save()) + Log_ErrorPrintf("Failed to save settings."); +} + +bool NoGUIHost::InBatchMode() +{ + return s_batch_mode; +} + +void NoGUIHost::SetBatchMode(bool enabled) +{ + s_batch_mode = enabled; + if (enabled) + GameList::Refresh(false, true); +} + +void NoGUIHost::StartSystem(SystemBootParameters params) +{ + Host::RunOnCPUThread([params = std::move(params)]() { System::BootSystem(std::move(params)); }); +} + +void NoGUIHost::ProcessPlatformWindowResize(s32 width, s32 height, float scale) +{ + Host::RunOnCPUThread([width, height, scale]() { + // TODO: Scale + g_host_display->ResizeRenderWindow(width, height); + ImGuiManager::WindowResized(); + System::HostDisplayResized(); + }); +} + +void NoGUIHost::ProcessPlatformMouseMoveEvent(float x, float y) +{ + if (g_host_display) + g_host_display->SetMousePosition(static_cast(x), static_cast(y)); + + InputManager::UpdatePointerAbsolutePosition(0, x, y); + ImGuiManager::UpdateMousePosition(x, y); +} + +void NoGUIHost::ProcessPlatformMouseButtonEvent(s32 button, bool pressed) +{ + Host::RunOnCPUThread([button, pressed]() { + InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f, + GenericInputBinding::Unknown); + }); +} + +void NoGUIHost::ProcessPlatformMouseWheelEvent(float x, float y) +{ + if (x != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, x); + if (y != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, y); +} + +void NoGUIHost::ProcessPlatformKeyEvent(s32 key, bool pressed) +{ + Host::RunOnCPUThread([key, pressed]() { + InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), pressed ? 1.0f : 0.0f, + GenericInputBinding::Unknown); + }); +} + +void NoGUIHost::PlatformWindowFocusGained() +{ + Host::RunOnCPUThread([]() { + if (!System::IsValid() || !s_was_paused_by_focus_loss) + return; + + System::PauseSystem(false); + s_was_paused_by_focus_loss = false; + }); +} + +void NoGUIHost::PlatformWindowFocusLost() +{ + Host::RunOnCPUThread([]() { + if (!System::IsRunning() || !g_settings.pause_on_focus_loss) + return; + + s_was_paused_by_focus_loss = true; + System::PauseSystem(true); + }); +} + +bool NoGUIHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height) +{ + auto lock = Host::GetSettingsLock(); + + bool result = s_base_settings_interface->GetIntValue("NoGUI", "WindowX", x); + result = result && s_base_settings_interface->GetIntValue("NoGUI", "WindowY", y); + result = result && s_base_settings_interface->GetIntValue("NoGUI", "WindowWidth", width); + result = result && s_base_settings_interface->GetIntValue("NoGUI", "WindowHeight", height); + return result; +} + +void NoGUIHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height) +{ + if (s_is_fullscreen) + return; + + auto lock = Host::GetSettingsLock(); + s_base_settings_interface->SetIntValue("NoGUI", "WindowX", x); + s_base_settings_interface->SetIntValue("NoGUI", "WindowY", y); + s_base_settings_interface->SetIntValue("NoGUI", "WindowWidth", width); + s_base_settings_interface->SetIntValue("NoGUI", "WindowHeight", height); + s_base_settings_interface->Save(); +} + +std::string NoGUIHost::GetAppNameAndVersion() +{ + return fmt::format("DuckStation {} ({})", g_scm_tag_str, g_scm_branch_str); +} + +std::string NoGUIHost::GetAppConfigSuffix() +{ +#if defined(_DEBUGFAST) + return " [DebugFast]"; +#elif defined(_DEBUG) + return " [Debug]"; +#else + return std::string(); +#endif +} + +void NoGUIHost::StartCPUThread() +{ + s_running.store(true, std::memory_order_release); + s_cpu_thread.Start(CPUThreadEntryPoint); +} + +void NoGUIHost::StopCPUThread() +{ + if (!s_cpu_thread.Joinable()) + return; + + { + std::unique_lock lock(s_cpu_thread_events_mutex); + s_running.store(false, std::memory_order_release); + s_cpu_thread_event_posted.notify_one(); + } + s_cpu_thread.Join(); +} + +void NoGUIHost::ProcessCPUThreadPlatformMessages() +{ + // This is lame. On Win32, we need to pump messages, even though *we* don't have any windows + // on the CPU thread, because SDL creates a hidden window for raw input for some game controllers. + // If we don't do this, we don't get any controller events. +#ifdef _WIN32 + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +#endif +} + +void NoGUIHost::ProcessCPUThreadEvents(bool block) +{ + std::unique_lock lock(s_cpu_thread_events_mutex); + + for (;;) + { + if (s_cpu_thread_events.empty()) + { + if (!block || !s_running.load(std::memory_order_acquire)) + return; + + // we still need to keep polling the controllers when we're paused + do + { + ProcessCPUThreadPlatformMessages(); + InputManager::PollSources(); + } while (!s_cpu_thread_event_posted.wait_for(lock, CPU_THREAD_POLL_INTERVAL, + []() { return !s_cpu_thread_events.empty(); })); + } + + // return after processing all events if we had one + block = false; + + auto event = std::move(s_cpu_thread_events.front()); + s_cpu_thread_events.pop_front(); + lock.unlock(); + event.first(); + lock.lock(); + + if (event.second) + { + s_blocking_cpu_events_pending--; + s_cpu_thread_event_done.notify_one(); + } + } +} + +void NoGUIHost::CPUThreadEntryPoint() +{ + Threading::SetNameOfCurrentThread("CPU Thread"); + + // input source setup must happen on emu thread + CommonHost::Initialize(); + + // start the GS thread up and get it going + if (AcquireHostDisplay(HostDisplay::GetPreferredAPI())) + { + // kick a game list refresh if we're not in batch mode + if (!InBatchMode()) + Host::RefreshGameListAsync(false); + + CPUThreadMainLoop(); + + Host::CancelGameListRefresh(); + } + else + { + g_nogui_window->ReportError("Error", "Failed to open host display."); + } + + // finish any events off (e.g. shutdown system with save) + ProcessCPUThreadEvents(false); + + if (System::IsValid()) + System::ShutdownSystem(false); + ReleaseHostDisplay(); + + CommonHost::Shutdown(); + g_nogui_window->QuitMessageLoop(); +} + +void NoGUIHost::CPUThreadMainLoop() +{ + while (s_running.load(std::memory_order_acquire)) + { + if (System::IsRunning()) + { + System::Execute(); + continue; + } + + Host::PumpMessagesOnCPUThread(); + Host::RenderDisplay(false); + } +} + +bool NoGUIHost::AcquireHostDisplay(HostDisplay::RenderAPI api) +{ + Assert(!g_host_display); + + g_nogui_window->ExecuteInMessageLoop([api]() { + if (g_nogui_window->CreatePlatformWindow(GetWindowTitle(System::GetRunningTitle()))) + { + const std::optional wi(g_nogui_window->GetPlatformWindowInfo()); + if (wi.has_value()) + { + g_host_display = Host::CreateDisplayForAPI(api); + if (g_host_display) + { + if (!g_host_display->CreateRenderDevice(wi.value(), g_settings.gpu_adapter, g_settings.gpu_use_debug_device, + g_settings.gpu_threaded_presentation)) + { + g_host_display.reset(); + } + } + } + + if (g_host_display) + g_host_display->DoneRenderContextCurrent(); + else + g_nogui_window->DestroyPlatformWindow(); + } + + s_host_display_created.Post(); + }); + + s_host_display_created.Wait(); + + if (!g_host_display) + { + g_nogui_window->ReportError("Error", "Failed to create host display."); + return false; + } + + if (!g_host_display->MakeRenderContextCurrent() || + !g_host_display->InitializeRenderDevice(EmuFolders::Cache, g_settings.gpu_use_debug_device, + g_settings.gpu_threaded_presentation) || + !ImGuiManager::Initialize() || !CommonHost::CreateHostDisplayResources()) + { + ImGuiManager::Shutdown(); + CommonHost::ReleaseHostDisplayResources(); + g_host_display->DestroyRenderDevice(); + g_host_display.reset(); + return false; + } + + if (!FullscreenUI::Initialize()) + { + g_nogui_window->ReportError("Error", "Failed to initialize fullscreen UI"); + ReleaseHostDisplay(); + return false; + } + + return true; +} + +bool Host::AcquireHostDisplay(HostDisplay::RenderAPI api) +{ + if (g_host_display && g_host_display->GetRenderAPI() == api) + { + // current is fine + return true; + } + + // otherwise we need to switch + NoGUIHost::ReleaseHostDisplay(); + return NoGUIHost::AcquireHostDisplay(api); +} + +void NoGUIHost::ReleaseHostDisplay() +{ + if (!g_host_display) + return; + + CommonHost::ReleaseHostDisplayResources(); + ImGuiManager::Shutdown(); + g_host_display->DestroyRenderDevice(); + g_host_display.reset(); + g_nogui_window->ExecuteInMessageLoop([]() { g_nogui_window->DestroyPlatformWindow(); }); +} + +void Host::ReleaseHostDisplay() +{ + // we keep the fsui going, so no need to do anything here +} + +void Host::OnSystemStarting() +{ + CommonHost::OnSystemStarting(); + Log_VerbosePrintf("Host::OnSystemStarting()"); + s_save_state_on_shutdown = false; + s_was_paused_by_focus_loss = false; +} + +void Host::OnSystemStarted() +{ + CommonHost::OnSystemStarted(); + Log_VerbosePrintf("Host::OnSystemStarted()"); +} + +void Host::OnSystemPaused() +{ + CommonHost::OnSystemPaused(); + Log_VerbosePrintf("Host::OnSystemPaused()"); +} + +void Host::OnSystemResumed() +{ + CommonHost::OnSystemResumed(); + Log_VerbosePrintf("Host::OnSystemResumed()"); +} + +void Host::OnSystemDestroyed() +{ + CommonHost::OnSystemDestroyed(); + Log_VerbosePrintf("Host::OnSystemDestroyed()"); +} + +void Host::InvalidateDisplay() +{ + RenderDisplay(false); +} + +void Host::RenderDisplay(bool skip_present) +{ + // acquire for IO.MousePos. + std::atomic_thread_fence(std::memory_order_acquire); + + if (!skip_present) + { + FullscreenUI::Render(); + ImGuiManager::RenderOverlays(); + ImGuiManager::RenderOSD(); + ImGuiManager::RenderDebugWindows(); + } + + g_host_display->Render(skip_present); + + ImGuiManager::NewFrame(); +} + +// void Host::ResizeHostDisplay(u32 new_window_width, u32 new_window_height, float new_window_scale) +// { +// s_host_display->ResizeRenderWindow(new_window_width, new_window_height, new_window_scale); +// ImGuiManager::WindowResized(); +// } + +void Host::RequestResizeHostDisplay(s32 width, s32 height) +{ + g_nogui_window->RequestRenderWindowSize(width, height); +} + +void Host::OnPerformanceCountersUpdated() +{ + // noop +} + +void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name) +{ + CommonHost::OnGameChanged(disc_path, game_serial, game_name); + Log_VerbosePrintf("Host::OnGameChanged(\"%s\", \"%s\", \"%s\")", disc_path.c_str(), game_serial.c_str(), + game_name.c_str()); + NoGUIHost::UpdateWindowTitle(game_name); +} + +#ifdef WITH_CHEEVOS +void Host::OnAchievementsRefreshed() +{ + // noop +} +#endif + +void Host::SetMouseMode(bool relative, bool hide_cursor) +{ + // TODO: Find a better home for this. + if (InputManager::HasPointerAxisBinds()) + { + relative = true; + hide_cursor = true; + } + + // emit g_emu_thread->mouseModeRequested(relative, hide_cursor); +} + +void Host::PumpMessagesOnCPUThread() +{ + NoGUIHost::ProcessCPUThreadPlatformMessages(); + NoGUIHost::ProcessCPUThreadEvents(false); + CommonHost::PumpMessagesOnCPUThread(); // calls InputManager::PollSources() +} + +std::unique_ptr NoGUIHost::CreatePlatform() +{ + std::unique_ptr ret; + +#if defined(_WIN32) + ret = NoGUIPlatform::CreateWin32Platform(); +#elif defined(__APPLE__) + // nothing yet +#else + // linux + const char* platform = std::getenv("DUCKSTATION_NOGUI_PLATFORM"); +#ifdef NOGUI_PLATFORM_WAYLAND + if (!ret && (!platform || StringUtil::Strcasecmp(platform, "wayland") == 0) && std::getenv("WAYLAND_DISPLAY")) + ret = NoGUIPlatform::CreateWaylandPlatform(); +#endif +#ifdef NOGUI_PLATFORM_X11 + if (!ret && (!platform || StringUtil::Strcasecmp(platform, "x11") == 0) && std::getenv("DISPLAY")) + ret = NoGUIPlatform::CreateX11Platform(); +#endif +#ifdef NOGUI_PLATFORM_VTY + if (!ret && (!platform || StringUtil::Strcasecmp(platform, "vty") == 0)) + ret = NoGUIPlatform::CreateVTYPlatform(); +#endif +#endif + + return ret; +} + +std::string NoGUIHost::GetWindowTitle(const std::string& game_title) +{ + std::string suffix(GetAppConfigSuffix()); + std::string window_title; + if (System::IsShutdown() || game_title.empty()) + window_title = GetAppNameAndVersion() + suffix; + else + window_title = game_title; + + return window_title; +} + +void NoGUIHost::UpdateWindowTitle(const std::string& game_title) +{ + g_nogui_window->SetPlatformWindowTitle(GetWindowTitle(game_title)); +} + +void Host::RunOnCPUThread(std::function function, bool block /* = false */) +{ + std::unique_lock lock(s_cpu_thread_events_mutex); + s_cpu_thread_events.emplace_back(std::move(function), block); + s_cpu_thread_event_posted.notify_one(); + if (block) + s_cpu_thread_event_done.wait(lock, []() { return s_blocking_cpu_events_pending == 0; }); +} + +void NoGUIHost::GameListRefreshThreadEntryPoint(bool invalidate_cache) +{ + Threading::SetNameOfCurrentThread("Game List Refresh"); + + FullscreenUI::ProgressCallback callback("game_list_refresh"); + std::unique_lock lock(s_game_list_refresh_lock); + s_game_list_refresh_progress = &callback; + + lock.unlock(); + GameList::Refresh(invalidate_cache, false, &callback); + lock.lock(); + + s_game_list_refresh_progress = nullptr; +} + +void Host::RefreshGameListAsync(bool invalidate_cache) +{ + CancelGameListRefresh(); + + s_game_list_refresh_thread = std::thread(NoGUIHost::GameListRefreshThreadEntryPoint, invalidate_cache); +} + +void Host::CancelGameListRefresh() +{ + std::unique_lock lock(s_game_list_refresh_lock); + if (!s_game_list_refresh_thread.joinable()) + return; + + if (s_game_list_refresh_progress) + s_game_list_refresh_progress->SetCancelled(); + + lock.unlock(); + s_game_list_refresh_thread.join(); +} + +bool Host::IsFullscreen() +{ + return s_is_fullscreen; +} + +void Host::SetFullscreen(bool enabled) +{ + if (s_is_fullscreen == enabled) + return; + + s_is_fullscreen = enabled; + g_nogui_window->SetFullscreen(enabled); +} + +void* Host::GetTopLevelWindowHandle() +{ + return g_nogui_window->GetPlatformWindowHandle(); +} + +void Host::RequestExit(bool save_state_if_running) +{ + if (System::IsValid()) + { + Host::RunOnCPUThread([save_state_if_running]() { System::ShutdownSystem(save_state_if_running); }); + } + + // clear the running flag, this'll break out of the main CPU loop once the VM is shutdown. + s_running.store(false, std::memory_order_release); +} + +void Host::RequestSystemShutdown(bool allow_confirm, bool allow_save_state) +{ + // TODO: Confirm + if (System::IsValid()) + { + Host::RunOnCPUThread([allow_save_state]() { System::ShutdownSystem(allow_save_state); }); + } +} + +std::optional InputManager::ConvertHostKeyboardStringToCode(const std::string_view& str) +{ + return g_nogui_window->ConvertHostKeyboardStringToCode(str); +} + +std::optional InputManager::ConvertHostKeyboardCodeToString(u32 code) +{ + return g_nogui_window->ConvertHostKeyboardCodeToString(code); +} + +BEGIN_HOTKEY_LIST(g_host_hotkeys) +END_HOTKEY_LIST() + +static void SignalHandler(int signal) +{ + // First try the normal (graceful) shutdown/exit. + static bool graceful_shutdown_attempted = false; + if (!graceful_shutdown_attempted) + { + std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n"); + graceful_shutdown_attempted = true; + Host::RequestExit(true); + return; + } + + std::signal(signal, SIG_DFL); + + // MacOS is missing std::quick_exit() despite it being C++11... +#ifndef __APPLE__ + std::quick_exit(1); +#else + _Exit(1); +#endif +} + +void NoGUIHost::HookSignals() +{ + std::signal(SIGINT, SignalHandler); + std::signal(SIGTERM, SignalHandler); +} + +void NoGUIHost::InitializeEarlyConsole() +{ + const bool was_console_enabled = Log::IsConsoleOutputEnabled(); + if (!was_console_enabled) + Log::SetConsoleOutputParams(true); +} + +void NoGUIHost::PrintCommandLineVersion() +{ + InitializeEarlyConsole(); + + std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str); + std::fprintf(stderr, "https://github.com/stenzek/duckstation\n"); + std::fprintf(stderr, "\n"); +} + +void NoGUIHost::PrintCommandLineHelp(const char* progname) +{ + InitializeEarlyConsole(); + + PrintCommandLineVersion(); + std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname); + std::fprintf(stderr, "\n"); + std::fprintf(stderr, " -help: Displays this information and exits.\n"); + std::fprintf(stderr, " -version: Displays version information and exits.\n"); + std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n"); + std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n"); + std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n"); + std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n"); + std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n" + " that game's resume state will be loaded, otherwise the most\n" + " recent resume save state will be loaded.\n"); + std::fprintf(stderr, " -state : Loads specified save state by index. If a boot\n" + " filename is provided, a per-game state will be loaded, otherwise\n" + " a global state will be loaded.\n"); + std::fprintf(stderr, " -statefile : Loads state from the specified filename.\n" + " No boot filename is required with this option.\n"); + std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n"); + std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n"); + std::fprintf(stderr, " -portable: Forces \"portable mode\", data in same directory.\n"); + std::fprintf(stderr, " -nocontroller: Prevents the emulator from polling for controllers.\n" + " Try this option if you're having difficulties starting\n" + " the emulator.\n"); + std::fprintf(stderr, " -settings : Loads a custom settings configuration from the\n" + " specified filename. Default settings applied if file not found.\n"); + std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n"); + std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" + " parameters make up the filename. Use when the filename contains\n" + " spaces or starts with a dash.\n"); + std::fprintf(stderr, "\n"); +} + +std::optional& AutoBoot(std::optional& autoboot) +{ + if (!autoboot) + autoboot.emplace(); + + return autoboot; +} + +bool NoGUIHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[], + std::optional& autoboot) +{ + std::optional state_index; + std::string settings_filename; + bool starting_bios = false; + + bool no_more_args = false; + + for (int i = 1; i < argc; i++) + { + if (!no_more_args) + { +#define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0) +#define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc)) + + if (CHECK_ARG("-help")) + { + PrintCommandLineHelp(argv[0]); + return false; + } + else if (CHECK_ARG("-version")) + { + PrintCommandLineVersion(); + return false; + } + else if (CHECK_ARG("-batch")) + { + Log_InfoPrintf("Command Line: Using batch mode."); + s_batch_mode = true; + continue; + } + else if (CHECK_ARG("-bios")) + { + Log_InfoPrintf("Command Line: Starting BIOS."); + AutoBoot(autoboot); + starting_bios = true; + continue; + } + else if (CHECK_ARG("-fastboot")) + { + Log_InfoPrintf("Command Line: Forcing fast boot."); + AutoBoot(autoboot)->override_fast_boot = true; + continue; + } + else if (CHECK_ARG("-slowboot")) + { + Log_InfoPrintf("Command Line: Forcing slow boot."); + AutoBoot(autoboot)->override_fast_boot = false; + continue; + } + else if (CHECK_ARG("-nocontroller")) + { + Log_InfoPrintf("Command Line: Disabling controller support."); + // m_flags.disable_controller_interface = true; + Panic("Fixme"); + continue; + } + else if (CHECK_ARG("-resume")) + { + state_index = -1; + Log_InfoPrintf("Command Line: Loading resume state."); + continue; + } + else if (CHECK_ARG_PARAM("-state")) + { + state_index = StringUtil::FromChars(argv[++i]); + if (!state_index.has_value()) + { + Log_ErrorPrintf("Invalid state index"); + return false; + } + + Log_InfoPrintf("Command Line: Loading state index: %d", state_index.value()); + continue; + } + else if (CHECK_ARG_PARAM("-statefile")) + { + AutoBoot(autoboot)->save_state = argv[++i]; + Log_InfoPrintf("Command Line: Loading state file: '%s'", autoboot->save_state.c_str()); + continue; + } + else if (CHECK_ARG("-fullscreen")) + { + Log_InfoPrintf("Command Line: Using fullscreen."); + AutoBoot(autoboot)->override_fullscreen = true; + // s_start_fullscreen_ui_fullscreen = true; + continue; + } + else if (CHECK_ARG("-nofullscreen")) + { + Log_InfoPrintf("Command Line: Not using fullscreen."); + AutoBoot(autoboot)->override_fullscreen = false; + continue; + } + else if (CHECK_ARG("-portable")) + { + Log_InfoPrintf("Command Line: Using portable mode."); + // SetUserDirectoryToProgramDirectory(); + Panic("Fixme"); + continue; + } + else if (CHECK_ARG_PARAM("-settings")) + { + settings_filename = argv[++i]; + Log_InfoPrintf("Command Line: Overriding settings filename: %s", settings_filename.c_str()); + continue; + } + else if (CHECK_ARG("-earlyconsole")) + { + InitializeEarlyConsole(); + continue; + } + else if (CHECK_ARG("--")) + { + no_more_args = true; + continue; + } + else if (argv[i][0] == '-') + { + g_nogui_window->ReportError("Error", fmt::format("Unknown parameter: {}", argv[i])); + return false; + } + +#undef CHECK_ARG +#undef CHECK_ARG_PARAM + } + + if (autoboot && !autoboot->filename.empty()) + autoboot->filename += ' '; + AutoBoot(autoboot)->filename += argv[i]; + } + + // To do anything useful, we need the config initialized. + if (!NoGUIHost::InitializeConfig(std::move(settings_filename))) + { + // NOTE: No point translating this, because no config means the language won't be loaded anyway. + g_nogui_window->ReportError("Error", "Failed to initialize config."); + return EXIT_FAILURE; + } + + // Check the file we're starting actually exists. + + if (autoboot && !autoboot->filename.empty() && !FileSystem::FileExists(autoboot->filename.c_str())) + { + g_nogui_window->ReportError("Error", fmt::format("File '{}' does not exist.", autoboot->filename)); + return false; + } + + if (state_index.has_value()) + { + AutoBoot(autoboot); + + if (autoboot->filename.empty()) + { + // loading global state, -1 means resume the last game + if (state_index.value() < 0) + autoboot->save_state = System::GetMostRecentResumeSaveStatePath(); + else + autoboot->save_state = System::GetGlobalSaveStateFileName(state_index.value()); + } + else + { + // loading game state + const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->filename.c_str())); + autoboot->save_state = System::GetGameSaveStateFileName(game_serial, state_index.value()); + } + + if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str())) + { + g_nogui_window->ReportError("Error", "The specified save state does not exist."); + return false; + } + } + + // check autoboot parameters, if we set something like fullscreen without a bios + // or disc, we don't want to actually start. + if (autoboot && autoboot->filename.empty() && autoboot->save_state.empty() && !starting_bios) + autoboot.reset(); + + return true; +} + +int main(int argc, char* argv[]) +{ + CrashHandler::Install(); + + g_nogui_window = NoGUIHost::CreatePlatform(); + if (!g_nogui_window) + return EXIT_FAILURE; + + std::optional autoboot; + if (!NoGUIHost::ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot)) + return EXIT_FAILURE; + + // the rest of initialization happens on the CPU thread. + NoGUIHost::HookSignals(); + NoGUIHost::StartCPUThread(); + + if (autoboot) + NoGUIHost::StartSystem(std::move(autoboot.value())); + + g_nogui_window->RunMessageLoop(); + + NoGUIHost::StopCPUThread(); + + // Ensure log is flushed. + Log::SetFileOutputParams(false, nullptr); + + s_base_settings_interface.reset(); + g_nogui_window.reset(); + return EXIT_SUCCESS; +} + +#ifdef _WIN32 + +int wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) +{ + std::vector argc_strings; + argc_strings.reserve(1); + + // CommandLineToArgvW() only adds the program path if the command line is empty?! + argc_strings.push_back(FileSystem::GetProgramPath()); + + if (std::wcslen(lpCmdLine) > 0) + { + int argc; + LPWSTR* argv_wide = CommandLineToArgvW(lpCmdLine, &argc); + if (argv_wide) + { + for (int i = 0; i < argc; i++) + argc_strings.push_back(StringUtil::WideStringToUTF8String(argv_wide[i])); + + LocalFree(argv_wide); + } + } + + std::vector argc_pointers; + argc_pointers.reserve(argc_strings.size()); + for (std::string& arg : argc_strings) + argc_pointers.push_back(arg.data()); + + return main(static_cast(argc_pointers.size()), argc_pointers.data()); +} + +#endif diff --git a/src/duckstation-nogui/nogui_host.h b/src/duckstation-nogui/nogui_host.h new file mode 100644 index 000000000..3d375f8e4 --- /dev/null +++ b/src/duckstation-nogui/nogui_host.h @@ -0,0 +1,34 @@ +#pragma once +#include "common/types.h" +#include "core/system.h" +#include +#include + +namespace NoGUIHost { +/// Sets batch mode (exit after game shutdown). +bool InBatchMode(); +void SetBatchMode(bool enabled); + +/// Starts the virtual machine. +void StartSystem(SystemBootParameters params); + +/// Returns the application name and version, optionally including debug/devel config indicator. +std::string GetAppNameAndVersion(); + +/// Returns the debug/devel config indicator. +std::string GetAppConfigSuffix(); + +/// Thread-safe settings access. +void SaveSettings(); + +/// Called on the UI thread in response to various events. +void ProcessPlatformWindowResize(s32 width, s32 height, float scale); +void ProcessPlatformMouseMoveEvent(float x, float y); +void ProcessPlatformMouseButtonEvent(s32 button, bool pressed); +void ProcessPlatformMouseWheelEvent(float x, float y); +void ProcessPlatformKeyEvent(s32 key, bool pressed); +void PlatformWindowFocusGained(); +void PlatformWindowFocusLost(); +bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height); +void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height); +} // namespace NoGUIHost \ No newline at end of file diff --git a/src/duckstation-nogui/nogui_host_interface.cpp b/src/duckstation-nogui/nogui_host_interface.cpp deleted file mode 100644 index ce6219065..000000000 --- a/src/duckstation-nogui/nogui_host_interface.cpp +++ /dev/null @@ -1,352 +0,0 @@ -#include "nogui_host_interface.h" -#include "common/assert.h" -#include "common/byte_stream.h" -#include "common/file_system.h" -#include "common/log.h" -#include "common/string_util.h" -#include "core/controller.h" -#include "core/gpu.h" -#include "core/host_display.h" -#include "core/imgui_styles.h" -#include "core/system.h" -#include "frontend-common/controller_interface.h" -#include "frontend-common/fullscreen_ui.h" -#include "frontend-common/icon.h" -#include "frontend-common/ini_settings_interface.h" -#include "frontend-common/opengl_host_display.h" -#include "frontend-common/vulkan_host_display.h" -#include "imgui.h" -#include "imgui_internal.h" -#include "imgui_stdlib.h" -#include -#include -Log_SetChannel(NoGUIHostInterface); - -#ifdef _WIN32 -#include "frontend-common/d3d11_host_display.h" -#include "frontend-common/d3d12_host_display.h" -#endif - -NoGUIHostInterface::NoGUIHostInterface() = default; - -NoGUIHostInterface::~NoGUIHostInterface() = default; - -const char* NoGUIHostInterface::GetFrontendName() const -{ - return "DuckStation NoGUI Frontend"; -} - -bool NoGUIHostInterface::Initialize() -{ - SetUserDirectory(); - m_settings_interface = std::make_unique(GetSettingsFileName()); - - // TODO: Make command line. - m_flags.force_fullscreen_ui = true; - - if (!CommonHostInterface::Initialize()) - return false; - - const bool start_fullscreen = m_flags.start_fullscreen || g_settings.start_fullscreen; - if (!CreatePlatformWindow()) - { - Log_ErrorPrintf("Failed to create platform window"); - return false; - } - - if (!CreateDisplay(start_fullscreen)) - { - Log_ErrorPrintf("Failed to create host display"); - DestroyPlatformWindow(); - return false; - } - - if (m_fullscreen_ui_enabled) - { - FullscreenUI::SetDebugMenuAllowed(true); - FullscreenUI::QueueGameListRefresh(); - } - - // process events to pick up controllers before updating input map - PollAndUpdate(); - UpdateInputMap(); - return true; -} - -void NoGUIHostInterface::Shutdown() -{ - DestroyDisplay(); - DestroyPlatformWindow(); - - CommonHostInterface::Shutdown(); -} - -void NoGUIHostInterface::SetDefaultSettings(SettingsInterface& si) -{ - CommonHostInterface::SetDefaultSettings(si); - - // TODO: Maybe we should bind this to F1 in the future. - si.SetStringValue("Hotkeys", "OpenQuickMenu", "Keyboard/Escape"); -} - -void NoGUIHostInterface::OnDisplayInvalidated() {} - -void NoGUIHostInterface::OnSystemPerformanceCountersUpdated() {} - -bool NoGUIHostInterface::CreateDisplay(bool fullscreen) -{ - std::optional wi = GetPlatformWindowInfo(); - if (!wi) - { - ReportError("Failed to get platform window info"); - return false; - } - - Assert(!m_display); - switch (g_settings.gpu_renderer) - { - case GPURenderer::HardwareVulkan: - m_display = std::make_unique(); - break; - - case GPURenderer::HardwareOpenGL: -#ifndef _WIN32 - default: -#endif - m_display = std::make_unique(); - break; - -#ifdef _WIN32 - case GPURenderer::HardwareD3D12: - m_display = std::make_unique(); - break; - - case GPURenderer::HardwareD3D11: - default: - m_display = std::make_unique(); - break; -#endif - } - - if (!m_display->CreateRenderDevice(wi.value(), g_settings.gpu_adapter, g_settings.gpu_use_debug_device, - g_settings.gpu_threaded_presentation) || - !m_display->InitializeRenderDevice(GetShaderCacheBasePath(), g_settings.gpu_use_debug_device, - g_settings.gpu_threaded_presentation) || - !CreateHostDisplayResources()) - { - m_display->DestroyRenderDevice(); - m_display.reset(); - ReportError("Failed to create/initialize display render device"); - return false; - } - - if (fullscreen) - SetFullscreen(true); - - if (!CreateHostDisplayResources()) - Log_WarningPrint("Failed to create host display resources"); - - Log_InfoPrintf("Host display initialized at %ux%u resolution", m_display->GetWindowWidth(), - m_display->GetWindowHeight()); - return true; -} - -void NoGUIHostInterface::DestroyDisplay() -{ - ReleaseHostDisplayResources(); - - if (m_display) - m_display->DestroyRenderDevice(); - - m_display.reset(); -} - -bool NoGUIHostInterface::AcquireHostDisplay() -{ - // Handle renderer switch if required. - const HostDisplay::RenderAPI render_api = m_display->GetRenderAPI(); - bool needs_switch = false; - switch (g_settings.gpu_renderer) - { -#ifdef _WIN32 - case GPURenderer::HardwareD3D11: - needs_switch = (render_api != HostDisplay::RenderAPI::D3D11); - break; -#endif - - case GPURenderer::HardwareVulkan: - needs_switch = (render_api != HostDisplay::RenderAPI::Vulkan); - break; - - case GPURenderer::HardwareOpenGL: - needs_switch = (render_api != HostDisplay::RenderAPI::OpenGL && render_api != HostDisplay::RenderAPI::OpenGLES); - break; - - case GPURenderer::Software: - default: - needs_switch = false; - break; - } - - if (needs_switch) - { - const bool was_fullscreen = IsFullscreen(); - - DestroyDisplay(); - - // We need to recreate the window, otherwise bad things happen... - DestroyPlatformWindow(); - if (!CreatePlatformWindow()) - Panic("Failed to recreate platform window on GPU renderer switch"); - - if (!CreateDisplay(was_fullscreen)) - Panic("Failed to recreate display on GPU renderer switch"); - } - - return true; -} - -void NoGUIHostInterface::ReleaseHostDisplay() -{ - // restore vsync, since we don't want to burn cycles at the menu - m_display->SetVSync(true); -} - -void NoGUIHostInterface::RequestExit() -{ - m_quit_request = true; -} - -void NoGUIHostInterface::Run() -{ - while (!m_quit_request) - { - RunCallbacks(); - PollAndUpdate(); - - ImGui::NewFrame(); - - if (System::IsRunning()) - { - if (m_display_all_frames) - System::RunFrame(); - else - System::RunFrames(); - - UpdateControllerMetaState(); - if (m_frame_step_request) - { - m_frame_step_request = false; - PauseSystem(true); - } - } - - // rendering - { - DrawImGuiWindows(); - ImGui::Render(); - ImGui::EndFrame(); - - m_display->Render(); - - if (System::IsRunning()) - { - System::UpdatePerformanceCounters(); - - if (m_throttler_enabled) - System::Throttle(); - } - } - } - - // Save state on exit so it can be resumed - if (!System::IsShutdown()) - PowerOffSystem(ShouldSaveResumeState()); -} - -void NoGUIHostInterface::ReportMessage(const char* message) -{ - Log_InfoPrint(message); - AddOSDMessage(message, 10.0f); -} - -void NoGUIHostInterface::ReportError(const char* message) -{ - Log_ErrorPrint(message); - - if (!m_display) - return; - - const bool was_in_frame = GImGui->FrameCount != GImGui->FrameCountEnded; - if (was_in_frame) - ImGui::EndFrame(); - - bool done = false; - while (!done) - { - RunCallbacks(); - PollAndUpdate(); - if (m_fullscreen_ui_enabled) - FullscreenUI::SetImGuiNavInputs(); - - ImGui::NewFrame(); - done = FullscreenUI::DrawErrorWindow(message); - ImGui::EndFrame(); - m_display->Render(); - } - - if (was_in_frame) - ImGui::NewFrame(); -} - -bool NoGUIHostInterface::ConfirmMessage(const char* message) -{ - Log_InfoPrintf("Confirm: %s", message); - - if (!m_display) - return true; - - const bool was_in_frame = GImGui->FrameCount != GImGui->FrameCountEnded; - if (was_in_frame) - ImGui::EndFrame(); - - bool done = false; - bool result = true; - while (!done) - { - RunCallbacks(); - PollAndUpdate(); - if (m_fullscreen_ui_enabled) - FullscreenUI::SetImGuiNavInputs(); - - ImGui::NewFrame(); - done = FullscreenUI::DrawConfirmWindow(message, &result); - ImGui::EndFrame(); - m_display->Render(); - } - - if (was_in_frame) - ImGui::NewFrame(); - - return result; -} - -void NoGUIHostInterface::RunLater(std::function callback) -{ - std::unique_lock lock(m_queued_callbacks_lock); - m_queued_callbacks.push_back(std::move(callback)); -} - -void NoGUIHostInterface::RunCallbacks() -{ - std::unique_lock lock(m_queued_callbacks_lock); - - while (!m_queued_callbacks.empty()) - { - auto callback = std::move(m_queued_callbacks.front()); - m_queued_callbacks.pop_front(); - lock.unlock(); - callback(); - lock.lock(); - } -} diff --git a/src/duckstation-nogui/nogui_host_interface.h b/src/duckstation-nogui/nogui_host_interface.h deleted file mode 100644 index d6b415383..000000000 --- a/src/duckstation-nogui/nogui_host_interface.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include "common/window_info.h" -#include "core/host_display.h" -#include "core/host_interface.h" -#include "frontend-common/common_host.h" -#include -#include -#include -#include -#include -#include -#include - -class INISettingsInterface; - -class NoGUIHostInterface : public CommonHostInterface -{ -public: - NoGUIHostInterface(); - ~NoGUIHostInterface(); - - const char* GetFrontendName() const override; - - virtual bool Initialize() override; - virtual void Shutdown() override; - virtual void Run(); - - void ReportMessage(const char* message) override; - void ReportError(const char* message) override; - bool ConfirmMessage(const char* message) override; - - void RunLater(std::function callback) override; - - virtual void OnDisplayInvalidated() override; - virtual void OnSystemPerformanceCountersUpdated() override; - -protected: - enum : u32 - { - DEFAULT_WINDOW_WIDTH = 1280, - DEFAULT_WINDOW_HEIGHT = 720 - }; - - bool AcquireHostDisplay() override; - void ReleaseHostDisplay() override; - - void RequestExit() override; - - virtual void SetDefaultSettings(SettingsInterface& si) override; - - virtual bool CreatePlatformWindow() = 0; - virtual void DestroyPlatformWindow() = 0; - virtual std::optional GetPlatformWindowInfo() = 0; - - bool CreateDisplay(bool fullscreen); - void DestroyDisplay(); - void RunCallbacks(); - - std::deque> m_queued_callbacks; - std::mutex m_queued_callbacks_lock; - - bool m_quit_request = false; -}; diff --git a/src/duckstation-nogui/nogui_platform.h b/src/duckstation-nogui/nogui_platform.h new file mode 100644 index 000000000..24bb37214 --- /dev/null +++ b/src/duckstation-nogui/nogui_platform.h @@ -0,0 +1,59 @@ +#pragma once + +#include "common/types.h" +#include "core/host_display.h" +#include +#include +#include +#include +#include + +class SettingsInterface; + +class NoGUIPlatform +{ +public: + virtual ~NoGUIPlatform() = default; + + virtual void ReportError(const std::string_view& title, const std::string_view& message) = 0; + + virtual void SetDefaultConfig(SettingsInterface& si) = 0; + + virtual bool CreatePlatformWindow(std::string title) = 0; + virtual void DestroyPlatformWindow() = 0; + + virtual std::optional GetPlatformWindowInfo() = 0; + virtual void SetPlatformWindowTitle(std::string title) = 0; + virtual void* GetPlatformWindowHandle() = 0; + + virtual std::optional ConvertHostKeyboardStringToCode(const std::string_view& str) = 0; + virtual std::optional ConvertHostKeyboardCodeToString(u32 code) = 0; + + virtual void RunMessageLoop() = 0; + virtual void ExecuteInMessageLoop(std::function func) = 0; + virtual void QuitMessageLoop() = 0; + + virtual void SetFullscreen(bool enabled) = 0; + + virtual bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) = 0; + +#ifdef _WIN32 + static std::unique_ptr CreateWin32Platform(); +#endif + +#ifdef NOGUI_PLATFORM_WAYLAND + static std::unique_ptr CreateWaylandPlatform(); +#endif +#ifdef NOGUI_PLATFORM_X11 + static std::unique_ptr CreateX11Platform(); +#endif +#ifdef NOGUI_PLATFORM_VTY + static std::unique_ptr CreateVTYPlatform(); +#endif + +protected: + static constexpr s32 DEFAULT_WINDOW_WIDTH = 1280; + static constexpr s32 DEFAULT_WINDOW_HEIGHT = 720; +}; + +extern std::unique_ptr g_nogui_window; \ No newline at end of file diff --git a/src/duckstation-nogui/sdl_host_interface.cpp b/src/duckstation-nogui/sdl_host_interface.cpp deleted file mode 100644 index 8039e272a..000000000 --- a/src/duckstation-nogui/sdl_host_interface.cpp +++ /dev/null @@ -1,438 +0,0 @@ -#include "sdl_host_interface.h" -#include "core/system.h" -#include "frontend-common/controller_interface.h" -#include "frontend-common/fullscreen_ui.h" -#include "frontend-common/icon.h" -#include "frontend-common/ini_settings_interface.h" -#include "frontend-common/sdl_controller_interface.h" -#include "frontend-common/sdl_initializer.h" -#include "imgui.h" -#include "imgui_impl_sdl.h" -#include "scmversion/scmversion.h" -#include "sdl_key_names.h" -#include -#include -#include -Log_SetChannel(SDLHostInterface); - -#ifdef __APPLE__ -#include -struct NSView; - -static NSView* GetContentViewFromWindow(NSWindow* window) -{ - // window.contentView - return reinterpret_cast(objc_msgSend)(reinterpret_cast(window), sel_getUid("contentView")); -} -#endif - -static float GetDPIScaleFactor(SDL_Window* window) -{ -#ifdef __APPLE__ - static constexpr float DEFAULT_DPI = 72.0f; -#else - static constexpr float DEFAULT_DPI = 96.0f; -#endif - - if (!window) - { - SDL_Window* dummy_window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1, 1, - SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); - if (!dummy_window) - return 1.0f; - - const float scale = GetDPIScaleFactor(dummy_window); - - SDL_DestroyWindow(dummy_window); - - return scale; - } - - int display_index = SDL_GetWindowDisplayIndex(window); - float display_dpi = DEFAULT_DPI; - if (SDL_GetDisplayDPI(display_index, &display_dpi, nullptr, nullptr) != 0) - return 1.0f; - - return display_dpi / DEFAULT_DPI; -} - -SDLHostInterface::SDLHostInterface() = default; - -SDLHostInterface::~SDLHostInterface() = default; - -const char* SDLHostInterface::GetFrontendName() const -{ - return "DuckStation NoGUI Frontend"; -} - -std::unique_ptr SDLHostInterface::Create() -{ - return std::make_unique(); -} - -bool SDLHostInterface::Initialize() -{ - FrontendCommon::EnsureSDLInitialized(); - - if (!NoGUIHostInterface::Initialize()) - return false; - - return true; -} - -void SDLHostInterface::Shutdown() -{ - NoGUIHostInterface::Shutdown(); -} - -bool SDLHostInterface::IsFullscreen() const -{ - return m_fullscreen; -} - -bool SDLHostInterface::SetFullscreen(bool enabled) -{ - if (m_fullscreen == enabled) - return true; - - const std::string fullscreen_mode(GetStringSettingValue("GPU", "FullscreenMode", "")); - const bool is_exclusive_fullscreen = (enabled && !fullscreen_mode.empty() && m_display->SupportsFullscreen()); - const bool was_exclusive_fullscreen = m_display->IsFullscreen(); - - if (was_exclusive_fullscreen) - m_display->SetFullscreen(false, 0, 0, 0.0f); - - SDL_SetWindowFullscreen(m_window, (enabled && !is_exclusive_fullscreen) ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); - - if (is_exclusive_fullscreen) - { - u32 width, height; - float refresh_rate; - bool result = false; - - if (ParseFullscreenMode(fullscreen_mode, &width, &height, &refresh_rate)) - { - result = m_display->SetFullscreen(true, width, height, refresh_rate); - if (result) - { - AddOSDMessage(TranslateStdString("OSDMessage", "Acquired exclusive fullscreen."), 10.0f); - } - else - { - AddOSDMessage(TranslateStdString("OSDMessage", "Failed to acquire exclusive fullscreen."), 10.0f); - enabled = false; - } - } - } - - m_fullscreen = enabled; - - const bool hide_cursor = (enabled && GetBoolSettingValue("Main", "HideCursorInFullscreen", true)); - SDL_ShowCursor(hide_cursor ? SDL_DISABLE : SDL_ENABLE); - return true; -} - -bool SDLHostInterface::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) -{ - if (new_window_width <= 0 || new_window_height <= 0 || m_fullscreen) - return false; - - // use imgui scale as the dpr - const float dpi_scale = ImGui::GetIO().DisplayFramebufferScale.x; - const s32 scaled_width = - std::max(static_cast(std::ceil(static_cast(new_window_width) * dpi_scale)), 1); - const s32 scaled_height = std::max( - static_cast(std::ceil(static_cast(new_window_height) * dpi_scale)) + m_display->GetDisplayTopMargin(), - 1); - - SDL_SetWindowSize(m_window, scaled_width, scaled_height); - return true; -} - -ALWAYS_INLINE static TinyString GetWindowTitle() -{ - return TinyString::FromFormat("DuckStation %s (%s)", g_scm_tag_str, g_scm_branch_str); -} - -bool SDLHostInterface::CreatePlatformWindow() -{ - // Create window. - const u32 window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; - - int window_x, window_y, window_width, window_height; - GetSavedWindowGeometry(&window_x, &window_y, &window_width, &window_height); - m_window = SDL_CreateWindow(GetWindowTitle(), window_x, window_y, window_width, window_height, window_flags); - if (!m_window) - return false; - - // Set window icon. - SDL_Surface* icon_surface = - SDL_CreateRGBSurfaceFrom(const_cast(WINDOW_ICON_DATA), WINDOW_ICON_WIDTH, WINDOW_ICON_HEIGHT, 32, - WINDOW_ICON_WIDTH * sizeof(u32), UINT32_C(0x000000FF), UINT32_C(0x0000FF00), - UINT32_C(0x00FF0000), UINT32_C(0xFF000000)); - if (icon_surface) - { - SDL_SetWindowIcon(m_window, icon_surface); - SDL_FreeSurface(icon_surface); - } - - ImGui_ImplSDL2_Init(m_window); - - // Process events so that we have everything sorted out before creating a child window for the GL context (X11). - SDL_PumpEvents(); - return true; -} - -void SDLHostInterface::DestroyPlatformWindow() -{ - SaveWindowGeometry(); - ImGui_ImplSDL2_Shutdown(); - SDL_DestroyWindow(m_window); - m_window = nullptr; - m_fullscreen = false; -} - -std::optional SDLHostInterface::GetPlatformWindowInfo() -{ - SDL_SysWMinfo syswm = {}; - SDL_VERSION(&syswm.version); - if (!SDL_GetWindowWMInfo(m_window, &syswm)) - { - Log_ErrorPrintf("SDL_GetWindowWMInfo failed"); - return std::nullopt; - } - - int window_width, window_height; - SDL_GetWindowSize(m_window, &window_width, &window_height); - - WindowInfo wi; - wi.surface_width = static_cast(window_width); - wi.surface_height = static_cast(window_height); - wi.surface_scale = GetDPIScaleFactor(m_window); - wi.surface_format = WindowInfo::SurfaceFormat::RGB8; - - switch (syswm.subsystem) - { -#ifdef SDL_VIDEO_DRIVER_WINDOWS - case SDL_SYSWM_WINDOWS: - wi.type = WindowInfo::Type::Win32; - wi.window_handle = syswm.info.win.window; - break; -#endif - -#ifdef SDL_VIDEO_DRIVER_COCOA - case SDL_SYSWM_COCOA: - wi.type = WindowInfo::Type::MacOS; - wi.window_handle = GetContentViewFromWindow(syswm.info.cocoa.window); - break; -#endif - -#ifdef SDL_VIDEO_DRIVER_X11 - case SDL_SYSWM_X11: - wi.type = WindowInfo::Type::X11; - wi.window_handle = reinterpret_cast(static_cast(syswm.info.x11.window)); - wi.display_connection = syswm.info.x11.display; - break; -#endif - -#ifdef SDL_VIDEO_DRIVER_WAYLAND - case SDL_SYSWM_WAYLAND: - wi.type = WindowInfo::Type::Wayland; - wi.window_handle = syswm.info.wl.surface; - wi.display_connection = syswm.info.wl.display; - break; -#endif - - default: - Log_ErrorPrintf("Unhandled syswm subsystem %u", static_cast(syswm.subsystem)); - return std::nullopt; - } - - return wi; -} - -std::optional SDLHostInterface::GetHostKeyCode(const std::string_view key_code) const -{ - const std::optional code = SDLKeyNames::ParseKeyString(key_code); - if (!code) - return std::nullopt; - - return static_cast(*code); -} - -void SDLHostInterface::SetMouseMode(bool relative, bool hide_cursor) {} - -void SDLHostInterface::PollAndUpdate() -{ - // Process SDL events before the controller interface can steal them. - const bool is_sdl_controller_interface = - (m_controller_interface && m_controller_interface->GetBackend() == ControllerInterface::Backend::SDL); - - for (;;) - { - SDL_Event ev; - if (!SDL_PollEvent(&ev)) - break; - - if (is_sdl_controller_interface && - static_cast(m_controller_interface.get())->ProcessSDLEvent(&ev)) - { - continue; - } - - HandleSDLEvent(&ev); - } - - ImGui_ImplSDL2_NewFrame(); - NoGUIHostInterface::PollAndUpdate(); -} - -void SDLHostInterface::HandleSDLEvent(const SDL_Event* event) -{ - ImGui_ImplSDL2_ProcessEvent(event); - - switch (event->type) - { - case SDL_WINDOWEVENT: - { - switch (event->window.event) - { - case SDL_WINDOWEVENT_SIZE_CHANGED: - { - s32 window_width, window_height; - SDL_GetWindowSize(m_window, &window_width, &window_height); - m_display->ResizeRenderWindow(window_width, window_height); - HostDisplayResized(); - } - break; - - case SDL_WINDOWEVENT_FOCUS_LOST: - { - if (g_settings.pause_on_focus_loss && System::IsRunning() && !m_was_paused_by_focus_loss) - { - PauseSystem(true); - m_was_paused_by_focus_loss = true; - } - } - break; - - case SDL_WINDOWEVENT_FOCUS_GAINED: - { - if (m_was_paused_by_focus_loss) - { - if (System::IsPaused()) - PauseSystem(false); - m_was_paused_by_focus_loss = false; - } - } - break; - - default: - break; - } - } - break; - - case SDL_QUIT: - m_quit_request = true; - break; - - case SDL_KEYDOWN: - case SDL_KEYUP: - { - const bool pressed = (event->type == SDL_KEYDOWN); - - // Binding mode - if (m_fullscreen_ui_enabled && FullscreenUI::IsBindingInput()) - { - if (event->key.repeat > 0) - return; - - TinyString key_string; - if (SDLKeyNames::KeyEventToString(event, key_string)) - { - if (FullscreenUI::HandleKeyboardBinding(key_string, pressed)) - return; - } - } - - if (!ImGui::GetIO().WantCaptureKeyboard && event->key.repeat == 0) - { - const u32 code = SDLKeyNames::KeyEventToInt(event); - HandleHostKeyEvent(code & SDLKeyNames::KEY_MASK, code & SDLKeyNames::MODIFIER_MASK, pressed); - } - } - break; - - case SDL_MOUSEMOTION: - { - m_display->SetMousePosition(event->motion.x, event->motion.y); - } - break; - - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - { - // map left -> 0, right -> 1, middle -> 2 to match with qt - static constexpr std::array mouse_mapping = {{1, 3, 2, 4, 5}}; - if (!ImGui::GetIO().WantCaptureMouse && event->button.button > 0 && event->button.button <= mouse_mapping.size()) - { - const s32 button = mouse_mapping[event->button.button - 1]; - const bool pressed = (event->type == SDL_MOUSEBUTTONDOWN); - HandleHostMouseEvent(button, pressed); - } - } - break; - } -} - -void SDLHostInterface::GetSavedWindowGeometry(int* x, int* y, int* width, int* height) -{ - auto lock = GetSettingsLock(); - *x = m_settings_interface->GetIntValue("SDLHostInterface", "WindowX", SDL_WINDOWPOS_UNDEFINED); - *y = m_settings_interface->GetIntValue("SDLHostInterface", "WindowY", SDL_WINDOWPOS_UNDEFINED); - - *width = m_settings_interface->GetIntValue("SDLHostInterface", "WindowWidth", -1); - *height = m_settings_interface->GetIntValue("SDLHostInterface", "WindowHeight", -1); - - if (*width < 0 || *height < 0) - { - *width = DEFAULT_WINDOW_WIDTH; - *height = DEFAULT_WINDOW_HEIGHT; - - // macOS does DPI scaling differently.. -#ifndef __APPLE__ - { - // scale by default monitor's DPI - float scale = GetDPIScaleFactor(nullptr); - *width = static_cast(std::round(static_cast(*width) * scale)); - *height = static_cast(std::round(static_cast(*height) * scale)); - } -#endif - } -} - -void SDLHostInterface::SaveWindowGeometry() -{ - if (m_fullscreen) - return; - - int x = 0; - int y = 0; - SDL_GetWindowPosition(m_window, &x, &y); - - int width = DEFAULT_WINDOW_WIDTH; - int height = DEFAULT_WINDOW_HEIGHT; - SDL_GetWindowSize(m_window, &width, &height); - - int old_x, old_y, old_width, old_height; - GetSavedWindowGeometry(&old_x, &old_y, &old_width, &old_height); - if (x == old_x && y == old_y && width == old_width && height == old_height) - return; - - auto lock = GetSettingsLock(); - m_settings_interface->SetIntValue("SDLHostInterface", "WindowX", x); - m_settings_interface->SetIntValue("SDLHostInterface", "WindowY", y); - m_settings_interface->SetIntValue("SDLHostInterface", "WindowWidth", width); - m_settings_interface->SetIntValue("SDLHostInterface", "WindowHeight", height); -} diff --git a/src/duckstation-nogui/sdl_host_interface.h b/src/duckstation-nogui/sdl_host_interface.h deleted file mode 100644 index 7ac5dbdae..000000000 --- a/src/duckstation-nogui/sdl_host_interface.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include "nogui_host_interface.h" -#include - -class SDLHostInterface final : public NoGUIHostInterface -{ -public: - SDLHostInterface(); - ~SDLHostInterface(); - - static std::unique_ptr Create(); - - const char* GetFrontendName() const override; - - bool Initialize() override; - void Shutdown() override; - - bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override; - - bool IsFullscreen() const override; - bool SetFullscreen(bool enabled) override; - -protected: - void SetMouseMode(bool relative, bool hide_cursor) override; - - void PollAndUpdate() override; - - std::optional GetHostKeyCode(const std::string_view key_code) const override; - - bool CreatePlatformWindow() override; - void DestroyPlatformWindow() override; - std::optional GetPlatformWindowInfo() override; - -private: - void HandleSDLEvent(const SDL_Event* event); - - void GetSavedWindowGeometry(int* x, int* y, int* width, int* height); - void SaveWindowGeometry(); - - SDL_Window* m_window = nullptr; - bool m_fullscreen = false; - bool m_was_paused_by_focus_loss = false; -}; diff --git a/src/duckstation-nogui/sdl_key_names.h b/src/duckstation-nogui/sdl_key_names.h deleted file mode 100644 index 825943389..000000000 --- a/src/duckstation-nogui/sdl_key_names.h +++ /dev/null @@ -1,368 +0,0 @@ -#pragma once -#include "common/string.h" -#include "common/types.h" -#include -#include -#include -#include -#include -#include - -namespace SDLKeyNames { - -enum : u32 -{ - MODIFIER_SHIFT = 16, - KEY_MASK = ((1 << MODIFIER_SHIFT) - 1), - MODIFIER_MASK = ~KEY_MASK, -}; - -static const std::map s_sdl_key_names = {{SDLK_RETURN, "Return"}, - {SDLK_ESCAPE, "Escape"}, - {SDLK_BACKSPACE, "Backspace"}, - {SDLK_TAB, "Tab"}, - {SDLK_SPACE, "Space"}, - {SDLK_EXCLAIM, "Exclam"}, - {SDLK_QUOTEDBL, "QuoteDbl"}, - {SDLK_HASH, "Hash"}, - {SDLK_PERCENT, "Percent"}, - {SDLK_DOLLAR, "Dollar"}, - {SDLK_AMPERSAND, "Ampersand"}, - {SDLK_QUOTE, "Apostrophe"}, - {SDLK_LEFTPAREN, "ParenLeft"}, - {SDLK_RIGHTPAREN, "ParenRight"}, - {SDLK_ASTERISK, "Asterisk"}, - {SDLK_PLUS, "PLus"}, - {SDLK_COMMA, "Comma"}, - {SDLK_MINUS, "Minus"}, - {SDLK_PERIOD, "Period"}, - {SDLK_SLASH, "Slash"}, - {SDLK_0, "0"}, - {SDLK_1, "1"}, - {SDLK_2, "2"}, - {SDLK_3, "3"}, - {SDLK_4, "4"}, - {SDLK_5, "5"}, - {SDLK_6, "6"}, - {SDLK_7, "7"}, - {SDLK_8, "8"}, - {SDLK_9, "9"}, - {SDLK_COLON, "Colon"}, - {SDLK_SEMICOLON, "Semcolon"}, - {SDLK_LESS, "Less"}, - {SDLK_EQUALS, "Equal"}, - {SDLK_GREATER, "Greater"}, - {SDLK_QUESTION, "Question"}, - {SDLK_AT, "AT"}, - {SDLK_LEFTBRACKET, "BracketLeft"}, - {SDLK_BACKSLASH, "Backslash"}, - {SDLK_RIGHTBRACKET, "BracketRight"}, - {SDLK_CARET, "Caret"}, - {SDLK_UNDERSCORE, "Underscore"}, - {SDLK_BACKQUOTE, "QuoteLeft"}, - {SDLK_a, "A"}, - {SDLK_b, "B"}, - {SDLK_c, "C"}, - {SDLK_d, "D"}, - {SDLK_e, "E"}, - {SDLK_f, "F"}, - {SDLK_g, "G"}, - {SDLK_h, "H"}, - {SDLK_i, "I"}, - {SDLK_j, "J"}, - {SDLK_k, "K"}, - {SDLK_l, "L"}, - {SDLK_m, "M"}, - {SDLK_n, "N"}, - {SDLK_o, "O"}, - {SDLK_p, "P"}, - {SDLK_q, "Q"}, - {SDLK_r, "R"}, - {SDLK_s, "S"}, - {SDLK_t, "T"}, - {SDLK_u, "U"}, - {SDLK_v, "V"}, - {SDLK_w, "W"}, - {SDLK_x, "X"}, - {SDLK_y, "Y"}, - {SDLK_z, "Z"}, - {SDLK_CAPSLOCK, "CapsLock"}, - {SDLK_F1, "F1"}, - {SDLK_F2, "F2"}, - {SDLK_F3, "F3"}, - {SDLK_F4, "F4"}, - {SDLK_F5, "F5"}, - {SDLK_F6, "F6"}, - {SDLK_F7, "F7"}, - {SDLK_F8, "F8"}, - {SDLK_F9, "F9"}, - {SDLK_F10, "F10"}, - {SDLK_F11, "F11"}, - {SDLK_F12, "F12"}, - {SDLK_PRINTSCREEN, "Print"}, - {SDLK_SCROLLLOCK, "ScrollLock"}, - {SDLK_PAUSE, "Pause"}, - {SDLK_INSERT, "Insert"}, - {SDLK_HOME, "Home"}, - {SDLK_PAGEUP, "PageUp"}, - {SDLK_DELETE, "Delete"}, - {SDLK_END, "End"}, - {SDLK_PAGEDOWN, "PageDown"}, - {SDLK_RIGHT, "Right"}, - {SDLK_LEFT, "Left"}, - {SDLK_DOWN, "Down"}, - {SDLK_UP, "Up"}, - {SDLK_NUMLOCKCLEAR, "NumLock"}, - {SDLK_KP_DIVIDE, "Keypad+Divide"}, - {SDLK_KP_MULTIPLY, "Keypad+Multiply"}, - {SDLK_KP_MINUS, "Keypad+Minus"}, - {SDLK_KP_PLUS, "Keypad+Plus"}, - {SDLK_KP_ENTER, "Keypad+Return"}, - {SDLK_KP_1, "Keypad+1"}, - {SDLK_KP_2, "Keypad+2"}, - {SDLK_KP_3, "Keypad+3"}, - {SDLK_KP_4, "Keypad+4"}, - {SDLK_KP_5, "Keypad+5"}, - {SDLK_KP_6, "Keypad+6"}, - {SDLK_KP_7, "Keypad+7"}, - {SDLK_KP_8, "Keypad+8"}, - {SDLK_KP_9, "Keypad+9"}, - {SDLK_KP_0, "Keypad+0"}, - {SDLK_KP_PERIOD, "Keypad+Period"}, - {SDLK_APPLICATION, "Application"}, - {SDLK_POWER, "Power"}, - {SDLK_KP_EQUALS, "Keypad+Equal"}, - {SDLK_F13, "F13"}, - {SDLK_F14, "F14"}, - {SDLK_F15, "F15"}, - {SDLK_F16, "F16"}, - {SDLK_F17, "F17"}, - {SDLK_F18, "F18"}, - {SDLK_F19, "F19"}, - {SDLK_F20, "F20"}, - {SDLK_F21, "F21"}, - {SDLK_F22, "F22"}, - {SDLK_F23, "F23"}, - {SDLK_F24, "F24"}, - {SDLK_EXECUTE, "Execute"}, - {SDLK_HELP, "Help"}, - {SDLK_MENU, "Menu"}, - {SDLK_SELECT, "Select"}, - {SDLK_STOP, "Stop"}, - {SDLK_AGAIN, "Again"}, - {SDLK_UNDO, "Undo"}, - {SDLK_CUT, "Cut"}, - {SDLK_COPY, "Copy"}, - {SDLK_PASTE, "Paste"}, - {SDLK_FIND, "Find"}, - {SDLK_MUTE, "Mute"}, - {SDLK_VOLUMEUP, "VolumeUp"}, - {SDLK_VOLUMEDOWN, "VolumeDown"}, - {SDLK_KP_COMMA, "Keypad+Comma"}, - {SDLK_KP_EQUALSAS400, "Keypad+EqualAS400"}, - {SDLK_ALTERASE, "AltErase"}, - {SDLK_SYSREQ, "SysReq"}, - {SDLK_CANCEL, "Cancel"}, - {SDLK_CLEAR, "Clear"}, - {SDLK_PRIOR, "Prior"}, - {SDLK_RETURN2, "Return2"}, - {SDLK_SEPARATOR, "Separator"}, - {SDLK_OUT, "Out"}, - {SDLK_OPER, "Oper"}, - {SDLK_CLEARAGAIN, "ClearAgain"}, - {SDLK_CRSEL, "CrSel"}, - {SDLK_EXSEL, "ExSel"}, - {SDLK_KP_00, "Keypad+00"}, - {SDLK_KP_000, "Keypad+000"}, - {SDLK_THOUSANDSSEPARATOR, "ThousandsSeparator"}, - {SDLK_DECIMALSEPARATOR, "DecimalSeparator"}, - {SDLK_CURRENCYUNIT, "CurrencyUnit"}, - {SDLK_CURRENCYSUBUNIT, "CurrencySubunit"}, - {SDLK_KP_LEFTPAREN, "Keypad+ParenLeft"}, - {SDLK_KP_RIGHTPAREN, "Keypad+ParenRight"}, - {SDLK_KP_LEFTBRACE, "Keypad+LeftBrace"}, - {SDLK_KP_RIGHTBRACE, "Keypad+RightBrace"}, - {SDLK_KP_TAB, "Keypad+Tab"}, - {SDLK_KP_BACKSPACE, "Keypad+Backspace"}, - {SDLK_KP_A, "Keypad+A"}, - {SDLK_KP_B, "Keypad+B"}, - {SDLK_KP_C, "Keypad+C"}, - {SDLK_KP_D, "Keypad+D"}, - {SDLK_KP_E, "Keypad+E"}, - {SDLK_KP_F, "Keypad+F"}, - {SDLK_KP_XOR, "Keypad+XOR"}, - {SDLK_KP_POWER, "Keypad+Power"}, - {SDLK_KP_PERCENT, "Keypad+Percent"}, - {SDLK_KP_LESS, "Keypad+Less"}, - {SDLK_KP_GREATER, "Keypad+Greater"}, - {SDLK_KP_AMPERSAND, "Keypad+Ampersand"}, - {SDLK_KP_DBLAMPERSAND, "Keypad+AmpersandDbl"}, - {SDLK_KP_VERTICALBAR, "Keypad+Bar"}, - {SDLK_KP_DBLVERTICALBAR, "Keypad+BarDbl"}, - {SDLK_KP_COLON, "Keypad+Colon"}, - {SDLK_KP_HASH, "Keypad+Hash"}, - {SDLK_KP_SPACE, "Keypad+Space"}, - {SDLK_KP_AT, "Keypad+At"}, - {SDLK_KP_EXCLAM, "Keypad+Exclam"}, - {SDLK_KP_MEMSTORE, "Keypad+MemStore"}, - {SDLK_KP_MEMRECALL, "Keypad+MemRecall"}, - {SDLK_KP_MEMCLEAR, "Keypad+MemClear"}, - {SDLK_KP_MEMADD, "Keypad+MemAdd"}, - {SDLK_KP_MEMSUBTRACT, "Keypad+MemSubtract"}, - {SDLK_KP_MEMMULTIPLY, "Keypad+MemMultiply"}, - {SDLK_KP_MEMDIVIDE, "Keypad+MemDivide"}, - {SDLK_KP_PLUSMINUS, "Keypad+PlusMinus"}, - {SDLK_KP_CLEAR, "Keypad+Clear"}, - {SDLK_KP_CLEARENTRY, "Keypad+ClearEntry"}, - {SDLK_KP_BINARY, "Keypad+Binary"}, - {SDLK_KP_OCTAL, "Keypad+Octal"}, - {SDLK_KP_DECIMAL, "Keypad+Decimal"}, - {SDLK_KP_HEXADECIMAL, "Keypad+Hexadecimal"}, - {SDLK_LCTRL, "LeftControl"}, - {SDLK_LSHIFT, "LeftShift"}, - {SDLK_LALT, "LeftAlt"}, - {SDLK_LGUI, "Super_L"}, - {SDLK_RCTRL, "RightCtrl"}, - {SDLK_RSHIFT, "RightShift"}, - {SDLK_RALT, "RightAlt"}, - {SDLK_RGUI, "RightSuper"}, - {SDLK_MODE, "Mode"}, - {SDLK_AUDIONEXT, "MediaNext"}, - {SDLK_AUDIOPREV, "MediaPrevious"}, - {SDLK_AUDIOSTOP, "MediaStop"}, - {SDLK_AUDIOPLAY, "MediaPlay"}, - {SDLK_AUDIOMUTE, "VolumeMute"}, - {SDLK_MEDIASELECT, "MediaSelect"}, - {SDLK_WWW, "WWW"}, - {SDLK_MAIL, "Mail"}, - {SDLK_CALCULATOR, "Calculator"}, - {SDLK_COMPUTER, "Computer"}, - {SDLK_AC_SEARCH, "Search"}, - {SDLK_AC_HOME, "Home"}, - {SDLK_AC_BACK, "Back"}, - {SDLK_AC_FORWARD, "Forward"}, - {SDLK_AC_STOP, "Stop"}, - {SDLK_AC_REFRESH, "Refresh"}, - {SDLK_AC_BOOKMARKS, "Bookmarks"}, - {SDLK_BRIGHTNESSDOWN, "BrightnessDown"}, - {SDLK_BRIGHTNESSUP, "BrightnessUp"}, - {SDLK_DISPLAYSWITCH, "DisplaySwitch"}, - {SDLK_KBDILLUMTOGGLE, "IllumToggle"}, - {SDLK_KBDILLUMDOWN, "IllumDown"}, - {SDLK_KBDILLUMUP, "IllumUp"}, - {SDLK_EJECT, "Eject"}, - {SDLK_SLEEP, "Sleep"}, - {SDLK_APP1, "App1"}, - {SDLK_APP2, "App2"}, - {SDLK_AUDIOREWIND, "MediaRewind"}, - {SDLK_AUDIOFASTFORWARD, "MediaFastForward"}}; - -struct SDLKeyModifierEntry -{ - SDL_Keymod mod; - SDL_Keymod mod_mask; - SDL_Keycode key_left; - SDL_Keycode key_right; - const char* name; -}; - -static const std::array s_sdl_key_modifiers = { - {{KMOD_LSHIFT, static_cast(KMOD_LSHIFT | KMOD_RSHIFT), SDLK_LSHIFT, SDLK_RSHIFT, "Shift"}, - {KMOD_LCTRL, static_cast(KMOD_LCTRL | KMOD_RCTRL), SDLK_LCTRL, SDLK_RCTRL, "Control"}, - {KMOD_LALT, static_cast(KMOD_LALT | KMOD_RALT), SDLK_LALT, SDLK_RALT, "Alt"}, - {KMOD_LGUI, static_cast(KMOD_LGUI | KMOD_RGUI), SDLK_LGUI, SDLK_RGUI, "Meta"}}}; - -static const char* GetKeyName(SDL_Keycode key) -{ - const auto it = s_sdl_key_names.find(key); - return it == s_sdl_key_names.end() ? nullptr : it->second; -} - -static std::optional GetKeyCodeForName(const std::string_view key_name) -{ - for (const auto& it : s_sdl_key_names) - { - if (key_name == it.second) - return it.first; - } - - return std::nullopt; -} - -static u32 KeyEventToInt(const SDL_Event* event) -{ - u32 code = static_cast(event->key.keysym.sym); - - const SDL_Keymod mods = static_cast(event->key.keysym.mod); - if (mods & (KMOD_LSHIFT | KMOD_RSHIFT)) - code |= static_cast(KMOD_LSHIFT) << MODIFIER_SHIFT; - if (mods & (KMOD_LCTRL | KMOD_RCTRL)) - code |= static_cast(KMOD_LCTRL) << MODIFIER_SHIFT; - if (mods & (KMOD_LALT | KMOD_RALT)) - code |= static_cast(KMOD_LALT) << MODIFIER_SHIFT; - if (mods & (KMOD_LGUI | KMOD_RGUI)) - code |= static_cast(KMOD_LGUI) << MODIFIER_SHIFT; - - return code; -} - -static bool KeyEventToString(const SDL_Event* event, String& out_string) -{ - const SDL_Keycode key = event->key.keysym.sym; - const SDL_Keymod mods = static_cast(event->key.keysym.mod); - const char* key_name = GetKeyName(event->key.keysym.sym); - if (!key_name) - return false; - - out_string.Clear(); - - for (const SDLKeyModifierEntry& mod : s_sdl_key_modifiers) - { - if (mods & mod.mod_mask && key != mod.key_left && key != mod.key_right) - { - out_string.AppendString(mod.name); - out_string.AppendCharacter('+'); - } - } - - out_string.AppendString(key_name); - return true; -} - -static std::optional ParseKeyString(const std::string_view key_str) -{ - u32 modifiers = 0; - std::string_view::size_type pos = 0; - for (;;) - { - std::string_view::size_type plus_pos = key_str.find('+', pos); - if (plus_pos == std::string_view::npos) - break; - - const std::string_view mod_part = key_str.substr(pos, plus_pos - pos); - - // Keypad in SDL is not a mod and should always be the last + in the string - bool known_mod = false; - for (const SDLKeyModifierEntry& mod : s_sdl_key_modifiers) - { - if (mod_part == mod.name) - { - modifiers |= static_cast(mod.mod); - known_mod = true; - break; - } - } - - if (!known_mod) - break; - - pos = plus_pos + 1; - } - - std::optional key_code = GetKeyCodeForName(key_str.substr(pos)); - if (!key_code) - return std::nullopt; - - return static_cast(key_code.value()) | (modifiers << MODIFIER_SHIFT); -} -} // namespace SDLKeyNames diff --git a/src/duckstation-nogui/vty_host_interface.cpp b/src/duckstation-nogui/vty_host_interface.cpp deleted file mode 100644 index 9b7b5a239..000000000 --- a/src/duckstation-nogui/vty_host_interface.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "vty_host_interface.h" -#include "common/log.h" -#include "common/string_util.h" -#include "evdev_key_names.h" -#include "frontend-common/ini_settings_interface.h" -#include "imgui.h" -#include -#include -#include -#include -Log_SetChannel(VTYHostInterface); - -#ifdef WITH_DRMKMS -#include "common/drm_display.h" -#endif - -VTYHostInterface::VTYHostInterface() = default; - -VTYHostInterface::~VTYHostInterface() -{ - CloseEVDevFDs(); -} - -std::unique_ptr VTYHostInterface::Create() -{ - return std::make_unique(); -} - -bool VTYHostInterface::Initialize() -{ - if (!NoGUIHostInterface::Initialize()) - return false; - - OpenEVDevFDs(); - - signal(SIGTERM, SIGTERMHandler); - signal(SIGINT, SIGTERMHandler); - signal(SIGQUIT, SIGTERMHandler); - return true; -} - -void VTYHostInterface::Shutdown() -{ - CloseEVDevFDs(); - NoGUIHostInterface::Shutdown(); -} - -bool VTYHostInterface::IsFullscreen() const -{ - return true; -} - -bool VTYHostInterface::SetFullscreen(bool enabled) -{ - return enabled; -} - -void VTYHostInterface::FixIncompatibleSettings(bool display_osd_messages) -{ - NoGUIHostInterface::FixIncompatibleSettings(display_osd_messages); - - // Some things we definitely don't want. - g_settings.confim_power_off = false; -} - -bool VTYHostInterface::CreatePlatformWindow() -{ - SetImGuiKeyMap(); - return true; -} - -void VTYHostInterface::DestroyPlatformWindow() -{ - // nothing to destroy, it's all in the context -} - -std::optional VTYHostInterface::GetPlatformWindowInfo() -{ - WindowInfo wi; - wi.type = WindowInfo::Type::Display; - wi.surface_width = 0; - wi.surface_height = 0; - wi.surface_refresh_rate = 0.0f; - wi.surface_format = WindowInfo::SurfaceFormat::Auto; - - const std::string fullscreen_mode = m_settings_interface->GetStringValue("GPU", "FullscreenMode", ""); - if (!fullscreen_mode.empty()) - { - if (!ParseFullscreenMode(fullscreen_mode, &wi.surface_width, &wi.surface_height, &wi.surface_refresh_rate)) - { - Log_ErrorPrintf("Failed to parse fullscreen mode '%s'", fullscreen_mode.c_str()); - } - } - -#ifdef WITH_DRMKMS - // set to current mode - if (wi.surface_width == 0) - { - if (!DRMDisplay::GetCurrentMode(&wi.surface_width, &wi.surface_height, &wi.surface_refresh_rate)) - Log_ErrorPrintf("Failed to get current mode, will use default."); - } -#endif - - return wi; -} - -void VTYHostInterface::PollAndUpdate() -{ - PollEvDevKeyboards(); - - NoGUIHostInterface::PollAndUpdate(); -} - -void VTYHostInterface::SetMouseMode(bool relative, bool hide_cursor) {} - -void VTYHostInterface::OpenEVDevFDs() -{ - for (int i = 0; i < 1000; i++) - { - TinyString path; - path.Format("/dev/input/event%d", i); - - int fd = open(path, O_RDONLY | O_NONBLOCK); - if (fd < 0) - break; - - struct libevdev* obj; - if (libevdev_new_from_fd(fd, &obj) != 0) - { - Log_ErrorPrintf("libevdev_new_from_fd(%s) failed", path.GetCharArray()); - close(fd); - continue; - } - - Log_DevPrintf("Input path: %s", path.GetCharArray()); - Log_DevPrintf("Input device name: \"%s\"", libevdev_get_name(obj)); - Log_DevPrintf("Input device ID: bus %#x vendor %#x product %#x", libevdev_get_id_bustype(obj), - libevdev_get_id_vendor(obj), libevdev_get_id_product(obj)); - if (!libevdev_has_event_code(obj, EV_KEY, KEY_SPACE)) - { - Log_DevPrintf("This device does not look like a keyboard"); - libevdev_free(obj); - close(fd); - continue; - } - - const int grab_res = libevdev_grab(obj, LIBEVDEV_GRAB); - if (grab_res != 0) - Log_WarningPrintf("Failed to grab '%s' (%s): %d", libevdev_get_name(obj), path.GetCharArray(), grab_res); - - m_evdev_keyboards.push_back({obj, fd}); - } -} - -void VTYHostInterface::CloseEVDevFDs() -{ - for (const EvDevKeyboard& kb : m_evdev_keyboards) - { - libevdev_grab(kb.obj, LIBEVDEV_UNGRAB); - libevdev_free(kb.obj); - close(kb.fd); - } - m_evdev_keyboards.clear(); -} - -void VTYHostInterface::PollEvDevKeyboards() -{ - for (const EvDevKeyboard& kb : m_evdev_keyboards) - { - struct input_event ev; - while (libevdev_next_event(kb.obj, LIBEVDEV_READ_FLAG_NORMAL, &ev) == 0) - { - // auto-repeat - if (ev.value == 2) - continue; - - const bool pressed = (ev.value == 1); - const HostKeyCode code = static_cast(ev.code); - if (static_cast(code) < countof(ImGuiIO::KeysDown)) - ImGui::GetIO().KeysDown[code] = pressed; - - HandleHostKeyEvent(code, 0, pressed); - } - } -} - -void VTYHostInterface::SetImGuiKeyMap() -{ - ImGuiIO& io = ImGui::GetIO(); - - io.KeyMap[ImGuiKey_Tab] = KEY_TAB; - io.KeyMap[ImGuiKey_LeftArrow] = KEY_LEFT; - io.KeyMap[ImGuiKey_RightArrow] = KEY_RIGHT; - io.KeyMap[ImGuiKey_UpArrow] = KEY_UP; - io.KeyMap[ImGuiKey_DownArrow] = KEY_DOWN; - io.KeyMap[ImGuiKey_PageUp] = KEY_PAGEUP; - io.KeyMap[ImGuiKey_PageDown] = KEY_PAGEDOWN; - io.KeyMap[ImGuiKey_Home] = KEY_HOME; - io.KeyMap[ImGuiKey_End] = KEY_END; - io.KeyMap[ImGuiKey_Insert] = KEY_INSERT; - io.KeyMap[ImGuiKey_Delete] = KEY_DELETE; - io.KeyMap[ImGuiKey_Backspace] = KEY_BACKSPACE; - io.KeyMap[ImGuiKey_Space] = KEY_SPACE; - io.KeyMap[ImGuiKey_Enter] = KEY_ENTER; - io.KeyMap[ImGuiKey_Escape] = KEY_ESC; - io.KeyMap[ImGuiKey_KeyPadEnter] = KEY_KPENTER; - io.KeyMap[ImGuiKey_A] = KEY_A; - io.KeyMap[ImGuiKey_C] = KEY_C; - io.KeyMap[ImGuiKey_V] = KEY_V; - io.KeyMap[ImGuiKey_X] = KEY_X; - io.KeyMap[ImGuiKey_Y] = KEY_Y; - io.KeyMap[ImGuiKey_Z] = KEY_Z; -} - -std::optional VTYHostInterface::GetHostKeyCode(const std::string_view key_code) const -{ - std::optional kc = EvDevKeyNames::GetKeyCodeForName(key_code); - if (!kc.has_value()) - return std::nullopt; - - return static_cast(kc.value()); -} - -void VTYHostInterface::SIGTERMHandler(int sig) -{ - Log_InfoPrintf("Recieved SIGTERM"); - static_cast(g_host_interface)->m_quit_request = true; - signal(sig, SIG_DFL); -} diff --git a/src/duckstation-nogui/vty_host_interface.h b/src/duckstation-nogui/vty_host_interface.h deleted file mode 100644 index 169d665ae..000000000 --- a/src/duckstation-nogui/vty_host_interface.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include "nogui_host_interface.h" -#include -#include -#include - -class VTYHostInterface final : public NoGUIHostInterface -{ -public: - VTYHostInterface(); - ~VTYHostInterface(); - - bool Initialize(); - void Shutdown(); - - bool IsFullscreen() const override; - bool SetFullscreen(bool enabled) override; - - static std::unique_ptr Create(); - -protected: - virtual void FixIncompatibleSettings(bool display_osd_messages) override; - - bool CreatePlatformWindow() override; - void DestroyPlatformWindow() override; - std::optional GetPlatformWindowInfo() override; - - std::optional GetHostKeyCode(const std::string_view key_code) const override; - - void PollAndUpdate() override; - - void SetMouseMode(bool relative, bool hide_cursor) override; - -private: - static void SIGTERMHandler(int sig); - - void OpenEVDevFDs(); - void CloseEVDevFDs(); - void PollEvDevKeyboards(); - void SetImGuiKeyMap(); - - struct EvDevKeyboard - { - struct libevdev* obj; - int fd; - }; - - std::vector m_evdev_keyboards; -}; diff --git a/src/duckstation-nogui/evdev_key_names.h b/src/duckstation-nogui/vty_key_names.h similarity index 99% rename from src/duckstation-nogui/evdev_key_names.h rename to src/duckstation-nogui/vty_key_names.h index 27b920ab2..c19a1b2d7 100644 --- a/src/duckstation-nogui/evdev_key_names.h +++ b/src/duckstation-nogui/vty_key_names.h @@ -8,7 +8,7 @@ #include #include -namespace EvDevKeyNames { +namespace VTYKeyNames { static const std::map s_evdev_key_names = {{KEY_ESC, "Escape"}, {KEY_1, "1"}, @@ -275,4 +275,4 @@ static inline std::optional GetKeyCodeForName(const std::string_view key_na return std::nullopt; } -} // namespace EvDevKeyNames +} // namespace VTYKeyNames diff --git a/src/duckstation-nogui/vty_nogui_platform.cpp b/src/duckstation-nogui/vty_nogui_platform.cpp new file mode 100644 index 000000000..96ee4e385 --- /dev/null +++ b/src/duckstation-nogui/vty_nogui_platform.cpp @@ -0,0 +1,233 @@ +#include "vty_nogui_platform.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/threading.h" +#include "core/host.h" +#include "core/host_settings.h" +#include "nogui_host.h" +#include "resource.h" +#include "vty_key_names.h" +#include +#include +#include +#include +Log_SetChannel(VTYNoGUIPlatform); + +#ifdef WITH_DRMKMS +#include "common/drm_display.h" +#endif + +VTYNoGUIPlatform::VTYNoGUIPlatform() +{ + m_message_loop_running.store(true, std::memory_order_release); +} + +VTYNoGUIPlatform::~VTYNoGUIPlatform() +{ + CloseEVDevFDs(); +} + +std::unique_ptr NoGUIPlatform::CreateVTYPlatform() +{ + std::unique_ptr platform(std::make_unique()); + if (!platform->Initialize()) + platform.reset(); + return platform; +} + +bool VTYNoGUIPlatform::Initialize() +{ + OpenEVDevFDs(); + return true; +} + +void VTYNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) +{ + const std::string title_copy(title); + const std::string message_copy(message); + Log_ErrorPrintf("%s: %s", title_copy.c_str(), message_copy.c_str()); +} + +void VTYNoGUIPlatform::SetDefaultConfig(SettingsInterface& si) +{ + // noop +} + +bool VTYNoGUIPlatform::CreatePlatformWindow(std::string title) +{ + return true; +} + +void VTYNoGUIPlatform::DestroyPlatformWindow() +{ + // noop +} + +std::optional VTYNoGUIPlatform::GetPlatformWindowInfo() +{ + WindowInfo wi; + wi.type = WindowInfo::Type::Display; + wi.surface_width = 0; + wi.surface_height = 0; + wi.surface_refresh_rate = 0.0f; + wi.surface_format = WindowInfo::SurfaceFormat::Auto; + + const std::string fullscreen_mode = Host::GetStringSettingValue("GPU", "FullscreenMode", ""); + if (!fullscreen_mode.empty()) + { + if (!HostDisplay::ParseFullscreenMode(fullscreen_mode, &wi.surface_width, &wi.surface_height, + &wi.surface_refresh_rate)) + { + Log_ErrorPrintf("Failed to parse fullscreen mode '%s'", fullscreen_mode.c_str()); + } + } + +#ifdef WITH_DRMKMS + // set to current mode + if (wi.surface_width == 0) + { + if (!DRMDisplay::GetCurrentMode(&wi.surface_width, &wi.surface_height, &wi.surface_refresh_rate)) + Log_ErrorPrintf("Failed to get current mode, will use default."); + } +#endif + + // This isn't great, but it's an approximation at least.. + if (wi.surface_width > 0) + wi.surface_scale = std::max(0.1f, static_cast(wi.surface_width) / 1280.0f); + + return wi; +} + +void VTYNoGUIPlatform::SetFullscreen(bool enabled) +{ + // already fullscreen :-) +} + +bool VTYNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) +{ + return false; +} + +void VTYNoGUIPlatform::SetPlatformWindowTitle(std::string title) +{ + Log_InfoPrintf("Window Title: %s", title.c_str()); +} + +void* VTYNoGUIPlatform::GetPlatformWindowHandle() +{ + return nullptr; +} + +void VTYNoGUIPlatform::RunMessageLoop() +{ + while (m_message_loop_running.load(std::memory_order_acquire)) + { + PollEvDevKeyboards(); + + { + std::unique_lock lock(m_callback_queue_mutex); + while (!m_callback_queue.empty()) + { + std::function func = std::move(m_callback_queue.front()); + m_callback_queue.pop_front(); + lock.unlock(); + func(); + lock.lock(); + } + } + + // TODO: Make this suck less. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void VTYNoGUIPlatform::ExecuteInMessageLoop(std::function func) +{ + std::unique_lock lock(m_callback_queue_mutex); + m_callback_queue.push_back(std::move(func)); +} + +void VTYNoGUIPlatform::QuitMessageLoop() +{ + m_message_loop_running.store(false, std::memory_order_release); +} + +void VTYNoGUIPlatform::OpenEVDevFDs() +{ + for (int i = 0; i < 1000; i++) + { + TinyString path; + path.Format("/dev/input/event%d", i); + + int fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd < 0) + break; + + struct libevdev* obj; + if (libevdev_new_from_fd(fd, &obj) != 0) + { + Log_ErrorPrintf("libevdev_new_from_fd(%s) failed", path.GetCharArray()); + close(fd); + continue; + } + + Log_DevPrintf("Input path: %s", path.GetCharArray()); + Log_DevPrintf("Input device name: \"%s\"", libevdev_get_name(obj)); + Log_DevPrintf("Input device ID: bus %#x vendor %#x product %#x", libevdev_get_id_bustype(obj), + libevdev_get_id_vendor(obj), libevdev_get_id_product(obj)); + if (!libevdev_has_event_code(obj, EV_KEY, KEY_SPACE)) + { + Log_DevPrintf("This device does not look like a keyboard"); + libevdev_free(obj); + close(fd); + continue; + } + + const int grab_res = libevdev_grab(obj, LIBEVDEV_GRAB); + if (grab_res != 0) + Log_WarningPrintf("Failed to grab '%s' (%s): %d", libevdev_get_name(obj), path.GetCharArray(), grab_res); + + m_evdev_keyboards.push_back({obj, fd}); + } +} + +void VTYNoGUIPlatform::CloseEVDevFDs() +{ + for (const EvDevKeyboard& kb : m_evdev_keyboards) + { + libevdev_grab(kb.obj, LIBEVDEV_UNGRAB); + libevdev_free(kb.obj); + close(kb.fd); + } + m_evdev_keyboards.clear(); +} + +void VTYNoGUIPlatform::PollEvDevKeyboards() +{ + for (const EvDevKeyboard& kb : m_evdev_keyboards) + { + struct input_event ev; + while (libevdev_next_event(kb.obj, LIBEVDEV_READ_FLAG_NORMAL, &ev) == 0) + { + // auto-repeat + // TODO: forward char to imgui + if (ev.value == 2) + continue; + + const bool pressed = (ev.value == 1); + NoGUIHost::ProcessPlatformKeyEvent(static_cast(ev.code), pressed); + } + } +} + +std::optional VTYNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) +{ + std::optional converted(VTYKeyNames::GetKeyCodeForName(str)); + return converted.has_value() ? std::optional(static_cast(converted.value())) : std::nullopt; +} + +std::optional VTYNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) +{ + const char* keyname = VTYKeyNames::GetKeyName(static_cast(code)); + return keyname ? std::optional(std::string(keyname)) : std::nullopt; +} diff --git a/src/duckstation-nogui/vty_nogui_platform.h b/src/duckstation-nogui/vty_nogui_platform.h new file mode 100644 index 000000000..46dda5ca8 --- /dev/null +++ b/src/duckstation-nogui/vty_nogui_platform.h @@ -0,0 +1,57 @@ +#pragma once +#include "nogui_platform.h" +#include +#include +#include +#include +#include +#include + +class VTYNoGUIPlatform : public NoGUIPlatform +{ +public: + VTYNoGUIPlatform(); + ~VTYNoGUIPlatform(); + + bool Initialize(); + + void ReportError(const std::string_view& title, const std::string_view& message) override; + + void SetDefaultConfig(SettingsInterface& si) override; + + bool CreatePlatformWindow(std::string title) override; + void DestroyPlatformWindow() override; + std::optional GetPlatformWindowInfo() override; + void SetPlatformWindowTitle(std::string title) override; + void* GetPlatformWindowHandle() override; + + std::optional ConvertHostKeyboardStringToCode(const std::string_view& str) override; + std::optional ConvertHostKeyboardCodeToString(u32 code) override; + + void RunMessageLoop() override; + void ExecuteInMessageLoop(std::function func) override; + void QuitMessageLoop() override; + + void SetFullscreen(bool enabled) override; + + bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override; + +private: + void OpenEVDevFDs(); + void CloseEVDevFDs(); + void PollEvDevKeyboards(); + void SetImGuiKeyMap(); + + struct EvDevKeyboard + { + struct libevdev* obj; + int fd; + }; + + std::vector m_evdev_keyboards; + + std::deque> m_callback_queue; + std::mutex m_callback_queue_mutex; + + std::atomic_bool m_message_loop_running{false}; +}; diff --git a/src/duckstation-nogui/wayland_nogui_platform.cpp b/src/duckstation-nogui/wayland_nogui_platform.cpp new file mode 100644 index 000000000..91ad309a7 --- /dev/null +++ b/src/duckstation-nogui/wayland_nogui_platform.cpp @@ -0,0 +1,465 @@ +#include "wayland_nogui_platform.h" +#include "common/assert.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/threading.h" +#include "core/host.h" +#include "core/host_settings.h" +#include "nogui_host.h" +#include "nogui_platform.h" + +#include +#include +#include +#include + +Log_SetChannel(WaylandNoGUIPlatform); + +WaylandNoGUIPlatform::WaylandNoGUIPlatform() +{ + m_message_loop_running.store(true, std::memory_order_release); +} + +WaylandNoGUIPlatform::~WaylandNoGUIPlatform() +{ + if (m_xkb_state) + xkb_state_unref(m_xkb_state); + if (m_xkb_keymap) + xkb_keymap_unref(m_xkb_keymap); + if (m_wl_keyboard) + wl_keyboard_destroy(m_wl_keyboard); + if (m_wl_pointer) + wl_pointer_destroy(m_wl_pointer); + if (m_wl_seat) + wl_seat_destroy(m_wl_seat); + if (m_xkb_context) + xkb_context_unref(m_xkb_context); + if (m_registry) + wl_registry_destroy(m_registry); +} + +bool WaylandNoGUIPlatform::Initialize() +{ + m_xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (!m_xkb_context) + { + Panic("Failed to create XKB context"); + return false; + } + + m_display = wl_display_connect(nullptr); + if (!m_display) + { + Panic("Failed to connect to Wayland display."); + return false; + } + + static const wl_registry_listener registry_listener = {GlobalRegistryHandler, GlobalRegistryRemover}; + m_registry = wl_display_get_registry(m_display); + wl_registry_add_listener(m_registry, ®istry_listener, this); + + // Call back to registry listener to get compositor/shell. + wl_display_dispatch_pending(m_display); + wl_display_roundtrip(m_display); + + // We need a shell/compositor, or at least one we understand. + if (!m_compositor || !m_xdg_wm_base) + { + Panic("Missing Wayland shell/compositor\n"); + return false; + } + + static const xdg_wm_base_listener xdg_wm_base_listener = {XDGWMBasePing}; + xdg_wm_base_add_listener(m_xdg_wm_base, &xdg_wm_base_listener, this); + wl_display_dispatch_pending(m_display); + wl_display_roundtrip(m_display); + return true; +} + +void WaylandNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) +{ + const std::string title_copy(title); + const std::string message_copy(message); + Log_ErrorPrintf("%s: %s", title_copy.c_str(), message_copy.c_str()); +} + +void WaylandNoGUIPlatform::SetDefaultConfig(SettingsInterface& si) {} + +bool WaylandNoGUIPlatform::CreatePlatformWindow(std::string title) +{ + s32 window_x, window_y, window_width, window_height; + bool has_window_pos = NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height); + if (!has_window_pos) + { + window_x = 0; + window_y = 0; + window_width = DEFAULT_WINDOW_WIDTH; + window_height = DEFAULT_WINDOW_HEIGHT; + } + + // Create the compositor and shell surface. + if (!(m_surface = wl_compositor_create_surface(m_compositor)) || + !(m_xdg_surface = xdg_wm_base_get_xdg_surface(m_xdg_wm_base, m_surface)) || + !(m_xdg_toplevel = xdg_surface_get_toplevel(m_xdg_surface))) + { + Log_ErrorPrintf("Failed to create compositor/shell surfaces"); + return false; + } + + static const xdg_surface_listener shell_surface_listener = {XDGSurfaceConfigure}; + xdg_surface_add_listener(m_xdg_surface, &shell_surface_listener, this); + + static const xdg_toplevel_listener toplevel_listener = {TopLevelConfigure, TopLevelClose}; + xdg_toplevel_add_listener(m_xdg_toplevel, &toplevel_listener, this); + + // Create region in the surface to draw into. + m_region = wl_compositor_create_region(m_compositor); + wl_region_add(m_region, 0, 0, window_width, window_height); + wl_surface_set_opaque_region(m_surface, m_region); + wl_surface_commit(m_surface); + + // This doesn't seem to have any effect on kwin... + if (has_window_pos) + { + xdg_surface_set_window_geometry(m_xdg_surface, window_x, window_y, window_width, window_height); + } + + if (m_decoration_manager) + { + m_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(m_decoration_manager, m_xdg_toplevel); + if (m_toplevel_decoration) + zxdg_toplevel_decoration_v1_set_mode(m_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + } + + m_window_info.surface_width = static_cast(window_width); + m_window_info.surface_height = static_cast(window_height); + m_window_info.surface_scale = 1.0f; + m_window_info.type = WindowInfo::Type::Wayland; + m_window_info.window_handle = m_surface; + m_window_info.display_connection = m_display; + + wl_display_dispatch_pending(m_display); + wl_display_roundtrip(m_display); + return true; +} + +void WaylandNoGUIPlatform::DestroyPlatformWindow() +{ + m_window_info = {}; + + if (m_toplevel_decoration) + { + zxdg_toplevel_decoration_v1_destroy(m_toplevel_decoration); + m_toplevel_decoration = {}; + } + + if (m_xdg_toplevel) + { + xdg_toplevel_destroy(m_xdg_toplevel); + m_xdg_toplevel = {}; + } + + if (m_xdg_surface) + { + xdg_surface_destroy(m_xdg_surface); + m_xdg_surface = {}; + } + + if (m_surface) + { + wl_surface_destroy(m_surface); + m_surface = {}; + } + + wl_display_dispatch_pending(m_display); + wl_display_roundtrip(m_display); +} + +std::optional WaylandNoGUIPlatform::GetPlatformWindowInfo() +{ + if (m_window_info.type == WindowInfo::Type::Wayland) + return m_window_info; + else + return std::nullopt; +} + +void WaylandNoGUIPlatform::SetPlatformWindowTitle(std::string title) +{ + if (m_xdg_toplevel) + xdg_toplevel_set_title(m_xdg_toplevel, title.c_str()); +} + +void* WaylandNoGUIPlatform::GetPlatformWindowHandle() +{ + return m_surface; +} + +std::optional WaylandNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) +{ + std::unique_lock lock(m_key_map_mutex); + for (const auto& it : m_key_map) + { + if (StringUtil::Strncasecmp(it.second.c_str(), str.data(), str.length()) == 0) + return it.first; + } + + return std::nullopt; +} + +std::optional WaylandNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) +{ + std::unique_lock lock(m_key_map_mutex); + const auto it = m_key_map.find(static_cast(code)); + return (it != m_key_map.end()) ? std::optional(it->second) : std::nullopt; +} + +void WaylandNoGUIPlatform::GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, const char* interface, + uint32_t version) +{ + WaylandNoGUIPlatform* platform = static_cast(data); + if (std::strcmp(interface, wl_compositor_interface.name) == 0) + { + platform->m_compositor = + static_cast(wl_registry_bind(platform->m_registry, id, &wl_compositor_interface, 1)); + } + else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) + { + platform->m_xdg_wm_base = + static_cast(wl_registry_bind(platform->m_registry, id, &xdg_wm_base_interface, 1)); + } + else if (std::strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) + { + platform->m_decoration_manager = static_cast( + wl_registry_bind(platform->m_registry, id, &zxdg_decoration_manager_v1_interface, 1)); + } + else if (std::strcmp(interface, wl_seat_interface.name) == 0) + { + static const wl_seat_listener seat_listener = {&WaylandNoGUIPlatform::SeatCapabilities}; + platform->m_wl_seat = static_cast(wl_registry_bind(registry, id, &wl_seat_interface, 1)); + wl_seat_add_listener(platform->m_wl_seat, &seat_listener, platform); + } +} + +void WaylandNoGUIPlatform::GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id) {} + +void WaylandNoGUIPlatform::XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial) +{ + xdg_wm_base_pong(xdg_wm_base, serial); +} + +void WaylandNoGUIPlatform::XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, uint32_t serial) +{ + xdg_surface_ack_configure(xdg_surface, serial); +} + +void WaylandNoGUIPlatform::TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width, + int32_t height, struct wl_array* states) +{ + // If this is zero, it's asking us to set the size. + if (width == 0 || height == 0) + return; + + WaylandNoGUIPlatform* platform = static_cast(data); + platform->m_window_info.surface_width = width; + platform->m_window_info.surface_height = height; + + NoGUIHost::ProcessPlatformWindowResize(width, height, platform->m_window_info.surface_scale); +} + +void WaylandNoGUIPlatform::TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel) +{ + Host::RunOnCPUThread([]() { Host::RequestExit(g_settings.save_state_on_exit); }); +} + +void WaylandNoGUIPlatform::SeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) +{ + WaylandNoGUIPlatform* platform = static_cast(data); + if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) + { + static const wl_keyboard_listener keyboard_listener = { + &WaylandNoGUIPlatform::KeyboardKeymap, &WaylandNoGUIPlatform::KeyboardEnter, &WaylandNoGUIPlatform::KeyboardLeave, + &WaylandNoGUIPlatform::KeyboardKey, &WaylandNoGUIPlatform::KeyboardModifiers}; + platform->m_wl_keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(platform->m_wl_keyboard, &keyboard_listener, platform); + } + if (capabilities & WL_SEAT_CAPABILITY_POINTER) + { + static const wl_pointer_listener pointer_listener = { + &WaylandNoGUIPlatform::PointerEnter, &WaylandNoGUIPlatform::PointerLeave, &WaylandNoGUIPlatform::PointerMotion, + &WaylandNoGUIPlatform::PointerButton, &WaylandNoGUIPlatform::PointerAxis}; + platform->m_wl_pointer = wl_seat_get_pointer(seat); + wl_pointer_add_listener(platform->m_wl_pointer, &pointer_listener, platform); + } +} + +void WaylandNoGUIPlatform::KeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size) +{ + WaylandNoGUIPlatform* platform = static_cast(data); + char* keymap_string = static_cast(mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0)); + if (platform->m_xkb_keymap) + xkb_keymap_unref(platform->m_xkb_keymap); + platform->m_xkb_keymap = xkb_keymap_new_from_string(platform->m_xkb_context, keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(keymap_string, size); + close(fd); + + if (platform->m_xkb_state) + xkb_state_unref(platform->m_xkb_state); + platform->m_xkb_state = xkb_state_new(platform->m_xkb_keymap); + + platform->InitializeKeyMap(); +} + +void WaylandNoGUIPlatform::InitializeKeyMap() +{ + m_key_map.clear(); + Log_VerbosePrintf("Init keymap"); + + const xkb_keycode_t min_keycode = xkb_keymap_min_keycode(m_xkb_keymap); + const xkb_keycode_t max_keycode = xkb_keymap_max_keycode(m_xkb_keymap); + DebugAssert(max_keycode >= min_keycode); + + for (xkb_keycode_t keycode = min_keycode; keycode <= max_keycode; keycode++) + { + const xkb_layout_index_t num_layouts = xkb_keymap_num_layouts_for_key(m_xkb_keymap, keycode); + if (num_layouts == 0) + continue; + + // Take the first layout which we find a valid keysym for. + bool found_keysym = false; + for (xkb_layout_index_t layout = 0; layout < num_layouts && !found_keysym; layout++) + { + const xkb_level_index_t num_levels = xkb_keymap_num_levels_for_key(m_xkb_keymap, keycode, layout); + if (num_levels == 0) + continue; + + // Take the first level which we find a valid keysym for. + for (xkb_level_index_t level = 0; level < num_levels; level++) + { + const xkb_keysym_t* keysyms; + const int num_syms = xkb_keymap_key_get_syms_by_level(m_xkb_keymap, keycode, layout, level, &keysyms); + if (num_syms == 0) + continue; + + // Just take the first. Should only be one in most cases anyway. + const xkb_keysym_t keysym = xkb_keysym_to_upper(keysyms[0]); + + char keysym_name_buf[64]; + if (xkb_keysym_get_name(keysym, keysym_name_buf, sizeof(keysym_name_buf)) <= 0) + continue; + + m_key_map.emplace(static_cast(keycode), keysym_name_buf); + found_keysym = false; + break; + } + } + } +} + +void WaylandNoGUIPlatform::KeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, + wl_array* keys) +{ +} + +void WaylandNoGUIPlatform::KeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface) {} + +void WaylandNoGUIPlatform::KeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, + uint32_t state) +{ + const xkb_keycode_t keycode = static_cast(key + 8); + const bool pressed = (state == WL_KEYBOARD_KEY_STATE_PRESSED); + NoGUIHost::ProcessPlatformKeyEvent(static_cast(keycode), pressed); +} + +void WaylandNoGUIPlatform::KeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, + uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, + uint32_t group) +{ + WaylandNoGUIPlatform* platform = static_cast(data); + xkb_state_update_mask(platform->m_xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); +} + +void WaylandNoGUIPlatform::PointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ +} + +void WaylandNoGUIPlatform::PointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface) {} + +void WaylandNoGUIPlatform::PointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y) +{ + const float pos_x = static_cast(wl_fixed_to_double(x)); + const float pos_y = static_cast(wl_fixed_to_double(y)); + NoGUIHost::ProcessPlatformMouseMoveEvent(static_cast(pos_x), static_cast(pos_y)); +} + +void WaylandNoGUIPlatform::PointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, + uint32_t button, uint32_t state) +{ + if (button < BTN_MOUSE || (button - BTN_MOUSE) >= 32) + return; + + const s32 button_index = (button - BTN_MOUSE); + const bool button_pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED); + NoGUIHost::ProcessPlatformMouseButtonEvent(button_index, button_pressed); +} + +void WaylandNoGUIPlatform::PointerAxis(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis, wl_fixed_t value) +{ + const float x = (axis == 1) ? std::clamp(static_cast(wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f; + const float y = (axis == 0) ? std::clamp(static_cast(-wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f; + NoGUIHost::ProcessPlatformMouseWheelEvent(x, y); +} + +void WaylandNoGUIPlatform::RunMessageLoop() +{ + while (m_message_loop_running.load(std::memory_order_acquire)) + { + wl_display_dispatch_pending(m_display); + + { + std::unique_lock lock(m_callback_queue_mutex); + while (!m_callback_queue.empty()) + { + std::function func = std::move(m_callback_queue.front()); + m_callback_queue.pop_front(); + lock.unlock(); + func(); + lock.lock(); + } + } + + // TODO: Make this suck less. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void WaylandNoGUIPlatform::ExecuteInMessageLoop(std::function func) +{ + std::unique_lock lock(m_callback_queue_mutex); + m_callback_queue.push_back(std::move(func)); +} + +void WaylandNoGUIPlatform::QuitMessageLoop() +{ + m_message_loop_running.store(false, std::memory_order_release); +} + +void WaylandNoGUIPlatform::SetFullscreen(bool enabled) +{ + // how the heck can we do this? +} + +bool WaylandNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) +{ + return false; +} + +std::unique_ptr NoGUIPlatform::CreateWaylandPlatform() +{ + std::unique_ptr ret = std::unique_ptr(new WaylandNoGUIPlatform()); + if (!ret->Initialize()) + return {}; + + return ret; +} diff --git a/src/duckstation-nogui/wayland_nogui_platform.h b/src/duckstation-nogui/wayland_nogui_platform.h new file mode 100644 index 000000000..a58422933 --- /dev/null +++ b/src/duckstation-nogui/wayland_nogui_platform.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include + +#include "nogui_platform.h" + +#include "wayland-xdg-decoration-client-protocol.h" +#include "wayland-xdg-shell-client-protocol.h" +#include +#include + +class WaylandNoGUIPlatform : public NoGUIPlatform +{ +public: + WaylandNoGUIPlatform(); + ~WaylandNoGUIPlatform(); + + bool Initialize(); + + void ReportError(const std::string_view& title, const std::string_view& message) override; + + void SetDefaultConfig(SettingsInterface& si) override; + + bool CreatePlatformWindow(std::string title) override; + void DestroyPlatformWindow() override; + std::optional GetPlatformWindowInfo() override; + void SetPlatformWindowTitle(std::string title) override; + void* GetPlatformWindowHandle() override; + + std::optional ConvertHostKeyboardStringToCode(const std::string_view& str) override; + std::optional ConvertHostKeyboardCodeToString(u32 code) override; + + void RunMessageLoop() override; + void ExecuteInMessageLoop(std::function func) override; + void QuitMessageLoop() override; + + void SetFullscreen(bool enabled) override; + + bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override; + +private: + void InitializeKeyMap(); + + static void GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, const char* interface, + uint32_t version); + static void GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id); + static void XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial); + static void XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, uint32_t serial); + static void TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width, int32_t height, + struct wl_array* states); + static void PointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, + wl_fixed_t surface_y); + static void PointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface); + static void PointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y); + static void PointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, + uint32_t state); + static void PointerAxis(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis, wl_fixed_t value); + static void KeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size); + static void KeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys); + static void KeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface); + static void KeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, + uint32_t state); + static void KeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, uint32_t group); + static void SeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities); + static void TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel); + + std::atomic_bool m_message_loop_running{false}; + // std::atomic_bool m_fullscreen{false}; + + WindowInfo m_window_info = {}; + + wl_display* m_display = nullptr; + wl_registry* m_registry = nullptr; + wl_compositor* m_compositor = nullptr; + xdg_wm_base* m_xdg_wm_base = nullptr; + wl_surface* m_surface = nullptr; + wl_region* m_region = nullptr; + xdg_surface* m_xdg_surface = nullptr; + xdg_toplevel* m_xdg_toplevel = nullptr; + zxdg_decoration_manager_v1* m_decoration_manager = nullptr; + zxdg_toplevel_decoration_v1* m_toplevel_decoration = nullptr; + wl_seat* m_wl_seat = nullptr; + wl_keyboard* m_wl_keyboard = nullptr; + wl_pointer* m_wl_pointer = nullptr; + xkb_context* m_xkb_context = nullptr; + xkb_keymap* m_xkb_keymap = nullptr; + xkb_state* m_xkb_state = nullptr; + + std::unordered_map m_key_map; + std::mutex m_key_map_mutex; + + std::deque> m_callback_queue; + std::mutex m_callback_queue_mutex; +}; diff --git a/src/duckstation-nogui/win32_host_interface.cpp b/src/duckstation-nogui/win32_host_interface.cpp deleted file mode 100644 index 51f99d483..000000000 --- a/src/duckstation-nogui/win32_host_interface.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "win32_host_interface.h" -#include "common/log.h" -#include "common/string_util.h" -#include "resource.h" -#include -Log_SetChannel(Win32HostInterface); - -static constexpr LPCWSTR WINDOW_CLASS_NAME = L"DuckStationNoGUI"; - -Win32HostInterface::Win32HostInterface() = default; - -Win32HostInterface::~Win32HostInterface() = default; - -std::unique_ptr Win32HostInterface::Create() -{ - return std::make_unique(); -} - -bool Win32HostInterface::Initialize() -{ - if (!RegisterWindowClass()) - return false; - - if (!NoGUIHostInterface::Initialize()) - return false; - - return true; -} - -void Win32HostInterface::Shutdown() -{ - NoGUIHostInterface::Shutdown(); -} - -bool Win32HostInterface::RegisterWindowClass() -{ - WNDCLASSEXW wc = {}; - wc.cbSize = sizeof(WNDCLASSEXW); - wc.style = 0; - wc.lpfnWndProc = WndProc; - wc.cbClsExtra = 0; - wc.cbWndExtra = 0; - wc.hInstance = GetModuleHandle(nullptr); - wc.hIcon = LoadIconA(NULL, (LPCSTR)IDI_ICON1); - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wc.lpszMenuName = NULL; - wc.lpszClassName = WINDOW_CLASS_NAME; - wc.hIconSm = LoadIconA(NULL, (LPCSTR)IDI_ICON1); - - if (!RegisterClassExW(&wc)) - { - MessageBoxA(nullptr, "Window registration failed.", "Error", MB_ICONERROR | MB_OK); - return false; - } - - return true; -} - -void Win32HostInterface::SetMouseMode(bool relative, bool hide_cursor) {} - -bool Win32HostInterface::CreatePlatformWindow() -{ - m_hwnd = CreateWindowExW(WS_EX_CLIENTEDGE, WINDOW_CLASS_NAME, L"DuckStation", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, - CW_USEDEFAULT, DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT, nullptr, nullptr, - GetModuleHandleA(nullptr), this); - if (!m_hwnd) - { - MessageBoxA(nullptr, "CreateWindowEx failed.", "Error", MB_ICONERROR | MB_OK); - return false; - } - - ShowWindow(m_hwnd, SW_SHOW); - UpdateWindow(m_hwnd); - ProcessWin32Events(); - - return true; -} - -void Win32HostInterface::DestroyPlatformWindow() -{ - if (m_hwnd) - { - DestroyWindow(m_hwnd); - m_hwnd = {}; - } -} - -std::optional Win32HostInterface::GetPlatformWindowInfo() -{ - RECT rc = {}; - GetClientRect(m_hwnd, &rc); - - WindowInfo wi; - wi.window_handle = static_cast(m_hwnd); - wi.type = WindowInfo::Type::Win32; - wi.surface_width = static_cast(rc.right - rc.left); - wi.surface_height = static_cast(rc.bottom - rc.top); - // wi.surface_format = WindowInfo::SurfaceFormat::Auto; - return wi; -} - -void Win32HostInterface::PollAndUpdate() -{ - ProcessWin32Events(); - - NoGUIHostInterface::PollAndUpdate(); -} - -void Win32HostInterface::ProcessWin32Events() -{ - MSG msg; - while (PeekMessage(&msg, m_hwnd, 0, 0, PM_REMOVE)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } -} - -LRESULT Win32HostInterface::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - Win32HostInterface* hi = static_cast(g_host_interface); - switch (msg) - { - case WM_SIZE: - { - const u32 width = LOWORD(lParam); - const u32 height = HIWORD(lParam); - if (hi->m_display) - hi->m_display->ResizeRenderWindow(static_cast(width), static_cast(height)); - } - break; - - case WM_CLOSE: - hi->m_quit_request = true; - break; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 0; -} - -std::optional Win32HostInterface::GetHostKeyCode(const std::string_view key_code) const -{ - std::optional kc; // = EvDevKeyNames::GetKeyCodeForName(key_code); - if (!kc.has_value()) - return std::nullopt; - - return static_cast(kc.value()); -} diff --git a/src/duckstation-nogui/win32_host_interface.h b/src/duckstation-nogui/win32_host_interface.h deleted file mode 100644 index 86bc60cb4..000000000 --- a/src/duckstation-nogui/win32_host_interface.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include "common/windows_headers.h" -#include "nogui_host_interface.h" -#include -#include - -class Win32HostInterface final : public NoGUIHostInterface -{ -public: - Win32HostInterface(); - ~Win32HostInterface(); - - bool Initialize(); - void Shutdown(); - - static std::unique_ptr Create(); - -protected: - void SetMouseMode(bool relative, bool hide_cursor) override; - - bool CreatePlatformWindow() override; - void DestroyPlatformWindow() override; - std::optional GetPlatformWindowInfo() override; - - std::optional GetHostKeyCode(const std::string_view key_code) const override; - - void PollAndUpdate() override; - -private: - static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); - - bool RegisterWindowClass(); - void ProcessWin32Events(); - - HWND m_hwnd{}; -}; diff --git a/src/duckstation-nogui/win32_key_names.h b/src/duckstation-nogui/win32_key_names.h new file mode 100644 index 000000000..fafea7564 --- /dev/null +++ b/src/duckstation-nogui/win32_key_names.h @@ -0,0 +1,164 @@ +#pragma once +#include "common/types.h" +#include "common/windows_headers.h" +#include +#include +#include +#include +#include + +namespace Win32KeyNames { +static const std::map s_win32_key_names = { + {VK_RETURN, "Return"}, + {VK_ESCAPE, "Escape"}, + {VK_BACK, "Backspace"}, + {VK_TAB, "Tab"}, + {VK_SPACE, "Space"}, + {0xDE, "Apostrophe"}, + {0xBC, "Comma"}, + {0xBD, "Minus"}, + {0xBE, "Period"}, + {0xBF, "Slash"}, + {'0', "0"}, + {'1', "1"}, + {'2', "2"}, + {'3', "3"}, + {'4', "4"}, + {'5', "5"}, + {'6', "6"}, + {'7', "7"}, + {'8', "8"}, + {'9', "9"}, + {0xBA, "Semcolon"}, + {0xBB, "Equal"}, + {0xDB, "BracketLeft"}, + {0xDC, "Backslash"}, + {0xDD, "BracketRight"}, + {0xC0, "QuoteLeft"}, + {'A', "A"}, + {'B', "B"}, + {'C', "C"}, + {'D', "D"}, + {'E', "E"}, + {'F', "F"}, + {'G', "G"}, + {'H', "H"}, + {'I', "I"}, + {'J', "J"}, + {'K', "K"}, + {'L', "L"}, + {'M', "M"}, + {'N', "N"}, + {'O', "O"}, + {'P', "P"}, + {'Q', "Q"}, + {'R', "R"}, + {'S', "S"}, + {'T', "T"}, + {'U', "U"}, + {'V', "V"}, + {'W', "W"}, + {'X', "X"}, + {'Y', "Y"}, + {'Z', "Z"}, + {VK_CAPITAL, "CapsLock"}, + {VK_F1, "F1"}, + {VK_F2, "F2"}, + {VK_F3, "F3"}, + {VK_F4, "F4"}, + {VK_F5, "F5"}, + {VK_F6, "F6"}, + {VK_F7, "F7"}, + {VK_F8, "F8"}, + {VK_F9, "F9"}, + {VK_F10, "F10"}, + {VK_F11, "F11"}, + {VK_F12, "F12"}, + {VK_PRINT, "Print"}, + {VK_SCROLL, "ScrollLock"}, + {VK_PAUSE, "Pause"}, + {VK_INSERT, "Insert"}, + {VK_HOME, "Home"}, + {VK_PRIOR, "PageUp"}, + {VK_DELETE, "Delete"}, + {VK_END, "End"}, + {VK_NEXT, "PageDown"}, + {VK_RIGHT, "Right"}, + {VK_LEFT, "Left"}, + {VK_DOWN, "Down"}, + {VK_UP, "Up"}, + {VK_NUMLOCK, "NumLock"}, + {VK_DIVIDE, "KeypadDivide"}, + {VK_MULTIPLY, "KeypadMultiply"}, + {VK_SUBTRACT, "KeypadMinus"}, + {VK_ADD, "KeypadPlus"}, + //{VK_KP_ENTER, "KeypadReturn"}, + {VK_NUMPAD1, "Keypad1"}, + {VK_NUMPAD2, "Keypad2"}, + {VK_NUMPAD3, "Keypad3"}, + {VK_NUMPAD4, "Keypad4"}, + {VK_NUMPAD5, "Keypad5"}, + {VK_NUMPAD6, "Keypad6"}, + {VK_NUMPAD7, "Keypad7"}, + {VK_NUMPAD8, "Keypad8"}, + {VK_NUMPAD9, "Keypad9"}, + {VK_NUMPAD0, "Keypad0"}, + {VK_SEPARATOR, "KeypadPeriod"}, + {VK_F13, "F13"}, + {VK_F14, "F14"}, + {VK_F15, "F15"}, + {VK_F16, "F16"}, + {VK_F17, "F17"}, + {VK_F18, "F18"}, + {VK_F19, "F19"}, + {VK_F20, "F20"}, + {VK_F21, "F21"}, + {VK_F22, "F22"}, + {VK_F23, "F23"}, + {VK_F24, "F24"}, + {VK_EXECUTE, "Execute"}, + {VK_HELP, "Help"}, + {VK_MENU, "Menu"}, + {VK_SELECT, "Select"}, + {VK_MEDIA_STOP, "Stop"}, + {VK_VOLUME_UP, "VolumeUp"}, + {VK_VOLUME_DOWN, "VolumeDown"}, + {VK_CANCEL, "Cancel"}, + {VK_CLEAR, "Clear"}, + {VK_PRIOR, "Prior"}, + {VK_SEPARATOR, "Separator"}, + {VK_CRSEL, "CrSel"}, + {VK_EXSEL, "ExSel"}, + {VK_LCONTROL, "LeftControl"}, + {VK_LSHIFT, "LeftShift"}, + {VK_LMENU, "LeftAlt"}, + {VK_LWIN, "Super_L"}, + {VK_RCONTROL, "RightCtrl"}, + {VK_RSHIFT, "RightShift"}, + {VK_RMENU, "RightAlt"}, + {VK_RWIN, "RightSuper"}, + {VK_MEDIA_NEXT_TRACK, "MediaNext"}, + {VK_MEDIA_PREV_TRACK, "MediaPrevious"}, + {VK_MEDIA_STOP, "MediaStop"}, + {VK_MEDIA_PLAY_PAUSE, "MediaPlay"}, + {VK_VOLUME_MUTE, "VolumeMute"}, + {VK_SLEEP, "Sleep"}, +}; + +static const char* GetKeyName(DWORD key) +{ + const auto it = s_win32_key_names.find(key); + return it == s_win32_key_names.end() ? nullptr : it->second; +} + +static std::optional GetKeyCodeForName(const std::string_view& key_name) +{ + for (const auto& it : s_win32_key_names) + { + if (key_name == it.second) + return it.first; + } + + return std::nullopt; +} +} // namespace Win32KeyNames \ No newline at end of file diff --git a/src/duckstation-nogui/win32_nogui_platform.cpp b/src/duckstation-nogui/win32_nogui_platform.cpp new file mode 100644 index 000000000..f13b2d09e --- /dev/null +++ b/src/duckstation-nogui/win32_nogui_platform.cpp @@ -0,0 +1,365 @@ +#include "win32_nogui_platform.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/threading.h" +#include "core/host.h" +#include "core/host_settings.h" +#include "nogui_host.h" +#include "resource.h" +#include "win32_key_names.h" +#include +Log_SetChannel(Win32HostInterface); + +static constexpr LPCWSTR WINDOW_CLASS_NAME = L"DuckStationNoGUI"; +static constexpr DWORD WINDOWED_STYLE = WS_OVERLAPPEDWINDOW | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_SIZEBOX; +static constexpr DWORD WINDOWED_EXSTYLE = WS_EX_DLGMODALFRAME | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE; +static constexpr DWORD FULLSCREEN_STYLE = WS_POPUP | WS_MINIMIZEBOX; + +static float GetWindowScale(HWND hwnd) +{ + static UINT(WINAPI * get_dpi_for_window)(HWND hwnd); + if (!get_dpi_for_window) + { + HMODULE mod = GetModuleHandleW(L"user32.dll"); + if (mod) + get_dpi_for_window = reinterpret_cast(GetProcAddress(mod, "GetDpiForWindow")); + } + if (!get_dpi_for_window) + return 1.0f; + + // less than 100% scaling seems unlikely. + const UINT dpi = hwnd ? get_dpi_for_window(hwnd) : 96; + return (dpi > 0) ? std::max(1.0f, static_cast(dpi) / 96.0f) : 1.0f; +} + +Win32NoGUIPlatform::Win32NoGUIPlatform() +{ + m_message_loop_running.store(true, std::memory_order_release); +} + +Win32NoGUIPlatform::~Win32NoGUIPlatform() +{ + UnregisterClassW(WINDOW_CLASS_NAME, GetModuleHandle(nullptr)); +} + +bool Win32NoGUIPlatform::Initialize() +{ + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(WNDCLASSEXW); + wc.style = 0; + wc.lpfnWndProc = WndProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = LoadIconA(wc.hInstance, (LPCSTR)IDI_ICON1); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszMenuName = NULL; + wc.lpszClassName = WINDOW_CLASS_NAME; + wc.hIconSm = LoadIconA(wc.hInstance, (LPCSTR)IDI_ICON1); + + if (!RegisterClassExW(&wc)) + { + MessageBoxW(nullptr, L"Window registration failed.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + m_window_thread_id = GetCurrentThreadId(); + return true; +} + +void Win32NoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) +{ + const std::wstring title_copy(StringUtil::UTF8StringToWideString(title)); + const std::wstring message_copy(StringUtil::UTF8StringToWideString(message)); + + MessageBoxW(m_hwnd, message_copy.c_str(), title_copy.c_str(), MB_ICONERROR | MB_OK); +} + +void Win32NoGUIPlatform::SetDefaultConfig(SettingsInterface& si) +{ + // noop +} + +bool Win32NoGUIPlatform::CreatePlatformWindow(std::string title) +{ + s32 window_x, window_y, window_width, window_height; + if (!NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height)) + { + window_x = CW_USEDEFAULT; + window_y = CW_USEDEFAULT; + window_width = DEFAULT_WINDOW_WIDTH; + window_height = DEFAULT_WINDOW_HEIGHT; + } + + HWND hwnd = CreateWindowExW(WS_EX_CLIENTEDGE, WINDOW_CLASS_NAME, StringUtil::UTF8StringToWideString(title).c_str(), + WINDOWED_STYLE, window_x, window_y, window_width, window_height, nullptr, nullptr, + GetModuleHandleW(nullptr), this); + if (!hwnd) + { + MessageBoxW(nullptr, L"CreateWindowEx failed.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + // deliberately not stored to m_hwnd yet, because otherwise the msg handlers will run + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + m_hwnd = hwnd; + m_window_scale = GetWindowScale(m_hwnd); + m_last_mouse_buttons = 0; + + if (m_fullscreen.load(std::memory_order_acquire)) + SetFullscreen(true); + + return true; +} + +void Win32NoGUIPlatform::DestroyPlatformWindow() +{ + if (!m_hwnd) + return; + + RECT rc; + if (!m_fullscreen.load(std::memory_order_acquire) && GetWindowRect(m_hwnd, &rc)) + { + NoGUIHost::SavePlatformWindowGeometry(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top); + } + + DestroyWindow(m_hwnd); + m_hwnd = {}; +} + +std::optional Win32NoGUIPlatform::GetPlatformWindowInfo() +{ + if (!m_hwnd) + return std::nullopt; + + RECT rc = {}; + GetWindowRect(m_hwnd, &rc); + + WindowInfo wi; + wi.surface_width = static_cast(rc.right - rc.left); + wi.surface_height = static_cast(rc.bottom - rc.top); + wi.surface_scale = m_window_scale; + wi.type = WindowInfo::Type::Win32; + wi.window_handle = m_hwnd; + return wi; +} + +void Win32NoGUIPlatform::SetPlatformWindowTitle(std::string title) +{ + if (!m_hwnd) + return; + + SetWindowTextW(m_hwnd, StringUtil::UTF8StringToWideString(title).c_str()); +} + +void* Win32NoGUIPlatform::GetPlatformWindowHandle() +{ + return m_hwnd; +} + +std::optional Win32NoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) +{ + std::optional converted(Win32KeyNames::GetKeyCodeForName(str)); + return converted.has_value() ? std::optional(static_cast(converted.value())) : std::nullopt; +} + +std::optional Win32NoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) +{ + const char* converted = Win32KeyNames::GetKeyName(code); + return converted ? std::optional(converted) : std::nullopt; +} + +void Win32NoGUIPlatform::RunMessageLoop() +{ + while (m_message_loop_running.load(std::memory_order_acquire)) + { + MSG msg; + if (GetMessageW(&msg, NULL, 0, 0)) + { + // handle self messages (when we don't have a window yet) + if (msg.hwnd == NULL && msg.message >= WM_FIRST && msg.message <= WM_LAST) + { + WndProc(NULL, msg.message, msg.wParam, msg.lParam); + } + else + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + } +} + +void Win32NoGUIPlatform::ExecuteInMessageLoop(std::function func) +{ + std::function* pfunc = new std::function(std::move(func)); + if (m_hwnd) + PostMessageW(m_hwnd, WM_FUNC, 0, reinterpret_cast(pfunc)); + else + PostThreadMessageW(m_window_thread_id, WM_FUNC, 0, reinterpret_cast(pfunc)); +} + +void Win32NoGUIPlatform::QuitMessageLoop() +{ + m_message_loop_running.store(false, std::memory_order_release); + PostThreadMessageW(m_window_thread_id, WM_WAKEUP, 0, 0); +} + +void Win32NoGUIPlatform::SetFullscreen(bool enabled) +{ + if (!m_hwnd || m_fullscreen.load(std::memory_order_acquire) == enabled) + return; + + LONG style = GetWindowLong(m_hwnd, GWL_STYLE); + LONG exstyle = GetWindowLong(m_hwnd, GWL_EXSTYLE); + RECT rc; + + if (enabled) + { + HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST); + if (!monitor) + return; + + MONITORINFO mi = {sizeof(MONITORINFO)}; + if (!GetMonitorInfo(monitor, &mi) || !GetWindowRect(m_hwnd, &m_windowed_rect)) + return; + + style = (style & ~WINDOWED_STYLE) | FULLSCREEN_STYLE; + exstyle = (style & ~WINDOWED_EXSTYLE); + rc = mi.rcMonitor; + } + else + { + style = (style & ~FULLSCREEN_STYLE) | WINDOWED_STYLE; + exstyle = exstyle | WINDOWED_EXSTYLE; + rc = m_windowed_rect; + } + + SetWindowLongPtrW(m_hwnd, GWL_STYLE, style); + SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, exstyle); + SetWindowPos(m_hwnd, NULL, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, SWP_SHOWWINDOW); + + m_fullscreen.store(enabled, std::memory_order_release); +} + +bool Win32NoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) +{ + RECT rc; + if (!m_hwnd || m_fullscreen.load(std::memory_order_acquire) || !GetWindowRect(m_hwnd, &rc)) + { + return false; + } + + return SetWindowPos(m_hwnd, NULL, rc.left, rc.top, new_window_width, new_window_height, SWP_SHOWWINDOW); +} + +LRESULT CALLBACK Win32NoGUIPlatform::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + Win32NoGUIPlatform* platform = static_cast(g_nogui_window.get()); + if (hwnd != platform->m_hwnd && msg != WM_FUNC) + return DefWindowProcW(hwnd, msg, wParam, lParam); + + switch (msg) + { + case WM_SIZE: + { + const u32 width = LOWORD(lParam); + const u32 height = HIWORD(lParam); + NoGUIHost::ProcessPlatformWindowResize(width, height, platform->m_window_scale); + } + break; + + case WM_KEYDOWN: + case WM_KEYUP: + { + const bool pressed = (msg == WM_KEYDOWN); + NoGUIHost::ProcessPlatformKeyEvent(static_cast(wParam), pressed); + } + break; + + case WM_MOUSEMOVE: + { + const float x = static_cast(static_cast(LOWORD(lParam))); + const float y = static_cast(static_cast(HIWORD(lParam))); + NoGUIHost::ProcessPlatformMouseMoveEvent(x, y); + } + break; + + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + { + const DWORD buttons = static_cast(wParam); + const DWORD changed = platform->m_last_mouse_buttons ^ buttons; + platform->m_last_mouse_buttons = buttons; + + static constexpr DWORD masks[] = {MK_LBUTTON, MK_RBUTTON, MK_MBUTTON, MK_XBUTTON1, MK_XBUTTON2}; + for (u32 i = 0; i < std::size(masks); i++) + { + if (changed & masks[i]) + NoGUIHost::ProcessPlatformMouseButtonEvent(i, (buttons & masks[i]) != 0); + } + } + break; + + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + { + const float d = + std::clamp(static_cast(static_cast(HIWORD(wParam))) / static_cast(WHEEL_DELTA), -1.0f, 1.0f); + NoGUIHost::ProcessPlatformMouseWheelEvent((msg == WM_MOUSEHWHEEL) ? d : 0.0f, (msg == WM_MOUSEWHEEL) ? d : 0.0f); + } + break; + + case WM_ACTIVATEAPP: + { + if (wParam) + NoGUIHost::PlatformWindowFocusGained(); + else + NoGUIHost::PlatformWindowFocusLost(); + } + break; + + case WM_CLOSE: + case WM_QUIT: + { + Host::RunOnCPUThread([]() { Host::RequestExit(g_settings.save_state_on_exit); }); + } + break; + + case WM_FUNC: + { + std::function* pfunc = reinterpret_cast*>(lParam); + if (pfunc) + { + (*pfunc)(); + delete pfunc; + } + } + break; + + case WM_WAKEUP: + break; + + default: + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + + return 0; +} + +std::unique_ptr NoGUIPlatform::CreateWin32Platform() +{ + std::unique_ptr ret(new Win32NoGUIPlatform()); + if (!ret->Initialize()) + return {}; + + return ret; +} \ No newline at end of file diff --git a/src/duckstation-nogui/win32_nogui_platform.h b/src/duckstation-nogui/win32_nogui_platform.h new file mode 100644 index 000000000..0e65c7e2d --- /dev/null +++ b/src/duckstation-nogui/win32_nogui_platform.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "common/windows_headers.h" + +#include "nogui_platform.h" + +class Win32NoGUIPlatform : public NoGUIPlatform +{ +public: + Win32NoGUIPlatform(); + ~Win32NoGUIPlatform(); + + bool Initialize(); + + void ReportError(const std::string_view& title, const std::string_view& message) override; + + void SetDefaultConfig(SettingsInterface& si) override; + + bool CreatePlatformWindow(std::string title) override; + void DestroyPlatformWindow() override; + std::optional GetPlatformWindowInfo() override; + void SetPlatformWindowTitle(std::string title) override; + void* GetPlatformWindowHandle() override; + + std::optional ConvertHostKeyboardStringToCode(const std::string_view& str) override; + std::optional ConvertHostKeyboardCodeToString(u32 code) override; + + void RunMessageLoop() override; + void ExecuteInMessageLoop(std::function func) override; + void QuitMessageLoop() override; + + void SetFullscreen(bool enabled) override; + + bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override; + +private: + enum : u32 + { + WM_FIRST = WM_USER + 1337, + WM_FUNC = WM_FIRST, + WM_WAKEUP, + WM_LAST = WM_WAKEUP + }; + + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + + HWND m_hwnd{}; + DWORD m_window_thread_id = 0; + RECT m_windowed_rect = {}; + float m_window_scale = 1.0f; + + std::atomic_bool m_message_loop_running{false}; + std::atomic_bool m_fullscreen{false}; + + DWORD m_last_mouse_buttons = 0; +}; \ No newline at end of file diff --git a/src/duckstation-nogui/x11_nogui_platform.cpp b/src/duckstation-nogui/x11_nogui_platform.cpp new file mode 100644 index 000000000..a27637bf2 --- /dev/null +++ b/src/duckstation-nogui/x11_nogui_platform.cpp @@ -0,0 +1,327 @@ +#include "x11_nogui_platform.h" + +Log_SetChannel(X11NoGUIPlatform); + +X11NoGUIPlatform::X11NoGUIPlatform() +{ + m_message_loop_running.store(true, std::memory_order_release); +} + +X11NoGUIPlatform::~X11NoGUIPlatform() +{ + if (m_display) + { + // Segfaults somewhere in an unloaded module on Ubuntu 22.04 :S + // I really don't care enough about X to figure out why. The application is shutting down + // anyway, so a leak here isn't a big deal. + // XCloseDisplay(m_display); + } +} + +bool X11NoGUIPlatform::Initialize() +{ + const int res = XInitThreads(); + if (res != 0) + Log_WarningPrintf("XInitThreads() returned %d, things might not be stable.", res); + + m_display = XOpenDisplay(nullptr); + if (!m_display) + { + Log_ErrorPrint("Failed to connect to X11 display."); + return false; + } + + return true; +} + +void X11NoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) +{ + const std::string title_copy(title); + const std::string message_copy(message); + Log_ErrorPrintf("%s: %s", title_copy.c_str(), message_copy.c_str()); +} + +void X11NoGUIPlatform::SetDefaultConfig(SettingsInterface& si) {} + +bool X11NoGUIPlatform::CreatePlatformWindow(std::string title) +{ + s32 window_x, window_y, window_width, window_height; + bool has_window_pos = NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height); + if (!has_window_pos) + { + window_x = 0; + window_y = 0; + window_width = DEFAULT_WINDOW_WIDTH; + window_height = DEFAULT_WINDOW_HEIGHT; + } + + XDisplayLocker locker(m_display); + { + m_window = XCreateSimpleWindow(m_display, DefaultRootWindow(m_display), window_x, window_y, window_width, + window_height, 0, 0, BlackPixel(m_display, 0)); + if (!m_window) + { + Log_ErrorPrintf("Failed to create X window"); + return false; + } + + XSelectInput(m_display, m_window, + StructureNotifyMask | KeyPressMask | KeyReleaseMask | FocusChangeMask | PointerMotionMask | + ButtonPressMask | ButtonReleaseMask); + XStoreName(m_display, m_window, title.c_str()); + + // Enable close notifications. + Atom wmProtocols[1]; + wmProtocols[0] = XInternAtom(m_display, "WM_DELETE_WINDOW", True); + XSetWMProtocols(m_display, m_window, wmProtocols, 1); + + m_window_info.surface_width = static_cast(window_width); + m_window_info.surface_height = static_cast(window_height); + m_window_info.surface_scale = 1.0f; + m_window_info.type = WindowInfo::Type::X11; + m_window_info.window_handle = reinterpret_cast(m_window); + m_window_info.display_connection = m_display; + + XMapRaised(m_display, m_window); + XFlush(m_display); + XSync(m_display, True); + InitializeKeyMap(); + } + + ProcessXEvents(); + return true; +} + +void X11NoGUIPlatform::DestroyPlatformWindow() +{ + m_window_info = {}; + + if (m_window) + { + XDisplayLocker locker(m_display); + SaveWindowGeometry(); + XUnmapWindow(m_display, m_window); + XDestroyWindow(m_display, m_window); + m_window = {}; + } +} + +std::optional X11NoGUIPlatform::GetPlatformWindowInfo() +{ + if (m_window_info.type == WindowInfo::Type::X11) + return m_window_info; + else + return std::nullopt; +} + +void X11NoGUIPlatform::SetPlatformWindowTitle(std::string title) +{ + ExecuteInMessageLoop([this, title = std::move(title)]() { + if (m_window) + { + XDisplayLocker locker(m_display); + XStoreName(m_display, m_window, title.c_str()); + } + }); +} + +void* X11NoGUIPlatform::GetPlatformWindowHandle() +{ + return reinterpret_cast(m_window); +} + +void X11NoGUIPlatform::InitializeKeyMap() +{ + int min_keycode = 0, max_keycode = -1; + XDisplayKeycodes(m_display, &min_keycode, &max_keycode); + for (int keycode = 0; keycode <= max_keycode; keycode++) + { + KeySym keysym = NoSymbol; + for (int i = 0; i < 8 && keysym == NoSymbol; i++) + keysym = XKeycodeToKeysym(m_display, static_cast(keycode), i); + if (keysym == NoSymbol) + continue; + + KeySym upper_sym; + XConvertCase(keysym, &keysym, &upper_sym); + + // Would this fail? + const char* keyname = XKeysymToString(keysym); + if (!keyname) + continue; + + m_key_map.emplace(static_cast(keysym), keyname); + } +} + +std::optional X11NoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) +{ + for (const auto& it : m_key_map) + { + if (StringUtil::Strncasecmp(it.second.c_str(), str.data(), str.length()) == 0) + return it.first; + } + + return std::nullopt; +} + +std::optional X11NoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) +{ + const auto it = m_key_map.find(static_cast(code)); + return (it != m_key_map.end()) ? std::optional(it->second) : std::nullopt; +} + +void X11NoGUIPlatform::ProcessXEvents() +{ + XLockDisplay(m_display); + + for (int num_events = XPending(m_display); num_events > 0; num_events--) + { + XEvent event; + XNextEvent(m_display, &event); + switch (event.type) + { + case KeyPress: + case KeyRelease: + { + const KeySym sym = XLookupKeysym(&event.xkey, 0); + if (sym != NoSymbol) + NoGUIHost::ProcessPlatformKeyEvent(static_cast(sym), (event.type == KeyPress)); + } + break; + + case ButtonPress: + case ButtonRelease: + { + if (event.xbutton.button >= Button1) + { + NoGUIHost::ProcessPlatformMouseButtonEvent(static_cast(event.xbutton.button - Button1), + event.type == ButtonPress); + } + } + break; + + case MotionNotify: + { + NoGUIHost::ProcessPlatformMouseMoveEvent(static_cast(event.xmotion.x), + static_cast(event.xmotion.y)); + } + break; + + case ConfigureNotify: + { + const s32 width = std::max(static_cast(event.xconfigure.width), 1); + const s32 height = std::max(static_cast(event.xconfigure.height), 1); + NoGUIHost::ProcessPlatformWindowResize(width, height, m_window_info.surface_scale); + } + break; + + case FocusIn: + { + NoGUIHost::PlatformWindowFocusGained(); + } + break; + + case FocusOut: + { + NoGUIHost::PlatformWindowFocusGained(); + } + break; + + case ClientMessage: + { + if (static_cast(event.xclient.data.l[0]) == XInternAtom(m_display, "WM_DELETE_WINDOW", False)) + Host::RequestExit(true); + } + break; + + default: + break; + } + } + + XUnlockDisplay(m_display); +} + +void X11NoGUIPlatform::RunMessageLoop() +{ + while (m_message_loop_running.load(std::memory_order_acquire)) + { + ProcessXEvents(); + + { + std::unique_lock lock(m_callback_queue_mutex); + while (!m_callback_queue.empty()) + { + std::function func = std::move(m_callback_queue.front()); + m_callback_queue.pop_front(); + lock.unlock(); + func(); + lock.lock(); + } + } + + // TODO: Make this suck less. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void X11NoGUIPlatform::ExecuteInMessageLoop(std::function func) +{ + std::unique_lock lock(m_callback_queue_mutex); + m_callback_queue.push_back(std::move(func)); +} + +void X11NoGUIPlatform::QuitMessageLoop() +{ + m_message_loop_running.store(false, std::memory_order_release); +} + +void X11NoGUIPlatform::SetFullscreen(bool enabled) +{ + if (!m_window || m_fullscreen.load(std::memory_order_acquire) == enabled) + return; + + XDisplayLocker locker(m_display); + + XEvent event; + event.xclient.type = ClientMessage; + event.xclient.message_type = XInternAtom(m_display, "_NET_WM_STATE", False); + event.xclient.window = m_window; + event.xclient.format = 32; + event.xclient.data.l[0] = _NET_WM_STATE_TOGGLE; + event.xclient.data.l[1] = XInternAtom(m_display, "_NET_WM_STATE_FULLSCREEN", False); + if (!XSendEvent(m_display, DefaultRootWindow(m_display), False, SubstructureRedirectMask | SubstructureNotifyMask, + &event)) + { + Log_ErrorPrintf("Failed to switch to %s", enabled ? "Fullscreen" : "windowed"); + return; + } + + m_fullscreen.store(enabled, std::memory_order_release); +} + +void X11NoGUIPlatform::SaveWindowGeometry() +{ + int x = 0, y = 0; + unsigned int width = 0, height = 0; + unsigned int dummy_border, dummy_depth; + Window dummy_window; + XGetGeometry(m_display, m_window, &dummy_window, &x, &y, &width, &height, &dummy_border, &dummy_depth); + if (width > 0 && height > 0) + NoGUIHost::SavePlatformWindowGeometry(x, y, width, height); +} + +bool X11NoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) +{ + return false; +} + +std::unique_ptr NoGUIPlatform::CreateX11Platform() +{ + std::unique_ptr ret = std::unique_ptr(new X11NoGUIPlatform()); + if (!ret->Initialize()) + return {}; + + return ret; +} diff --git a/src/duckstation-nogui/x11_nogui_platform.h b/src/duckstation-nogui/x11_nogui_platform.h new file mode 100644 index 000000000..071a4c22c --- /dev/null +++ b/src/duckstation-nogui/x11_nogui_platform.h @@ -0,0 +1,91 @@ +#pragma once + +#include "nogui_platform.h" + +// Why do we have all these here instead of in the source? +// Because X11 is a giant turd and #defines commonly used words. +#include "common/assert.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/threading.h" +#include "core/host.h" +#include "core/host_settings.h" +#include "nogui_host.h" +#include "nogui_platform.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Include X stuff *last*. +#include +#include +#include +#include +#define _NET_WM_STATE_REMOVE 0 +#define _NET_WM_STATE_ADD 1 +#define _NET_WM_STATE_TOGGLE 2 + +class X11NoGUIPlatform : public NoGUIPlatform +{ +public: + X11NoGUIPlatform(); + ~X11NoGUIPlatform(); + + bool Initialize(); + + void ReportError(const std::string_view& title, const std::string_view& message) override; + + void SetDefaultConfig(SettingsInterface& si) override; + + bool CreatePlatformWindow(std::string title) override; + void DestroyPlatformWindow() override; + std::optional GetPlatformWindowInfo() override; + void SetPlatformWindowTitle(std::string title) override; + void* GetPlatformWindowHandle() override; + + std::optional ConvertHostKeyboardStringToCode(const std::string_view& str) override; + std::optional ConvertHostKeyboardCodeToString(u32 code) override; + + void RunMessageLoop() override; + void ExecuteInMessageLoop(std::function func) override; + void QuitMessageLoop() override; + + void SetFullscreen(bool enabled) override; + + bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override; + +private: + void InitializeKeyMap(); + void SaveWindowGeometry(); + void ProcessXEvents(); + + std::atomic_bool m_message_loop_running{false}; + std::atomic_bool m_fullscreen{false}; + + WindowInfo m_window_info = {}; + + Display* m_display = nullptr; + Window m_window = {}; + + std::unordered_map m_key_map; + + std::deque> m_callback_queue; + std::mutex m_callback_queue_mutex; +}; + +class XDisplayLocker +{ +public: + XDisplayLocker(Display* dpy) : m_display(dpy) { XLockDisplay(m_display); } + + ~XDisplayLocker() { XUnlockDisplay(m_display); } + +private: + Display* m_display; +};