diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index f13e3badb..39456bd77 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -709,12 +709,14 @@ std::string MainWindow::getDeviceDiscPath(const QString& title)
 
 void MainWindow::recreate()
 {
-  if (s_system_valid)
+  const bool was_display_created = m_display_created;
+  if (was_display_created)
   {
-    requestShutdown(false, true, true);
-
-    while (s_system_valid)
+    g_emu_thread->setSurfaceless(true);
+    while (m_display_widget || !g_emu_thread->isSurfaceless())
       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
+
+    m_display_created = false;
   }
 
   // We need to close input sources, because e.g. DInput uses our window handle.
@@ -724,11 +726,19 @@ void MainWindow::recreate()
   g_main_window = nullptr;
 
   MainWindow* new_main_window = new MainWindow();
+  DebugAssert(g_main_window == new_main_window);
   new_main_window->show();
   deleteLater();
 
   // Reload the sources we just closed.
   g_emu_thread->reloadInputSources();
+
+  if (was_display_created)
+  {
+    g_emu_thread->setSurfaceless(false);
+    g_main_window->updateEmulationActions(false, System::IsValid(), Achievements::IsHardcoreModeActive());
+    g_main_window->onFullscreenUIStateChange(g_emu_thread->isRunningFullscreenUI());
+  }
 }
 
 void MainWindow::destroySubWindows()
@@ -2464,10 +2474,11 @@ void MainWindow::showEvent(QShowEvent* event)
 void MainWindow::closeEvent(QCloseEvent* event)
 {
   // If there's no VM, we can just exit as normal.
-  if (!s_system_valid)
+  if (!s_system_valid || !m_display_created)
   {
     saveGeometryToConfig();
-    g_emu_thread->stopFullscreenUI();
+    if (m_display_created)
+      g_emu_thread->stopFullscreenUI();
     destroySubWindows();
     QMainWindow::closeEvent(event);
     return;
diff --git a/src/duckstation-qt/qttranslations.cpp b/src/duckstation-qt/qttranslations.cpp
index 2464280fa..99dc9a127 100644
--- a/src/duckstation-qt/qttranslations.cpp
+++ b/src/duckstation-qt/qttranslations.cpp
@@ -53,8 +53,6 @@ static QString FixLanguageName(const QString& language);
 static std::string GetFontPath(const GlyphInfo* gi);
 static void UpdateGlyphRanges(const std::string_view& language);
 static const GlyphInfo* GetGlyphInfo(const std::string_view& language);
-
-static std::vector<ImWchar> s_glyph_ranges;
 } // namespace QtHost
 
 static std::vector<QTranslator*> s_translators;
@@ -133,9 +131,18 @@ void QtHost::InstallTranslator()
   qApp->installTranslator(translator);
   s_translators.push_back(translator);
 
-  UpdateGlyphRanges(language.toStdString());
-
-  Host::ClearTranslationCache();
+  // We end up here both on language change, and on startup.
+  if (g_emu_thread)
+  {
+    Host::RunOnCPUThread([language = language.toStdString()]() {
+      UpdateGlyphRanges(language);
+      Host::ClearTranslationCache();
+    });
+  }
+  else
+  {
+    UpdateGlyphRanges(language.toStdString());
+  }
 }
 
 QString QtHost::FixLanguageName(const QString& language)
@@ -233,10 +240,11 @@ void QtHost::UpdateGlyphRanges(const std::string_view& language)
   const GlyphInfo* gi = GetGlyphInfo(language);
 
   std::string font_path;
-  s_glyph_ranges.clear();
+  std::vector<ImWchar> glyph_ranges;
+  glyph_ranges.clear();
 
   // Base Latin range is always included.
-  s_glyph_ranges.insert(s_glyph_ranges.begin(), std::begin(s_base_latin_range), std::end(s_base_latin_range));
+  glyph_ranges.insert(glyph_ranges.begin(), std::begin(s_base_latin_range), std::end(s_base_latin_range));
 
   if (gi)
   {
@@ -247,8 +255,8 @@ void QtHost::UpdateGlyphRanges(const std::string_view& language)
       {
         // Always should be in pairs.
         DebugAssert(ptr[0] != 0 && ptr[1] != 0);
-        s_glyph_ranges.push_back(*(ptr++));
-        s_glyph_ranges.push_back(*(ptr++));
+        glyph_ranges.push_back(*(ptr++));
+        glyph_ranges.push_back(*(ptr++));
       }
     }
 
