Duckstation/src/duckstation-qt/qthostinterface.cpp

697 lines
18 KiB
C++
Raw Normal View History

2019-12-31 06:17:17 +00:00
#include "qthostinterface.h"
2020-01-10 03:31:12 +00:00
#include "common/assert.h"
#include "common/audio_stream.h"
2020-01-10 03:31:12 +00:00
#include "common/byte_stream.h"
#include "common/log.h"
#include "common/string_util.h"
#include "core/controller.h"
2019-12-31 06:17:17 +00:00
#include "core/game_list.h"
#include "core/gpu.h"
#include "core/system.h"
2020-02-25 13:40:46 +00:00
#include "frontend-common/sdl_audio_stream.h"
#include "frontend-common/sdl_controller_interface.h"
2019-12-31 06:17:17 +00:00
#include "qtsettingsinterface.h"
#include "qtutils.h"
2019-12-31 06:17:17 +00:00
#include <QtCore/QCoreApplication>
2020-02-15 15:14:04 +00:00
#include <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <QtCore/QEventLoop>
#include <QtCore/QTimer>
2020-02-15 15:14:04 +00:00
#include <QtWidgets/QMenu>
2019-12-31 06:17:17 +00:00
#include <QtWidgets/QMessageBox>
#include <memory>
Log_SetChannel(QtHostInterface);
#ifdef WIN32
#include "d3d11displaywindow.h"
#endif
2019-12-31 06:17:17 +00:00
QtHostInterface::QtHostInterface(QObject* parent)
: QObject(parent), CommonHostInterface(),
m_qsettings(QString::fromStdString(GetSettingsFileName()), QSettings::IniFormat)
2019-12-31 06:17:17 +00:00
{
qRegisterMetaType<SystemBootParameters>();
loadSettings();
2020-01-24 04:51:07 +00:00
refreshGameList();
2019-12-31 06:17:17 +00:00
createThread();
}
QtHostInterface::~QtHostInterface()
{
Assert(!m_display_window);
2019-12-31 06:17:17 +00:00
stopThread();
}
void QtHostInterface::ReportError(const char* message)
{
HostInterface::ReportError(message);
emit errorReported(QString::fromLocal8Bit(message));
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::ReportMessage(const char* message)
{
HostInterface::ReportMessage(message);
emit messageReported(QString::fromLocal8Bit(message));
2019-12-31 06:17:17 +00:00
}
bool QtHostInterface::ConfirmMessage(const char* message)
{
return messageConfirmed(QString::fromLocal8Bit(message));
}
QVariant QtHostInterface::getSettingValue(const QString& name, const QVariant& default_value)
{
std::lock_guard<std::mutex> guard(m_qsettings_mutex);
return m_qsettings.value(name, default_value);
}
void QtHostInterface::putSettingValue(const QString& name, const QVariant& value)
{
std::lock_guard<std::mutex> guard(m_qsettings_mutex);
m_qsettings.setValue(name, value);
2020-01-01 04:01:58 +00:00
}
void QtHostInterface::removeSettingValue(const QString& name)
{
std::lock_guard<std::mutex> guard(m_qsettings_mutex);
m_qsettings.remove(name);
}
void QtHostInterface::setDefaultSettings()
2020-01-01 04:01:58 +00:00
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "setDefaultSettings", Qt::QueuedConnection);
return;
}
std::lock_guard<std::mutex> guard(m_qsettings_mutex);
2020-01-01 04:01:58 +00:00
QtSettingsInterface si(m_qsettings);
UpdateSettings([this, &si]() { m_settings.Load(si); });
UpdateInputMap(si);
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::applySettings()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "applySettings", Qt::QueuedConnection);
return;
}
std::lock_guard<std::mutex> guard(m_qsettings_mutex);
QtSettingsInterface si(m_qsettings);
UpdateSettings([this, &si]() { m_settings.Load(si); });
UpdateInputMap(si);
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::loadSettings()
2019-12-31 06:17:17 +00:00
{
// no need to lock here because the emu thread doesn't exist yet
QtSettingsInterface si(m_qsettings);
2019-12-31 06:17:17 +00:00
const QSettings::Status settings_status = m_qsettings.status();
if (settings_status != QSettings::NoError)
{
m_qsettings.clear();
SetDefaultSettings(si);
2019-12-31 06:17:17 +00:00
}
2020-01-01 04:01:58 +00:00
CheckSettings(si);
m_settings.Load(si);
// input map update is done on the emu thread
2019-12-31 06:17:17 +00:00
}
2020-01-08 03:37:43 +00:00
void QtHostInterface::refreshGameList(bool invalidate_cache /* = false */, bool invalidate_database /* = false */)
2019-12-31 06:17:17 +00:00
{
std::lock_guard<std::mutex> lock(m_qsettings_mutex);
2019-12-31 06:17:17 +00:00
QtSettingsInterface si(m_qsettings);
m_game_list->SetSearchDirectoriesFromSettings(si);
2020-01-08 03:37:43 +00:00
m_game_list->Refresh(invalidate_cache, invalidate_database);
2019-12-31 06:17:17 +00:00
emit gameListRefreshed();
}
QtDisplayWindow* QtHostInterface::createDisplayWindow()
2019-12-31 06:17:17 +00:00
{
Assert(!m_display_window);
#ifdef WIN32
2020-01-07 08:55:36 +00:00
if (m_settings.gpu_renderer == GPURenderer::HardwareOpenGL)
m_display_window = new OpenGLDisplayWindow(this, nullptr);
else
m_display_window = new D3D11DisplayWindow(this, nullptr);
#else
m_display_window = new OpenGLDisplayWindow(this, nullptr);
#endif
2020-01-02 09:14:16 +00:00
connect(m_display_window, &QtDisplayWindow::windowResizedEvent, this, &QtHostInterface::onDisplayWindowResized);
return m_display_window;
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::bootSystem(const SystemBootParameters& params)
2020-01-24 04:49:51 +00:00
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "bootSystem", Qt::QueuedConnection, Q_ARG(const SystemBootParameters&, params));
return;
}
2020-01-24 04:49:51 +00:00
HostInterface::BootSystem(params);
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::resumeSystemFromState(const QString& filename, bool boot_on_failure)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "resumeSystemFromState", Qt::QueuedConnection, Q_ARG(const QString&, filename),
Q_ARG(bool, boot_on_failure));
return;
}
if (filename.isEmpty())
HostInterface::ResumeSystemFromMostRecentState();
else
HostInterface::ResumeSystemFromState(filename.toStdString().c_str(), boot_on_failure);
}
void QtHostInterface::handleKeyEvent(int key, bool pressed)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "handleKeyEvent", Qt::QueuedConnection, Q_ARG(int, key), Q_ARG(bool, pressed));
return;
}
HandleHostKeyEvent(key, pressed);
}
2020-01-02 09:14:16 +00:00
void QtHostInterface::onDisplayWindowResized(int width, int height)
{
// this can be null if it was destroyed and the main thread is late catching up
if (m_display_window)
m_display_window->WindowResized(width, height);
2020-01-02 09:14:16 +00:00
}
bool QtHostInterface::AcquireHostDisplay()
{
DebugAssert(!m_display_window);
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;
}
m_display = m_display_window->getHostDisplayInterface();
return true;
}
void QtHostInterface::ReleaseHostDisplay()
{
DebugAssert(m_display_window && m_display == m_display_window->getHostDisplayInterface());
m_display = nullptr;
m_display_window->destroyDeviceContext();
m_display_window = nullptr;
emit destroyDisplayWindowRequested();
}
void QtHostInterface::SetFullscreen(bool enabled)
{
emit setFullscreenRequested(enabled);
}
void QtHostInterface::ToggleFullscreen()
{
emit toggleFullscreenRequested();
}
std::optional<CommonHostInterface::HostKeyCode> QtHostInterface::GetHostKeyCode(const std::string_view key_code) const
{
const std::optional<int> code =
QtUtils::ParseKeyString(QString::fromUtf8(key_code.data(), static_cast<int>(key_code.length())));
if (!code)
return std::nullopt;
2020-02-25 13:40:46 +00:00
return static_cast<s32>(*code);
}
void QtHostInterface::OnSystemCreated()
{
HostInterface::OnSystemCreated();
wakeThread();
destroyBackgroundControllerPollTimer();
emit emulationStarted();
}
void QtHostInterface::OnSystemPaused(bool paused)
{
HostInterface::OnSystemPaused(paused);
emit emulationPaused(paused);
if (m_background_controller_polling_enable_count > 0)
{
if (paused)
createBackgroundControllerPollTimer();
else
destroyBackgroundControllerPollTimer();
}
if (!paused)
{
wakeThread();
emit focusDisplayWidgetRequested();
}
}
void QtHostInterface::OnSystemDestroyed()
{
HostInterface::OnSystemDestroyed();
if (m_background_controller_polling_enable_count > 0)
createBackgroundControllerPollTimer();
emit emulationStopped();
}
void QtHostInterface::OnSystemPerformanceCountersUpdated()
{
HostInterface::OnSystemPerformanceCountersUpdated();
DebugAssert(m_system);
emit systemPerformanceCountersUpdated(m_system->GetEmulationSpeed(), m_system->GetFPS(), m_system->GetVPS(),
m_system->GetAverageFrameTime(), m_system->GetWorstFrameTime());
}
void QtHostInterface::OnRunningGameChanged()
{
HostInterface::OnRunningGameChanged();
if (m_system)
{
emit runningGameChanged(QString::fromStdString(m_system->GetRunningPath()),
QString::fromStdString(m_system->GetRunningCode()),
QString::fromStdString(m_system->GetRunningTitle()));
}
else
{
emit runningGameChanged(QString(), QString(), QString());
}
}
void QtHostInterface::OnSystemStateSaved(bool global, s32 slot)
{
emit stateSaved(QString::fromStdString(m_system->GetRunningCode()), global, slot);
}
void QtHostInterface::OnControllerTypeChanged(u32 slot)
{
HostInterface::OnControllerTypeChanged(slot);
// this assumes the settings mutex is already locked - as it comes from updateSettings().
QtSettingsInterface si(m_qsettings);
UpdateInputMap(si);
}
void QtHostInterface::updateInputMap()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "updateInputMap", Qt::QueuedConnection);
return;
}
std::lock_guard<std::mutex> lock(m_qsettings_mutex);
QtSettingsInterface si(m_qsettings);
UpdateInputMap(si);
2020-02-17 15:06:28 +00:00
}
void QtHostInterface::powerOffSystem()
2019-12-31 06:17:17 +00:00
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "powerOffSystem", Qt::QueuedConnection);
2019-12-31 06:17:17 +00:00
return;
}
if (!m_system)
return;
if (m_settings.save_state_on_exit)
SaveResumeSaveState();
DestroySystem();
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::synchronousPowerOffSystem()
{
if (!isOnWorkerThread())
QMetaObject::invokeMethod(this, "powerOffSystem", Qt::BlockingQueuedConnection);
else
powerOffSystem();
}
2019-12-31 06:17:17 +00:00
void QtHostInterface::resetSystem()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "resetSystem", Qt::QueuedConnection);
return;
}
if (!m_system)
{
Log_ErrorPrintf("resetSystem() called without system");
return;
}
HostInterface::ResetSystem();
}
void QtHostInterface::pauseSystem(bool paused)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "pauseSystem", Qt::QueuedConnection, Q_ARG(bool, paused));
return;
}
if (!m_system)
return;
2019-12-31 06:17:17 +00:00
m_paused = paused;
2020-01-07 04:17:41 +00:00
m_audio_stream->PauseOutput(paused);
if (!paused)
wakeThread();
2019-12-31 06:17:17 +00:00
emit emulationPaused(paused);
}
void QtHostInterface::changeDisc(const QString& new_disc_filename)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "changeDisc", Qt::QueuedConnection, Q_ARG(const QString&, new_disc_filename));
return;
}
if (!m_system)
return;
m_system->InsertMedia(new_disc_filename.toStdString().c_str());
}
2019-12-31 06:17:17 +00:00
static QString FormatTimestampForSaveStateMenu(u64 timestamp)
{
const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp)));
return qtime.toString(Qt::SystemLocaleShortDate);
}
2020-02-15 15:14:04 +00:00
void QtHostInterface::populateSaveStateMenus(const char* game_code, QMenu* load_menu, QMenu* save_menu)
{
auto add_slot = [this, game_code, load_menu, save_menu](const char* title, const char* empty_title, bool global,
s32 slot) {
std::optional<SaveStateInfo> ssi = GetSaveStateInfo(global ? nullptr : game_code, slot);
2020-02-15 15:14:04 +00:00
const QString menu_title = ssi.has_value() ?
tr(title).arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) :
tr(empty_title).arg(slot);
QAction* load_action = load_menu->addAction(menu_title);
load_action->setEnabled(ssi.has_value());
if (ssi.has_value())
2020-02-15 15:14:04 +00:00
{
const QString path(QString::fromStdString(ssi->path));
connect(load_action, &QAction::triggered, [this, path]() { loadState(path); });
2020-02-15 15:14:04 +00:00
}
QAction* save_action = save_menu->addAction(menu_title);
connect(save_action, &QAction::triggered, [this, global, slot]() { saveState(global, slot); });
};
load_menu->clear();
2020-02-15 15:14:04 +00:00
save_menu->clear();
2020-02-15 15:14:04 +00:00
if (game_code && std::strlen(game_code) > 0)
{
for (u32 slot = 1; slot <= PER_GAME_SAVE_STATE_SLOTS; slot++)
add_slot("Game Save %1 (%2)", "Game Save %1 (Empty)", false, static_cast<s32>(slot));
2020-02-15 15:14:04 +00:00
load_menu->addSeparator();
2020-02-15 15:14:04 +00:00
save_menu->addSeparator();
}
for (u32 slot = 1; slot <= GLOBAL_SAVE_STATE_SLOTS; slot++)
add_slot("Global Save %1 (%2)", "Global Save %1 (Empty)", true, static_cast<s32>(slot));
2020-02-15 15:14:04 +00:00
}
void QtHostInterface::populateGameListContextMenu(const char* game_code, QWidget* parent_window, QMenu* menu)
{
QAction* resume_action = menu->addAction(tr("Resume"));
resume_action->setEnabled(false);
QMenu* load_state_menu = menu->addMenu(tr("Load State"));
load_state_menu->setEnabled(false);
const std::vector<SaveStateInfo> available_states(GetAvailableSaveStates(game_code));
for (const SaveStateInfo& ssi : available_states)
{
if (ssi.global)
continue;
const s32 slot = ssi.slot;
const QDateTime timestamp(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ssi.timestamp)));
const QString timestamp_str(timestamp.toString(Qt::SystemLocaleShortDate));
const QString path(QString::fromStdString(ssi.path));
QAction* action;
if (slot < 0)
{
resume_action->setText(tr("Resume (%1)").arg(timestamp_str));
resume_action->setEnabled(true);
action = resume_action;
}
else
{
load_state_menu->setEnabled(true);
action = load_state_menu->addAction(tr("%1 Save %2 (%3)").arg(tr("Game")).arg(slot).arg(timestamp_str));
}
connect(action, &QAction::triggered, [this, path]() { loadState(path); });
}
const bool has_any_states = resume_action->isEnabled() || load_state_menu->isEnabled();
QAction* delete_save_states_action = menu->addAction(tr("Delete Save States..."));
delete_save_states_action->setEnabled(has_any_states);
if (has_any_states)
{
const std::string game_code_copy(game_code);
connect(delete_save_states_action, &QAction::triggered, [this, parent_window, game_code_copy] {
if (QMessageBox::warning(
parent_window, tr("Confirm Save State Deletion"),
tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.")
.arg(game_code_copy.c_str()),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
{
return;
}
DeleteSaveStates(game_code_copy.c_str(), true);
});
}
}
void QtHostInterface::loadState(const QString& filename)
2020-02-15 15:14:04 +00:00
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "loadState", Qt::QueuedConnection, Q_ARG(const QString&, filename));
2020-02-15 15:14:04 +00:00
return;
}
LoadState(filename.toStdString().c_str());
2020-02-15 15:14:04 +00:00
}
void QtHostInterface::loadState(bool global, qint32 slot)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "loadState", Qt::QueuedConnection, Q_ARG(bool, global), Q_ARG(qint32, slot));
return;
}
LoadState(global, slot);
2020-02-15 15:14:04 +00:00
}
void QtHostInterface::saveState(bool global, qint32 slot, bool block_until_done /* = false */)
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "saveState", block_until_done ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
Q_ARG(bool, global), Q_ARG(qint32, slot), Q_ARG(bool, block_until_done));
return;
}
if (m_system)
SaveState(global, slot);
}
void QtHostInterface::enableBackgroundControllerPolling()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "enableBackgroundControllerPolling", Qt::BlockingQueuedConnection);
return;
}
if (m_background_controller_polling_enable_count++ > 0)
return;
if (!m_system || m_paused)
{
createBackgroundControllerPollTimer();
// drain the event queue so we don't get events late
g_sdl_controller_interface.PumpSDLEvents();
}
}
void QtHostInterface::disableBackgroundControllerPolling()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "disableBackgroundControllerPolling");
return;
}
Assert(m_background_controller_polling_enable_count > 0);
if (--m_background_controller_polling_enable_count > 0)
return;
if (!m_system || m_paused)
destroyBackgroundControllerPollTimer();
}
void QtHostInterface::doBackgroundControllerPoll()
{
g_sdl_controller_interface.PumpSDLEvents();
}
void QtHostInterface::createBackgroundControllerPollTimer()
{
DebugAssert(!m_background_controller_polling_timer);
m_background_controller_polling_timer = new QTimer(this);
m_background_controller_polling_timer->setSingleShot(false);
m_background_controller_polling_timer->setTimerType(Qt::VeryCoarseTimer);
connect(m_background_controller_polling_timer, &QTimer::timeout, this, &QtHostInterface::doBackgroundControllerPoll);
m_background_controller_polling_timer->start(BACKGROUND_CONTROLLER_POLLING_INTERVAL);
}
void QtHostInterface::destroyBackgroundControllerPollTimer()
{
delete m_background_controller_polling_timer;
m_background_controller_polling_timer = nullptr;
}
2019-12-31 06:17:17 +00:00
void QtHostInterface::createThread()
{
m_original_thread = QThread::currentThread();
m_worker_thread = new Thread(this);
m_worker_thread->start();
moveToThread(m_worker_thread);
}
void QtHostInterface::stopThread()
{
Assert(!isOnWorkerThread());
QMetaObject::invokeMethod(this, "doStopThread", Qt::QueuedConnection);
m_worker_thread->wait();
}
void QtHostInterface::doStopThread()
{
m_shutdown_flag.store(true);
m_worker_thread_event_loop->quit();
2019-12-31 06:17:17 +00:00
}
void QtHostInterface::threadEntryPoint()
{
m_worker_thread_event_loop = new QEventLoop();
// set up controller interface and immediate poll to pick up the controller attached events
g_sdl_controller_interface.Initialize(this);
g_sdl_controller_interface.PumpSDLEvents();
updateInputMap();
// TODO: Event which flags the thread as ready
2019-12-31 06:17:17 +00:00
while (!m_shutdown_flag.load())
{
if (!m_system || m_paused)
2019-12-31 06:17:17 +00:00
{
// wait until we have a system before running
m_worker_thread_event_loop->exec();
2019-12-31 06:17:17 +00:00
continue;
}
m_system->RunFrame();
2019-12-31 06:17:17 +00:00
2020-02-09 13:16:37 +00:00
m_system->GetGPU()->ResetGraphicsAPIState();
2019-12-31 06:17:17 +00:00
2020-02-09 13:16:37 +00:00
DrawDebugWindows();
DrawOSDMessages();
2019-12-31 06:17:17 +00:00
2020-02-09 13:16:37 +00:00
m_display->Render();
2019-12-31 06:17:17 +00:00
2020-02-09 13:16:37 +00:00
m_system->GetGPU()->RestoreGraphicsAPIState();
2019-12-31 06:17:17 +00:00
2020-02-09 13:16:37 +00:00
if (m_speed_limiter_enabled)
m_system->Throttle();
2019-12-31 06:17:17 +00:00
m_worker_thread_event_loop->processEvents(QEventLoop::AllEvents);
g_sdl_controller_interface.PumpSDLEvents();
2019-12-31 06:17:17 +00:00
}
m_system.reset();
m_audio_stream.reset();
g_sdl_controller_interface.Shutdown();
delete m_worker_thread_event_loop;
m_worker_thread_event_loop = nullptr;
2019-12-31 06:17:17 +00:00
// move back to UI thread
moveToThread(m_original_thread);
}
void QtHostInterface::wakeThread()
{
if (isOnWorkerThread())
m_worker_thread_event_loop->quit();
else
QMetaObject::invokeMethod(m_worker_thread_event_loop, "quit", Qt::QueuedConnection);
}
2019-12-31 06:17:17 +00:00
QtHostInterface::Thread::Thread(QtHostInterface* parent) : QThread(parent), m_parent(parent) {}
QtHostInterface::Thread::~Thread() = default;
void QtHostInterface::Thread::run()
{
m_parent->threadEntryPoint();
}