From fd8ed083072ee6f49a4543194f19b16b26adbd71 Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Sun, 16 Feb 2020 00:14:28 +0900
Subject: [PATCH] Move more logic from frontend to base HostInterface

---
 src/core/host_interface.cpp                 | 179 ++++++--
 src/core/host_interface.h                   |  51 ++-
 src/duckstation-qt/d3d11displaywindow.cpp   |   5 +
 src/duckstation-qt/d3d11displaywindow.h     |   1 +
 src/duckstation-qt/mainwindow.cpp           | 111 ++---
 src/duckstation-qt/mainwindow.h             |   8 +-
 src/duckstation-qt/opengldisplaywindow.cpp  |   5 +
 src/duckstation-qt/opengldisplaywindow.h    |   1 +
 src/duckstation-qt/qtdisplaywindow.cpp      |   5 +
 src/duckstation-qt/qtdisplaywindow.h        |   1 +
 src/duckstation-qt/qthostinterface.cpp      | 230 ++++------
 src/duckstation-qt/qthostinterface.h        |  24 +-
 src/duckstation-sdl/d3d11_host_display.h    |   2 +-
 src/duckstation-sdl/main.cpp                |  27 +-
 src/duckstation-sdl/opengl_host_display.cpp |   2 +
 src/duckstation-sdl/sdl_host_interface.cpp  | 474 +++++++-------------
 src/duckstation-sdl/sdl_host_interface.h    |  32 +-
 17 files changed, 539 insertions(+), 619 deletions(-)

diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp
index 2740752bf..45fa4c637 100644
--- a/src/core/host_interface.cpp
+++ b/src/core/host_interface.cpp
@@ -54,44 +54,87 @@ HostInterface::HostInterface()
   m_game_list->SetDatabaseFilename(GetGameListDatabaseFileName());
 }
 
-HostInterface::~HostInterface() = default;
-
-bool HostInterface::CreateSystem()
+HostInterface::~HostInterface()
 {
-  m_system = System::Create(this);
+  // system should be shut down prior to the destructor
+  Assert(!m_system && !m_audio_stream && !m_display);
+}
 
-  // Pull in any invalid settings which have been reset.
-  m_settings = m_system->GetSettings();
-  m_paused = true;
+bool HostInterface::BootSystemFromFile(const char* filename)
+{
+  if (!AcquireHostDisplay())
+  {
+    ReportFormattedError("Failed to acquire host display");
+    return false;
+  }
+
+  // set host display settings
+  m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
+
+  // create the audio stream. this will never fail, since we'll just fall back to null
+  m_audio_stream = CreateAudioStream(m_settings.audio_backend);
+  if (!m_audio_stream || !m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4))
+  {
+    ReportFormattedError("Failed to create or configure audio stream, falling back to null output.");
+    m_audio_stream.reset();
+    m_audio_stream = AudioStream::CreateNullAudioStream();
+    m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4);
+  }
+
+  m_system = System::Create(this);
+  if (!m_system->Boot(filename))
+  {
+    ReportFormattedError("System failed to boot. The log may contain more information.");
+    DestroySystem();
+    return false;
+  }
+
+  OnSystemCreated();
+
+  m_paused = m_settings.start_paused;
+  m_audio_stream->PauseOutput(m_paused);
   UpdateSpeedLimiterState();
+
+  if (m_paused)
+    OnSystemPaused(true);
+
   return true;
 }
 
-bool HostInterface::BootSystem(const char* filename, const char* state_filename)
+bool HostInterface::BootSystemFromBIOS()
 {
-  if (!m_system->Boot(filename))
-    return false;
+  return BootSystemFromFile(nullptr);
+}
 
-  m_paused = m_settings.start_paused;
+void HostInterface::PauseSystem(bool paused)
+{
+  if (paused == m_paused)
+    return;
+
+  m_paused = paused;
+  m_audio_stream->PauseOutput(m_paused);
+  OnSystemPaused(paused);
   UpdateSpeedLimiterState();
-
-  if (state_filename && !LoadState(state_filename))
-    return false;
-
-  return true;
 }
 
 void HostInterface::ResetSystem()
 {
   m_system->Reset();
+  m_system->ResetPerformanceCounters();
   AddOSDMessage("System reset.");
 }
 
 void HostInterface::DestroySystem()
 {
+  if (!m_system)
+    return;
+
   m_system.reset();
   m_paused = false;
-  UpdateSpeedLimiterState();
+  m_audio_stream.reset();
+  ReleaseHostDisplay();
+  OnSystemDestroyed();
+  OnRunningGameChanged();
 }
 
 void HostInterface::ReportError(const char* message)
@@ -283,11 +326,6 @@ void HostInterface::DrawDebugWindows()
     m_system->GetMDEC()->DrawDebugStateWindow();
 }
 
-void HostInterface::ClearImGuiFocus()
-{
-  ImGui::SetWindowFocus(nullptr);
-}
-
 std::optional<std::vector<u8>> HostInterface::GetBIOSImage(ConsoleRegion region)
 {
   // Try the other default filenames in the directory of the configured BIOS.
@@ -349,28 +387,48 @@ bool HostInterface::LoadState(const char* filename)
   if (!stream)
     return false;
 
-  AddFormattedOSDMessage(2.0f, "Loading state from %s...", filename);
+  AddFormattedOSDMessage(2.0f, "Loading state from '%s'...", filename);
 
-  const bool result = m_system->LoadState(stream.get());
-  if (!result)
+  if (m_system)
   {
-    ReportFormattedError("Loading state from %s failed. Resetting.", filename);
-    m_system->Reset();
+    if (!m_system->LoadState(stream.get()))
+    {
+      ReportFormattedError("Loading state from '%s' failed. Resetting.", filename);
+      m_system->Reset();
+      return false;
+    }
+
+    m_system->ResetPerformanceCounters();
+  }
+  else
+  {
+    if (!BootSystemFromFile(nullptr))
+    {
+      ReportFormattedError("Failed to boot system to load state from '%s'.", filename);
+      return false;
+    }
+
+    if (!m_system->LoadState(stream.get()))
+    {
+      ReportFormattedError("Failed to load state. The log may contain more information. Shutting down system.");
+      DestroySystem();
+      return false;
+    }
   }
 
-  return result;
+  return true;
 }
 
 bool HostInterface::LoadState(bool global, u32 slot)
 {
-  const std::string& code = m_system->GetRunningCode();
-  if (!global && code.empty())
+  if (!global && (!m_system || m_system->GetRunningCode().empty()))
   {
     ReportFormattedError("Can't save per-game state without a running game code.");
     return false;
   }
 
-  std::string save_path = global ? GetGlobalSaveStateFileName(slot) : GetGameSaveStateFileName(code.c_str(), slot);
+  std::string save_path =
+    global ? GetGlobalSaveStateFileName(slot) : GetGameSaveStateFileName(m_system->GetRunningCode().c_str(), slot);
   return LoadState(save_path.c_str());
 }
 
@@ -385,12 +443,12 @@ bool HostInterface::SaveState(const char* filename)
   const bool result = m_system->SaveState(stream.get());
   if (!result)
   {
-    ReportFormattedError("Saving state to %s failed.", filename);
+    ReportFormattedError("Saving state to '%s' failed.", filename);
     stream->Discard();
   }
   else
   {
-    AddFormattedOSDMessage(2.0f, "State saved to %s.", filename);
+    AddFormattedOSDMessage(2.0f, "State saved to '%s'.", filename);
     stream->Commit();
   }
 
@@ -431,7 +489,17 @@ void HostInterface::UpdateSpeedLimiterState()
     m_system->ResetPerformanceCounters();
 }
 
-void HostInterface::SwitchGPURenderer() {}
+void HostInterface::OnSystemCreated() {}
+
+void HostInterface::OnSystemPaused(bool paused)
+{
+  ReportFormattedMessage("System %s.", paused ? "paused" : "resumed");
+}
+
+void HostInterface::OnSystemDestroyed()
+{
+  ReportFormattedMessage("System shut down.");
+}
 
 void HostInterface::OnSystemPerformanceCountersUpdated() {}
 
@@ -644,16 +712,16 @@ void HostInterface::UpdateSettings(const std::function<void()>& apply_callback)
   apply_callback();
 
   if (m_settings.gpu_renderer != old_gpu_renderer)
-    SwitchGPURenderer();
-
-  if (m_settings.video_sync_enabled != old_vsync_enabled || m_settings.audio_sync_enabled != old_audio_sync_enabled ||
-      m_settings.speed_limiter_enabled != old_speed_limiter_enabled)
-  {
-    UpdateSpeedLimiterState();
-  }
+    RecreateSystem();
 
   if (m_system)
   {
+    if (m_settings.video_sync_enabled != old_vsync_enabled || m_settings.audio_sync_enabled != old_audio_sync_enabled ||
+        m_settings.speed_limiter_enabled != old_speed_limiter_enabled)
+    {
+      UpdateSpeedLimiterState();
+    }
+
     if (m_settings.emulation_speed != old_emulation_speed)
     {
       m_system->UpdateThrottlePeriod();
@@ -672,7 +740,7 @@ void HostInterface::UpdateSettings(const std::function<void()>& apply_callback)
     }
   }
 
-  if (m_settings.display_linear_filtering != old_display_linear_filtering)
+  if (m_display && m_settings.display_linear_filtering != old_display_linear_filtering)
     m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
 }
 
@@ -704,3 +772,30 @@ void HostInterface::ModifyResolutionScale(s32 increment)
                          GPU::VRAM_WIDTH * m_settings.gpu_resolution_scale,
                          GPU::VRAM_HEIGHT * m_settings.gpu_resolution_scale);
 }