@@ -258,16 +266,15 @@ void QtHost::UpdateGlyphRanges(const std::string_view& language)
   // If we don't have any specific glyph range, assume Central European, except if English, then keep the size down.
   if ((!gi || !gi->used_glyphs) && language != "en")
   {
-    s_glyph_ranges.insert(s_glyph_ranges.begin(), std::begin(s_central_european_ranges),
-                          std::end(s_central_european_ranges));
+    glyph_ranges.insert(glyph_ranges.begin(), std::begin(s_central_european_ranges),
+                        std::end(s_central_european_ranges));
   }
 
   // List terminator.
-  s_glyph_ranges.push_back(0);
-  s_glyph_ranges.push_back(0);
+  glyph_ranges.push_back(0);
+  glyph_ranges.push_back(0);
 
-  ImGuiManager::SetFontPath(std::move(font_path));
-  ImGuiManager::SetFontRange(s_glyph_ranges.data());
+  ImGuiManager::SetFontPathAndRange(std::move(font_path), std::move(glyph_ranges));
 }
 
 // clang-format off
diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp
index fc9f764d2..25056f788 100644
--- a/src/util/imgui_manager.cpp
+++ b/src/util/imgui_manager.cpp
@@ -74,7 +74,7 @@ static float s_global_prescale = 1.0f; // before window scale
 static float s_global_scale = 1.0f;
 
 static std::string s_font_path;
-static const ImWchar* s_font_range = nullptr;
+static std::vector<ImWchar> s_font_range;
 
 static ImFont* s_standard_font;
 static ImFont* s_fixed_font;
@@ -109,16 +109,30 @@ static bool s_global_prescale_changed = false;
 static std::array<ImGuiManager::SoftwareCursor, InputManager::MAX_SOFTWARE_CURSORS> s_software_cursors = {};
 } // namespace ImGuiManager
 
-void ImGuiManager::SetFontPath(std::string path)
+void ImGuiManager::SetFontPathAndRange(std::string path, std::vector<u16> range)
 {
-  s_font_path = std::move(path);
-  s_standard_font_data = {};
-}
+  if (s_font_path == path && s_font_range == range)
+    return;
 
-void ImGuiManager::SetFontRange(const u16* range)
-{
-  s_font_range = range;
+  s_font_path = std::move(path);
+  s_font_range = std::move(range);
   s_standard_font_data = {};
+
+  if (ImGui::GetCurrentContext())
+  {
+    ImGui::EndFrame();
+
+    if (!LoadFontData())
+      Panic("Failed to load font data");
+
+    if (!AddImGuiFonts(HasFullscreenFonts()))
+      Panic("Failed to create ImGui font text");
+
+    if (!g_gpu_device->UpdateImGuiFontTexture())
+      Panic("Failed to recreate font texture after scale+resize");
+
+    NewFrame();
+  }
 }
 
 void ImGuiManager::SetGlobalScale(float global_scale)
@@ -527,7 +541,7 @@ ImFont* ImGuiManager::AddTextFont(float size)
   cfg.FontDataOwnedByAtlas = false;
   return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_standard_font_data.data(),
                                                     static_cast<int>(s_standard_font_data.size()), size, &cfg,
-                                                    s_font_range ? s_font_range : default_ranges);
+                                                    s_font_range.empty() ? default_ranges : s_font_range.data());
 }
 
 ImFont* ImGuiManager::AddFixedFont(float size)
diff --git a/src/util/imgui_manager.h b/src/util/imgui_manager.h
index 29e2930ed..ce47d1cf5 100644
--- a/src/util/imgui_manager.h
+++ b/src/util/imgui_manager.h
@@ -13,10 +13,7 @@ enum class GenericInputBinding : u8;
 
 namespace ImGuiManager {
 /// Sets the path to the font to use. Empty string means to use the default.
-void SetFontPath(std::string path);
-
-/// Sets the glyph range to use when loading fonts.
-void SetFontRange(const u16* range);
+void SetFontPathAndRange(std::string path, std::vector<u16> range);
 
 /// Changes the global scale.
 void SetGlobalScale(float global_scale);