mirror of
				https://github.com/RetroDECK/Duckstation.git
				synced 2025-04-10 19:15:14 +00:00 
			
		
		
		
	Qt: Safer GPU renderer switching
This commit is contained in:
		
							parent
							
								
									c5282b99e1
								
							
						
					
					
						commit
						d343743768
					
				|  | @ -6,6 +6,7 @@ | |||
| #include "qtsettingsinterface.h" | ||||
| #include "settingsdialog.h" | ||||
| #include <QtWidgets/QFileDialog> | ||||
| #include <QtWidgets/QMessageBox> | ||||
| #include <cmath> | ||||
| 
 | ||||
| static constexpr char DISC_IMAGE_FILTER[] = | ||||
|  | @ -76,26 +77,13 @@ void MainWindow::toggleFullscreen() | |||
|   m_ui.actionFullscreen->setChecked(fullscreen); | ||||
| } | ||||
| 
 | ||||
| void MainWindow::switchRenderer() | ||||
| void MainWindow::recreateDisplayWidget(bool create_device_context) | ||||
| { | ||||
|   const bool was_fullscreen = m_display_widget->isFullScreen(); | ||||
|   if (was_fullscreen) | ||||
|     toggleFullscreen(); | ||||
| 
 | ||||
|   QByteArray state; | ||||
|   if (m_emulation_running) | ||||
|   { | ||||
|     // we need to basically restart the emulator core
 | ||||
|     state = m_host_interface->saveStateToMemory(); | ||||
|     if (state.isEmpty()) | ||||
|     { | ||||
|       m_host_interface->ReportError("Failed to save emulator state to memory"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // stop the emulation
 | ||||
|     m_host_interface->blockingPowerOffSystem(); | ||||
|   } | ||||
|   switchToGameListView(); | ||||
| 
 | ||||
|   // recreate the display widget using the potentially-new renderer
 | ||||
|   m_ui.mainContainer->removeWidget(m_display_widget); | ||||
|  | @ -104,25 +92,21 @@ void MainWindow::switchRenderer() | |||
|   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 (!state.isEmpty()) | ||||
|   if (create_device_context && !m_host_interface->createDisplayDeviceContext()) | ||||
|   { | ||||
|     // restart the system with the new state
 | ||||
|     m_host_interface->bootSystem(QString(), QString()); | ||||
|     m_host_interface->loadStateFromMemory(std::move(state)); | ||||
|     QMessageBox::critical(this, tr("DuckStation Error"), | ||||
|                           tr("Failed to create new device context on renderer switch. Cannot continue.")); | ||||
|     QCoreApplication::exit(); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // update the menu with the selected renderer
 | ||||
|   QObjectList renderer_menu_items = m_ui.menuRenderer->children(); | ||||
|   QString current_renderer_name(Settings::GetRendererDisplayName(m_host_interface->GetCoreSettings().gpu_renderer)); | ||||
|   for (QObject* obj : renderer_menu_items) | ||||
|   { | ||||
|     QAction* action = qobject_cast<QAction*>(obj); | ||||
|     if (action) | ||||
|       action->setChecked(action->text() == current_renderer_name); | ||||
|   } | ||||
|   updateDebugMenuGPURenderer(); | ||||
| } | ||||
| 
 | ||||
| void MainWindow::onPerformanceCountersUpdated(float speed, float fps, float vps, float average_frame_time, | ||||
|  | @ -214,14 +198,12 @@ void MainWindow::setupAdditionalUi() | |||
|     const GPURenderer renderer = static_cast<GPURenderer>(i); | ||||
|     QAction* action = m_ui.menuRenderer->addAction(tr(Settings::GetRendererDisplayName(renderer))); | ||||
|     action->setCheckable(true); | ||||
|     action->setChecked(m_host_interface->GetCoreSettings().gpu_renderer == renderer); | ||||
|     connect(action, &QAction::triggered, [this, action, renderer]() { | ||||
|       m_host_interface->putSettingValue(QStringLiteral("GPU/Renderer"), QString(Settings::GetRendererName(renderer))); | ||||
|       m_host_interface->applySettings(); | ||||
|       action->setChecked(true); | ||||
|       switchRenderer(); | ||||
|     }); | ||||
|   } | ||||
|   updateDebugMenuGPURenderer(); | ||||
| } | ||||
| 
 | ||||
| void MainWindow::updateEmulationActions(bool starting, bool running) | ||||
|  | @ -307,6 +289,8 @@ void MainWindow::connectSignals() | |||
|   connect(m_host_interface, &QtHostInterface::toggleFullscreenRequested, this, &MainWindow::toggleFullscreen); | ||||
|   connect(m_host_interface, &QtHostInterface::performanceCountersUpdated, this, | ||||
|           &MainWindow::onPerformanceCountersUpdated); | ||||
|   connect(m_host_interface, &QtHostInterface::recreateDisplayWidgetRequested, this, &MainWindow::recreateDisplayWidget, | ||||
|           Qt::BlockingQueuedConnection); | ||||
| 
 | ||||
|   connect(m_game_list_widget, &GameListWidget::bootEntryRequested, [this](const GameList::GameListEntry* entry) { | ||||
|     // if we're not running, boot the system, otherwise swap discs
 | ||||
|  | @ -347,3 +331,21 @@ void MainWindow::doSettings(SettingsDialog::Category category) | |||
|   if (category != SettingsDialog::Category::Count) | ||||
|     m_settings_dialog->setCategory(category); | ||||
| } | ||||
| 
 | ||||
| void MainWindow::updateDebugMenuGPURenderer() | ||||
| { | ||||
|   // update the menu with the new selected renderer
 | ||||
|   std::optional<GPURenderer> current_renderer = Settings::ParseRendererName( | ||||
|     m_host_interface->getSettingValue(QStringLiteral("GPU/Renderer")).toString().toStdString().c_str()); | ||||
|   if (current_renderer.has_value()) | ||||
|   { | ||||
|     const QString current_renderer_display_name( | ||||
|       QString::fromUtf8(Settings::GetRendererDisplayName(current_renderer.value()))); | ||||
|     for (QObject* obj : m_ui.menuRenderer->children()) | ||||
|     { | ||||
|       QAction* action = qobject_cast<QAction*>(obj); | ||||
|       if (action) | ||||
|         action->setChecked(action->text() == current_renderer_display_name); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ private Q_SLOTS: | |||
|   void onEmulationStopped(); | ||||
|   void onEmulationPaused(bool paused); | ||||
|   void toggleFullscreen(); | ||||
|   void switchRenderer(); | ||||
|   void recreateDisplayWidget(bool create_device_context); | ||||
|   void onPerformanceCountersUpdated(float speed, float fps, float vps, float average_frame_time, | ||||
|                                     float worst_frame_time); | ||||
| 
 | ||||
|  | @ -48,6 +48,7 @@ private: | |||
|   void switchToGameListView(); | ||||
|   void switchToEmulationView(); | ||||
|   void doSettings(SettingsDialog::Category category = SettingsDialog::Category::Count); | ||||
|   void updateDebugMenuGPURenderer(); | ||||
| 
 | ||||
|   Ui::MainWindow m_ui; | ||||
| 
 | ||||
|  |  | |||
|  | @ -105,6 +105,7 @@ void QtHostInterface::applySettings() | |||
|   } | ||||
| 
 | ||||
|   // TODO: Should we move this to the base class?
 | ||||
|   const GPURenderer old_gpu_renderer = m_settings.gpu_renderer; | ||||
|   const bool old_vsync_enabled = m_settings.video_sync_enabled; | ||||
|   const bool old_audio_sync_enabled = m_settings.audio_sync_enabled; | ||||
|   const bool old_speed_limiter_enabled = m_settings.speed_limiter_enabled; | ||||
|  | @ -120,6 +121,10 @@ void QtHostInterface::applySettings() | |||
|   { | ||||
|     UpdateSpeedLimiterState(); | ||||
|   } | ||||
| 
 | ||||
|   // TODO: Fast path for hardware->software switches
 | ||||
|   if (m_settings.gpu_renderer != old_gpu_renderer) | ||||
|     switchGPURenderer(); | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::checkSettings() | ||||
|  | @ -180,6 +185,11 @@ QWidget* QtHostInterface::createDisplayWidget(QWidget* parent) | |||
|   return QWidget::createWindowContainer(m_display_window, parent); | ||||
| } | ||||
| 
 | ||||
| bool QtHostInterface::createDisplayDeviceContext() | ||||
| { | ||||
|   return m_display_window->createDeviceContext(m_worker_thread, m_settings.gpu_use_debug_device); | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::displayWidgetDestroyed() | ||||
| { | ||||
|   m_display.release(); | ||||
|  | @ -191,7 +201,7 @@ void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_ | |||
|   Assert(!isOnWorkerThread()); | ||||
|   emit emulationStarting(); | ||||
| 
 | ||||
|   if (!m_display_window->createDeviceContext(m_worker_thread, m_settings.gpu_use_debug_device)) | ||||
|   if (!createDisplayDeviceContext()) | ||||
|   { | ||||
|     emit emulationStopped(); | ||||
|     return; | ||||
|  | @ -383,17 +393,6 @@ void QtHostInterface::powerOffSystem() | |||
|   emit emulationStopped(); | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::blockingPowerOffSystem() | ||||
| { | ||||
|   if (!isOnWorkerThread()) | ||||
|   { | ||||
|     QMetaObject::invokeMethod(this, "powerOffSystem", Qt::BlockingQueuedConnection); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   powerOffSystem(); | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::resetSystem() | ||||
| { | ||||
|   if (!isOnWorkerThread()) | ||||
|  | @ -428,44 +427,6 @@ void QtHostInterface::pauseSystem(bool paused) | |||
| 
 | ||||
| void QtHostInterface::changeDisc(QString new_disc_filename) {} | ||||
| 
 | ||||
| void QtHostInterface::loadStateFromMemory(QByteArray arr) | ||||
| { | ||||
|   if (!isOnWorkerThread()) | ||||
|   { | ||||
|     QMetaObject::invokeMethod(this, "loadStateFromMemory", Qt::QueuedConnection, Q_ARG(QByteArray, arr)); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   std::unique_ptr<ByteStream> stream = ByteStream_CreateGrowableMemoryStream(); | ||||
|   if (!m_system || !QtUtils::WriteQByteArrayToStream(arr, stream.get()) || !stream->SeekAbsolute(0) || | ||||
|       !m_system->LoadState(stream.get())) | ||||
|   { | ||||
|     Log_ErrorPrintf("Failed to load memory state"); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| QByteArray QtHostInterface::saveStateToMemory() | ||||
| { | ||||
|   if (!isOnWorkerThread()) | ||||
|   { | ||||
|     QByteArray return_value; | ||||
|     QMetaObject::invokeMethod(this, "saveStateToMemory", Qt::BlockingQueuedConnection, | ||||
|                               Q_RETURN_ARG(QByteArray, return_value)); | ||||
|     return return_value; | ||||
|   } | ||||
| 
 | ||||
|   QByteArray ret; | ||||
|   if (!m_system) | ||||
|     return {}; | ||||
| 
 | ||||
|   std::unique_ptr<ByteStream> stream = ByteStream_CreateGrowableMemoryStream(); | ||||
|   if (m_system->SaveState(stream.get())) | ||||
|     return QtUtils::ReadStreamToQByteArray(stream.get(), true); | ||||
|   else | ||||
|     return {}; | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename) | ||||
| { | ||||
|   if (!m_display_window->initializeDeviceContext(m_settings.gpu_use_debug_device)) | ||||
|  | @ -517,6 +478,50 @@ void QtHostInterface::createAudioStream() | |||
|   } | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::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(); | ||||
|     m_audio_stream->PauseOutput(true); | ||||
|     m_display_window->destroyDeviceContext(); | ||||
|   } | ||||
| 
 | ||||
|   const bool restore_state = static_cast<bool>(stream); | ||||
|   emit recreateDisplayWidgetRequested(restore_state); | ||||
|   Assert(m_display_window != nullptr); | ||||
| 
 | ||||
|   if (restore_state) | ||||
|   { | ||||
|     if (!m_display_window->initializeDeviceContext(m_settings.gpu_use_debug_device)) | ||||
|     { | ||||
|       emit emulationStopped(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     CreateSystem(); | ||||
|     if (!BootSystem(nullptr, nullptr) || !m_system->LoadState(stream.get())) | ||||
|     { | ||||
|       ReportError("Failed to load state after GPU renderer switch, resetting"); | ||||
|       m_system->Reset(); | ||||
|     } | ||||
| 
 | ||||
|     if (!m_paused) | ||||
|     { | ||||
|       m_audio_stream->PauseOutput(false); | ||||
|       UpdateSpeedLimiterState(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ResetPerformanceCounters(); | ||||
| } | ||||
| 
 | ||||
| void QtHostInterface::createThread() | ||||
| { | ||||
|   m_original_thread = QThread::currentThread(); | ||||
|  |  | |||
|  | @ -49,10 +49,10 @@ 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); | ||||
|   void blockingPowerOffSystem(); | ||||
| 
 | ||||
|   void updateInputMap(); | ||||
|   void handleKeyEvent(int key, bool pressed); | ||||
|  | @ -72,7 +72,7 @@ Q_SIGNALS: | |||
|   void emulationPaused(bool paused); | ||||
|   void gameListRefreshed(); | ||||
|   void toggleFullscreenRequested(); | ||||
|   void switchRendererRequested(); | ||||
|   void recreateDisplayWidgetRequested(bool create_device_context); | ||||
|   void performanceCountersUpdated(float speed, float fps, float vps, float avg_frame_time, float worst_frame_time); | ||||
| 
 | ||||
| public Q_SLOTS: | ||||
|  | @ -81,8 +81,6 @@ public Q_SLOTS: | |||
|   void resetSystem(); | ||||
|   void pauseSystem(bool paused); | ||||
|   void changeDisc(QString new_disc_filename); | ||||
|   void loadStateFromMemory(QByteArray arr); | ||||
|   QByteArray saveStateToMemory(); | ||||
| 
 | ||||
| private Q_SLOTS: | ||||
|   void doStopThread(); | ||||
|  | @ -123,6 +121,7 @@ private: | |||
|   void updateHotkeyInputMap(); | ||||
|   void addButtonToInputMap(const QString& binding, InputButtonHandler handler); | ||||
|   void createAudioStream(); | ||||
|   void switchGPURenderer(); | ||||
|   void createThread(); | ||||
|   void stopThread(); | ||||
|   void threadEntryPoint(); | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Connor McLaughlin
						Connor McLaughlin