+
+void HostInterface::RecreateSystem()
+{
+  std::unique_ptr<ByteStream> stream = ByteStream_CreateGrowableMemoryStream(nullptr, 8 * 1024);
+  if (!m_system->SaveState(stream.get()) || !stream->SeekAbsolute(0))
+  {
+    ReportError("Failed to save state before system recreation. Shutting down.");
+    DestroySystem();
+    return;
+  }
+
+  DestroySystem();
+  if (!BootSystemFromFile(nullptr))
+  {
+    ReportError("Failed to boot system after recreation.");
+    return;
+  }
+
+  if (!m_system->LoadState(stream.get()))
+  {
+    ReportError("Failed to load state after system recreation. Shutting down.");
+    DestroySystem();
+    return;
+  }
+
+  m_system->ResetPerformanceCounters();
+}
diff --git a/src/core/host_interface.h b/src/core/host_interface.h
index c3d551111..0892bcf93 100644
--- a/src/core/host_interface.h
+++ b/src/core/host_interface.h
@@ -12,6 +12,7 @@
 #include <vector>
 
 class AudioStream;
+class ByteStream;
 class CDImage;
 class HostDisplay;
 class GameList;
@@ -27,22 +28,31 @@ public:
   virtual ~HostInterface();
 
   /// Access to host display.
-  ALWAYS_INLINE HostDisplay* GetDisplay() const { return m_display.get(); }
+  ALWAYS_INLINE HostDisplay* GetDisplay() const { return m_display; }
 
   /// Access to host audio stream.
-  AudioStream* GetAudioStream() const { return m_audio_stream.get(); }
+  ALWAYS_INLINE AudioStream* GetAudioStream() const { return m_audio_stream.get(); }
 
   /// Returns a settings object which can be modified.
-  Settings& GetSettings() { return m_settings; }
+  ALWAYS_INLINE Settings& GetSettings() { return m_settings; }
 
   /// Returns the game list.
-  const GameList* GetGameList() const { return m_game_list.get(); }
+  ALWAYS_INLINE const GameList* GetGameList() const { return m_game_list.get(); }
 
-  bool CreateSystem();
-  bool BootSystem(const char* filename, const char* state_filename);
+  bool BootSystemFromFile(const char* filename);
+  bool BootSystemFromBIOS();
+  void PauseSystem(bool paused);
   void ResetSystem();
   void DestroySystem();
 
+  /// Loads the current emulation state from file. Specifying a slot of -1 loads the "resume" game state.
+  bool LoadState(bool global, u32 slot);
+  bool LoadState(const char* filename);
+
+  /// Saves the current emulation state to a file. Specifying a slot of -1 saves the "resume" save state.
+  bool SaveState(bool global, u32 slot);
+  bool SaveState(const char* filename);
+
   virtual void ReportError(const char* message);
   virtual void ReportMessage(const char* message);
 
@@ -53,12 +63,8 @@ public:
   void AddOSDMessage(const char* message, float duration = 2.0f);
   void AddFormattedOSDMessage(float duration, const char* format, ...);
 
-  /// Loads the BIOS image for the specified region.
-  virtual std::optional<std::vector<u8>> GetBIOSImage(ConsoleRegion region);
-
-
   /// Returns the base user directory path.
-  const std::string& GetUserDirectory() const { return m_user_directory; }
+  ALWAYS_INLINE const std::string& GetUserDirectory() const { return m_user_directory; }
 
   /// Returns a path relative to the user directory.
   std::string GetUserDirectoryRelativePath(const char* format, ...) const;
@@ -89,7 +95,13 @@ protected:
     bool global;
   };
 
-  virtual void SwitchGPURenderer();
+  virtual bool AcquireHostDisplay() = 0;
+  virtual void ReleaseHostDisplay() = 0;
+  virtual std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) = 0;
+
+  virtual void OnSystemCreated();
+  virtual void OnSystemPaused(bool paused);
+  virtual void OnSystemDestroyed();
   virtual void OnSystemPerformanceCountersUpdated();
   virtual void OnRunningGameChanged();
 
@@ -122,13 +134,8 @@ protected:
   /// Returns a list of save states for the specified game code.
   std::vector<SaveStateInfo> GetAvailableSaveStates(const char* game_code) const;
 
-  /// Loads the current emulation state from file. Specifying a slot of -1 loads the "resume" game state.
-  bool LoadState(bool global, u32 slot);
-  bool LoadState(const char* filename);
-
-  /// Saves the current emulation state to a file. Specifying a slot of -1 saves the "resume" save state.
-  bool SaveState(bool global, u32 slot);
-  bool SaveState(const char* filename);
+  /// Loads the BIOS image for the specified region.
+  std::optional<std::vector<u8>> GetBIOSImage(ConsoleRegion region);
 
   /// Restores all settings to defaults.
   void SetDefaultSettings();
@@ -143,14 +150,16 @@ protected:
   /// Adjusts the internal (render) resolution of the hardware backends.
   void ModifyResolutionScale(s32 increment);
 
+  /// Switches the GPU renderer by saving state, recreating the display window, and restoring state (if needed).
+  void RecreateSystem();
+
   void UpdateSpeedLimiterState();
 
   void DrawFPSWindow();
   void DrawOSDMessages();
   void DrawDebugWindows();
-  void ClearImGuiFocus();
 
-  std::unique_ptr<HostDisplay> m_display;
+  HostDisplay* m_display = nullptr;
   std::unique_ptr<AudioStream> m_audio_stream;
   std::unique_ptr<System> m_system;
   std::unique_ptr<GameList> m_game_list;
diff --git a/src/duckstation-qt/d3d11displaywindow.cpp b/src/duckstation-qt/d3d11displaywindow.cpp
index b7a8ad2f8..910ea7124 100644
--- a/src/duckstation-qt/d3d11displaywindow.cpp
+++ b/src/duckstation-qt/d3d11displaywindow.cpp
@@ -169,6 +169,11 @@ void D3D11DisplayWindow::onWindowResized(int width, int height)
     Panic("Failed to recreate swap chain RTV after resize");
 }
 
+bool D3D11DisplayWindow::hasDeviceContext() const
+{
+  return static_cast<bool>(m_device);
+}
+
 bool D3D11DisplayWindow::createDeviceContext(QThread* worker_thread, bool debug_device)
 {
   ComPtr<IDXGIFactory> dxgi_factory;
diff --git a/src/duckstation-qt/d3d11displaywindow.h b/src/duckstation-qt/d3d11displaywindow.h
index c3a529cca..9eab0fc25 100644
--- a/src/duckstation-qt/d3d11displaywindow.h
+++ b/src/duckstation-qt/d3d11displaywindow.h
@@ -21,6 +21,7 @@ public:
 
   HostDisplay* getHostDisplayInterface() override;
 
+  bool hasDeviceContext() const override;
   bool createDeviceContext(QThread* worker_thread, bool debug_device) override;
   bool initializeDeviceContext(bool debug_device) override;
   void destroyDeviceContext() override;
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index 6bd691226..868184ec8 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -1,8 +1,10 @@
 #include "mainwindow.h"
+#include "common/assert.h"
 #include "core/game_list.h"
 #include "core/settings.h"
 #include "gamelistsettingswidget.h"
 #include "gamelistwidget.h"
+#include "qtdisplaywindow.h"
 #include "qthostinterface.h"
 #include "qtsettingsinterface.h"
 #include "settingsdialog.h"
@@ -28,8 +30,7 @@ MainWindow::MainWindow(QtHostInterface* host_interface) : QMainWindow(nullptr),
 
 MainWindow::~MainWindow()
 {
-  delete m_display_widget;
-  m_host_interface->displayWidgetDestroyed();
+  Assert(!m_display_widget);
 }
 
 void MainWindow::reportError(QString message)
@@ -42,31 +43,40 @@ void MainWindow::reportMessage(QString message)
   m_ui.statusBar->showMessage(message, 2000);
 }
 
-void MainWindow::onEmulationStarting()
+void MainWindow::createDisplayWindow(QThread* worker_thread, bool use_debug_device)
 {
+  DebugAssert(!m_display_widget);
+
+  QtDisplayWindow* display_window = m_host_interface->createDisplayWindow();
+  DebugAssert(display_window);
+
+  m_display_widget = QWidget::createWindowContainer(display_window, m_ui.mainContainer);
+  DebugAssert(m_display_widget);
+
+  m_display_widget->setFocusPolicy(Qt::StrongFocus);
+  m_ui.mainContainer->insertWidget(1, m_display_widget);
+
+  // we need the surface visible.. this might be able to be replaced with something else
   switchToEmulationView();
-  updateEmulationActions(true, false);
-
-  // we need the surface visible..
   QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
+
+  display_window->createDeviceContext(worker_thread, use_debug_device);
 }
 
-void MainWindow::onEmulationStarted()
+void MainWindow::destroyDisplayWindow()
 {
-  m_emulation_running = true;
-  updateEmulationActions(false, true);
-}
+  DebugAssert(m_display_widget);
+
+  const bool was_fullscreen = m_display_widget->isFullScreen();
+  if (was_fullscreen)
+    toggleFullscreen();
 
-void MainWindow::onEmulationStopped()
-{
-  m_emulation_running = false;
-  updateEmulationActions(false, false);
   switchToGameListView();
-}
 
-void MainWindow::onEmulationPaused(bool paused)
-{
-  m_ui.actionPause->setChecked(paused);
+  // recreate the display widget using the potentially-new renderer
+  m_ui.mainContainer->removeWidget(m_display_widget);
+  delete m_display_widget;
+  m_display_widget = nullptr;
 }
 
 void MainWindow::toggleFullscreen()
@@ -91,36 +101,22 @@ void MainWindow::toggleFullscreen()
   m_ui.actionFullscreen->setChecked(fullscreen);
 }
 
