ImGuiManager: Allow renderer switch without destroying context

Fixes UI getting reset when changing renderers, and works around being
left in an inconsistent state if FSUI wasn't started and renderers are
changed from the ingame settings menu.
This commit is contained in:
Stenzek
2026-01-25 19:08:43 +10:00
parent 81bee3191e
commit dedc59037f
8 changed files with 159 additions and 93 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<body>
<pre>DuckStation PS1 Emulator
Copyright (C) 2019-2025 Connor McLaughlin &lt;stenzek@gmail.com&gt;
Copyright (C) 2019-2026 Connor McLaughlin &lt;stenzek@gmail.com&gt;
This program is licensed under the terms of the Creative Commons<br>
Attribution-NonCommercial-NoDerivatives International License<br>
+24 -26
View File
@@ -67,7 +67,6 @@ static void DrawShaderBackgroundCallback(const ImDrawList* parent_list, const Im
//////////////////////////////////////////////////////////////////////////
// Resources
//////////////////////////////////////////////////////////////////////////
static void DestroyResources();
static GPUTexture* GetUserThemeableTexture(
const std::string_view png_name, const std::string_view svg_name, bool* is_colorable = nullptr,
const ImVec2& svg_size = LayoutScale(LAYOUT_HORIZONTAL_MENU_ITEM_IMAGE_SIZE, LAYOUT_HORIZONTAL_MENU_ITEM_IMAGE_SIZE));
@@ -169,7 +168,7 @@ ALIGN_TO_CACHE_LINE static Locals s_locals;
// Main
//////////////////////////////////////////////////////////////////////////
void FullscreenUI::Initialize(bool preserve_state /*= false */)
void FullscreenUI::Initialize()
{
// some achievement callbacks fire early while e.g. there is a load state popup blocking system init
if (s_locals.initialized || !ImGuiManager::IsInitialized())
@@ -178,7 +177,7 @@ void FullscreenUI::Initialize(bool preserve_state /*= false */)
s_locals.initialized = true;
// in case we open the pause menu while the game is running
if (!preserve_state && s_locals.current_main_window == MainWindowType::None && !GPUThread::HasGPUBackend() &&
if (s_locals.current_main_window == MainWindowType::None && !GPUThread::HasGPUBackend() &&
!GPUThread::IsGPUBackendRequested())
{
ReturnToMainWindow();
@@ -455,31 +454,37 @@ void FullscreenUI::ReturnToMainWindow(float transition_time)
});
}
void FullscreenUI::Shutdown(bool preserve_fsui_state)
void FullscreenUI::Shutdown()
{
if (!preserve_fsui_state)
{
SoundEffectManager::Shutdown();
DestroyGPUResources();
s_locals.current_main_window = MainWindowType::None;
s_locals.current_pause_submenu = PauseSubMenu::None;
s_locals.pause_menu_was_open = false;
s_locals.was_paused_on_quick_menu_open = false;
SoundEffectManager::Shutdown();
ClearAchievementsState();
ClearSaveStateEntryList();
ClearSettingsState();
ClearGameListState();
s_locals.current_time_string = {};
s_locals.current_time = 0;
}
s_locals.current_main_window = MainWindowType::None;
s_locals.current_pause_submenu = PauseSubMenu::None;
s_locals.pause_menu_was_open = false;
s_locals.was_paused_on_quick_menu_open = false;
DestroyResources();
ClearAchievementsState();
ClearSettingsState();
ClearGameListState();
s_locals.current_time_string = {};
s_locals.current_time = 0;
s_locals.initialized = false;
UpdateRunIdleState();
}
void FullscreenUI::DestroyGPUResources()
{
// has textures...
ClearSaveStateEntryList();
s_locals.app_background_texture.reset();
s_locals.app_background_shader.reset();
s_locals.background_loaded = false;
}
void FullscreenUI::Render()
{
if (!s_locals.initialized)
@@ -535,13 +540,6 @@ void FullscreenUI::Render()
UpdateTransitionState();
}
void FullscreenUI::DestroyResources()
{
s_locals.app_background_texture.reset();
s_locals.app_background_shader.reset();
s_locals.background_loaded = false;
}
GPUTexture* FullscreenUI::GetUserThemeableTexture(const std::string_view png_name, const std::string_view svg_name,
bool* is_colorable, const ImVec2& svg_size)
{
+6 -2
View File
@@ -14,12 +14,13 @@
#include <string_view>
#include <vector>
class Error;
class SmallStringBase;
struct GPUSettings;
namespace FullscreenUI {
void Initialize(bool preserve_state = false);
void Initialize();
bool IsInitialized();
bool HasActiveWindow();
void CheckForConfigChanges(const GPUSettings& old_settings);
@@ -28,7 +29,10 @@ void OnSystemPaused();
void OnSystemResumed();
void OnSystemDestroyed();
void Shutdown(bool preserve_state);
void Shutdown();
void DestroyGPUResources();
void Render();
void InvalidateCoverCache(std::string path = {});
+39 -38
View File
@@ -401,8 +401,6 @@ struct WidgetsState
u32 loading_screen_valid_samples = 0;
bool loading_screen_open = false;
ImGuiInputSource resource_input_source = ImGuiInputSource_None;
std::array<std::pair<Timer::Value, s32>, LOADING_PROGRESS_SAMPLE_COUNT> loading_screen_samples;
};
@@ -418,30 +416,14 @@ void FullscreenUI::SetFont(ImFont* ui_font)
UIStyle.Font = ui_font;
}
bool FullscreenUI::InitializeWidgets(bool preserve_fsui_state, Error* error)
bool FullscreenUI::InitializeWidgets(Error* error)
{
{
std::unique_lock lock(s_state.shared_state_mutex);
if (!(s_state.placeholder_texture = LoadTexture("images/placeholder.png")))
{
Error::SetStringView(error, "Failed to load placeholder.png");
return false;
}
}
if (!CompileTransitionPipelines(error))
{
ShutdownWidgets(preserve_fsui_state);
if (!CreateWidgetsGPUResources(error))
return false;
}
if (!preserve_fsui_state)
{
s_state.focus_reset_queued = FocusResetType::ViewChanged;
s_state.close_button_state = CloseButtonState::None;
ResetMenuButtonFrame();
}
s_state.focus_reset_queued = FocusResetType::ViewChanged;
s_state.close_button_state = CloseButtonState::None;
ResetMenuButtonFrame();
UpdateWidgetsSettings();
@@ -449,24 +431,14 @@ bool FullscreenUI::InitializeWidgets(bool preserve_fsui_state, Error* error)
return true;
}
void FullscreenUI::ShutdownWidgets(bool preserve_fsui_state)
void FullscreenUI::ShutdownWidgets()
{
std::unique_lock lock(s_state.shared_state_mutex);
DestroyWidgetsGPUResources();
s_state.transition_blend_pipeline.reset();
g_gpu_device->RecycleTexture(std::move(s_state.transition_prev_texture));
g_gpu_device->RecycleTexture(std::move(s_state.transition_current_texture));
s_state.texture_upload_queue.clear();
s_state.placeholder_texture.reset();
UIStyle.Font = nullptr;
s_state.texture_cache.Clear();
s_state.has_initialized = false;
if (!preserve_fsui_state)
{
std::unique_lock lock(s_state.shared_state_mutex);
s_state.texture_upload_queue.clear();
s_state.transition_state = TransitionState::Inactive;
if (s_state.transition_start_callback) [[unlikely]]
WARNING_LOG("Shutting down FullscreenUI while a transition callback is still set.");
@@ -484,6 +456,10 @@ void FullscreenUI::ShutdownWidgets(bool preserve_fsui_state)
s_state.file_selector_dialog.ClearState();
}
s_state.has_initialized = false;
UIStyle.Font = nullptr;
UpdateLoadingScreenRunIdle();
UpdateNotificationsRunIdle();
}
@@ -512,6 +488,31 @@ void FullscreenUI::UpdateWidgetsSettings()
ImGuiManager::SetGamepadFaceButtonsSwapped(swap_face_buttons);
}
bool FullscreenUI::CreateWidgetsGPUResources(Error* error)
{
if (!(s_state.placeholder_texture = LoadTexture("images/placeholder.png")))
{
Error::SetStringView(error, "Failed to load placeholder.png");
return false;
}
if (!CompileTransitionPipelines(error))
return false;
return true;
}
void FullscreenUI::DestroyWidgetsGPUResources()
{
s_state.transition_blend_pipeline.reset();
g_gpu_device->RecycleTexture(std::move(s_state.transition_prev_texture));
g_gpu_device->RecycleTexture(std::move(s_state.transition_current_texture));
s_state.placeholder_texture.reset();
s_state.texture_cache.Clear();
}
const std::shared_ptr<GPUTexture>& FullscreenUI::GetPlaceholderTexture()
{
return s_state.placeholder_texture;
+5 -2
View File
@@ -218,14 +218,17 @@ ImRect CenterImage(const ImRect& fit_rect, const GPUTexture* texture);
ImRect FitImage(const ImVec2& fit_size, const ImVec2& image_size);
/// Initializes, setting up any state.
bool InitializeWidgets(bool preserve_fsui_state, Error* error);
bool InitializeWidgets(Error* error);
/// Shuts down, clearing all state.
void ShutdownWidgets(bool preserve_fsui_state);
void ShutdownWidgets();
/// Loads settings from the settings interface.
void UpdateWidgetsSettings();
bool CreateWidgetsGPUResources(Error* error);
void DestroyWidgetsGPUResources();
std::span<const char* const> GetThemeNames();
std::span<const char* const> GetThemeDisplayNames();
std::vector<std::string_view> GetLocalizedThemeDisplayNames();
+34 -15
View File
@@ -74,8 +74,8 @@ static void WakeGPUThread();
static void WakeGPUThreadIfSleeping();
static bool SleepGPUThread(bool allow_sleep);
static bool CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preserve_fsui_state, Error* error);
static void DestroyDeviceOnThread(bool preserve_fsui_state);
static bool CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preserve_imgui_on_failure, Error* error);
static void DestroyDeviceOnThread(bool preserve_imgui_state);
static void ResizeDisplayWindowOnThread(u32 width, u32 height, float scale, float refresh_rate);
static void UpdateDisplayWindowOnThread(bool fullscreen, bool allow_exclusive_fullscreen);
static void DisplayWindowResizedOnThread();
@@ -632,7 +632,7 @@ bool GPUThread::IsFullscreen()
return s_state.requested_fullscreen;
}
bool GPUThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preserve_fsui_state, Error* error)
bool GPUThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preserve_imgui_on_failure, Error* error)
{
DebugAssert(!g_gpu_device);
@@ -710,11 +710,18 @@ bool GPUThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preser
return false;
}
if (!ImGuiManager::Initialize(preserve_fsui_state, &create_error))
// might be initialized already if we're recreating the device
const bool imgui_was_initialized = ImGuiManager::IsInitialized();
if (!(imgui_was_initialized ? ImGuiManager::CreateGPUResources(&create_error) :
ImGuiManager::Initialize(&create_error)))
{
ERROR_LOG("Failed to initialize ImGuiManager: {}", create_error.GetDescription());
Error::SetStringFmt(error, "Failed to initialize ImGuiManager: {}", create_error.GetDescription());
ImGuiManager::Shutdown(preserve_fsui_state);
if (preserve_imgui_on_failure)
ImGuiManager::DestroyGPUResources();
else
ImGuiManager::Shutdown();
g_gpu_device->Destroy();
g_gpu_device.reset();
if (wi.has_value())
@@ -722,8 +729,16 @@ bool GPUThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preser
return false;
}
// still need to update the window size
if (imgui_was_initialized && g_gpu_device->HasMainSwapChain())
{
const GPUSwapChain* sc = g_gpu_device->GetMainSwapChain();
ImGuiManager::WindowResized(sc->GetWindowInfo().surface_format, static_cast<float>(sc->GetWidth()),
static_cast<float>(sc->GetHeight()));
}
if (s_state.requested_fullscreen_ui)
FullscreenUI::Initialize(preserve_fsui_state);
FullscreenUI::Initialize();
if (const GPUSwapChain* swap_chain = g_gpu_device->GetMainSwapChain())
s_state.render_window_info = swap_chain->GetWindowInfo();
@@ -745,7 +760,7 @@ bool GPUThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool preser
return true;
}
void GPUThread::DestroyDeviceOnThread(bool preserve_fsui_state)
void GPUThread::DestroyDeviceOnThread(bool preserve_imgui_state)
{
if (!g_gpu_device)
return;
@@ -753,8 +768,15 @@ void GPUThread::DestroyDeviceOnThread(bool preserve_fsui_state)
// Presenter should be gone by this point
Assert(!s_state.gpu_presenter);
FullscreenUI::Shutdown(preserve_fsui_state);
ImGuiManager::Shutdown(preserve_fsui_state);
if (preserve_imgui_state)
{
DEV_LOG("Preserving ImGui state on destroy");
ImGuiManager::DestroyGPUResources();
}
else
{
ImGuiManager::Shutdown();
}
INFO_LOG("Destroying {} GPU device...", GPUDevice::RenderAPIToString(g_gpu_device->GetRenderAPI()));
g_gpu_device->Destroy();
@@ -884,14 +906,12 @@ void GPUThread::ReconfigureOnThread(GPUThreadReconfigureCommand* cmd)
Settings::GetRenderAPIForRenderer(s_state.requested_renderer.value_or(g_gpu_settings.gpu_renderer));
if (cmd->force_recreate_device || !GPUDevice::IsSameRenderAPI(current_api, expected_api))
{
const bool preserve_fsui_state = FullscreenUI::IsInitialized();
Timer timer;
DestroyGPUPresenterOnThread();
DestroyDeviceOnThread(preserve_fsui_state);
DestroyDeviceOnThread(true);
Error local_error;
if (!CreateDeviceOnThread(expected_api, cmd->fullscreen, preserve_fsui_state, &local_error))
if (!CreateDeviceOnThread(expected_api, cmd->fullscreen, current_api != RenderAPI::None, &local_error))
{
Host::AddIconOSDMessage(
OSDMessageType::Error, "DeviceSwitchFailed", ICON_FA_PAINT_ROLLER,
@@ -900,8 +920,7 @@ void GPUThread::ReconfigureOnThread(GPUThreadReconfigureCommand* cmd)
local_error.GetDescription()));
Host::ReleaseRenderWindow();
if (current_api == RenderAPI::None ||
!CreateDeviceOnThread(current_api, cmd->fullscreen, preserve_fsui_state, &local_error))
if (current_api == RenderAPI::None || !CreateDeviceOnThread(current_api, cmd->fullscreen, false, &local_error))
{
if (cmd->error_ptr)
*cmd->error_ptr = local_error;
+45 -7
View File
@@ -193,7 +193,7 @@ void ImGuiManager::SetTextFontOrder(const TextFontOrder& order)
ReloadFontDataIfActive();
}
bool ImGuiManager::Initialize(bool preserve_fsui_state, Error* error)
bool ImGuiManager::Initialize(Error* error)
{
if (!LoadFontData(error))
{
@@ -232,11 +232,8 @@ bool ImGuiManager::Initialize(bool preserve_fsui_state, Error* error)
FullscreenUI::UpdateTheme();
FullscreenUI::UpdateLayoutScale();
if (!CreateFontAtlas(error) || !CompilePipelines(error) ||
!FullscreenUI::InitializeWidgets(preserve_fsui_state, error))
{
if (!CreateFontAtlas(error) || !CompilePipelines(error) || !FullscreenUI::InitializeWidgets(error))
return false;
}
NewFrame(Timer::GetCurrentValue());
@@ -244,11 +241,12 @@ bool ImGuiManager::Initialize(bool preserve_fsui_state, Error* error)
return true;
}
void ImGuiManager::Shutdown(bool preserve_fsui_state)
void ImGuiManager::Shutdown()
{
DestroySoftwareCursorTextures();
FullscreenUI::ShutdownWidgets(preserve_fsui_state);
FullscreenUI::Shutdown();
FullscreenUI::ShutdownWidgets();
s_state.text_font = nullptr;
s_state.fixed_font = nullptr;
@@ -272,6 +270,46 @@ void ImGuiManager::Shutdown(bool preserve_fsui_state)
}
}
bool ImGuiManager::CreateGPUResources(Error* error)
{
if (!CompilePipelines(error))
return false;
if (!FullscreenUI::CreateWidgetsGPUResources(error))
return false;
CreateSoftwareCursorTextures();
// Font textures get created on render.
return true;
}
void ImGuiManager::DestroyGPUResources()
{
DestroySoftwareCursorTextures();
FullscreenUI::DestroyGPUResources();
FullscreenUI::DestroyWidgetsGPUResources();
s_state.imgui_pipeline.reset();
if (s_state.imgui_context)
{
for (ImTextureData* tex : s_state.imgui_context->IO.Fonts->TexList)
{
if (tex->Status == ImTextureStatus_Destroyed)
continue;
delete std::exchange(tex->TexID, nullptr);
if (tex->Status == ImTextureStatus_WantDestroy)
tex->Status = ImTextureStatus_Destroyed;
else
tex->Status = ImTextureStatus_WantCreate;
}
}
}
ImGuiContext* ImGuiManager::GetMainContext()
{
return s_state.imgui_context;
+5 -2
View File
@@ -91,10 +91,13 @@ TextFontOrder GetDefaultTextFontOrder();
void SetTextFontOrder(const TextFontOrder& order);
/// Initializes ImGui, creates fonts, etc.
bool Initialize(bool preserve_fsui_state, Error* error);
bool Initialize(Error* error);
/// Frees all ImGui resources.
void Shutdown(bool preserve_fsui_state);
void Shutdown();
bool CreateGPUResources(Error* error);
void DestroyGPUResources();
/// Returns main ImGui context.
ImGuiContext* GetMainContext();