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 New Issue