-void MainWindow::recreateDisplayWidget(bool create_device_context)
+void MainWindow::onEmulationStarted()
 {
-  const bool was_fullscreen = m_display_widget->isFullScreen();
-  if (was_fullscreen)
-    toggleFullscreen();
+  m_emulation_running = true;
+  updateEmulationActions(false, true);
+}
 
+void MainWindow::onEmulationStopped()
+{
+  m_emulation_running = false;
+  updateEmulationActions(false, false);
   switchToGameListView();
+}
 
-  // recreate the display widget using the potentially-new renderer
-  m_ui.mainContainer->removeWidget(m_display_widget);
-  m_host_interface->displayWidgetDestroyed();
-  delete m_display_widget;
-  m_display_widget = m_host_interface->createDisplayWidget(m_ui.mainContainer);
-  m_ui.mainContainer->insertWidget(1, m_display_widget);
-
-  if (create_device_context)
-    switchToEmulationView();
-
-  // we need the surface visible..
-  QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
-
-  if (create_device_context && !m_host_interface->createDisplayDeviceContext())
-  {
-    QMessageBox::critical(this, tr("DuckStation Error"),
-                          tr("Failed to create new device context on renderer switch. Cannot continue."));
-    QCoreApplication::exit();
-    return;
-  }
-
-  updateDebugMenuGPURenderer();
+void MainWindow::onEmulationPaused(bool paused)
+{
+  m_ui.actionPause->setChecked(paused);
 }
 
 void MainWindow::onSystemPerformanceCountersUpdated(float speed, float fps, float vps, float average_frame_time,
@@ -149,7 +145,7 @@ void MainWindow::onStartDiscActionTriggered()
   if (filename.isEmpty())
     return;
 
-  m_host_interface->bootSystem(std::move(filename), QString());
+  m_host_interface->bootSystemFromFile(std::move(filename));
 }
 
 void MainWindow::onChangeDiscFromFileActionTriggered()
@@ -168,11 +164,6 @@ void MainWindow::onChangeDiscFromGameListActionTriggered()
   switchToGameListView();
 }
 
-void MainWindow::onStartBiosActionTriggered()
-{
-  m_host_interface->bootSystem(QString(), QString());
-}
-
 static void OpenURL(QWidget* parent, const char* url)
 {
   const QUrl qurl(QUrl::fromEncoded(QByteArray(url, static_cast<int>(std::strlen(url)))));
@@ -200,10 +191,6 @@ void MainWindow::setupAdditionalUi()
   m_game_list_widget = new GameListWidget(m_ui.mainContainer);
   m_game_list_widget->initialize(m_host_interface);
   m_ui.mainContainer->insertWidget(0, m_game_list_widget);
-
-  m_display_widget = m_host_interface->createDisplayWidget(m_ui.mainContainer);
-  m_ui.mainContainer->insertWidget(1, m_display_widget);
-
   m_ui.mainContainer->setCurrentIndex(0);
 
   m_status_speed_widget = new QLabel(m_ui.statusBar);
@@ -304,14 +291,14 @@ void MainWindow::connectSignals()
   onEmulationPaused(false);
 
   connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered);
-  connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBiosActionTriggered);
+  connect(m_ui.actionStartBios, &QAction::triggered, m_host_interface, &QtHostInterface::bootSystemFromBIOS);
   connect(m_ui.actionChangeDisc, &QAction::triggered, [this] { m_ui.menuChangeDisc->exec(QCursor::pos()); });
   connect(m_ui.actionChangeDiscFromFile, &QAction::triggered, this, &MainWindow::onChangeDiscFromFileActionTriggered);
   connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this,
           &MainWindow::onChangeDiscFromGameListActionTriggered);
   connect(m_ui.actionAddGameDirectory, &QAction::triggered,
           [this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); });
-  connect(m_ui.actionPowerOff, &QAction::triggered, [this]() { m_host_interface->powerOffSystem(true, false); });
+  connect(m_ui.actionPowerOff, &QAction::triggered, [this]() { m_host_interface->destroySystem(true, false); });
   connect(m_ui.actionReset, &QAction::triggered, m_host_interface, &QtHostInterface::resetSystem);
   connect(m_ui.actionPause, &QAction::toggled, m_host_interface, &QtHostInterface::pauseSystem);
   connect(m_ui.actionLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); });
@@ -336,14 +323,14 @@ void MainWindow::connectSignals()
 
   connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
           Qt::BlockingQueuedConnection);
+  connect(m_host_interface, &QtHostInterface::createDisplayWindowRequested, this, &MainWindow::createDisplayWindow,
+          Qt::BlockingQueuedConnection);
+  connect(m_host_interface, &QtHostInterface::destroyDisplayWindowRequested, this, &MainWindow::destroyDisplayWindow);
+  connect(m_host_interface, &QtHostInterface::toggleFullscreenRequested, this, &MainWindow::toggleFullscreen);
   connect(m_host_interface, &QtHostInterface::messageReported, this, &MainWindow::reportMessage);
-  connect(m_host_interface, &QtHostInterface::emulationStarting, this, &MainWindow::onEmulationStarting);
   connect(m_host_interface, &QtHostInterface::emulationStarted, this, &MainWindow::onEmulationStarted);
   connect(m_host_interface, &QtHostInterface::emulationStopped, this, &MainWindow::onEmulationStopped);
   connect(m_host_interface, &QtHostInterface::emulationPaused, this, &MainWindow::onEmulationPaused);
-  connect(m_host_interface, &QtHostInterface::toggleFullscreenRequested, this, &MainWindow::toggleFullscreen);
-  connect(m_host_interface, &QtHostInterface::recreateDisplayWidgetRequested, this, &MainWindow::recreateDisplayWidget,
-          Qt::BlockingQueuedConnection);
   connect(m_host_interface, &QtHostInterface::systemPerformanceCountersUpdated, this,
           &MainWindow::onSystemPerformanceCountersUpdated);
   connect(m_host_interface, &QtHostInterface::runningGameChanged, this, &MainWindow::onRunningGameChanged);
@@ -353,7 +340,7 @@ void MainWindow::connectSignals()
     QString path = QString::fromStdString(entry->path);
     if (!m_emulation_running)
     {
-      m_host_interface->bootSystem(path, QString());
+      m_host_interface->bootSystemFromFile(path);
     }
     else
     {
@@ -446,6 +433,6 @@ void MainWindow::updateDebugMenuGPURenderer()
 
 void MainWindow::closeEvent(QCloseEvent* event)
 {
-  m_host_interface->powerOffSystem(true, true);
+  m_host_interface->destroySystem(true, true);
   QMainWindow::closeEvent(event);
 }
diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h
index a6f1f92e3..a5edd98fc 100644
--- a/src/duckstation-qt/mainwindow.h
+++ b/src/duckstation-qt/mainwindow.h
@@ -8,6 +8,7 @@
 #include "ui_mainwindow.h"
 
 class QLabel;
+class QThread;
 
 class GameListWidget;
 class QtHostInterface;
@@ -23,12 +24,12 @@ public:
 private Q_SLOTS:
   void reportError(QString message);
   void reportMessage(QString message);
-  void onEmulationStarting();
+  void createDisplayWindow(QThread* worker_thread, bool use_debug_device);
+  void destroyDisplayWindow();
+  void toggleFullscreen();
   void onEmulationStarted();
   void onEmulationStopped();
   void onEmulationPaused(bool paused);
-  void toggleFullscreen();
-  void recreateDisplayWidget(bool create_device_context);
   void onSystemPerformanceCountersUpdated(float speed, float fps, float vps, float average_frame_time,
                                           float worst_frame_time);
   void onRunningGameChanged(QString filename, QString game_code, QString game_title);
@@ -36,7 +37,6 @@ private Q_SLOTS:
   void onStartDiscActionTriggered();
   void onChangeDiscFromFileActionTriggered();
   void onChangeDiscFromGameListActionTriggered();
-  void onStartBiosActionTriggered();
   void onGitHubRepositoryActionTriggered();
   void onIssueTrackerActionTriggered();
   void onAboutActionTriggered();
diff --git a/src/duckstation-qt/opengldisplaywindow.cpp b/src/duckstation-qt/opengldisplaywindow.cpp
index d93f4f75e..37ac63253 100644
--- a/src/duckstation-qt/opengldisplaywindow.cpp
+++ b/src/duckstation-qt/opengldisplaywindow.cpp
@@ -223,6 +223,11 @@ static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLen
   }
 }
 
+bool OpenGLDisplayWindow::hasDeviceContext() const
+{
+  return static_cast<bool>(m_gl_context);
+}
+
 bool OpenGLDisplayWindow::createDeviceContext(QThread* worker_thread, bool debug_device)
 {
   m_gl_context = std::make_unique<QOpenGLContext>();
diff --git a/src/duckstation-qt/opengldisplaywindow.h b/src/duckstation-qt/opengldisplaywindow.h
index a4d35e205..4c3ca04fb 100644
--- a/src/duckstation-qt/opengldisplaywindow.h
+++ b/src/duckstation-qt/opengldisplaywindow.h
@@ -27,6 +27,7 @@ public:
 
   HostDisplay* getHostDisplayInterface() override;
 
+  bool hasDeviceContext() const override;
   bool createDeviceContext(QThread* worker_thread, bool debug_device) override;
   bool initializeDeviceContext(bool debug_device) override;
   void destroyDeviceContext() override;
diff --git a/src/duckstation-qt/qtdisplaywindow.cpp b/src/duckstation-qt/qtdisplaywindow.cpp
index 0605db321..14e3c53c9 100644
--- a/src/duckstation-qt/qtdisplaywindow.cpp
+++ b/src/duckstation-qt/qtdisplaywindow.cpp
@@ -18,6 +18,11 @@ HostDisplay* QtDisplayWindow::getHostDisplayInterface()
   return nullptr;
 }
 
