2019-12-31 06:17:17 +00:00
|
|
|
#include "qthostinterface.h"
|
2020-01-07 08:55:36 +00:00
|
|
|
#include "YBaseLib/AutoReleasePtr.h"
|
|
|
|
#include "YBaseLib/ByteStream.h"
|
2019-12-31 06:17:17 +00:00
|
|
|
#include "YBaseLib/Log.h"
|
2020-01-05 02:46:03 +00:00
|
|
|
#include "YBaseLib/String.h"
|
2019-12-31 06:17:17 +00:00
|
|
|
#include "common/null_audio_stream.h"
|
2020-01-02 06:13:03 +00:00
|
|
|
#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-01-07 04:17:41 +00:00
|
|
|
#include "qtaudiostream.h"
|
2019-12-31 06:17:17 +00:00
|
|
|
#include "qtsettingsinterface.h"
|
2020-01-02 06:13:03 +00:00
|
|
|
#include "qtutils.h"
|
2019-12-31 06:17:17 +00:00
|
|
|
#include <QtCore/QCoreApplication>
|
2020-01-02 06:13:03 +00:00
|
|
|
#include <QtCore/QDebug>
|
2019-12-31 06:17:17 +00:00
|
|
|
#include <QtWidgets/QMessageBox>
|
|
|
|
#include <memory>
|
|
|
|
Log_SetChannel(QtHostInterface);
|
|
|
|
|
2020-01-07 05:54:37 +00:00
|
|
|
#ifdef WIN32
|
|
|
|
#include "d3d11displaywindow.h"
|
|
|
|
#endif
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
QtHostInterface::QtHostInterface(QObject* parent)
|
|
|
|
: QObject(parent), m_qsettings("duckstation-qt.ini", QSettings::IniFormat)
|
|
|
|
{
|
|
|
|
checkSettings();
|
|
|
|
createGameList();
|
2020-01-02 06:13:03 +00:00
|
|
|
doUpdateInputMap();
|
2020-01-07 04:17:41 +00:00
|
|
|
createAudioStream();
|
2019-12-31 06:17:17 +00:00
|
|
|
createThread();
|
|
|
|
}
|
|
|
|
|
|
|
|
QtHostInterface::~QtHostInterface()
|
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
Assert(!m_display_window);
|
2019-12-31 06:17:17 +00:00
|
|
|
stopThread();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::ReportError(const char* message)
|
|
|
|
{
|
|
|
|
// QMessageBox::critical(nullptr, tr("DuckStation Error"), message, QMessageBox::Ok);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::ReportMessage(const char* message)
|
|
|
|
{
|
|
|
|
// QMessageBox::information(nullptr, tr("DuckStation Information"), message, QMessageBox::Ok);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::setDefaultSettings()
|
|
|
|
{
|
|
|
|
m_settings.SetDefaults();
|
2020-01-02 06:13:03 +00:00
|
|
|
|
|
|
|
// default input settings for Qt
|
|
|
|
m_settings.controller_types[0] = ControllerType::DigitalController;
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonUp"), QStringLiteral("Keyboard/W"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonDown"), QStringLiteral("Keyboard/S"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonLeft"), QStringLiteral("Keyboard/A"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonRight"), QStringLiteral("Keyboard/D"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonSelect"), QStringLiteral("Keyboard/Backspace"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonStart"), QStringLiteral("Keyboard/Return"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonTriangle"), QStringLiteral("Keyboard/8"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonCross"), QStringLiteral("Keyboard/2"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonSquare"), QStringLiteral("Keyboard/4"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonCircle"), QStringLiteral("Keyboard/6"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonL1"), QStringLiteral("Keyboard/Q"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonL2"), QStringLiteral("Keyboard/1"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonR1"), QStringLiteral("Keyboard/E"));
|
|
|
|
m_qsettings.setValue(QStringLiteral("Controller1/ButtonR2"), QStringLiteral("Keyboard/3"));
|
|
|
|
|
2020-01-01 04:01:58 +00:00
|
|
|
updateQSettings();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::updateQSettings()
|
|
|
|
{
|
|
|
|
QtSettingsInterface si(m_qsettings);
|
2019-12-31 06:17:17 +00:00
|
|
|
m_settings.Save(si);
|
2020-01-01 04:01:58 +00:00
|
|
|
// m_qsettings.sync();
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::applySettings()
|
|
|
|
{
|
|
|
|
QtSettingsInterface si(m_qsettings);
|
|
|
|
m_settings.Load(si);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::checkSettings()
|
|
|
|
{
|
|
|
|
const QSettings::Status settings_status = m_qsettings.status();
|
|
|
|
if (settings_status != QSettings::NoError)
|
|
|
|
m_qsettings.clear();
|
|
|
|
|
|
|
|
const QString settings_version_key = QStringLiteral("General/SettingsVersion");
|
|
|
|
const int expected_version = 1;
|
|
|
|
const QVariant settings_version_var = m_qsettings.value(settings_version_key);
|
|
|
|
bool settings_version_okay;
|
|
|
|
int settings_version = settings_version_var.toInt(&settings_version_okay);
|
|
|
|
if (!settings_version_okay)
|
|
|
|
settings_version = 0;
|
|
|
|
if (settings_version != expected_version)
|
|
|
|
{
|
|
|
|
Log_WarningPrintf("Settings version %d does not match expected version %d, resetting", settings_version,
|
|
|
|
expected_version);
|
|
|
|
m_qsettings.clear();
|
|
|
|
m_qsettings.setValue(settings_version_key, expected_version);
|
|
|
|
setDefaultSettings();
|
|
|
|
}
|
2020-01-01 04:01:58 +00:00
|
|
|
|
|
|
|
applySettings();
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::createGameList()
|
|
|
|
{
|
|
|
|
m_game_list = std::make_unique<GameList>();
|
|
|
|
updateGameListDatabase(false);
|
|
|
|
refreshGameList(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::updateGameListDatabase(bool refresh_list /*= true*/)
|
|
|
|
{
|
|
|
|
m_game_list->ClearDatabase();
|
|
|
|
|
|
|
|
const QString redump_dat_path = m_qsettings.value("GameList/RedumpDatabasePath").toString();
|
|
|
|
if (!redump_dat_path.isEmpty())
|
|
|
|
m_game_list->ParseRedumpDatabase(redump_dat_path.toStdString().c_str());
|
|
|
|
|
|
|
|
if (refresh_list)
|
|
|
|
refreshGameList(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::refreshGameList(bool invalidate_cache /*= false*/)
|
|
|
|
{
|
|
|
|
QtSettingsInterface si(m_qsettings);
|
|
|
|
m_game_list->SetDirectoriesFromSettings(si);
|
|
|
|
m_game_list->RescanAllDirectories();
|
|
|
|
emit gameListRefreshed();
|
|
|
|
}
|
|
|
|
|
|
|
|
QWidget* QtHostInterface::createDisplayWidget(QWidget* parent)
|
|
|
|
{
|
2020-01-07 05:54:37 +00:00
|
|
|
#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);
|
2020-01-07 05:54:37 +00:00
|
|
|
#else
|
2020-01-02 07:45:25 +00:00
|
|
|
m_display_window = new OpenGLDisplayWindow(this, nullptr);
|
2020-01-07 05:54:37 +00:00
|
|
|
#endif
|
2020-01-02 09:14:16 +00:00
|
|
|
connect(m_display_window, &QtDisplayWindow::windowResizedEvent, this, &QtHostInterface::onDisplayWindowResized);
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
m_display.release();
|
2020-01-02 07:45:25 +00:00
|
|
|
m_display = std::unique_ptr<HostDisplay>(m_display_window->getHostDisplayInterface());
|
|
|
|
return QWidget::createWindowContainer(m_display_window, parent);
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-01-07 08:55:36 +00:00
|
|
|
void QtHostInterface::displayWidgetDestroyed()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
m_display.release();
|
2020-01-02 07:45:25 +00:00
|
|
|
delete m_display_window;
|
|
|
|
m_display_window = nullptr;
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_state_filename)
|
|
|
|
{
|
2020-01-07 08:55:36 +00:00
|
|
|
Assert(!isOnWorkerThread());
|
2019-12-31 06:17:17 +00:00
|
|
|
emit emulationStarting();
|
|
|
|
|
2020-01-02 07:45:25 +00:00
|
|
|
if (!m_display_window->createDeviceContext(m_worker_thread))
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
emit emulationStopped();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(this, "doBootSystem", Qt::QueuedConnection, Q_ARG(QString, initial_filename),
|
|
|
|
Q_ARG(QString, initial_save_state_filename));
|
|
|
|
}
|
|
|
|
|
2020-01-02 06:13:03 +00:00
|
|
|
void QtHostInterface::handleKeyEvent(int key, bool pressed)
|
|
|
|
{
|
|
|
|
if (!isOnWorkerThread())
|
|
|
|
{
|
|
|
|
QMetaObject::invokeMethod(this, "doHandleKeyEvent", Qt::QueuedConnection, Q_ARG(int, key), Q_ARG(bool, pressed));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
doHandleKeyEvent(key, pressed);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::doHandleKeyEvent(int key, bool pressed)
|
|
|
|
{
|
|
|
|
const auto iter = m_keyboard_input_handlers.find(key);
|
|
|
|
if (iter == m_keyboard_input_handlers.end())
|
|
|
|
return;
|
|
|
|
|
|
|
|
iter->second(pressed);
|
|
|
|
}
|
|
|
|
|
2020-01-02 09:14:16 +00:00
|
|
|
void QtHostInterface::onDisplayWindowResized(int width, int height)
|
|
|
|
{
|
|
|
|
m_display_window->onWindowResized(width, height);
|
|
|
|
}
|
|
|
|
|
2020-01-02 06:13:03 +00:00
|
|
|
void QtHostInterface::updateInputMap()
|
|
|
|
{
|
|
|
|
if (!isOnWorkerThread())
|
|
|
|
{
|
|
|
|
QMetaObject::invokeMethod(this, "doUpdateInputMap", Qt::QueuedConnection);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
doUpdateInputMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::doUpdateInputMap()
|
|
|
|
{
|
|
|
|
m_keyboard_input_handlers.clear();
|
|
|
|
|
2020-01-05 02:46:03 +00:00
|
|
|
updateControllerInputMap();
|
|
|
|
updateHotkeyInputMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::updateControllerInputMap()
|
|
|
|
{
|
2020-01-02 06:13:03 +00:00
|
|
|
for (u32 controller_index = 0; controller_index < 2; controller_index++)
|
|
|
|
{
|
|
|
|
const ControllerType ctype = m_settings.controller_types[controller_index];
|
|
|
|
if (ctype == ControllerType::None)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
const auto button_names = Controller::GetButtonNames(ctype);
|
|
|
|
for (const auto& [button_name, button_code] : button_names)
|
|
|
|
{
|
|
|
|
QVariant var = m_qsettings.value(
|
|
|
|
QStringLiteral("Controller%1/Button%2").arg(controller_index + 1).arg(QString::fromStdString(button_name)));
|
|
|
|
if (!var.isValid())
|
|
|
|
continue;
|
|
|
|
|
2020-01-05 02:46:03 +00:00
|
|
|
addButtonToInputMap(var.toString(), [this, controller_index, button_code](bool pressed) {
|
2020-01-02 06:13:03 +00:00
|
|
|
if (!m_system)
|
|
|
|
return;
|
|
|
|
|
|
|
|
Controller* controller = m_system->GetController(controller_index);
|
|
|
|
if (controller)
|
|
|
|
controller->SetButtonState(button_code, pressed);
|
2020-01-05 02:46:03 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<QtHostInterface::HotkeyInfo> QtHostInterface::getHotkeyList() const
|
|
|
|
{
|
|
|
|
std::vector<HotkeyInfo> hotkeys = {
|
|
|
|
{QStringLiteral("FastForward"), QStringLiteral("Toggle Fast Forward"), QStringLiteral("General")},
|
|
|
|
{QStringLiteral("Fullscreen"), QStringLiteral("Toggle Fullscreen"), QStringLiteral("General")},
|
|
|
|
{QStringLiteral("Pause"), QStringLiteral("Toggle Pause"), QStringLiteral("General")}};
|
|
|
|
|
|
|
|
for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
|
|
|
|
{
|
|
|
|
hotkeys.push_back(
|
|
|
|
{QStringLiteral("LoadState%1").arg(i), QStringLiteral("Load State %1").arg(i), QStringLiteral("Save States")});
|
|
|
|
}
|
|
|
|
for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
|
|
|
|
{
|
|
|
|
hotkeys.push_back(
|
|
|
|
{QStringLiteral("SaveState%1").arg(i), QStringLiteral("Save State %1").arg(i), QStringLiteral("Save States")});
|
|
|
|
}
|
|
|
|
|
|
|
|
return hotkeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::updateHotkeyInputMap()
|
|
|
|
{
|
|
|
|
auto hk = [this](const QString& hotkey_name, InputButtonHandler handler) {
|
|
|
|
QVariant var = m_qsettings.value(QStringLiteral("Hotkeys/%1").arg(hotkey_name));
|
|
|
|
if (!var.isValid())
|
|
|
|
return;
|
|
|
|
|
|
|
|
addButtonToInputMap(var.toString(), std::move(handler));
|
|
|
|
};
|
|
|
|
|
|
|
|
hk(QStringLiteral("FastForward"), [this](bool pressed) {
|
|
|
|
m_speed_limiter_temp_disabled = pressed;
|
|
|
|
HostInterface::UpdateSpeedLimiterState();
|
|
|
|
});
|
|
|
|
|
|
|
|
hk(QStringLiteral("Fullscreen"), [this](bool pressed) {
|
|
|
|
if (!pressed)
|
2020-01-06 06:27:39 +00:00
|
|
|
emit toggleFullscreenRequested();
|
2020-01-05 02:46:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
hk(QStringLiteral("Pause"), [this](bool pressed) {
|
|
|
|
if (!pressed)
|
|
|
|
pauseSystem(!m_paused);
|
|
|
|
});
|
|
|
|
|
|
|
|
for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
|
|
|
|
{
|
|
|
|
hk(QStringLiteral("LoadState%1").arg(i), [this, i](bool pressed) {
|
|
|
|
if (!pressed)
|
|
|
|
HostInterface::LoadState(TinyString::FromFormat("savestate_%u.bin", i));
|
|
|
|
});
|
|
|
|
|
|
|
|
hk(QStringLiteral("SaveState%1").arg(i), [this, i](bool pressed) {
|
|
|
|
if (!pressed)
|
|
|
|
HostInterface::SaveState(TinyString::FromFormat("savestate_%u.bin", i));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::addButtonToInputMap(const QString& binding, InputButtonHandler handler)
|
|
|
|
{
|
|
|
|
const QString device = binding.section('/', 0, 0);
|
|
|
|
const QString button = binding.section('/', 1, 1);
|
|
|
|
if (device == QStringLiteral("Keyboard"))
|
|
|
|
{
|
2020-01-06 05:14:47 +00:00
|
|
|
std::optional<int> key_id = QtUtils::ParseKeyString(button);
|
2020-01-05 02:46:03 +00:00
|
|
|
if (!key_id.has_value())
|
|
|
|
{
|
|
|
|
qWarning() << "Unknown keyboard key " << button;
|
|
|
|
return;
|
2020-01-02 06:13:03 +00:00
|
|
|
}
|
2020-01-05 02:46:03 +00:00
|
|
|
|
|
|
|
m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
qWarning() << "Unknown input device: " << device;
|
|
|
|
return;
|
2020-01-02 06:13:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
void QtHostInterface::powerOffSystem()
|
|
|
|
{
|
|
|
|
if (!isOnWorkerThread())
|
|
|
|
{
|
2020-01-07 08:55:36 +00:00
|
|
|
QMetaObject::invokeMethod(this, "powerOffSystem", Qt::QueuedConnection);
|
2019-12-31 06:17:17 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!m_system)
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("powerOffSystem() called without system");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_system.reset();
|
2020-01-07 04:17:41 +00:00
|
|
|
m_audio_stream->PauseOutput(true);
|
2020-01-02 07:45:25 +00:00
|
|
|
m_display_window->destroyDeviceContext();
|
2019-12-31 06:17:17 +00:00
|
|
|
|
|
|
|
emit emulationStopped();
|
|
|
|
}
|
|
|
|
|
2020-01-07 08:55:36 +00:00
|
|
|
void QtHostInterface::blockingPowerOffSystem()
|
|
|
|
{
|
|
|
|
if (!isOnWorkerThread())
|
|
|
|
{
|
|
|
|
QMetaObject::invokeMethod(this, "powerOffSystem", Qt::BlockingQueuedConnection);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_paused = paused;
|
2020-01-07 04:17:41 +00:00
|
|
|
m_audio_stream->PauseOutput(paused);
|
2019-12-31 06:17:17 +00:00
|
|
|
emit emulationPaused(paused);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::changeDisc(QString new_disc_filename) {}
|
|
|
|
|
2020-01-07 08:55:36 +00:00
|
|
|
void QtHostInterface::loadStateFromMemory(QByteArray arr)
|
|
|
|
{
|
|
|
|
if (!isOnWorkerThread())
|
|
|
|
{
|
|
|
|
QMetaObject::invokeMethod(this, "loadStateFromMemory", Qt::QueuedConnection, Q_ARG(QByteArray, arr));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AutoReleasePtr<ByteStream> stream = ByteStream_CreateGrowableMemoryStream();
|
|
|
|
if (!m_system || !QtUtils::WriteQByteArrayToStream(arr, stream) || !stream->SeekAbsolute(0) ||
|
|
|
|
!m_system->LoadState(stream))
|
|
|
|
{
|
|
|
|
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 {};
|
|
|
|
|
|
|
|
AutoReleasePtr<ByteStream> stream = ByteStream_CreateGrowableMemoryStream();
|
|
|
|
if (m_system->SaveState(stream))
|
|
|
|
return QtUtils::ReadStreamToQByteArray(stream, true);
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename)
|
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
if (!m_display_window->initializeDeviceContext())
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
emit emulationStopped();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string initial_filename_str = initial_filename.toStdString();
|
|
|
|
std::string initial_save_state_filename_str = initial_save_state_filename.toStdString();
|
|
|
|
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()))
|
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
m_display_window->destroyDeviceContext();
|
2019-12-31 06:17:17 +00:00
|
|
|
emit emulationStopped();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-07 04:17:41 +00:00
|
|
|
m_audio_stream->PauseOutput(false);
|
2019-12-31 06:17:17 +00:00
|
|
|
emit emulationStarted();
|
|
|
|
}
|
|
|
|
|
2020-01-07 04:17:41 +00:00
|
|
|
void QtHostInterface::createAudioStream()
|
|
|
|
{
|
|
|
|
// Qt at least on Windows seems to want a buffer size of at least 8KB.
|
2020-01-07 08:55:36 +00:00
|
|
|
// m_audio_stream = QtAudioStream::Create();
|
|
|
|
// if (!m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4))
|
2020-01-07 04:17:41 +00:00
|
|
|
{
|
|
|
|
qWarning() << "Failed to configure audio stream, falling back to null output";
|
|
|
|
|
|
|
|
// fall back to null output
|
|
|
|
m_audio_stream.reset();
|
|
|
|
m_audio_stream = NullAudioStream::Create();
|
|
|
|
m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QtHostInterface::threadEntryPoint()
|
|
|
|
{
|
|
|
|
while (!m_shutdown_flag.load())
|
|
|
|
{
|
|
|
|
if (!m_system)
|
|
|
|
{
|
|
|
|
// wait until we have a system before running
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 1000);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// execute the system, polling events inbetween frames
|
|
|
|
// simulate the system if not paused
|
|
|
|
if (m_system && !m_paused)
|
|
|
|
m_system->RunFrame();
|
|
|
|
|
|
|
|
// rendering
|
|
|
|
{
|
|
|
|
if (m_system)
|
2020-01-02 09:14:16 +00:00
|
|
|
{
|
|
|
|
DrawDebugWindows();
|
2019-12-31 06:17:17 +00:00
|
|
|
m_system->GetGPU()->ResetGraphicsAPIState();
|
2020-01-02 09:14:16 +00:00
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-01-02 09:14:16 +00:00
|
|
|
DrawFPSWindow();
|
|
|
|
DrawOSDMessages();
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-01-02 09:14:16 +00:00
|
|
|
m_display->Render();
|
2019-12-31 06:17:17 +00:00
|
|
|
|
|
|
|
if (m_system)
|
|
|
|
{
|
|
|
|
m_system->GetGPU()->RestoreGraphicsAPIState();
|
|
|
|
|
|
|
|
if (m_speed_limiter_enabled)
|
|
|
|
Throttle();
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdatePerformanceCounters();
|
|
|
|
}
|
|
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, m_paused ? 16 : 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
m_system.reset();
|
|
|
|
|
|
|
|
// move back to UI thread
|
|
|
|
moveToThread(m_original_thread);
|
|
|
|
}
|
|
|
|
|
|
|
|
QtHostInterface::Thread::Thread(QtHostInterface* parent) : QThread(parent), m_parent(parent) {}
|
|
|
|
|
|
|
|
QtHostInterface::Thread::~Thread() = default;
|
|
|
|
|
|
|
|
void QtHostInterface::Thread::run()
|
|
|
|
{
|
|
|
|
m_parent->threadEntryPoint();
|
|
|
|
}
|