From 72dfbaf6ccd37ecd4233d9ded7f496479e2df815 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 23 Oct 2022 14:09:54 +1000 Subject: [PATCH] Misc: Backports from PCSX2 UI --- src/common/file_system.cpp | 12 +- src/common/gl/context_wgl.cpp | 59 +- src/common/scoped_guard.h | 13 +- src/common/string.h | 11 +- src/common/vulkan/context.cpp | 4 +- src/common/vulkan/context.h | 2 +- src/core/gpu_hw_vulkan.cpp | 2 +- src/duckstation-nogui/nogui_host.cpp | 13 + src/duckstation-nogui/nogui_host.h | 1 + .../win32_nogui_platform.cpp | 19 + src/duckstation-nogui/win32_nogui_platform.h | 2 + src/duckstation-qt/mainwindow.cpp | 77 +- src/duckstation-qt/mainwindow.h | 11 + src/duckstation-qt/qthost.cpp | 24 +- src/duckstation-qt/qthost.h | 2 + src/frontend-common/achievements.cpp | 9 +- src/frontend-common/achievements.h | 2 +- src/frontend-common/common_host.cpp | 13 +- src/frontend-common/common_host.h | 3 + src/frontend-common/dinput_source.cpp | 35 +- src/frontend-common/dinput_source.h | 6 +- src/frontend-common/fullscreen_ui.cpp | 678 ++++++++++++++---- src/frontend-common/game_list.cpp | 142 ++-- src/frontend-common/game_list.h | 7 +- src/frontend-common/imgui_fullscreen.cpp | 70 +- src/frontend-common/imgui_fullscreen.h | 15 +- src/frontend-common/imgui_overlays.cpp | 3 +- src/frontend-common/input_manager.cpp | 13 + src/frontend-common/input_manager.h | 4 + src/frontend-common/input_source.h | 1 + src/frontend-common/sdl_input_source.cpp | 7 + src/frontend-common/sdl_input_source.h | 1 + src/frontend-common/vulkan_host_display.cpp | 9 +- .../win32_raw_input_source.cpp | 5 + src/frontend-common/win32_raw_input_source.h | 1 + src/frontend-common/xinput_source.cpp | 31 + src/frontend-common/xinput_source.h | 1 + 37 files changed, 1037 insertions(+), 271 deletions(-) diff --git a/src/common/file_system.cpp b/src/common/file_system.cpp index 817d289be..213fc2c11 100644 --- a/src/common/file_system.cpp +++ b/src/common/file_system.cpp @@ -1922,14 +1922,22 @@ FileSystem::POSIXLock::POSIXLock(int fd) } else { - Log_ErrorPrintf("lockf() failed: %d", fd); + Log_ErrorPrintf("lockf() failed: %d", errno); m_fd = -1; } } FileSystem::POSIXLock::POSIXLock(std::FILE* fp) { - POSIXLock(fileno(fp)); + m_fd = fileno(fp); + if (m_fd >= 0) + { + if (lockf(m_fd, F_LOCK, 0) != 0) + { + Log_ErrorPrintf("lockf() failed: %d", errno); + m_fd = -1; + } + } } FileSystem::POSIXLock::~POSIXLock() diff --git a/src/common/gl/context_wgl.cpp b/src/common/gl/context_wgl.cpp index ebf6ed8f5..c64d74323 100644 --- a/src/common/gl/context_wgl.cpp +++ b/src/common/gl/context_wgl.cpp @@ -18,6 +18,17 @@ static void* GetProcAddressCallback(const char* name) return ::GetProcAddress(GetModuleHandleA("opengl32.dll"), name); } +static bool ReloadWGL(HDC dc) +{ + if (!gladLoadWGLLoader([](const char* name) -> void* { return wglGetProcAddress(name); }, dc)) + { + Log_ErrorPrint("Loading GLAD WGL functions failed"); + return false; + } + + return true; +} + namespace GL { ContextWGL::ContextWGL(const WindowInfo& wi) : Context(wi) {} @@ -51,8 +62,8 @@ bool ContextWGL::Initialize(const Version* versions_to_try, size_t num_versions_ } else { - Log_ErrorPrint("ContextWGL must always start with a valid surface."); - return false; + if (!CreatePBuffer()) + return false; } // Everything including core/ES requires a dummy profile to load the WGL extensions. @@ -149,8 +160,8 @@ std::unique_ptr ContextWGL::CreateSharedContext(const WindowInfo& wi) } else { - Log_ErrorPrint("PBuffer not implemented"); - return nullptr; + if (!context->CreatePBuffer()) + return nullptr; } if (m_version.profile == Profile::NoProfile) @@ -305,11 +316,37 @@ bool ContextWGL::CreatePBuffer() static constexpr const int pb_attribs[] = {0, 0}; + HGLRC temp_rc = nullptr; + ScopedGuard temp_rc_guard([&temp_rc, hdc]() { + if (temp_rc) + { + wglMakeCurrent(hdc, nullptr); + wglDeleteContext(temp_rc); + } + }); + + if (!GLAD_WGL_ARB_pbuffer) + { + // we're probably running completely surfaceless... need a temporary context. + temp_rc = wglCreateContext(hdc); + if (!temp_rc || !wglMakeCurrent(hdc, temp_rc)) + { + Log_ErrorPrint("Failed to create temporary context to load WGL for pbuffer."); + return false; + } + + if (!ReloadWGL(hdc) || !GLAD_WGL_ARB_pbuffer) + { + Log_ErrorPrint("Missing WGL_ARB_pbuffer"); + return false; + } + } + AssertMsg(m_pixel_format.has_value(), "Has pixel format for pbuffer"); HPBUFFERARB pbuffer = wglCreatePbufferARB(hdc, m_pixel_format.value(), 1, 1, pb_attribs); if (!pbuffer) { - Log_ErrorPrint("(ContextWGL::CreatePBuffer) wglCreatePbufferARB() failed"); + Log_ErrorPrintf("(ContextWGL::CreatePBuffer) wglCreatePbufferARB() failed"); return false; } @@ -318,7 +355,7 @@ bool ContextWGL::CreatePBuffer() m_dc = wglGetPbufferDCARB(pbuffer); if (!m_dc) { - Log_ErrorPrint("(ContextWGL::CreatePbuffer) wglGetPbufferDCARB() failed"); + Log_ErrorPrintf("(ContextWGL::CreatePbuffer) wglGetPbufferDCARB() failed"); return false; } @@ -326,6 +363,7 @@ bool ContextWGL::CreatePBuffer() m_dummy_dc = hdc; m_pbuffer = pbuffer; + temp_rc_guard.Run(); pbuffer_guard.Cancel(); hdc_guard.Cancel(); hwnd_guard.Cancel(); @@ -401,7 +439,7 @@ bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_contex if ((version.major_version >= 2 && !GLAD_WGL_EXT_create_context_es2_profile) || (version.major_version < 2 && !GLAD_WGL_EXT_create_context_es_profile)) { - Log_ErrorPrint("WGL_EXT_create_context_es_profile not supported"); + Log_ErrorPrintf("WGL_EXT_create_context_es_profile not supported"); return false; } @@ -437,11 +475,8 @@ bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_contex } // re-init glad-wgl - if (make_current && !gladLoadWGLLoader([](const char* name) -> void* { return wglGetProcAddress(name); }, m_dc)) - { - Log_ErrorPrint("Loading GLAD WGL functions failed"); + if (make_current && !ReloadWGL(m_dc)) return false; - } wglDeleteContext(m_rc); } @@ -449,4 +484,4 @@ bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_contex m_rc = new_rc; return true; } -} // namespace GL +} // namespace GL \ No newline at end of file diff --git a/src/common/scoped_guard.h b/src/common/scoped_guard.h index 25ab6fd49..a82242786 100644 --- a/src/common/scoped_guard.h +++ b/src/common/scoped_guard.h @@ -12,16 +12,14 @@ class ScopedGuard final public: ALWAYS_INLINE ScopedGuard(T&& func) : m_func(std::forward(func)) {} ALWAYS_INLINE ScopedGuard(ScopedGuard&& other) : m_func(std::move(other.m_func)) { other.m_func = nullptr; } - ALWAYS_INLINE ~ScopedGuard() { Invoke(); } + + ALWAYS_INLINE ~ScopedGuard() { Run(); } ScopedGuard(const ScopedGuard&) = delete; void operator=(const ScopedGuard&) = delete; - /// Prevents the function from being invoked when we go out of scope. - ALWAYS_INLINE void Cancel() { m_func.reset(); } - - /// Explicitly fires the function. - ALWAYS_INLINE void Invoke() + /// Runs the destructor function now instead of when we go out of scope. + ALWAYS_INLINE void Run() { if (!m_func.has_value()) return; @@ -30,6 +28,9 @@ public: m_func.reset(); } + /// Prevents the function from being invoked when we go out of scope. + ALWAYS_INLINE void Cancel() { m_func.reset(); } + private: std::optional m_func; }; diff --git a/src/common/string.h b/src/common/string.h index 9a344ce2e..6322ab7ed 100644 --- a/src/common/string.h +++ b/src/common/string.h @@ -241,6 +241,12 @@ public: return m_pStringData->pBuffer; } + // returns a string view for this string + std::string_view GetStringView() const + { + return IsEmpty() ? std::string_view() : std::string_view(GetCharArray(), GetLength()); + } + // creates a new string from the specified format static String FromFormat(const char* FormatString, ...) printflike(1, 2); @@ -250,10 +256,7 @@ public: // m_pStringData->pBuffer[i]; } operator const char*() const { return GetCharArray(); } operator char*() { return GetWriteableCharArray(); } - operator std::string_view() const - { - return IsEmpty() ? std::string_view() : std::string_view(GetCharArray(), GetLength()); - } + operator std::string_view() const { return GetStringView(); } // Will use the string data provided. String& operator=(const String& copyString) diff --git a/src/common/vulkan/context.cpp b/src/common/vulkan/context.cpp index fff8ff3aa..ffb51c56f 100644 --- a/src/common/vulkan/context.cpp +++ b/src/common/vulkan/context.cpp @@ -549,7 +549,7 @@ bool Vulkan::Context::CreateDevice(VkSurfaceKHR surface, bool enable_validation_ Log_ErrorPrintf("Vulkan: Failed to find an acceptable graphics queue."); return false; } - if (surface && m_present_queue_family_index == queue_family_count) + if (surface != VK_NULL_HANDLE && m_present_queue_family_index == queue_family_count) { Log_ErrorPrintf("Vulkan: Failed to find an acceptable present queue."); return false; @@ -583,7 +583,7 @@ bool Vulkan::Context::CreateDevice(VkSurfaceKHR surface, bool enable_validation_ }}; device_info.queueCreateInfoCount = 1; - if (m_graphics_queue_family_index != m_present_queue_family_index) + if (surface != VK_NULL_HANDLE && m_graphics_queue_family_index != m_present_queue_family_index) { device_info.queueCreateInfoCount = 2; } diff --git a/src/common/vulkan/context.h b/src/common/vulkan/context.h index 95783b9fc..d6b5fcd7e 100644 --- a/src/common/vulkan/context.h +++ b/src/common/vulkan/context.h @@ -25,7 +25,7 @@ class Context public: enum : u32 { - NUM_COMMAND_BUFFERS = 2 + NUM_COMMAND_BUFFERS = 3 }; struct OptionalExtensions diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp index a1b96d231..46b04f17d 100644 --- a/src/core/gpu_hw_vulkan.cpp +++ b/src/core/gpu_hw_vulkan.cpp @@ -1067,7 +1067,7 @@ bool GPU_HW_Vulkan::CompilePipelines() } } - batch_shader_guard.Invoke(); + batch_shader_guard.Run(); VkShaderModule fullscreen_quad_vertex_shader = g_vulkan_shader_cache->GetVertexShader(shadergen.GenerateScreenQuadVertexShader()); diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp index cea402fc4..025dad4a7 100644 --- a/src/duckstation-nogui/nogui_host.cpp +++ b/src/duckstation-nogui/nogui_host.cpp @@ -478,6 +478,11 @@ void NoGUIHost::PlatformWindowFocusLost() }); } +void NoGUIHost::PlatformDevicesChanged() +{ + Host::RunOnCPUThread([]() { InputManager::ReloadDevices(); }); +} + bool NoGUIHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height) { auto lock = Host::GetSettingsLock(); @@ -687,6 +692,11 @@ bool NoGUIHost::AcquireHostDisplay(RenderAPI api) return false; } + // reload input sources, since it might use the window handle + { + auto lock = Host::GetSettingsLock(); + InputManager::ReloadSources(*Host::GetSettingsInterface(), lock); + } return true; } @@ -708,6 +718,9 @@ void NoGUIHost::ReleaseHostDisplay() if (!g_host_display) return; + // close input sources, since it might use the window handle + InputManager::CloseSources(); + CommonHost::ReleaseHostDisplayResources(); ImGuiManager::Shutdown(); g_host_display.reset(); diff --git a/src/duckstation-nogui/nogui_host.h b/src/duckstation-nogui/nogui_host.h index 82abb4142..dc7565724 100644 --- a/src/duckstation-nogui/nogui_host.h +++ b/src/duckstation-nogui/nogui_host.h @@ -30,6 +30,7 @@ void ProcessPlatformKeyEvent(s32 key, bool pressed); void ProcessPlatformTextEvent(const char* text); void PlatformWindowFocusGained(); void PlatformWindowFocusLost(); +void PlatformDevicesChanged(); 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/win32_nogui_platform.cpp b/src/duckstation-nogui/win32_nogui_platform.cpp index 460d2c5b2..000516adf 100644 --- a/src/duckstation-nogui/win32_nogui_platform.cpp +++ b/src/duckstation-nogui/win32_nogui_platform.cpp @@ -9,6 +9,7 @@ #include "nogui_host.h" #include "resource.h" #include "win32_key_names.h" +#include #include #include Log_SetChannel(Win32HostInterface); @@ -122,6 +123,11 @@ bool Win32NoGUIPlatform::CreatePlatformWindow(std::string title) if (m_fullscreen.load(std::memory_order_acquire)) SetFullscreen(true); + // We use these notifications to detect when a controller is connected or disconnected. + DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE}; + m_dev_notify_handle = + RegisterDeviceNotificationW(hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); + return true; } @@ -130,6 +136,12 @@ void Win32NoGUIPlatform::DestroyPlatformWindow() if (!m_hwnd) return; + if (m_dev_notify_handle) + { + UnregisterDeviceNotification(m_dev_notify_handle); + m_dev_notify_handle = NULL; + } + RECT rc; if (!m_fullscreen.load(std::memory_order_acquire) && GetWindowRect(m_hwnd, &rc)) { @@ -393,6 +405,13 @@ LRESULT CALLBACK Win32NoGUIPlatform::WndProc(HWND hwnd, UINT msg, WPARAM wParam, } break; + case WM_DEVICECHANGE: + { + if (wParam == DBT_DEVNODES_CHANGED) + NoGUIHost::PlatformDevicesChanged(); + } + break; + case WM_FUNC: { std::function* pfunc = reinterpret_cast*>(lParam); diff --git a/src/duckstation-nogui/win32_nogui_platform.h b/src/duckstation-nogui/win32_nogui_platform.h index c58a66d39..cc4f496d0 100644 --- a/src/duckstation-nogui/win32_nogui_platform.h +++ b/src/duckstation-nogui/win32_nogui_platform.h @@ -59,4 +59,6 @@ private: std::atomic_bool m_fullscreen{false}; DWORD m_last_mouse_buttons = 0; + + HDEVNOTIFY m_dev_notify_handle = NULL; }; \ No newline at end of file diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 8fbc43b76..e03cc9410 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -24,10 +24,6 @@ #include "settingwidgetbinder.h" #include "util/cd_image.h" -#ifdef WITH_CHEEVOS -#include "frontend-common/achievements.h" -#endif - #include #include #include @@ -43,6 +39,15 @@ #include #include +#ifdef WITH_CHEEVOS +#include "frontend-common/achievements.h" +#endif + +#ifdef _WIN32 +#include "common/windows_headers.h" +#include +#endif + Log_SetChannel(MainWindow); static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( @@ -101,6 +106,10 @@ MainWindow::~MainWindow() // we compare here, since recreate destroys the window later if (g_main_window == this) g_main_window = nullptr; + +#ifdef _WIN32 + unregisterForDeviceNotifications(); +#endif } void MainWindow::updateApplicationTheme() @@ -129,6 +138,10 @@ void MainWindow::initialize() if (Achievements::IsUsingRAIntegration()) Achievements::RAIntegration::MainWindowChanged((void*)winId()); #endif + +#ifdef _WIN32 + registerForDeviceNotifications(); +#endif } void MainWindow::reportError(const QString& title, const QString& message) @@ -143,6 +156,48 @@ bool MainWindow::confirmMessage(const QString& title, const QString& message) return (QMessageBox::question(this, title, message) == QMessageBox::Yes); } +void MainWindow::registerForDeviceNotifications() +{ +#ifdef _WIN32 + // We use these notifications to detect when a controller is connected or disconnected. + DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE}; + m_device_notification_handle = RegisterDeviceNotificationW( + (HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); +#endif +} + +void MainWindow::unregisterForDeviceNotifications() +{ +#ifdef _WIN32 + if (!m_device_notification_handle) + return; + + UnregisterDeviceNotification(static_cast(m_device_notification_handle)); + m_device_notification_handle = nullptr; +#endif +} + +#ifdef _WIN32 + +bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result) +{ + static constexpr const char win_type[] = "windows_generic_MSG"; + if (eventType == QByteArray(win_type, sizeof(win_type) - 1)) + { + const MSG* msg = static_cast(message); + if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED) + { + g_emu_thread->reloadInputDevices(); + *result = 1; + return true; + } + } + + return QMainWindow::nativeEvent(eventType, message, result); +} + +#endif + bool MainWindow::createDisplay(bool fullscreen, bool render_to_main) { Log_DevPrintf("createDisplay(%u, %u)", static_cast(fullscreen), static_cast(render_to_main)); @@ -650,6 +705,9 @@ void MainWindow::recreate() if (s_system_valid) requestShutdown(false, true, true); + // We need to close input sources, because e.g. DInput uses our window handle. + g_emu_thread->closeInputSources(); + close(); g_main_window = nullptr; @@ -657,6 +715,9 @@ void MainWindow::recreate() new_main_window->initialize(); new_main_window->show(); deleteLater(); + + // Reload the sources we just closed. + g_emu_thread->reloadInputSources(); } void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu) @@ -1658,7 +1719,7 @@ void MainWindow::updateWindowState(bool force_visible) return; const bool hide_window = !isRenderingToMain() && shouldHideMainWindow(); - const bool disable_resize = Host::GetBaseBoolSettingValue("Main", "DisableWindowResize", false); + const bool disable_resize = Host::GetBoolSettingValue("Main", "DisableWindowResize", false); const bool has_window = s_system_valid || m_display_widget; // Need to test both valid and display widget because of startup (vm invalid while window is created). @@ -1730,8 +1791,8 @@ bool MainWindow::shouldHideMouseCursor() const bool MainWindow::shouldHideMainWindow() const { - return Host::GetBaseBoolSettingValue("Main", "HideMainWindowWhenRunning", false) || isRenderingFullscreen() || - QtHost::InNoGUIMode(); + return Host::GetBaseBoolSettingValue("Main", "HideMainWindowWhenRunning", false) || + (g_emu_thread->shouldRenderToMain() && isRenderingFullscreen()) || QtHost::InNoGUIMode(); } void MainWindow::switchToGameListView() @@ -2412,7 +2473,7 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav // reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches // would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in // batch mode, when we're going to exit anyway. - if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode()) + if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode() && !g_emu_thread->isRunningFullscreenUI()) updateWindowState(true); // Now we can actually shut down the VM. diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index be3760c02..6c16cb7ac 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -169,6 +169,10 @@ protected: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; +#ifdef _WIN32 + bool nativeEvent(const QByteArray& eventType, void* message, qintptr* result) override; +#endif + private: static void setStyleFromSettings(); static void setIconThemeFromSettings(); @@ -218,6 +222,9 @@ private: void setTheme(const QString& theme); void recreate(); + void registerForDeviceNotifications(); + void unregisterForDeviceNotifications(); + /// Fills menu with save state info and handlers. void populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu); @@ -271,6 +278,10 @@ private: bool m_is_closing = false; GDBServer* m_gdb_server = nullptr; + +#ifdef _WIN32 + void* m_device_notification_handle = nullptr; +#endif }; extern MainWindow* g_main_window; diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index f7f5c5a81..45d504962 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -376,7 +376,7 @@ void QtHost::SetDefaultSettings(SettingsInterface& si, bool system, bool control bool EmuThread::shouldRenderToMain() const { - return !Host::GetBaseBoolSettingValue("Main", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode(); + return !Host::GetBoolSettingValue("Main", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode(); } void Host::RequestResizeHostDisplay(s32 new_window_width, s32 new_window_height) @@ -895,6 +895,28 @@ void EmuThread::reloadInputBindings() InputManager::ReloadBindings(*si, *bindings_si); } +void EmuThread::reloadInputDevices() +{ + if (!isOnThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::QueuedConnection); + return; + } + + InputManager::ReloadDevices(); +} + +void EmuThread::closeInputSources() +{ + if (!isOnThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::BlockingQueuedConnection); + return; + } + + InputManager::CloseSources(); +} + void EmuThread::enumerateInputDevices() { if (!isOnThread()) diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index b506f9122..d73ffde55 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -148,6 +148,8 @@ public Q_SLOTS: void updateEmuFolders(); void reloadInputSources(); void reloadInputBindings(); + void reloadInputDevices(); + void closeInputSources(); void enumerateInputDevices(); void enumerateVibrationMotors(); void startFullscreenUI(); diff --git a/src/frontend-common/achievements.cpp b/src/frontend-common/achievements.cpp index d568dee69..1cbc0d570 100644 --- a/src/frontend-common/achievements.cpp +++ b/src/frontend-common/achievements.cpp @@ -478,7 +478,7 @@ void Achievements::UpdateSettings(const Settings& old_config) if (!g_settings.achievements_enabled) { // we're done here - OnSystemShutdown(); + Shutdown(); return; } @@ -510,7 +510,7 @@ void Achievements::UpdateSettings(const Settings& old_config) g_settings.achievements_use_first_disc_from_playlist != old_config.achievements_use_first_disc_from_playlist || g_settings.achievements_rich_presence != old_config.achievements_rich_presence) { - OnSystemShutdown(); + Shutdown(); Initialize(); return; } @@ -603,14 +603,11 @@ void Achievements::SetChallengeMode(bool enabled) GetUserUnlocks(); } -bool Achievements::OnSystemShutdown() +bool Achievements::Shutdown() { #ifdef WITH_RAINTEGRATION if (IsUsingRAIntegration()) { - if (!RA_ConfirmLoadNewRom(true)) - return false; - RA_SetPaused(false); RA_ActivateGame(0); return true; diff --git a/src/frontend-common/achievements.h b/src/frontend-common/achievements.h index ca64b89c4..34405c8a4 100644 --- a/src/frontend-common/achievements.h +++ b/src/frontend-common/achievements.h @@ -91,7 +91,7 @@ void UpdateSettings(const Settings& old_config); bool ConfirmSystemReset(); /// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted. -bool OnSystemShutdown(); +bool Shutdown(); /// Called when the system is being paused and resumed. void OnSystemPaused(bool paused); diff --git a/src/frontend-common/common_host.cpp b/src/frontend-common/common_host.cpp index 901902654..2146e4476 100644 --- a/src/frontend-common/common_host.cpp +++ b/src/frontend-common/common_host.cpp @@ -32,8 +32,8 @@ #include "imgui_fullscreen.h" #include "imgui_manager.h" #include "imgui_overlays.h" -#include "platform_misc.h" #include "input_manager.h" +#include "platform_misc.h" #include "scmversion/scmversion.h" #include "util/audio_stream.h" #include "util/ini_settings_interface.h" @@ -116,7 +116,7 @@ void CommonHost::Shutdown() #endif #ifdef WITH_CHEEVOS - Achievements::OnSystemShutdown(); + Achievements::Shutdown(); #endif InputManager::CloseSources(); @@ -391,7 +391,8 @@ void CommonHost::UpdateSessionTime(const std::string& new_serial) if (!s_session_serial.empty()) { // round up to seconds - const std::time_t etime = static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); + const std::time_t etime = + static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); const std::time_t wtime = std::time(nullptr); GameList::AddPlayedTimeForSerial(s_session_serial, wtime, etime); } @@ -400,6 +401,12 @@ void CommonHost::UpdateSessionTime(const std::string& new_serial) s_session_start_time = ctime; } +u64 CommonHost::GetSessionPlayedTime() +{ + const u64 ctime = Common::Timer::GetCurrentValue(); + return static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); +} + void Host::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity) { InputManager::SetPadVibrationIntensity(pad_index, large_or_single_motor_intensity, small_motor_intensity); diff --git a/src/frontend-common/common_host.h b/src/frontend-common/common_host.h index 41747ff8e..6bb941724 100644 --- a/src/frontend-common/common_host.h +++ b/src/frontend-common/common_host.h @@ -32,6 +32,9 @@ void PumpMessagesOnCPUThread(); bool CreateHostDisplayResources(); void ReleaseHostDisplayResources(); +/// Returns the time elapsed in the current play session. +u64 GetSessionPlayedTime(); + #ifdef WITH_CUBEB std::unique_ptr CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, AudioStretchMode stretch); diff --git a/src/frontend-common/dinput_source.cpp b/src/frontend-common/dinput_source.cpp index 19bdba2d8..8bcaeb07e 100644 --- a/src/frontend-common/dinput_source.cpp +++ b/src/frontend-common/dinput_source.cpp @@ -85,10 +85,10 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock(Host::GetTopLevelWindowHandle()); - AddDevices(toplevel_window); + m_toplevel_window = static_cast(Host::GetTopLevelWindowHandle()); settings_lock.lock(); + ReloadDevices(); return true; } @@ -112,16 +112,29 @@ static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef) return DIENUM_CONTINUE; } -void DInputSource::AddDevices(HWND toplevel_window) +bool DInputSource::ReloadDevices() { + // detect any removals + PollEvents(); + std::vector devices; m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY); - Log_InfoPrintf("Enumerated %zu devices", devices.size()); + Log_VerbosePrintf("Enumerated %zu devices", devices.size()); + bool changed = false; for (DIDEVICEINSTANCEW inst : devices) { + // do we already have this one? + if (std::any_of(m_controllers.begin(), m_controllers.end(), + [&inst](const ControllerData& cd) { return inst.guidInstance == cd.guid; })) + { + // yup, so skip it + continue; + } + ControllerData cd; + cd.guid = inst.guidInstance; HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr); if (FAILED(hr)) { @@ -130,21 +143,24 @@ void DInputSource::AddDevices(HWND toplevel_window) } const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName)); - if (AddDevice(cd, toplevel_window, name)) + if (AddDevice(cd, name)) { const u32 index = static_cast(m_controllers.size()); m_controllers.push_back(std::move(cd)); Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name); + changed = true; } } + + return changed; } -bool DInputSource::AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name) +bool DInputSource::AddDevice(ControllerData& cd, const std::string& name) { - HRESULT hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); + HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); if (FAILED(hr)) { - hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); + hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); if (FAILED(hr)) { Log_ErrorPrintf("Failed to set cooperative level for '%s'", name.c_str()); @@ -225,9 +241,6 @@ void DInputSource::PollEvents() for (size_t i = 0; i < m_controllers.size();) { ControllerData& cd = m_controllers[i]; - if (!cd.device) - continue; - if (cd.needs_poll) cd.device->Poll(); diff --git a/src/frontend-common/dinput_source.h b/src/frontend-common/dinput_source.h index c8682ca6e..497f0736e 100644 --- a/src/frontend-common/dinput_source.h +++ b/src/frontend-common/dinput_source.h @@ -32,6 +32,7 @@ public: bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override; @@ -54,6 +55,7 @@ private: { ComPtr device; DIJOYSTATE last_state = {}; + GUID guid = {}; std::vector axis_offsets; u32 num_buttons = 0; @@ -68,8 +70,7 @@ private: static std::array GetHatButtons(DWORD hat); static std::string GetDeviceIdentifier(u32 index); - void AddDevices(HWND toplevel_window); - bool AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name); + bool AddDevice(ControllerData& cd, const std::string& name); void CheckForStateChanges(size_t index, const DIJOYSTATE& new_state); @@ -78,4 +79,5 @@ private: HMODULE m_dinput_module{}; LPCDIDATAFORMAT m_joystick_data_format{}; ComPtr m_dinput; + HWND m_toplevel_window = NULL; }; diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 1fc7b8c1d..eb55146c7 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -85,6 +85,7 @@ using ImGuiFullscreen::CenterImage; using ImGuiFullscreen::CloseChoiceDialog; using ImGuiFullscreen::CloseFileSelector; using ImGuiFullscreen::DPIScale; +using ImGuiFullscreen::DrawShadowedText; using ImGuiFullscreen::EndFullscreenColumns; using ImGuiFullscreen::EndFullscreenColumnWindow; using ImGuiFullscreen::EndFullscreenWindow; @@ -308,6 +309,11 @@ static void DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const const char* format = "%d", bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +static void DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, int default_value, int min_value, int max_value, int step_value, + const char* format = "%d", bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); static void DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, float default_value, float min_value, float max_value, const char* format = "%f", float multiplier = 1.0f, bool enabled = true, @@ -395,7 +401,7 @@ static u32 PopulateSaveStateListEntries(const std::string& title, const std::str static bool OpenLoadStateSelectorForGame(const std::string& game_path); static bool OpenSaveStateSelector(bool is_loading); static void CloseSaveStateSelector(); -static void DrawSaveStateSelector(bool is_loading, bool fullscreen); +static void DrawSaveStateSelector(bool is_loading); static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry); static void DrawResumeStateSelector(); static void DoLoadState(std::string path); @@ -403,6 +409,7 @@ static void DoSaveState(s32 slot, bool global); static std::vector s_save_state_selector_slots; static std::string s_save_state_selector_game_path; +static s32 s_save_state_selector_submenu_index = -1; static bool s_save_state_selector_open = false; static bool s_save_state_selector_loading = true; static bool s_save_state_selector_resuming = false; @@ -773,7 +780,7 @@ void FullscreenUI::Render() if (s_save_state_selector_resuming) DrawResumeStateSelector(); else - DrawSaveStateSelector(s_save_state_selector_loading, false); + DrawSaveStateSelector(s_save_state_selector_loading); } if (s_about_window_open) @@ -1748,6 +1755,133 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, ImGui::PopFont(); } +void FullscreenUI::DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, + const char* section, const char* key, int default_value, int min_value, + int max_value, int step_value, const char* format, bool enabled, float height, + ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value = + bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional(default_value)); + TinyString value_text; + if (value.has_value()) + value_text.Format(format, value.value()); + else + value_text = "Use Global Setting"; + + static bool manual_input = false; + static u32 repeat_count = 0; + + if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font)) + { + ImGui::OpenPopup(title); + manual_input = false; + } + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + bool is_open = true; + if (ImGui::BeginPopupModal(title, &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + BeginMenuButtons(); + + s32 dlg_value = static_cast(value.value_or(default_value)); + bool dlg_value_changed = false; + + char str_value[32]; + std::snprintf(str_value, std::size(str_value), format, dlg_value); + + if (manual_input) + { + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); + + std::snprintf(str_value, std::size(str_value), "%d", dlg_value); + if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal)) + { + dlg_value = StringUtil::FromChars(str_value).value_or(dlg_value); + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + } + else + { + const ImVec2& padding(ImGui::GetStyle().FramePadding); + ImVec2 button_pos(ImGui::GetCursorPos()); + + // Align value text in middle. + ImGui::SetCursorPosY( + button_pos.y + + ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - g_large_font->FontSize) * 0.5f); + ImGui::TextUnformatted(str_value); + + s32 step = 0; + if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font, + &button_pos, true)) + { + step = step_value; + } + if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + g_large_font, &button_pos, true)) + { + step = -step_value; + } + if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + g_large_font, &button_pos)) + { + manual_input = true; + } + if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + g_large_font, &button_pos)) + { + dlg_value = default_value; + dlg_value_changed = true; + } + + if (step != 0) + { + dlg_value += step; + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) + + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f)); + } + + if (dlg_value_changed) + { + dlg_value = std::clamp(dlg_value, min_value, max_value); + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetIntValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f))) + { + ImGui::CloseCurrentPopup(); + } + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(4); + ImGui::PopFont(); +} + void FullscreenUI::DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, const char* default_value, const char* const* options, const char* const* option_values, @@ -1955,6 +2089,7 @@ void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, const char* title, SetSettingsChanged(bsi); // Host::RunOnCPUThread(&Host::Internal::UpdateEmuFolders); + s_cover_image_map.clear(); CloseFileSelector(); }); @@ -2368,8 +2503,9 @@ void FullscreenUI::DrawInterfaceSettingsPage() #endif MenuHeading("On-Screen Display"); - DrawIntRangeSetting(bsi, ICON_FA_SEARCH " OSD Scale", "Determines how large the on-screen messages and monitor are.", - "Display", "OSDScale", 100, 25, 500, "%d%%"); + DrawIntSpinBoxSetting(bsi, ICON_FA_SEARCH " OSD Scale", + "Determines how large the on-screen messages and monitor are.", "Display", "OSDScale", 100, 25, + 500, 1, "%d%%"); DrawToggleSetting(bsi, ICON_FA_LIST " Show OSD Messages", "Shows on-screen-display messages when events occur.", "Display", "ShowOSDMessages", true); DrawToggleSetting( @@ -4200,6 +4336,8 @@ void FullscreenUI::DrawAdvancedSettingsPage() void FullscreenUI::DrawPauseMenu(MainWindowType type) { + SmallString buffer; + ImDrawList* dl = ImGui::GetBackgroundDrawList(); const ImVec2 display_size(ImGui::GetIO().DisplaySize); dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size, IM_COL32(0x21, 0x21, 0x21, 200)); @@ -4209,15 +4347,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) const std::string& title = System::GetRunningTitle(); const std::string& serial = System::GetRunningSerial(); - SmallString subtitle; if (!serial.empty()) - subtitle.Format("%s - ", serial.c_str()); - subtitle.AppendString(Path::GetFileName(System::GetRunningPath())); + buffer.Format("%s - ", serial.c_str()); + buffer.AppendString(Path::GetFileName(System::GetRunningPath())); const ImVec2 title_size( g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, title.c_str())); const ImVec2 subtitle_size( - g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), -1.0f, subtitle)); + g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), -1.0f, buffer)); ImVec2 title_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - title_size.x, display_size.y - LayoutScale(20.0f + 50.0f)); @@ -4244,14 +4381,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) subtitle_pos.x -= rp_height; subtitle_pos.y -= rp_height; - dl->AddText(g_medium_font, g_medium_font->FontSize, rp_pos, IM_COL32(255, 255, 255, 255), rp.data(), - rp.data() + rp.size(), wrap_width); + DrawShadowedText(dl, g_medium_font, rp_pos, IM_COL32(255, 255, 255, 255), rp.data(), rp.data() + rp.size(), + wrap_width); } } #endif - dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), title.c_str()); - dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), subtitle); + DrawShadowedText(dl, g_large_font, title_pos, IM_COL32(255, 255, 255, 255), title.c_str()); + DrawShadowedText(dl, g_medium_font, subtitle_pos, IM_COL32(255, 255, 255, 255), buffer); const ImVec2 image_min(display_size.x - LayoutScale(20.0f + 50.0f) - rp_height, display_size.y - LayoutScale(20.0f + 50.0f) - rp_height); @@ -4259,6 +4396,43 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) dl->AddImage(GetCoverForCurrentGame(), image_min, image_max); } + // current time / play time + { + buffer.Fmt("{:%X}", fmt::localtime(std::time(nullptr))); + + const ImVec2 time_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, + buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength())); + const ImVec2 time_pos(display_size.x - LayoutScale(10.0f) - time_size.x, LayoutScale(10.0f)); + DrawShadowedText(dl, g_large_font, time_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength()); + + const std::string& serial = System::GetRunningSerial(); + if (!serial.empty()) + { + const std::time_t cached_played_time = GameList::GetCachedPlayedTimeForSerial(serial); + const std::time_t session_time = static_cast(CommonHost::GetSessionPlayedTime()); + + buffer.Fmt("Session: {}", GameList::FormatTimespan(session_time, true).GetStringView()); + const ImVec2 session_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), + -1.0f, buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength())); + const ImVec2 session_pos(display_size.x - LayoutScale(10.0f) - session_size.x, + time_pos.y + g_large_font->FontSize + LayoutScale(4.0f)); + DrawShadowedText(dl, g_medium_font, session_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength()); + + buffer.Fmt("All Time: {}", GameList::FormatTimespan(cached_played_time + session_time, true).GetStringView()); + const ImVec2 total_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), + -1.0f, buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength())); + const ImVec2 total_pos(display_size.x - LayoutScale(10.0f) - total_size.x, + session_pos.y + g_medium_font->FontSize + LayoutScale(4.0f)); + DrawShadowedText(dl, g_medium_font, total_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(), + buffer.GetCharArray() + buffer.GetLength()); + } + } + const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT)); const ImVec2 window_pos(0.0f, display_size.y - window_size.y); @@ -4420,12 +4594,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, const std::string& serial, s32 slot, bool global) { - li->title = fmt::format("{0} {1} Slot {2}##{1}_slot_{2}", title, global ? "Global" : "Game", slot); - li->summary = "No Save State"; + li->title = (global || slot > 0) ? fmt::format("{0} Slot {1}##{0}_slot_{1}", global ? "Global" : "Game", slot) : + std::string("Quick Save"); + li->summary = "No save present in this slot."; li->path = {}; li->timestamp = 0; li->slot = slot; li->preview_texture = {}; + li->global = global; } bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const std::string& title, @@ -4442,17 +4618,18 @@ bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const st if (global) { - li->title = StringUtil::StdStringFromFormat("Global Save %d - %s##global_slot_%d", slot, ssi->title.c_str(), slot); + li->title = fmt::format("Global Slot {0} - {1}##global_slot_{0}", slot, ssi->serial); } else { - li->title = StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", ssi->title.c_str(), slot, slot); + li->title = (slot > 0) ? fmt::format("Game Slot {0}##game_slot_{0}", slot) : std::string("Game Quick Save"); } - li->summary = fmt::format("{} - Saved {:%c}", ssi->serial.c_str(), fmt::localtime(ssi->timestamp)); + li->summary = fmt::format("Saved {:%c}", fmt::localtime(ssi->timestamp)); li->timestamp = ssi->timestamp; li->slot = slot; li->path = std::move(filename); + li->global = global; PopulateSaveStateScreenshot(li, &ssi.value()); return true; @@ -4571,125 +4748,297 @@ void FullscreenUI::CloseSaveStateSelector() ReturnToMainWindow(); } -void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) +void FullscreenUI::DrawSaveStateSelector(bool is_loading) { - if (fullscreen) + ImGuiIO& io = ImGui::GetIO(); + + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); + ImGui::SetNextWindowSize(io.DisplaySize); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f); + + const char* window_title = is_loading ? "Load State" : "Save State"; + ImGui::OpenPopup(window_title); + + bool is_open = true; + const bool valid = + ImGui::BeginPopupModal(window_title, &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoBackground); + if (!valid || !is_open) { - if (!BeginFullscreenColumns()) - { - EndFullscreenColumns(); - return; - } + if (valid) + ImGui::EndPopup(); - if (!BeginFullscreenColumnWindow(0.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots")) - { - EndFullscreenColumnWindow(); - EndFullscreenColumns(); - return; - } - } - else - { - const char* window_title = is_loading ? "Load State" : "Save State"; - - ImGui::PushFont(g_large_font); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, - ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); - - ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(window_title); - bool is_open = !WantsToCloseMenu(); - if (!ImGui::BeginPopupModal(window_title, &is_open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove) || - !is_open) - { - ImGui::PopStyleVar(2); - ImGui::PopFont(); + ImGui::PopStyleVar(5); + if (!is_open) CloseSaveStateSelector(); - return; - } + return; } - BeginMenuButtons(); + ImVec2 heading_size = ImVec2( + io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f)); - static constexpr float padding = 10.0f; - static constexpr float button_height = 96.0f; - static constexpr float max_image_width = 96.0f; - static constexpr float max_image_height = 96.0f; + ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIPrimaryColor, 0.9f)); - for (const SaveStateListEntry& entry : s_save_state_selector_slots) + if (ImGui::BeginChild("state_titlebar", heading_size, false, ImGuiWindowFlags_NoNav)) { - ImRect bb; - bool visible, hovered; - bool pressed = MenuButtonFrame(entry.title.c_str(), true, button_height, &visible, &hovered, &bb.Min, &bb.Max); - if (!visible) - continue; + BeginNavBar(); + if (NavButton(ICON_FA_BACKWARD, true, true)) + CloseSaveStateSelector(); - ImVec2 pos(bb.Min); + NavTitle(is_loading ? "Load State" : "Save State"); + EndNavBar(); + ImGui::EndChild(); + } - // use aspect ratio of screenshot to determine height - const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); - const float image_height = - max_image_width / (static_cast(image->GetWidth()) / static_cast(image->GetHeight())); - const float image_margin = (max_image_height - image_height) / 2.0f; - const ImRect image_bb(ImVec2(pos.x, pos.y + LayoutScale(image_margin)), - pos + LayoutScale(max_image_width, image_margin + image_height)); - pos.x += LayoutScale(max_image_width + padding); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIBackgroundColor, 0.9f)); + ImGui::SetCursorPos(ImVec2(0.0f, heading_size.y)); - ImRect text_bb(pos, ImVec2(bb.Max.x, pos.y + g_large_font->FontSize)); - ImGui::PushFont(g_large_font); - ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), - &text_bb); - ImGui::PopFont(); + bool close_handled = false; + if (s_save_state_selector_open && + ImGui::BeginChild("state_list", ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), false, + ImGuiWindowFlags_NavFlattened)) + { + BeginMenuButtons(); - ImGui::PushFont(g_medium_font); + const ImGuiStyle& style = ImGui::GetStyle(); - if (!entry.summary.empty()) + const float title_spacing = LayoutScale(10.0f); + const float summary_spacing = LayoutScale(4.0f); + const float item_spacing = LayoutScale(20.0f); + const float item_width_with_spacing = std::floor(LayoutScale(LAYOUT_SCREEN_WIDTH / 4.0f)); + const float item_width = item_width_with_spacing - item_spacing; + const float image_width = item_width - (style.FramePadding.x * 2.0f); + const float image_height = image_width / 1.33f; + const ImVec2 image_size(image_width, image_height); + const float item_height = (style.FramePadding.y * 2.0f) + image_height + title_spacing + g_large_font->FontSize + + summary_spacing + g_medium_font->FontSize; + const ImVec2 item_size(item_width, item_height); + const u32 grid_count_x = static_cast(std::floor(ImGui::GetWindowWidth() / item_width_with_spacing)); + const float start_x = + (static_cast(ImGui::GetWindowWidth()) - (item_width_with_spacing * static_cast(grid_count_x))) * + 0.5f; + + u32 grid_x = 0; + u32 grid_y = 0; + ImGui::SetCursorPos(ImVec2(start_x, 0.0f)); + for (u32 i = 0; i < s_save_state_selector_slots.size(); i++) { - text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); - text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; - ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), - &text_bb); - } + if (i == 0) + ResetFocusHere(); - if (!entry.path.empty()) - { - text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); - text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; - ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), - &text_bb); - } + const SaveStateListEntry& entry = s_save_state_selector_slots[i]; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + continue; - ImGui::PopFont(); + const ImGuiID id = window->GetID(static_cast(i)); + const ImVec2 pos(window->DC.CursorPos); + ImRect bb(pos, pos + item_size); + ImGui::ItemSize(item_size); + if (ImGui::ItemAdd(bb, id)) + { + bool held; + bool hovered; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); + if (hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); - ImGui::GetWindowDrawList()->AddImage( - static_cast(entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get()), - image_bb.Min, image_bb.Max); + const float t = std::min(static_cast(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t)); - if (pressed) - { - if (is_loading) - DoLoadState(entry.path); + ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + + ImGui::PopStyleColor(); + } + + bb.Min += style.FramePadding; + bb.Max -= style.FramePadding; + + GPUTexture* const screenshot = + entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); + const ImRect image_rect( + CenterImage(ImRect(bb.Min, bb.Min + image_size), + ImVec2(static_cast(screenshot->GetWidth()), static_cast(screenshot->GetHeight())))); + + ImGui::GetWindowDrawList()->AddImage(screenshot, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255)); + + const ImVec2 title_pos(bb.Min.x, bb.Min.y + image_height + title_spacing); + const ImRect title_bb(title_pos, ImVec2(bb.Max.x, title_pos.y + g_large_font->FontSize)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), + &title_bb); + ImGui::PopFont(); + + if (!entry.summary.empty()) + { + const ImVec2 summary_pos(bb.Min.x, title_pos.y + g_large_font->FontSize + summary_spacing); + const ImRect summary_bb(summary_pos, ImVec2(bb.Max.x, summary_pos.y + g_medium_font->FontSize)); + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, entry.summary.c_str(), nullptr, nullptr, + ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (pressed) + { + if (is_loading) + { + DoLoadState(entry.path); + CloseSaveStateSelector(); + break; + } + else + { + DoSaveState(entry.slot, entry.global); + CloseSaveStateSelector(); + break; + } + } + + if (hovered && (ImGui::IsItemClicked(ImGuiMouseButton_Right) || + ImGui::IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed))) + { + s_save_state_selector_submenu_index = static_cast(i); + } + + if (static_cast(i) == s_save_state_selector_submenu_index) + { + // can't use a choice dialog here, because we're already in a modal... + ImGuiFullscreen::PushResetLayout(); + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f)); + + const float width = LayoutScale(600.0f); + const float title_height = + g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f; + const float height = + title_height + + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * 3.0f; + ImGui::SetNextWindowSize(ImVec2(width, height)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(entry.title.c_str()); + + // don't let the back button flow through to the main window + bool submenu_open = !WantsToCloseMenu(); + close_handled ^= submenu_open; + + bool closed = false; + if (ImGui::BeginPopupModal(entry.title.c_str(), &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); + + BeginMenuButtons(); + + if (ActiveButton(is_loading ? ICON_FA_FOLDER_OPEN " Load State" : ICON_FA_FOLDER_OPEN " Save State", false, + true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + if (is_loading) + DoLoadState(std::move(entry.path)); + else + DoSaveState(entry.slot, entry.global); + + CloseSaveStateSelector(); + closed = true; + } + + if (ActiveButton(ICON_FA_FOLDER_MINUS " Delete Save", false, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + if (!FileSystem::FileExists(entry.path.c_str())) + { + ShowToast({}, fmt::format("{} does not exist.", ImGuiFullscreen::RemoveHash(entry.title))); + is_open = true; + } + else if (FileSystem::DeleteFile(entry.path.c_str())) + { + ShowToast({}, fmt::format("{} deleted.", ImGuiFullscreen::RemoveHash(entry.title))); + s_save_state_selector_slots.erase(s_save_state_selector_slots.begin() + i); + + if (s_save_state_selector_slots.empty()) + { + CloseSaveStateSelector(); + closed = true; + } + else + { + is_open = false; + } + } + else + { + ShowToast({}, fmt::format("Failed to delete {}.", ImGuiFullscreen::RemoveHash(entry.title))); + is_open = false; + } + } + + if (ActiveButton(ICON_FA_WINDOW_CLOSE " Close Menu", false, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + is_open = false; + } + + EndMenuButtons(); + + ImGui::PopStyleColor(); + ImGui::EndPopup(); + } + if (!is_open) + { + s_save_state_selector_submenu_index = -1; + if (!closed) + QueueResetFocus(); + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(3); + ImGui::PopFont(); + ImGuiFullscreen::PopResetLayout(); + + if (closed) + break; + } + } + + grid_x++; + if (grid_x == grid_count_x) + { + grid_x = 0; + grid_y++; + ImGui::SetCursorPosX(start_x); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_spacing); + } else - DoSaveState(entry.slot, entry.global); + { + ImGui::SameLine(start_x + static_cast(grid_x) * (item_width + item_spacing)); + } } + + EndMenuButtons(); + ImGui::EndChild(); } - EndMenuButtons(); + ImGui::PopStyleColor(); - if (fullscreen) - { - EndFullscreenColumnWindow(); - EndFullscreenColumns(); - } - else - { - ImGui::EndPopup(); - ImGui::PopStyleVar(2); - ImGui::PopFont(); - } + ImGui::EndPopup(); + ImGui::PopStyleVar(5); + + if (!close_handled && WantsToCloseMenu()) + CloseSaveStateSelector(); } bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry) @@ -4821,15 +5170,79 @@ void FullscreenUI::DoSaveState(s32 slot, bool global) void FullscreenUI::PopulateGameListEntryList() { + const s32 sort = Host::GetBaseIntSettingValue("Main", "FullscreenUIGameSort", 0); + const bool reverse = Host::GetBaseBoolSettingValue("Main", "FullscreenUIGameSortReverse", false); + const u32 count = GameList::GetEntryCount(); s_game_list_sorted_entries.resize(count); for (u32 i = 0; i < count; i++) s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i); - // TODO: Custom sort types std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(), - [](const GameList::Entry* lhs, const GameList::Entry* rhs) { - return StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str()) < 0; + [sort, reverse](const GameList::Entry* lhs, const GameList::Entry* rhs) { + switch (sort) + { + case 0: // Type + { + if (lhs->type != rhs->type) + return reverse ? (lhs->type > rhs->type) : (lhs->type < rhs->type); + } + break; + + case 1: // Serial + { + if (lhs->serial != rhs->serial) + return reverse ? (lhs->serial > rhs->serial) : (lhs->serial < rhs->serial); + } + break; + + case 2: // Title + break; + + case 3: // File Title + { + const std::string_view lhs_title(Path::GetFileTitle(lhs->path)); + const std::string_view rhs_title(Path::GetFileTitle(rhs->path)); + const int res = StringUtil::Strncasecmp(lhs_title.data(), rhs_title.data(), + std::min(lhs_title.size(), rhs_title.size())); + if (res != 0) + return reverse ? (res > 0) : (res < 0); + } + break; + + case 4: // Time Played + { + if (lhs->total_played_time != rhs->total_played_time) + { + return reverse ? (lhs->total_played_time > rhs->total_played_time) : + (lhs->total_played_time < rhs->total_played_time); + } + } + break; + + case 5: // Last Played (reversed by default) + { + if (lhs->last_played_time != rhs->last_played_time) + { + return reverse ? (lhs->last_played_time < rhs->last_played_time) : + (lhs->last_played_time > rhs->last_played_time); + } + } + break; + + case 6: // Size + { + if (lhs->total_size != rhs->total_size) + { + return reverse ? (lhs->total_size > rhs->total_size) : (lhs->total_size < rhs->total_size); + } + } + break; + } + + // fallback to title when all else is equal + const int res = StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str()); + return reverse ? (res > 0) : (res < 0); }); } @@ -5371,20 +5784,35 @@ void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size) } } - static constexpr const char* view_types[] = {"Game Grid", "Game List"}; + MenuHeading("List Settings"); + { + static constexpr const char* view_types[] = {"Game Grid", "Game List"}; + static constexpr const char* sort_types[] = {"Type", "Serial", "Title", "File Title", + "Time Played", "Last Played", "Size"}; + + DrawIntListSetting(bsi, ICON_FA_BORDER_ALL " Default View", "Sets which view the game list will open to.", "Main", + "DefaultFullscreenUIGameView", 0, view_types, std::size(view_types)); + DrawIntListSetting(bsi, ICON_FA_SORT " Sort By", "Determines which field the game list will be sorted by.", "Main", + "FullscreenUIGameSort", 0, sort_types, std::size(sort_types)); + DrawToggleSetting(bsi, ICON_FA_SORT_ALPHA_DOWN " Sort Reversed", + "Reverses the game list sort order from the default (usually ascending to descending).", "Main", + "FullscreenUIGameSortReverse", false); + } MenuHeading("Cover Settings"); - DrawFolderSetting(bsi, ICON_FA_FOLDER " Covers Directory", "Folders", "Covers", EmuFolders::Covers); - if (MenuButton(ICON_FA_DOWNLOAD " Download Covers", "Downloads covers from a user-specified URL template.")) - ImGui::OpenPopup("Download Covers"); - DrawIntListSetting(bsi, ICON_FA_BORDER_ALL " Default View", "Sets which view the game list will open to.", "Main", - "DefaultFullscreenUIGameView", 0, view_types, std::size(view_types)); + { + DrawFolderSetting(bsi, ICON_FA_FOLDER " Covers Directory", "Folders", "Covers", EmuFolders::Covers); + if (MenuButton(ICON_FA_DOWNLOAD " Download Covers", "Downloads covers from a user-specified URL template.")) + ImGui::OpenPopup("Download Covers"); + } MenuHeading("Operations"); - if (MenuButton(ICON_FA_SEARCH " Scan For New Games", "Identifies any new files added to the game directories.")) - Host::RefreshGameListAsync(false); - if (MenuButton(ICON_FA_SEARCH_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) - Host::RefreshGameListAsync(true); + { + if (MenuButton(ICON_FA_SEARCH " Scan For New Games", "Identifies any new files added to the game directories.")) + Host::RefreshGameListAsync(false); + if (MenuButton(ICON_FA_SEARCH_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) + Host::RefreshGameListAsync(true); + } EndMenuButtons(); diff --git a/src/frontend-common/game_list.cpp b/src/frontend-common/game_list.cpp index 0ae2a683f..80878423f 100644 --- a/src/frontend-common/game_list.cpp +++ b/src/frontend-common/game_list.cpp @@ -82,8 +82,8 @@ static PlayedTimeEntry UpdatePlayedTimeFile(const std::string& path, const std:: static std::vector s_entries; static std::recursive_mutex s_mutex; -static GameList::CacheMap m_cache_map; -static std::unique_ptr m_cache_write_stream; +static GameList::CacheMap s_cache_map; +static std::unique_ptr s_cache_write_stream; static bool m_game_list_loaded = false; @@ -276,12 +276,12 @@ bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry) bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry) { - auto iter = m_cache_map.find(path); - if (iter == m_cache_map.end()) + auto iter = UnorderedStringMapFind(s_cache_map, path); + if (iter == s_cache_map.end()) return false; *entry = std::move(iter->second); - m_cache_map.erase(iter); + s_cache_map.erase(iter); return true; } @@ -324,11 +324,11 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream) ge.type = static_cast(type); ge.compatibility = static_cast(compatibility_rating); - auto iter = m_cache_map.find(ge.path); - if (iter != m_cache_map.end()) + auto iter = UnorderedStringMapFind(s_cache_map, ge.path); + if (iter != s_cache_map.end()) iter->second = std::move(ge); else - m_cache_map.emplace(std::move(path), std::move(ge)); + s_cache_map.emplace(std::move(path), std::move(ge)); } return true; @@ -337,23 +337,23 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream) bool GameList::WriteEntryToCache(const Entry* entry) { bool result = true; - result &= m_cache_write_stream->WriteU8(static_cast(entry->type)); - result &= m_cache_write_stream->WriteU8(static_cast(entry->region)); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->path); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->serial); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->title); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->genre); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->publisher); - result &= m_cache_write_stream->WriteSizePrefixedString(entry->developer); - result &= m_cache_write_stream->WriteU64(entry->total_size); - result &= m_cache_write_stream->WriteU64(entry->last_modified_time); - result &= m_cache_write_stream->WriteU64(entry->release_date); - result &= m_cache_write_stream->WriteU32(entry->supported_controllers); - result &= m_cache_write_stream->WriteU8(entry->min_players); - result &= m_cache_write_stream->WriteU8(entry->max_players); - result &= m_cache_write_stream->WriteU8(entry->min_blocks); - result &= m_cache_write_stream->WriteU8(entry->max_blocks); - result &= m_cache_write_stream->WriteU8(static_cast(entry->compatibility)); + result &= s_cache_write_stream->WriteU8(static_cast(entry->type)); + result &= s_cache_write_stream->WriteU8(static_cast(entry->region)); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->path); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->serial); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->title); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->genre); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->publisher); + result &= s_cache_write_stream->WriteSizePrefixedString(entry->developer); + result &= s_cache_write_stream->WriteU64(entry->total_size); + result &= s_cache_write_stream->WriteU64(entry->last_modified_time); + result &= s_cache_write_stream->WriteU64(entry->release_date); + result &= s_cache_write_stream->WriteU32(entry->supported_controllers); + result &= s_cache_write_stream->WriteU8(entry->min_players); + result &= s_cache_write_stream->WriteU8(entry->max_players); + result &= s_cache_write_stream->WriteU8(entry->min_blocks); + result &= s_cache_write_stream->WriteU8(entry->max_blocks); + result &= s_cache_write_stream->WriteU8(static_cast(entry->compatibility)); return result; } @@ -374,7 +374,7 @@ void GameList::LoadCache() { Log_WarningPrintf("Deleting corrupted cache file '%s'", filename.c_str()); stream.reset(); - m_cache_map.clear(); + s_cache_map.clear(); DeleteCacheFile(); return; } @@ -383,37 +383,37 @@ void GameList::LoadCache() bool GameList::OpenCacheForWriting() { const std::string cache_filename(GetCacheFilename()); - Assert(!m_cache_write_stream); + Assert(!s_cache_write_stream); - m_cache_write_stream = ByteStream::OpenFile(cache_filename.c_str(), + s_cache_write_stream = ByteStream::OpenFile(cache_filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_SEEKABLE); - if (m_cache_write_stream) + if (s_cache_write_stream) { // check the header u32 signature, version; - if (m_cache_write_stream->ReadU32(&signature) && signature == GAME_LIST_CACHE_SIGNATURE && - m_cache_write_stream->ReadU32(&version) && version == GAME_LIST_CACHE_VERSION && - m_cache_write_stream->SeekToEnd()) + if (s_cache_write_stream->ReadU32(&signature) && signature == GAME_LIST_CACHE_SIGNATURE && + s_cache_write_stream->ReadU32(&version) && version == GAME_LIST_CACHE_VERSION && + s_cache_write_stream->SeekToEnd()) { return true; } - m_cache_write_stream.reset(); + s_cache_write_stream.reset(); } Log_InfoPrintf("Creating new game list cache file: '%s'", cache_filename.c_str()); - m_cache_write_stream = ByteStream::OpenFile( + s_cache_write_stream = ByteStream::OpenFile( cache_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE); - if (!m_cache_write_stream) + if (!s_cache_write_stream) return false; // new cache file, write header - if (!m_cache_write_stream->WriteU32(GAME_LIST_CACHE_SIGNATURE) || - !m_cache_write_stream->WriteU32(GAME_LIST_CACHE_VERSION)) + if (!s_cache_write_stream->WriteU32(GAME_LIST_CACHE_SIGNATURE) || + !s_cache_write_stream->WriteU32(GAME_LIST_CACHE_VERSION)) { Log_ErrorPrintf("Failed to write game list cache header"); - m_cache_write_stream.reset(); + s_cache_write_stream.reset(); FileSystem::DeleteFile(cache_filename.c_str()); return false; } @@ -423,16 +423,16 @@ bool GameList::OpenCacheForWriting() void GameList::CloseCacheFileStream() { - if (!m_cache_write_stream) + if (!s_cache_write_stream) return; - m_cache_write_stream->Commit(); - m_cache_write_stream.reset(); + s_cache_write_stream->Commit(); + s_cache_write_stream.reset(); } void GameList::DeleteCacheFile() { - Assert(!m_cache_write_stream); + Assert(!s_cache_write_stream); const std::string filename(GetCacheFilename()); if (!FileSystem::FileExists(filename.c_str())) @@ -528,7 +528,7 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc entry.path = std::move(path); entry.last_modified_time = timestamp; - if (m_cache_write_stream || OpenCacheForWriting()) + if (s_cache_write_stream || OpenCacheForWriting()) { if (!WriteEntryToCache(&entry)) Log_WarningPrintf("Failed to write entry '%s' to cache", entry.path.c_str()); @@ -606,9 +606,9 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* old_entries.swap(s_entries); } - const std::vector excluded_paths(Host::GetStringListSetting("GameList", "ExcludedPaths")); - const std::vector dirs(Host::GetStringListSetting("GameList", "Paths")); - const std::vector recursive_dirs(Host::GetStringListSetting("GameList", "RecursivePaths")); + const std::vector excluded_paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths")); + const std::vector dirs(Host::GetBaseStringListSetting("GameList", "Paths")); + const std::vector recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths")); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); if (!dirs.empty() || !recursive_dirs.empty()) @@ -638,7 +638,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* // don't need unused cache entries CloseCacheFileStream(); - m_cache_map.clear(); + s_cache_map.clear(); } std::string GameList::GetCoverImagePathForEntry(const Entry* entry) @@ -774,7 +774,8 @@ GameList::PlayedTimeMap GameList::LoadPlayedTimeMap(const std::string& path) { PlayedTimeMap ret; - auto fp = FileSystem::OpenManagedCFile(path.c_str(), "rb"); + // Use write mode here, even though we're not writing, so we can lock the file from other updates. + auto fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b"); #ifdef _WIN32 // On Windows, the file is implicitly locked. @@ -902,6 +903,21 @@ void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t las } } +std::time_t GameList::GetCachedPlayedTimeForSerial(const std::string& serial) +{ + if (serial.empty()) + return 0; + + std::unique_lock lock(s_mutex); + for (GameList::Entry& entry : s_entries) + { + if (entry.serial == serial) + return entry.total_played_time; + } + + return 0; +} + TinyString GameList::FormatTimestamp(std::time_t timestamp) { TinyString ret; @@ -943,23 +959,33 @@ TinyString GameList::FormatTimestamp(std::time_t timestamp) return ret; } -TinyString GameList::FormatTimespan(std::time_t timespan) +TinyString GameList::FormatTimespan(std::time_t timespan, bool long_format) { const u32 hours = static_cast(timespan / 3600); const u32 minutes = static_cast((timespan % 3600) / 60); const u32 seconds = static_cast((timespan % 3600) % 60); TinyString ret; - if (hours >= 100) - ret.Fmt(Host::TranslateString("GameList", "{}h {}m").GetCharArray(), hours, minutes); - else if (hours > 0) - ret.Fmt(Host::TranslateString("GameList", "{}h {}m {}s").GetCharArray(), hours, minutes, seconds); - else if (minutes > 0) - ret.Fmt(Host::TranslateString("GameList", "{}m {}s").GetCharArray(), minutes, seconds); - else if (seconds > 0) - ret.Fmt(Host::TranslateString("GameList", "{}s").GetCharArray(), seconds); + if (!long_format) + { + if (hours >= 100) + ret.Fmt(Host::TranslateString("GameList", "{}h {}m").GetCharArray(), hours, minutes); + else if (hours > 0) + ret.Fmt(Host::TranslateString("GameList", "{}h {}m {}s").GetCharArray(), hours, minutes, seconds); + else if (minutes > 0) + ret.Fmt(Host::TranslateString("GameList", "{}m {}s").GetCharArray(), minutes, seconds); + else if (seconds > 0) + ret.Fmt(Host::TranslateString("GameList", "{}s").GetCharArray(), seconds); + else + ret = Host::TranslateString("GameList", "None"); + } else - ret = Host::TranslateString("GameList", "None"); + { + if (hours > 0) + ret = fmt::format(Host::TranslateString("GameList", "{} hours").GetCharArray(), hours); + else + ret = fmt::format(Host::TranslateString("GameList", "{} minutes").GetCharArray(), minutes); + } return ret; } diff --git a/src/frontend-common/game_list.h b/src/frontend-common/game_list.h index e73e48c2e..7224c3dfa 100644 --- a/src/frontend-common/game_list.h +++ b/src/frontend-common/game_list.h @@ -79,11 +79,14 @@ void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* p /// Add played time for the specified serial. void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time); +/// Returns the total time played for a game. Requires the game to be scanned in the list. +std::time_t GetCachedPlayedTimeForSerial(const std::string& serial); + /// Formats a timestamp to something human readable (e.g. Today, Yesterday, 10/11/12). TinyString FormatTimestamp(std::time_t timestamp); -/// Formats a timespan to something human readable (e.g. 1h2m3s). -TinyString FormatTimespan(std::time_t timespan); +/// Formats a timespan to something human readable (e.g. 1h2m3s or 1 hour). +TinyString FormatTimespan(std::time_t timespan, bool long_format = false); std::string GetCoverImagePathForEntry(const Entry* entry); std::string GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title); diff --git a/src/frontend-common/imgui_fullscreen.cpp b/src/frontend-common/imgui_fullscreen.cpp index 809868415..c6bf3bd87 100644 --- a/src/frontend-common/imgui_fullscreen.cpp +++ b/src/frontend-common/imgui_fullscreen.cpp @@ -440,7 +440,31 @@ void ImGuiFullscreen::BeginLayout() // we evict from the texture cache at the start of the frame, in case we go over mid-frame, // we need to keep all those textures alive until the end of the frame s_texture_cache.ManualEvict(); + PushResetLayout(); +} +void ImGuiFullscreen::EndLayout() +{ + DrawFileSelector(); + DrawChoiceDialog(); + DrawInputDialog(); + DrawMessageDialog(); + + const float notification_margin = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float notification_vertical_pos = GetNotificationVerticalPosition(); + ImVec2 position(notification_margin, + notification_vertical_pos * ImGui::GetIO().DisplaySize.y + + ((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin)); + DrawBackgroundProgressDialogs(position, spacing); + DrawNotifications(position, spacing); + DrawToast(); + + PopResetLayout(); +} + +void ImGuiFullscreen::PushResetLayout() +{ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f)); @@ -465,23 +489,8 @@ void ImGuiFullscreen::BeginLayout() ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIPrimaryDarkColor); } -void ImGuiFullscreen::EndLayout() +void ImGuiFullscreen::PopResetLayout() { - DrawFileSelector(); - DrawChoiceDialog(); - DrawInputDialog(); - DrawMessageDialog(); - - const float notification_margin = LayoutScale(10.0f); - const float spacing = LayoutScale(10.0f); - const float notification_vertical_pos = GetNotificationVerticalPosition(); - ImVec2 position(notification_margin, - notification_vertical_pos * ImGui::GetIO().DisplaySize.y + - ((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin)); - DrawBackgroundProgressDialogs(position, spacing); - DrawNotifications(position, spacing); - DrawToast(); - ImGui::PopStyleColor(10); ImGui::PopStyleVar(12); } @@ -665,7 +674,7 @@ void ImGuiFullscreen::BeginMenuButtons(u32 num_items, float y_align, float x_pad ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); if (y_align != 0.0f) @@ -993,7 +1002,8 @@ bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, Im } bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float width, float height, float anchor_x, - float anchor_y, bool enabled, ImFont* font, ImVec2* out_position) + float anchor_y, bool enabled, ImFont* font, ImVec2* out_position, + bool repeat_button) { const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), 0.0f, text)); const ImVec2& padding(ImGui::GetStyle().FramePadding); @@ -1047,11 +1057,14 @@ bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float w bool pressed; if (enabled) { - pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); + pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, repeat_button ? ImGuiButtonFlags_Repeat : 0); if (hovered) { + const float t = std::min(static_cast(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f); const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t)); ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + ImGui::PopStyleColor(); } } else @@ -1456,6 +1469,13 @@ bool ImGuiFullscreen::EnumChoiceButtonImpl(const char* title, const char* summar return changed; } +void ImGuiFullscreen::DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text, + const char* text_end /*= nullptr*/, float wrap_width /*= 0.0f*/) +{ + dl->AddText(font, font->FontSize, pos + LayoutScale(1.0f, 1.0f), IM_COL32(0, 0, 0, 100), text, text_end, wrap_width); + dl->AddText(font, font->FontSize, pos, col, text, text_end, wrap_width); +} + void ImGuiFullscreen::BeginNavBar(float x_padding /*= LAYOUT_MENU_BUTTON_X_PADDING*/, float y_padding /*= LAYOUT_MENU_BUTTON_Y_PADDING*/) { @@ -1711,6 +1731,7 @@ void ImGuiFullscreen::DrawFileSelector() ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); @@ -1755,7 +1776,7 @@ void ImGuiFullscreen::DrawFileSelector() } ImGui::PopStyleColor(4); - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(3); ImGui::PopFont(); if (selected) @@ -1821,6 +1842,7 @@ void ImGuiFullscreen::DrawChoiceDialog() ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); @@ -1893,7 +1915,7 @@ void ImGuiFullscreen::DrawChoiceDialog() } ImGui::PopStyleColor(4); - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(3); ImGui::PopFont(); if (choice >= 0) @@ -1938,6 +1960,7 @@ void ImGuiFullscreen::DrawInputDialog() ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); @@ -1995,7 +2018,7 @@ void ImGuiFullscreen::DrawInputDialog() CloseInputDialog(); ImGui::PopStyleColor(4); - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(3); ImGui::PopFont(); } @@ -2087,6 +2110,7 @@ void ImGuiFullscreen::DrawMessageDialog() ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); @@ -2120,7 +2144,7 @@ void ImGuiFullscreen::DrawMessageDialog() } ImGui::PopStyleColor(4); - ImGui::PopStyleVar(3); + ImGui::PopStyleVar(4); ImGui::PopFont(); if (!is_open || result.has_value()) diff --git a/src/frontend-common/imgui_fullscreen.h b/src/frontend-common/imgui_fullscreen.h index 3724afaf0..11e247c85 100644 --- a/src/frontend-common/imgui_fullscreen.h +++ b/src/frontend-common/imgui_fullscreen.h @@ -106,6 +106,12 @@ static ALWAYS_INLINE ImVec4 MulAlpha(const ImVec4& v, float a) return ImVec4(v.x, v.y, v.z, v.w * a); } +static ALWAYS_INLINE std::string_view RemoveHash(const std::string_view& s) +{ + const std::string_view::size_type pos = s.find('#'); + return (pos != std::string_view::npos) ? s.substr(0, pos) : s; +} + /// Centers an image within the specified bounds, scaling up or down as needed. ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size); ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size); @@ -131,6 +137,9 @@ void UploadAsyncTextures(); void BeginLayout(); void EndLayout(); +void PushResetLayout(); +void PopResetLayout(); + void QueueResetFocus(); bool ResetFocusHere(); bool WantsToCloseMenu(); @@ -181,7 +190,8 @@ bool MenuImageButton(const char* title, const char* summary, ImTextureID user_te ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); bool FloatingButton(const char* text, float x, float y, float width = -1.0f, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, float anchor_x = 0.0f, float anchor_y = 0.0f, - bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr); + bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr, + bool repeat_button = false); bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); @@ -221,6 +231,9 @@ ALWAYS_INLINE static bool EnumChoiceButton(const char* title, const char* summar } } +void DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text, + const char* text_end = nullptr, float wrap_width = 0.0f); + void BeginNavBar(float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING); void EndNavBar(); void NavTitle(const char* title, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); diff --git a/src/frontend-common/imgui_overlays.cpp b/src/frontend-common/imgui_overlays.cpp index ac252085b..441d3b522 100644 --- a/src/frontend-common/imgui_overlays.cpp +++ b/src/frontend-common/imgui_overlays.cpp @@ -240,7 +240,8 @@ void ImGuiManager::DrawPerformanceOverlay() } } } - else if (g_settings.display_show_status_indicators && state == System::State::Paused) + else if (g_settings.display_show_status_indicators && state == System::State::Paused && + !FullscreenUI::HasActiveWindow()) { text.Assign(ICON_FA_PAUSE); DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255)); diff --git a/src/frontend-common/input_manager.cpp b/src/frontend-common/input_manager.cpp index 95bc28aff..ce5c520bb 100644 --- a/src/frontend-common/input_manager.cpp +++ b/src/frontend-common/input_manager.cpp @@ -1390,6 +1390,19 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind // Source Management // ------------------------------------------------------------------------ +bool InputManager::ReloadDevices() +{ + bool changed = false; + + for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++) + { + if (s_input_sources[i]) + changed |= s_input_sources[i]->ReloadDevices(); + } + + return changed; +} + void InputManager::CloseSources() { for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++) diff --git a/src/frontend-common/input_manager.h b/src/frontend-common/input_manager.h index 690b9b846..e3ca63fa0 100644 --- a/src/frontend-common/input_manager.h +++ b/src/frontend-common/input_manager.h @@ -220,6 +220,10 @@ void ReloadBindings(SettingsInterface& si, SettingsInterface& binding_si); /// Re-parses the sources part of the config and initializes any backends. void ReloadSources(SettingsInterface& si, std::unique_lock& settings_lock); +/// Called when a device change is triggered by the system (DBT_DEVNODES_CHANGED on Windows). +/// Returns true if any device changes are detected. +bool ReloadDevices(); + /// Shuts down any enabled input sources. void CloseSources(); diff --git a/src/frontend-common/input_source.h b/src/frontend-common/input_source.h index 007e64098..f92f9171a 100644 --- a/src/frontend-common/input_source.h +++ b/src/frontend-common/input_source.h @@ -20,6 +20,7 @@ public: virtual bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) = 0; virtual void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) = 0; + virtual bool ReloadDevices() = 0; virtual void Shutdown() = 0; virtual void PollEvents() = 0; diff --git a/src/frontend-common/sdl_input_source.cpp b/src/frontend-common/sdl_input_source.cpp index 2454b93e4..a073c0d84 100644 --- a/src/frontend-common/sdl_input_source.cpp +++ b/src/frontend-common/sdl_input_source.cpp @@ -130,6 +130,13 @@ void SDLInputSource::LoadSettings(SettingsInterface& si) m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false); } +bool SDLInputSource::ReloadDevices() +{ + // We'll get a GC added/removed event here. + PollEvents(); + return false; +} + void SDLInputSource::SetHints() { SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); diff --git a/src/frontend-common/sdl_input_source.h b/src/frontend-common/sdl_input_source.h index 027404d8c..f38f3a9fd 100644 --- a/src/frontend-common/sdl_input_source.h +++ b/src/frontend-common/sdl_input_source.h @@ -16,6 +16,7 @@ public: bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override; diff --git a/src/frontend-common/vulkan_host_display.cpp b/src/frontend-common/vulkan_host_display.cpp index 0e88b023f..a87deb5f7 100644 --- a/src/frontend-common/vulkan_host_display.cpp +++ b/src/frontend-common/vulkan_host_display.cpp @@ -548,7 +548,14 @@ void VulkanHostDisplay::DestroyResources() bool VulkanHostDisplay::CreateImGuiContext() { - return ImGui_ImplVulkan_Init(m_swap_chain->GetClearRenderPass()); + const VkRenderPass render_pass = + m_swap_chain ? m_swap_chain->GetClearRenderPass() : + g_vulkan_context->GetRenderPass(VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_UNDEFINED, VK_SAMPLE_COUNT_1_BIT, + VK_ATTACHMENT_LOAD_OP_CLEAR); + if (render_pass == VK_NULL_HANDLE) + return false; + + return ImGui_ImplVulkan_Init(render_pass); } void VulkanHostDisplay::DestroyImGuiContext() diff --git a/src/frontend-common/win32_raw_input_source.cpp b/src/frontend-common/win32_raw_input_source.cpp index bd34a3a70..cfa4d0a96 100644 --- a/src/frontend-common/win32_raw_input_source.cpp +++ b/src/frontend-common/win32_raw_input_source.cpp @@ -48,6 +48,11 @@ bool Win32RawInputSource::Initialize(SettingsInterface& si, std::unique_lock& settings_lock) {} +bool Win32RawInputSource::ReloadDevices() +{ + return false; +} + void Win32RawInputSource::Shutdown() { CloseDevices(); diff --git a/src/frontend-common/win32_raw_input_source.h b/src/frontend-common/win32_raw_input_source.h index 75e0abb15..68aaa91e2 100644 --- a/src/frontend-common/win32_raw_input_source.h +++ b/src/frontend-common/win32_raw_input_source.h @@ -16,6 +16,7 @@ public: bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override; diff --git a/src/frontend-common/xinput_source.cpp b/src/frontend-common/xinput_source.cpp index 24fa7e9b0..b0d0fa408 100644 --- a/src/frontend-common/xinput_source.cpp +++ b/src/frontend-common/xinput_source.cpp @@ -126,6 +126,35 @@ bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock& settings_lock) {} +bool XInputSource::ReloadDevices() +{ + bool changed = false; + for (u32 i = 0; i < NUM_CONTROLLERS; i++) + { + XINPUT_STATE new_state; + DWORD result = m_xinput_get_state(i, &new_state); + + if (result == ERROR_SUCCESS) + { + if (m_controllers[i].connected) + continue; + + HandleControllerConnection(i); + changed = true; + } + else if (result == ERROR_DEVICE_NOT_CONNECTED) + { + if (!m_controllers[i].connected) + continue; + + HandleControllerDisconnection(i); + changed = true; + } + } + + return changed; +} + void XInputSource::Shutdown() { for (u32 i = 0; i < NUM_CONTROLLERS; i++) @@ -152,6 +181,8 @@ void XInputSource::PollEvents() for (u32 i = 0; i < NUM_CONTROLLERS; i++) { const bool was_connected = m_controllers[i].connected; + if (!was_connected) + continue; XINPUT_STATE new_state; DWORD result = m_xinput_get_state(i, &new_state); diff --git a/src/frontend-common/xinput_source.h b/src/frontend-common/xinput_source.h index c3cb8b8ae..c499a5752 100644 --- a/src/frontend-common/xinput_source.h +++ b/src/frontend-common/xinput_source.h @@ -17,6 +17,7 @@ public: bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override;