+bool QtDisplayWindow::hasDeviceContext() const
+{
+  return true;
+}
+
 bool QtDisplayWindow::createDeviceContext(QThread* worker_thread, bool debug_device)
 {
   return true;
diff --git a/src/duckstation-qt/qtdisplaywindow.h b/src/duckstation-qt/qtdisplaywindow.h
index abbd6c19c..01ad8b243 100644
--- a/src/duckstation-qt/qtdisplaywindow.h
+++ b/src/duckstation-qt/qtdisplaywindow.h
@@ -18,6 +18,7 @@ public:
 
   virtual HostDisplay* getHostDisplayInterface();
 
+  virtual bool hasDeviceContext() const;
   virtual bool createDeviceContext(QThread* worker_thread, bool debug_device);
   virtual bool initializeDeviceContext(bool debug_device);
   virtual void destroyDeviceContext();
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
index cef694278..7b787d06b 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -151,8 +151,10 @@ void QtHostInterface::refreshGameList(bool invalidate_cache /* = false */, bool
   emit gameListRefreshed();
 }
 
-QWidget* QtHostInterface::createDisplayWidget(QWidget* parent)
+QtDisplayWindow* QtHostInterface::createDisplayWindow()
 {
+  Assert(!m_display_window);
+
 #ifdef WIN32
   if (m_settings.gpu_renderer == GPURenderer::HardwareOpenGL)
     m_display_window = new OpenGLDisplayWindow(this, nullptr);
@@ -162,40 +164,29 @@ QWidget* QtHostInterface::createDisplayWidget(QWidget* parent)
   m_display_window = new OpenGLDisplayWindow(this, nullptr);
 #endif
   connect(m_display_window, &QtDisplayWindow::windowResizedEvent, this, &QtHostInterface::onDisplayWindowResized);
-
-  m_display.release();
-  m_display = std::unique_ptr<HostDisplay>(m_display_window->getHostDisplayInterface());
-  m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
-
-  QWidget* widget = QWidget::createWindowContainer(m_display_window, parent);
-  widget->setFocusPolicy(Qt::StrongFocus);
-  return widget;
+  return m_display_window;
 }
 
-bool QtHostInterface::createDisplayDeviceContext()
+void QtHostInterface::bootSystemFromFile(QString filename)
 {
-  return m_display_window->createDeviceContext(m_worker_thread, m_settings.gpu_use_debug_device);
-}
-
-void QtHostInterface::displayWidgetDestroyed()
-{
-  m_display.release();
-  m_display_window = nullptr;
-}
-
-void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_state_filename)
-{
-  Assert(!isOnWorkerThread());
-  emit emulationStarting();
-
-  if (!createDisplayDeviceContext())
+  if (!isOnWorkerThread())
   {
-    emit emulationStopped();
+    QMetaObject::invokeMethod(this, "bootSystemFromFile", Qt::QueuedConnection, Q_ARG(QString, filename));
     return;
   }
 
-  QMetaObject::invokeMethod(this, "doBootSystem", Qt::QueuedConnection, Q_ARG(QString, initial_filename),
-                            Q_ARG(QString, initial_save_state_filename));
+  HostInterface::BootSystemFromFile(filename.toStdString().c_str());
+}
+
+void QtHostInterface::bootSystemFromBIOS()
+{
+  if (!isOnWorkerThread())
+  {
+    QMetaObject::invokeMethod(this, "bootSystemFromBIOS", Qt::QueuedConnection);
+    return;
+  }
+
+  HostInterface::BootSystemFromBIOS();
 }
 
 void QtHostInterface::handleKeyEvent(int key, bool pressed)
@@ -223,51 +214,82 @@ void QtHostInterface::onDisplayWindowResized(int width, int height)
   m_display_window->onWindowResized(width, height);
 }
 
-void QtHostInterface::SwitchGPURenderer()
+bool QtHostInterface::AcquireHostDisplay()
 {
-  // Due to the GPU class owning textures, we have to shut the system down.
-  std::unique_ptr<ByteStream> stream;
-  if (m_system)
-  {
-    stream = ByteStream_CreateGrowableMemoryStream(nullptr, 8 * 1024);
-    if (!m_system->SaveState(stream.get()) || !stream->SeekAbsolute(0))
-      ReportError("Failed to save state before GPU renderer switch");
+  DebugAssert(!m_display_window);
 
-    DestroySystem();
-    m_audio_stream->PauseOutput(true);
+  emit createDisplayWindowRequested(m_worker_thread, m_settings.gpu_use_debug_device);
+  if (!m_display_window->hasDeviceContext())
+  {
+    m_display_window = nullptr;
+    emit destroyDisplayWindowRequested();
+    return false;
+  }
+
+  if (!m_display_window->initializeDeviceContext(m_settings.gpu_use_debug_device))
+  {
     m_display_window->destroyDeviceContext();
+    m_display_window = nullptr;
+    emit destroyDisplayWindowRequested();
+    return false;
   }
 
-  const bool restore_state = static_cast<bool>(stream);
-  emit recreateDisplayWidgetRequested(restore_state);
-  Assert(m_display_window != nullptr);
+  m_display = m_display_window->getHostDisplayInterface();
+  return true;
+}
 
-  if (restore_state)
+void QtHostInterface::ReleaseHostDisplay()
+{
+  DebugAssert(m_display_window && m_display == m_display_window->getHostDisplayInterface());
+  m_display = nullptr;
+  m_display_window->disconnect(this);
+  m_display_window->destroyDeviceContext();
+  m_display_window = nullptr;
+  emit destroyDisplayWindowRequested();
+}
+
+std::unique_ptr<AudioStream> QtHostInterface::CreateAudioStream(AudioBackend backend)
+{
+  switch (backend)
   {
-    if (!m_display_window->initializeDeviceContext(m_settings.gpu_use_debug_device))
-    {
-      emit runningGameChanged(QString(), QString(), QString());
-      emit emulationStopped();
-      return;
-    }
+    case AudioBackend::Default:
+    case AudioBackend::Cubeb:
+      return AudioStream::CreateCubebAudioStream();
 
-    CreateSystem();
-    if (!BootSystem(nullptr, nullptr) || !m_system->LoadState(stream.get()))
-    {
-      ReportError("Failed to load state after GPU renderer switch, resetting");
-      m_system->Reset();
-    }
+    case AudioBackend::Null:
+      return AudioStream::CreateNullAudioStream();
 
-    if (!m_paused)
-    {
-      m_audio_stream->PauseOutput(false);
-      UpdateSpeedLimiterState();
-    }
-
-    m_system->ResetPerformanceCounters();
+    default:
+      return nullptr;
   }
 }
 
+void QtHostInterface::OnSystemCreated()
+{
+  HostInterface::OnSystemCreated();
+
+  wakeThread();
+
+  emit emulationStarted();
+}
+
+void QtHostInterface::OnSystemPaused(bool paused)
+{
+  HostInterface::OnSystemPaused(paused);
+
+  if (!paused)
+    wakeThread();
+
+  emit emulationPaused(paused);
+}
+
+void QtHostInterface::OnSystemDestroyed()
+{
+  HostInterface::OnSystemDestroyed();
+
+  emit emulationStopped();
+}
+
 void QtHostInterface::OnSystemPerformanceCountersUpdated()
 {
   HostInterface::OnSystemPerformanceCountersUpdated();
@@ -457,11 +479,11 @@ void QtHostInterface::addButtonToInputMap(const QString& binding, InputButtonHan
   }
 }
 
-void QtHostInterface::powerOffSystem(bool save_resume_state /* = false */, bool block_until_done /* = false */)
+void QtHostInterface::destroySystem(bool save_resume_state /* = false */, bool block_until_done /* = false */)
 {
   if (!isOnWorkerThread())
   {
-    QMetaObject::invokeMethod(this, "powerOffSystem",
+    QMetaObject::invokeMethod(this, "destroySystem",
                               block_until_done ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
                               Q_ARG(bool, save_resume_state), Q_ARG(bool, block_until_done));
     return;
@@ -470,15 +492,7 @@ void QtHostInterface::powerOffSystem(bool save_resume_state /* = false */, bool
   if (!m_system)
     return;
 
-  if (save_resume_state)
-    Log_InfoPrintf("TODO: Save resume state");
-
   DestroySystem();
-  m_audio_stream->PauseOutput(true);
-  m_display_window->destroyDeviceContext();
-
-  emit runningGameChanged(QString(), QString(), QString());
-  emit emulationStopped();
 }
 
 void QtHostInterface::resetSystem()
@@ -515,59 +529,6 @@ void QtHostInterface::pauseSystem(bool paused)
 
 void QtHostInterface::changeDisc(QString new_disc_filename) {}
 
-void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename)
-{
-  if (!m_display_window->initializeDeviceContext(m_settings.gpu_use_debug_device))
-  {
-    emit emulationStopped();
-    return;
-  }
-
-  std::string initial_filename_str = initial_filename.toStdString();
-  std::string initial_save_state_filename_str = initial_save_state_filename.toStdString();
-  std::lock_guard<std::mutex> lock(m_qsettings_mutex);
-  if (!CreateSystem() ||
-      !BootSystem(initial_filename_str.empty() ? nullptr : initial_filename_str.c_str(),
-                  initial_save_state_filename_str.empty() ? nullptr : initial_save_state_filename_str.c_str()))
-  {
-    DestroySystem();
-    m_display_window->destroyDeviceContext();
-    emit emulationStopped();
-    return;
-  }
-
-  wakeThread();
-  m_audio_stream->PauseOutput(false);
-  UpdateSpeedLimiterState();
-  emit emulationStarted();
-}
-
-void QtHostInterface::createAudioStream()
-{
-  switch (m_settings.audio_backend)
-  {
-    case AudioBackend::Default:
-    case AudioBackend::Cubeb:
-      m_audio_stream = AudioStream::CreateCubebAudioStream();
-      break;
-
-    case AudioBackend::Null:
-    default:
-      m_audio_stream = AudioStream::CreateNullAudioStream();
-      break;
-  }
-
-  if (!m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4))
-  {
-    qWarning() << "Failed to configure audio stream, falling back to null output";
-
-    // fall back to null output
-    m_audio_stream.reset();
-    m_audio_stream = AudioStream::CreateNullAudioStream();
-    m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4);
-  }
-}
-
 void QtHostInterface::populateSaveStateMenus(const char* game_code, QMenu* load_menu, QMenu* save_menu)
 {
   const std::vector<SaveStateInfo> available_states(GetAvailableSaveStates(game_code));
@@ -626,10 +587,7 @@ void QtHostInterface::loadState(QString filename)
     return;
   }
 
-  if (m_system)
-    LoadState(filename.toStdString().c_str());
-  else
-    doBootSystem(QString(), filename);
+  LoadState(filename.toStdString().c_str());
 }
 
 void QtHostInterface::loadState(bool global, qint32 slot)
@@ -640,19 +598,7 @@ void QtHostInterface::loadState(bool global, qint32 slot)
     return;
   }
 
-  if (m_system)
-  {
-    LoadState(slot, global);
-    return;
-  }
-
-  if (!global)
-  {
-    // can't load a non-global system without a game code
-    return;
-  }
-
-  loadState(QString::fromStdString(GetGlobalSaveStateFileName(slot)));
+  LoadState(global, slot);
 }
 
 void QtHostInterface::saveState(bool global, qint32 slot, bool block_until_done /* = false */)
@@ -694,8 +640,6 @@ void QtHostInterface::threadEntryPoint()
 {
   m_worker_thread_event_loop = new QEventLoop();
 
-  createAudioStream();
-
   // TODO: Event which flags the thread as ready
   while (!m_shutdown_flag.load())
   {
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
index 63d3aa698..e0ff40d65 100644
--- a/src/duckstation-qt/qthostinterface.h
+++ b/src/duckstation-qt/qthostinterface.h
@@ -45,11 +45,7 @@ public:
 
   bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; }
 
-  QWidget* createDisplayWidget(QWidget* parent);
-  bool createDisplayDeviceContext();
-  void displayWidgetDestroyed();
-
-  void bootSystem(QString initial_filename, QString initial_save_state_filename);
+  QtDisplayWindow* createDisplayWindow();
 
   void updateInputMap();
   void handleKeyEvent(int key, bool pressed);
@@ -67,20 +63,22 @@ public:
 Q_SIGNALS:
   void errorReported(QString message);
   void messageReported(QString message);
-  void emulationStarting();
   void emulationStarted();
   void emulationStopped();
   void emulationPaused(bool paused);
   void gameListRefreshed();
+  void createDisplayWindowRequested(QThread* worker_thread, bool use_debug_device);
+  void destroyDisplayWindowRequested();
   void toggleFullscreenRequested();
-  void recreateDisplayWidgetRequested(bool create_device_context);
   void systemPerformanceCountersUpdated(float speed, float fps, float vps, float avg_frame_time,
                                         float worst_frame_time);
   void runningGameChanged(QString filename, QString game_code, QString game_title);
 
 public Q_SLOTS:
   void applySettings();
-  void powerOffSystem(bool save_resume_state = false, bool block_until_done = false);
+  void bootSystemFromFile(QString filename);
+  void bootSystemFromBIOS();
+  void destroySystem(bool save_resume_state = false, bool block_until_done = false);
   void resetSystem();
   void pauseSystem(bool paused);
   void changeDisc(QString new_disc_filename);
@@ -90,13 +88,18 @@ public Q_SLOTS:
 
 private Q_SLOTS:
   void doStopThread();
-  void doBootSystem(QString initial_filename, QString initial_save_state_filename);
   void doUpdateInputMap();
   void doHandleKeyEvent(int key, bool pressed);
   void onDisplayWindowResized(int width, int height);
 
 protected:
-  void SwitchGPURenderer() override;
+  bool AcquireHostDisplay() override;
+  void ReleaseHostDisplay() override;
+  std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
+
+  void OnSystemCreated() override;
+  void OnSystemPaused(bool paused) override;
+  void OnSystemDestroyed() override;
   void OnSystemPerformanceCountersUpdated() override;
   void OnRunningGameChanged() override;
 
@@ -122,7 +125,6 @@ private:
   void updateControllerInputMap();
   void updateHotkeyInputMap();
   void addButtonToInputMap(const QString& binding, InputButtonHandler handler);
-  void createAudioStream();
   void createThread();
   void stopThread();
   void threadEntryPoint();
diff --git a/src/duckstation-sdl/d3d11_host_display.h b/src/duckstation-sdl/d3d11_host_display.h
index 9260c7582..285558fe5 100644
--- a/src/duckstation-sdl/d3d11_host_display.h
+++ b/src/duckstation-sdl/d3d11_host_display.h
@@ -69,5 +69,5 @@ private:
   D3D11::StreamBuffer m_display_uniform_buffer;
 
   bool m_allow_tearing_supported = false;
-  bool m_vsync = false;
+  bool m_vsync = true;
 };
diff --git a/src/duckstation-sdl/main.cpp b/src/duckstation-sdl/main.cpp
index f670da553..9e831aa2d 100644
--- a/src/duckstation-sdl/main.cpp
+++ b/src/duckstation-sdl/main.cpp
@@ -15,28 +15,26 @@ static int Run(int argc, char* argv[])
   }
 
   // parameters
-  const char* filename = nullptr;
-  const char* exp1_filename = nullptr;
-  std::string state_filename;
+  std::optional<s32> state_index;
+  const char* boot_filename = nullptr;
   for (int i = 1; i < argc; i++)
   {
 #define CHECK_ARG(str) !std::strcmp(argv[i], str)
 #define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))
 
     if (CHECK_ARG_PARAM("-state"))
-      state_filename = SDLHostInterface::GetSaveStateFilename(std::strtoul(argv[++i], nullptr, 10));
-    else if (CHECK_ARG_PARAM("-exp1"))
-      exp1_filename = argv[++i];
+      state_index = std::atoi(argv[++i]);
+    if (CHECK_ARG_PARAM("-resume"))
+      state_index = -1;
     else
-      filename = argv[i];
+      boot_filename = argv[i];
 
 #undef CHECK_ARG
 #undef CHECK_ARG_PARAM
   }
 
   // create display and host interface
-  std::unique_ptr<SDLHostInterface> host_interface =
-    SDLHostInterface::Create(filename, exp1_filename, state_filename.empty() ? nullptr : state_filename.c_str());
+  std::unique_ptr<SDLHostInterface> host_interface = SDLHostInterface::Create();
   if (!host_interface)
   {
     Panic("Failed to create host interface");
@@ -44,6 +42,17 @@ static int Run(int argc, char* argv[])
     return -1;
   }
 
+  // boot/load state
+  if (boot_filename)
+  {
+    if (host_interface->BootSystemFromFile(boot_filename) && state_index.has_value())
+      host_interface->LoadState(false, state_index.value());
+  }
+  else if (state_index.has_value())
+  {
+    host_interface->LoadState(true, state_index.value());
+  }
+
   // run
   host_interface->Run();
 
diff --git a/src/duckstation-sdl/opengl_host_display.cpp b/src/duckstation-sdl/opengl_host_display.cpp
index 5948b9857..50b2c0fa6 100644
--- a/src/duckstation-sdl/opengl_host_display.cpp
+++ b/src/duckstation-sdl/opengl_host_display.cpp
@@ -262,6 +262,8 @@ bool OpenGLHostDisplay::CreateGLContext(bool debug_device)
     glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
   }
 
+  // start with vsync on
+  SDL_GL_SetSwapInterval(1);
   return true;
 }
 
diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp
index 009edcb41..e0467353e 100644
--- a/src/duckstation-sdl/sdl_host_interface.cpp
+++ b/src/duckstation-sdl/sdl_host_interface.cpp
@@ -33,17 +33,20 @@ SDLHostInterface::SDLHostInterface()
   timeBeginPeriod(1);
 #endif
 
-  m_switch_gpu_renderer_event_id = SDL_RegisterEvents(1);
+  m_update_settings_event_id = SDL_RegisterEvents(1);
 }
 
 SDLHostInterface::~SDLHostInterface()
 {
   CloseGameControllers();
-  m_display.reset();
-  ImGui::DestroyContext();
+  if (m_display)
+  {
+    DestroyDisplay();
+    ImGui::DestroyContext();
+  }
 
   if (m_window)
-    SDL_DestroyWindow(m_window);
+    DestroySDLWindow();
 
 #ifdef WIN32
   timeEndPeriod(1);
@@ -87,30 +90,31 @@ void SDLHostInterface::DestroySDLWindow()
 bool SDLHostInterface::CreateDisplay()
 {
   const bool debug_device = m_settings.gpu_use_debug_device;
+  std::unique_ptr<HostDisplay> display;
 #ifdef WIN32
-  m_display = UseOpenGLRenderer() ? OpenGLHostDisplay::Create(m_window, debug_device) :
-                                    D3D11HostDisplay::Create(m_window, debug_device);
+  display = UseOpenGLRenderer() ? OpenGLHostDisplay::Create(m_window, debug_device) :
+                                           D3D11HostDisplay::Create(m_window, debug_device);
 #else
-  m_display = OpenGLHostDisplay::Create(m_window, debug_device);
+  display = OpenGLHostDisplay::Create(m_window, debug_device);
 #endif
 
-  if (!m_display)
+  if (!display)
     return false;
 
-  m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
-
   m_app_icon_texture =
-    m_display->CreateTexture(APP_ICON_WIDTH, APP_ICON_HEIGHT, APP_ICON_DATA, APP_ICON_WIDTH * sizeof(u32));
-  if (!m_app_icon_texture)
+    display->CreateTexture(APP_ICON_WIDTH, APP_ICON_HEIGHT, APP_ICON_DATA, APP_ICON_WIDTH * sizeof(u32));
+  if (!display)
     return false;
 
+  m_display = display.release();
   return true;
 }
 
 void SDLHostInterface::DestroyDisplay()
 {
   m_app_icon_texture.reset();
-  m_display.reset();
+  delete m_display;
+  m_display = nullptr;
 }
 
 void SDLHostInterface::CreateImGuiContext()
@@ -123,101 +127,93 @@ void SDLHostInterface::CreateImGuiContext()
   ImGui::AddRobotoRegularFont();
 }
 
-void SDLHostInterface::CreateAudioStream()
+bool SDLHostInterface::AcquireHostDisplay()
+{
+  // Handle renderer switch if required on Windows.
+#ifdef WIN32
+  const HostDisplay::RenderAPI render_api = m_display->GetRenderAPI();
+  const bool render_api_is_gl =
+    render_api == HostDisplay::RenderAPI::OpenGL || render_api == HostDisplay::RenderAPI::OpenGLES;
+  const bool render_api_wants_gl = UseOpenGLRenderer();
+  if (render_api_is_gl != render_api_wants_gl)
+  {
+    ImGui::EndFrame();
+    DestroyDisplay();
+    DestroySDLWindow();
+
+    if (!CreateSDLWindow())
+      Panic("Failed to recreate SDL window on GPU renderer switch");
+
+    if (!CreateDisplay())
+      Panic("Failed to recreate display on GPU renderer switch");
+
+    ImGui::NewFrame();
+  }
+#endif
+
+  return true;
+}
+
+void SDLHostInterface::ReleaseHostDisplay()
+{
+  // restore vsync, since we don't want to burn cycles at the menu
+  m_display->SetVSync(true);
+}
+
+std::unique_ptr<AudioStream> SDLHostInterface::CreateAudioStream(AudioBackend backend)
 {
   switch (m_settings.audio_backend)
   {
     case AudioBackend::Null:
-      m_audio_stream = AudioStream::CreateNullAudioStream();
-      break;
+      return AudioStream::CreateNullAudioStream();
 
     case AudioBackend::Cubeb:
-      m_audio_stream = AudioStream::CreateCubebAudioStream();
-      break;
+      return AudioStream::CreateCubebAudioStream();
 
     case AudioBackend::Default:
-    default:
-      m_audio_stream = std::make_unique<SDLAudioStream>();
-      break;
-  }
+      return std::make_unique<SDLAudioStream>();
 
-  if (!m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS))
-  {
-    ReportError("Failed to recreate audio stream, falling back to null");
-    m_audio_stream.reset();
-    m_audio_stream = AudioStream::CreateNullAudioStream();
-    if (!m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS))
-      Panic("Failed to reconfigure null audio stream");
+    default:
+      return nullptr;
   }
 }
 
+void SDLHostInterface::OnSystemCreated()
+{
+  HostInterface::OnSystemCreated();
+
+  UpdateKeyboardControllerMapping();
+  UpdateControllerControllerMapping();
+  ClearImGuiFocus();
+}
+
+void SDLHostInterface::OnSystemPaused(bool paused)
+{
+  HostInterface::OnSystemPaused(paused);
+
+  if (!paused)
+    ClearImGuiFocus();
+}
+
+void SDLHostInterface::OnSystemDestroyed()
+{
+  HostInterface::OnSystemDestroyed();
+}
+
 void SDLHostInterface::SaveSettings()
 {
   SDLSettingsInterface si(GetSettingsFileName().c_str());
   m_settings.Save(si);
 }
 
-void SDLHostInterface::QueueSwitchGPURenderer()
+void SDLHostInterface::QueueUpdateSettings()
 {
   SDL_Event ev = {};
   ev.type = SDL_USEREVENT;
-  ev.user.code = m_switch_gpu_renderer_event_id;
+  ev.user.code = m_update_settings_event_id;
   SDL_PushEvent(&ev);
 }
 
-void SDLHostInterface::SwitchGPURenderer()
-{
-  // Due to the GPU class owning textures, we have to shut the system down.
-  std::unique_ptr<ByteStream> stream;
-  if (m_system)
-  {
-    stream = ByteStream_CreateGrowableMemoryStream(nullptr, 8 * 1024);
-    if (!m_system->SaveState(stream.get()) || !stream->SeekAbsolute(0))
-      ReportError("Failed to save state before GPU renderer switch");
-
-    DestroySystem();
-  }
-
-  ImGui::EndFrame();
-  DestroyDisplay();
-  DestroySDLWindow();
-
-  if (!CreateSDLWindow())
-    Panic("Failed to recreate SDL window on GPU renderer switch");
-
-  if (!CreateDisplay())
-    Panic("Failed to recreate display on GPU renderer switch");
-
-  ImGui::NewFrame();
-
-  if (stream)
-  {
-    CreateSystem();
-    if (!BootSystem(nullptr, nullptr) || !m_system->LoadState(stream.get()))
-    {
-      ReportError("Failed to load state after GPU renderer switch, resetting");
-      m_system->Reset();
-    }
-  }
-
-  UpdateFullscreen();
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
-}
-
-void SDLHostInterface::SwitchAudioBackend()
-{
-  m_audio_stream.reset();
-  CreateAudioStream();
-
-  if (m_system)
-  {
-    m_audio_stream->PauseOutput(false);
-    UpdateSpeedLimiterState();
-  }
-}
-
 void SDLHostInterface::UpdateFullscreen()
 {
   SDL_SetWindowFullscreen(m_window, m_settings.display_fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
@@ -227,15 +223,7 @@ void SDLHostInterface::UpdateFullscreen()
     m_settings.display_fullscreen ? 0 : static_cast<int>(20.0f * ImGui::GetIO().DisplayFramebufferScale.x));
 }
 
-void SDLHostInterface::UpdateControllerMapping()
-{
-  UpdateKeyboardControllerMapping();
-  UpdateControllerControllerMapping();
-}
-
-std::unique_ptr<SDLHostInterface> SDLHostInterface::Create(const char* filename /* = nullptr */,
-                                                           const char* exp1_filename /* = nullptr */,
-                                                           const char* save_state_filename /* = nullptr */)
+std::unique_ptr<SDLHostInterface> SDLHostInterface::Create()
 {
   std::unique_ptr<SDLHostInterface> intf = std::make_unique<SDLHostInterface>();
 
@@ -256,34 +244,13 @@ std::unique_ptr<SDLHostInterface> SDLHostInterface::Create(const char* filename
     return nullptr;
   }
 
-  intf->CreateAudioStream();
-
   ImGui::NewFrame();
 
-  intf->UpdateSpeedLimiterState();
-
-  const bool boot = (filename != nullptr || exp1_filename != nullptr || save_state_filename != nullptr);
-  if (boot)
-  {
-    if (!intf->CreateSystem() || !intf->BootSystem(filename, exp1_filename))
-      return nullptr;
-
-    if (save_state_filename)
-      intf->LoadState(save_state_filename);
-
-    intf->UpdateControllerMapping();
-  }
-
   intf->UpdateFullscreen();
 
   return intf;
 }
 
-std::string SDLHostInterface::GetSaveStateFilename(u32 index)
-{
-  return StringUtil::StdStringFromFormat("savestate_%u.bin", index);
-}
-
 void SDLHostInterface::ReportError(const char* message)
 {
   SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DuckStation Error", message, m_window);
@@ -291,7 +258,7 @@ void SDLHostInterface::ReportError(const char* message)
 
 void SDLHostInterface::ReportMessage(const char* message)
 {
-  SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "DuckStation Information", message, m_window);
+  AddOSDMessage(message, 2.0f);
 }
 
 void SDLHostInterface::HandleSDLEvent(const SDL_Event* event)
@@ -352,8 +319,13 @@ void SDLHostInterface::HandleSDLEvent(const SDL_Event* event)
 
     case SDL_USEREVENT:
     {
-      if (static_cast<u32>(event->user.code) == m_switch_gpu_renderer_event_id)
-        SwitchGPURenderer();
+      if (static_cast<u32>(event->user.code) == m_update_settings_event_id)
+      {
+        UpdateSettings([this]() {
+          SDLSettingsInterface si(GetSettingsFileName().c_str());
+          m_settings.Load(si);
+        });
+      }
     }
     break;
   }
@@ -381,9 +353,9 @@ void SDLHostInterface::HandleSDLKeyEvent(const SDL_Event* event)
       {
         const u32 index = event->key.keysym.scancode - SDL_SCANCODE_F1 + 1;
         if (event->key.keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT))
-          DoSaveState(index);
+          SaveState(true, index);
         else
-          DoLoadState(index);
+          LoadState(true, index);
       }
     }
     break;
@@ -408,7 +380,7 @@ void SDLHostInterface::HandleSDLKeyEvent(const SDL_Event* event)
     case SDL_SCANCODE_PAUSE:
     {
       if (pressed)
-        DoTogglePause();
+        PauseSystem(!m_paused);
     }
     break;
 
@@ -792,51 +764,78 @@ void SDLHostInterface::DrawMainMenuBar()
   if (ImGui::BeginMenu("System"))
   {
     if (ImGui::MenuItem("Start Disc", nullptr, false, !system_enabled))
+    {
       DoStartDisc();
+      ClearImGuiFocus();
+    }
     if (ImGui::MenuItem("Start BIOS", nullptr, false, !system_enabled))
-      DoStartBIOS();
+    {
+      BootSystemFromBIOS();
+      ClearImGuiFocus();
+    }
 
     ImGui::Separator();
 
     if (ImGui::MenuItem("Power Off", nullptr, false, system_enabled))
-      DoPowerOff();
+    {
+      DestroySystem();
+      ClearImGuiFocus();
+    }
 
     if (ImGui::MenuItem("Reset", nullptr, false, system_enabled))
+    {
       ResetSystem();
+      ClearImGuiFocus();
+    }
 
     if (ImGui::MenuItem("Pause", nullptr, m_paused, system_enabled))
-      DoTogglePause();
+    {
+      PauseSystem(!m_paused);
+      ClearImGuiFocus();
+    }
 
     ImGui::Separator();
 
     if (ImGui::MenuItem("Change Disc", nullptr, false, system_enabled))
+    {
       DoChangeDisc();
+      ClearImGuiFocus();
+    }
 
     if (ImGui::MenuItem("Frame Step", nullptr, false, system_enabled))
+    {
       DoFrameStep();
+      ClearImGuiFocus();
+    }
 
     ImGui::Separator();
 
     if (ImGui::BeginMenu("Load State"))
     {
-      for (u32 i = 1; i <= NUM_QUICK_SAVE_STATES; i++)
+      for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
       {
         char buf[16];
         std::snprintf(buf, sizeof(buf), "State %u", i);
         if (ImGui::MenuItem(buf))
-          DoLoadState(i);
+        {
+          LoadState(true, i);
+          ClearImGuiFocus();
+        }
       }
       ImGui::EndMenu();
     }
 
     if (ImGui::BeginMenu("Save State", system_enabled))
     {
-      for (u32 i = 1; i <= NUM_QUICK_SAVE_STATES; i++)
+      for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
       {
         char buf[16];
         std::snprintf(buf, sizeof(buf), "State %u", i);
         if (ImGui::MenuItem(buf))
-          DoSaveState(i);
+        {
+          SaveState(true, i);
+          ClearImGuiFocus();
+        }
       }
       ImGui::EndMenu();
     }
@@ -916,12 +915,7 @@ void SDLHostInterface::DrawMainMenuBar()
 void SDLHostInterface::DrawQuickSettingsMenu()
 {
   bool settings_changed = false;
-  bool gpu_settings_changed = false;
-  if (ImGui::MenuItem("Enable Speed Limiter", nullptr, &m_settings.speed_limiter_enabled))
-  {
-    settings_changed = true;
-    UpdateSpeedLimiterState();
-  }
+  settings_changed |= ImGui::MenuItem("Enable Speed Limiter", nullptr, &m_settings.speed_limiter_enabled);
 
   ImGui::Separator();
 
@@ -935,8 +929,6 @@ void SDLHostInterface::DrawQuickSettingsMenu()
       {
         m_settings.cpu_execution_mode = static_cast<CPUExecutionMode>(i);
         settings_changed = true;
-        if (m_system)
-          m_system->SetCPUExecutionMode(m_settings.cpu_execution_mode);
       }
     }
 
@@ -955,7 +947,6 @@ void SDLHostInterface::DrawQuickSettingsMenu()
       {
         m_settings.gpu_renderer = static_cast<GPURenderer>(i);
         settings_changed = true;
-        QueueSwitchGPURenderer();
       }
     }
 
@@ -968,11 +959,7 @@ void SDLHostInterface::DrawQuickSettingsMenu()
     UpdateFullscreen();
   }
 
-  if (ImGui::MenuItem("VSync", nullptr, &m_settings.video_sync_enabled))
-  {
-    settings_changed = true;
-    UpdateSpeedLimiterState();
-  }
+  settings_changed |= ImGui::MenuItem("VSync", nullptr, &m_settings.video_sync_enabled);
 
   ImGui::Separator();
 
@@ -987,26 +974,22 @@ void SDLHostInterface::DrawQuickSettingsMenu()
       if (ImGui::MenuItem(buf, nullptr, current_internal_resolution == scale))
       {
         m_settings.gpu_resolution_scale = scale;
-        gpu_settings_changed = true;
+        settings_changed = true;
       }
     }
 
     ImGui::EndMenu();
   }
 
-  gpu_settings_changed |= ImGui::MenuItem("True (24-Bit) Color", nullptr, &m_settings.gpu_true_color);
-  gpu_settings_changed |= ImGui::MenuItem("Texture Filtering", nullptr, &m_settings.gpu_texture_filtering);
-  if (ImGui::MenuItem("Display Linear Filtering", nullptr, &m_settings.display_linear_filtering))
+  settings_changed |= ImGui::MenuItem("True (24-Bit) Color", nullptr, &m_settings.gpu_true_color);
+  settings_changed |= ImGui::MenuItem("Texture Filtering", nullptr, &m_settings.gpu_texture_filtering);
+  settings_changed |= ImGui::MenuItem("Display Linear Filtering", nullptr, &m_settings.display_linear_filtering);
+
+  if (settings_changed)
   {
-    m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
-    settings_changed = true;
-  }
-
-  if (settings_changed || gpu_settings_changed)
     SaveSettings();
-
-  if (gpu_settings_changed && m_system)
-    m_system->GetGPU()->UpdateSettings();
+    QueueUpdateSettings();
+  }
 }
 
 void SDLHostInterface::DrawDebugMenu()
@@ -1068,18 +1051,23 @@ void SDLHostInterface::DrawPoweredOffWindow()
   ImGui::PushStyleColor(ImGuiCol_ButtonHovered, 0xFF575757);
 
   ImGui::SetCursorPosX(button_left);
-  if (ImGui::Button("Resume", button_size))
-    DoResume();
+  ImGui::Button("Resume", button_size);
   ImGui::NewLine();
 
   ImGui::SetCursorPosX(button_left);
   if (ImGui::Button("Start Disc", button_size))
+  {
     DoStartDisc();
+    ClearImGuiFocus();
+  }
   ImGui::NewLine();
 
   ImGui::SetCursorPosX(button_left);
   if (ImGui::Button("Start BIOS", button_size))
-    DoStartBIOS();
+  {
+    BootSystemFromFile(nullptr);
+    ClearImGuiFocus();
+  }
   ImGui::NewLine();
 
   ImGui::SetCursorPosX(button_left);
@@ -1087,12 +1075,15 @@ void SDLHostInterface::DrawPoweredOffWindow()
     ImGui::OpenPopup("PowerOffWindow_LoadStateMenu");
   if (ImGui::BeginPopup("PowerOffWindow_LoadStateMenu"))
   {
-    for (u32 i = 1; i <= NUM_QUICK_SAVE_STATES; i++)
+    for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
     {
       char buf[16];
       std::snprintf(buf, sizeof(buf), "State %u", i);
       if (ImGui::MenuItem(buf))
-        DoLoadState(i);
+      {
+        LoadState(true, i);
+        ClearImGuiFocus();
+      }
     }
     ImGui::EndPopup();
   }
@@ -1133,7 +1124,6 @@ void SDLHostInterface::DrawSettingsWindow()
   }
 
   bool settings_changed = false;
-  bool gpu_settings_changed = false;
 
   if (ImGui::BeginTabBar("SettingsTabBar", 0))
   {
@@ -1173,19 +1163,8 @@ void SDLHostInterface::DrawSettingsWindow()
         ImGui::Text("Emulation Speed:");
         ImGui::SameLine(indent);
 
-        if (ImGui::SliderFloat("##speed", &m_settings.emulation_speed, 0.25f, 5.0f))
-        {
-          settings_changed = true;
-          if (m_system)
-            m_system->UpdateThrottlePeriod();
-        }
-
-        if (ImGui::Checkbox("Enable Speed Limiter", &m_settings.speed_limiter_enabled))
-        {
-          settings_changed = true;
-          UpdateSpeedLimiterState();
-        }
-
+        settings_changed |= ImGui::SliderFloat("##speed", &m_settings.emulation_speed, 0.25f, 5.0f);
+        settings_changed |= ImGui::Checkbox("Enable Speed Limiter", &m_settings.speed_limiter_enabled);
         settings_changed |= ImGui::Checkbox("Pause On Start", &m_settings.start_paused);
       }
 
@@ -1206,14 +1185,9 @@ void SDLHostInterface::DrawSettingsWindow()
         {
           m_settings.audio_backend = static_cast<AudioBackend>(backend);
           settings_changed = true;
-          SwitchAudioBackend();
         }
 
-        if (ImGui::Checkbox("Output Sync", &m_settings.audio_sync_enabled))
-        {
-          settings_changed = true;
-          UpdateSpeedLimiterState();
-        }
+        settings_changed |= ImGui::Checkbox("Output Sync", &m_settings.audio_sync_enabled);
       }
 
       ImGui::EndTabItem();
@@ -1242,11 +1216,6 @@ void SDLHostInterface::DrawSettingsWindow()
           {
             m_settings.controller_types[i] = static_cast<ControllerType>(controller_type);
             settings_changed = true;
-            if (m_system)
-            {
-              m_system->UpdateControllers();
-              UpdateControllerControllerMapping();
-            }
           }
         }
 
@@ -1255,19 +1224,12 @@ void SDLHostInterface::DrawSettingsWindow()
 
         std::string* path_ptr = &m_settings.memory_card_paths[i];
         std::snprintf(buf, sizeof(buf), "##memcard_%c_path", 'a' + i);
-        if (DrawFileChooser(buf, path_ptr))
-        {
-          settings_changed = true;
-          if (m_system)
-            m_system->UpdateMemoryCards();
-        }
+        settings_changed |= DrawFileChooser(buf, path_ptr);
 
         if (ImGui::Button("Eject Memory Card"))
         {
           path_ptr->clear();
           settings_changed = true;
-          if (m_system)
-            m_system->UpdateMemoryCards();
         }
 
         ImGui::NewLine();
@@ -1292,8 +1254,6 @@ void SDLHostInterface::DrawSettingsWindow()
       {
         m_settings.cpu_execution_mode = static_cast<CPUExecutionMode>(execution_mode);
         settings_changed = true;
-        if (m_system)
-          m_system->SetCPUExecutionMode(m_settings.cpu_execution_mode);
       }
 
       ImGui::EndTabItem();
@@ -1317,7 +1277,6 @@ void SDLHostInterface::DrawSettingsWindow()
         {
           m_settings.gpu_renderer = static_cast<GPURenderer>(gpu_renderer);
           settings_changed = true;
-          QueueSwitchGPURenderer();
         }
       }
 
@@ -1331,17 +1290,8 @@ void SDLHostInterface::DrawSettingsWindow()
           settings_changed = true;
         }
 
-        if (ImGui::Checkbox("Linear Filtering", &m_settings.display_linear_filtering))
-        {
-          m_display->SetDisplayLinearFiltering(m_settings.display_linear_filtering);
-          settings_changed = true;
-        }
-
-        if (ImGui::Checkbox("VSync", &m_settings.video_sync_enabled))
-        {
-          settings_changed = true;
-          UpdateSpeedLimiterState();
-        }
+        settings_changed |= ImGui::Checkbox("Linear Filtering", &m_settings.display_linear_filtering);
+        settings_changed |= ImGui::Checkbox("VSync", &m_settings.video_sync_enabled);
       }
 
       ImGui::NewLine();
@@ -1375,12 +1325,12 @@ void SDLHostInterface::DrawSettingsWindow()
                          static_cast<int>(resolutions.size())))
         {
           m_settings.gpu_resolution_scale = static_cast<u32>(current_resolution_index + 1);
-          gpu_settings_changed = true;
+          settings_changed = true;
         }
 
-        gpu_settings_changed |= ImGui::Checkbox("True 24-bit Color (disables dithering)", &m_settings.gpu_true_color);
-        gpu_settings_changed |= ImGui::Checkbox("Texture Filtering", &m_settings.gpu_texture_filtering);
-        gpu_settings_changed |= ImGui::Checkbox("Force Progressive Scan", &m_settings.gpu_force_progressive_scan);
+        settings_changed |= ImGui::Checkbox("True 24-bit Color (disables dithering)", &m_settings.gpu_true_color);
+        settings_changed |= ImGui::Checkbox("Texture Filtering", &m_settings.gpu_texture_filtering);
+        settings_changed |= ImGui::Checkbox("Force Progressive Scan", &m_settings.gpu_force_progressive_scan);
       }
 
       ImGui::EndTabItem();
@@ -1397,11 +1347,11 @@ void SDLHostInterface::DrawSettingsWindow()
 
   ImGui::End();
 
-  if (settings_changed || gpu_settings_changed)
+  if (settings_changed)
+  {
     SaveSettings();
-
-  if (gpu_settings_changed && m_system)
-    m_system->GetGPU()->UpdateSettings();
+    QueueUpdateSettings();
+  }
 }
 
 void SDLHostInterface::DrawAboutWindow()
@@ -1454,26 +1404,9 @@ bool SDLHostInterface::DrawFileChooser(const char* label, std::string* path, con
   return result;
 }
 
-void SDLHostInterface::DoPowerOff()
+void SDLHostInterface::ClearImGuiFocus()
 {
-  Assert(m_system);
-  DestroySystem();
-  AddOSDMessage("System powered off.");
-}
-
-void SDLHostInterface::DoResume()
-{
-  Assert(!m_system);
-  if (!CreateSystem() || !BootSystem(nullptr, RESUME_SAVESTATE_FILENAME))
-  {
-    DestroySystem();
-    return;
-  }
-
-  UpdateControllerMapping();
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
+  ImGui::SetWindowFocus(nullptr);
 }
 
 void SDLHostInterface::DoStartDisc()
@@ -1485,33 +1418,7 @@ void SDLHostInterface::DoStartDisc()
     return;
 
   AddFormattedOSDMessage(2.0f, "Starting disc from '%s'...", path);
-  if (!CreateSystem() || !BootSystem(path, nullptr))
-  {
-    DestroySystem();
-    return;
-  }
-
-  UpdateControllerMapping();
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
-}
-
-void SDLHostInterface::DoStartBIOS()
-{
-  Assert(!m_system);
-
-  AddOSDMessage("Starting BIOS...");
-  if (!CreateSystem() || !BootSystem(nullptr, nullptr))
-  {
-    DestroySystem();
-    return;
-  }
-
-  UpdateControllerMapping();
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
+  BootSystemFromFile(path);
 }
 
 void SDLHostInterface::DoChangeDisc()
@@ -1527,47 +1434,7 @@ void SDLHostInterface::DoChangeDisc()
   else
     AddOSDMessage("Failed to switch CD. The log may contain further information.");
 
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
-}
-
-void SDLHostInterface::DoLoadState(u32 index)
-{
-  if (HasSystem())
-  {
-    LoadState(GetSaveStateFilename(index).c_str());
-  }
-  else
-  {
-    if (!CreateSystem() || !BootSystem(nullptr, GetSaveStateFilename(index).c_str()))
-    {
-      DestroySystem();
-      return;
-    }
-  }
-
-  UpdateControllerMapping();
-  if (m_system)
-    m_system->ResetPerformanceCounters();
-  ClearImGuiFocus();
-}
-
-void SDLHostInterface::DoSaveState(u32 index)
-{
-  Assert(m_system);
-  SaveState(GetSaveStateFilename(index).c_str());
-  ClearImGuiFocus();
-}
-
-void SDLHostInterface::DoTogglePause()
-{
-  if (!m_system)
-    return;
-
-  m_paused = !m_paused;
-  if (!m_paused)
-    m_system->ResetPerformanceCounters();
+  m_system->ResetPerformanceCounters();
 }
 
 void SDLHostInterface::DoFrameStep()
@@ -1587,8 +1454,6 @@ void SDLHostInterface::DoToggleFullscreen()
 
 void SDLHostInterface::Run()
 {
-  m_audio_stream->PauseOutput(false);
-
   while (!m_quit_request)
   {
     for (;;)
@@ -1636,10 +1501,5 @@ void SDLHostInterface::Run()
 
   // Save state on exit so it can be resumed
   if (m_system)
-  {
-    if (!SaveState(RESUME_SAVESTATE_FILENAME))
-      ReportError("Saving state failed, you will not be able to resume this session.");
-
     DestroySystem();
-  }
 }
diff --git a/src/duckstation-sdl/sdl_host_interface.h b/src/duckstation-sdl/sdl_host_interface.h
index d7933438e..14f55c114 100644
--- a/src/duckstation-sdl/sdl_host_interface.h
+++ b/src/duckstation-sdl/sdl_host_interface.h
@@ -22,16 +22,22 @@ public:
   SDLHostInterface();
   ~SDLHostInterface();
 
-  static std::unique_ptr<SDLHostInterface> Create(const char* filename = nullptr, const char* exp1_filename = nullptr,
-                                                  const char* save_state_filename = nullptr);
-
-  static std::string GetSaveStateFilename(u32 index);
+  static std::unique_ptr<SDLHostInterface> Create();
 
   void ReportError(const char* message) override;
   void ReportMessage(const char* message) override;
 
   void Run();
 
+protected:
+  bool AcquireHostDisplay() override;
+  void ReleaseHostDisplay() override;
+  std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
+
+  void OnSystemCreated() override;
+  void OnSystemPaused(bool paused) override;
+  void OnSystemDestroyed();
+
 private:
   enum class KeyboardControllerAction
   {
@@ -62,9 +68,6 @@ private:
     float last_rumble_strength;
   };
 
-  static constexpr u32 NUM_QUICK_SAVE_STATES = 10;
-  static constexpr char RESUME_SAVESTATE_FILENAME[] = "savestate_resume.bin";
-
   bool HasSystem() const { return static_cast<bool>(m_system); }
 
 #ifdef WIN32
@@ -78,26 +81,16 @@ private:
   bool CreateDisplay();
   void DestroyDisplay();
   void CreateImGuiContext();
-  void CreateAudioStream();
 
   void SaveSettings();
+  void QueueUpdateSettings();
 
-  void QueueSwitchGPURenderer();
-  void SwitchGPURenderer();
-  void SwitchAudioBackend();
   void UpdateFullscreen();
-  void UpdateControllerMapping();
 
   // We only pass mouse input through if it's grabbed
   void DrawImGui();
-  void DoPowerOff();
-  void DoResume();
   void DoStartDisc();
-  void DoStartBIOS();
   void DoChangeDisc();
-  void DoLoadState(u32 index);
-  void DoSaveState(u32 index);
-  void DoTogglePause();
   void DoFrameStep();
   void DoToggleFullscreen();
 
@@ -122,6 +115,7 @@ private:
   void DrawSettingsWindow();
   void DrawAboutWindow();
   bool DrawFileChooser(const char* label, std::string* path, const char* filter = nullptr);
+  void ClearImGuiFocus();
 
   SDL_Window* m_window = nullptr;
   std::unique_ptr<HostDisplayTexture> m_app_icon_texture;
@@ -132,7 +126,7 @@ private:
   std::array<s32, SDL_CONTROLLER_AXIS_MAX> m_controller_axis_mapping{};
   std::array<s32, SDL_CONTROLLER_BUTTON_MAX> m_controller_button_mapping{};
 
-  u32 m_switch_gpu_renderer_event_id = 0;
+  u32 m_update_settings_event_id = 0;
 
   bool m_quit_request = false;
   bool m_frame_step_request = false;