diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 004eb2b86f..3d3bf12ed4 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -540,6 +540,15 @@ else() install(TARGETS dolphin-emu RUNTIME DESTINATION ${bindir}) endif() +if(USE_MGBA) + target_sources(dolphin-emu PRIVATE + GBAHost.cpp + GBAHost.h + GBAWidget.cpp + GBAWidget.h + ) +endif() + if(USE_DISCORD_PRESENCE) target_compile_definitions(dolphin-emu PRIVATE -DUSE_DISCORD_PRESENCE) endif() diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index e8e3822cd1..3647f255df 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -142,6 +142,8 @@ + + @@ -208,6 +210,7 @@ + @@ -313,6 +316,7 @@ + diff --git a/Source/Core/DolphinQt/GBAHost.cpp b/Source/Core/DolphinQt/GBAHost.cpp new file mode 100644 index 0000000000..918426e320 --- /dev/null +++ b/Source/Core/DolphinQt/GBAHost.cpp @@ -0,0 +1,61 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/GBAHost.h" + +#include + +#include "Core/HW/GBACore.h" +#include "DolphinQt/GBAWidget.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" + +GBAHost::GBAHost(std::weak_ptr core) +{ + m_widget_controller = new GBAWidgetController(); + m_widget_controller->moveToThread(qApp->thread()); + m_core = std::move(core); + auto core_ptr = m_core.lock(); + + int device_number = core_ptr->GetDeviceNumber(); + std::string game_title = core_ptr->GetGameTitle(); + u32 width, height; + core_ptr->GetVideoDimensions(&width, &height); + + QueueOnObject(m_widget_controller, [widget_controller = m_widget_controller, core = m_core, + device_number, game_title, width, height] { + widget_controller->Create(core, device_number, game_title, width, height); + }); +} + +GBAHost::~GBAHost() +{ + m_widget_controller->deleteLater(); +} + +void GBAHost::GameChanged() +{ + auto core_ptr = m_core.lock(); + if (!core_ptr || !core_ptr->IsStarted()) + return; + + std::string game_title = core_ptr->GetGameTitle(); + u32 width, height; + core_ptr->GetVideoDimensions(&width, &height); + + QueueOnObject(m_widget_controller, + [widget_controller = m_widget_controller, game_title, width, height] { + widget_controller->GameChanged(game_title, width, height); + }); +} + +void GBAHost::FrameEnded(const std::vector& video_buffer) +{ + QueueOnObject(m_widget_controller, [widget_controller = m_widget_controller, video_buffer] { + widget_controller->FrameEnded(video_buffer); + }); +} + +std::unique_ptr Host_CreateGBAHost(std::weak_ptr core) +{ + return std::make_unique(core); +} diff --git a/Source/Core/DolphinQt/GBAHost.h b/Source/Core/DolphinQt/GBAHost.h new file mode 100644 index 0000000000..99e28bd2af --- /dev/null +++ b/Source/Core/DolphinQt/GBAHost.h @@ -0,0 +1,28 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Core/Host.h" + +namespace HW::GBA +{ +class Core; +} // namespace HW::GBA + +class GBAWidgetController; + +class GBAHost : public GBAHostInterface +{ +public: + explicit GBAHost(std::weak_ptr core); + ~GBAHost(); + void GameChanged() override; + void FrameEnded(const std::vector& video_buffer) override; + +private: + GBAWidgetController* m_widget_controller{}; + std::weak_ptr m_core; +}; diff --git a/Source/Core/DolphinQt/GBAWidget.cpp b/Source/Core/DolphinQt/GBAWidget.cpp new file mode 100644 index 0000000000..df9423a67f --- /dev/null +++ b/Source/Core/DolphinQt/GBAWidget.cpp @@ -0,0 +1,421 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/GBAWidget.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AudioCommon/AudioCommon.h" +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/CoreTiming.h" +#include "Core/HW/GBACore.h" +#include "Core/HW/GBAPad.h" +#include "Core/HW/SI/SI.h" +#include "Core/HW/SI/SI_Device.h" +#include "Core/Movie.h" +#include "Core/NetPlayProto.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/Resources.h" +#include "DolphinQt/Settings.h" +#include "DolphinQt/Settings/GameCubePane.h" + +static void RestartCore(const std::weak_ptr& core, std::string_view rom_path = {}) +{ + Core::RunOnCPUThread( + [core, rom_path = std::string(rom_path)] { + if (auto core_ptr = core.lock()) + { + auto& info = Config::MAIN_GBA_ROM_PATHS[core_ptr->GetDeviceNumber()]; + core_ptr->Stop(); + Config::SetCurrent(info, rom_path); + if (core_ptr->Start(CoreTiming::GetTicks())) + return; + Config::SetCurrent(info, Config::GetBase(info)); + core_ptr->Start(CoreTiming::GetTicks()); + } + }, + false); +} + +GBAWidget::GBAWidget(std::weak_ptr core, int device_number, + std::string_view game_title, int width, int height, QWidget* parent, + Qt::WindowFlags flags) + : QWidget(parent, flags), m_core(std::move(core)), m_device_number(device_number), + m_local_pad(device_number), m_game_title(game_title), m_width(width), m_height(height), + m_is_local_pad(true), m_volume(0), m_muted(false), m_force_disconnect(false) +{ + bool visible = true; + + setWindowIcon(Resources::GetAppIcon()); + setAcceptDrops(true); + resize(m_width, m_height); + setVisible(visible); + + SetVolume(100); + if (!visible) + ToggleMute(); + + LoadGeometry(); + UpdateTitle(); +} + +GBAWidget::~GBAWidget() +{ + SaveGeometry(); +} + +void GBAWidget::GameChanged(std::string_view game_title, int width, int height) +{ + m_game_title = game_title; + m_width = width; + m_height = height; + UpdateTitle(); + update(); +} + +void GBAWidget::SetVideoBuffer(std::vector video_buffer) +{ + m_video_buffer = std::move(video_buffer); + update(); +} + +void GBAWidget::SetVolume(int volume) +{ + m_muted = false; + m_volume = std::clamp(volume, 0, 100); + UpdateVolume(); +} + +void GBAWidget::VolumeDown() +{ + SetVolume(m_volume - 10); +} + +void GBAWidget::VolumeUp() +{ + SetVolume(m_volume + 10); +} + +bool GBAWidget::IsMuted() +{ + return m_muted; +} + +void GBAWidget::ToggleMute() +{ + m_muted = !m_muted; + UpdateVolume(); +} + +void GBAWidget::ToggleDisconnect() +{ + if (!CanControlCore()) + return; + + m_force_disconnect = !m_force_disconnect; + + Core::RunOnCPUThread( + [core = m_core, force_disconnect = m_force_disconnect] { + if (auto core_ptr = core.lock()) + core_ptr->SetForceDisconnect(force_disconnect); + }, + false); +} + +void GBAWidget::LoadROM() +{ + if (!CanControlCore()) + return; + + std::string rom_path = GameCubePane::GetOpenGBARom(""); + if (rom_path.empty()) + return; + + RestartCore(m_core, rom_path); +} + +void GBAWidget::UnloadROM() +{ + if (!CanControlCore() || m_game_title.empty()) + return; + + RestartCore(m_core); +} + +void GBAWidget::ResetCore() +{ + if (!CanResetCore()) + return; + + Pad::SetGBAReset(m_local_pad, true); +} + +void GBAWidget::DoState(bool export_state) +{ + if (!CanControlCore() && !export_state) + return; + + QString state_path = QDir::toNativeSeparators( + (export_state ? QFileDialog::getSaveFileName : QFileDialog::getOpenFileName)( + this, tr("Select a File"), QString(), + tr("mGBA Save States (*.ss0 *.ss1 *.ss2 *.ss3 *.ss4 *.ss5 *.ss6 *.ss7 *.ss8 *.ss9);;" + "All Files (*)"), + nullptr, QFileDialog::Options())); + + if (state_path.isEmpty()) + return; + + Core::RunOnCPUThread( + [export_state, core = m_core, state_path = state_path.toStdString()] { + if (auto core_ptr = core.lock()) + { + if (export_state) + core_ptr->ExportState(state_path); + else + core_ptr->ImportState(state_path); + } + }, + false); +} + +void GBAWidget::Resize(int scale) +{ + resize(m_width * scale, m_height * scale); +} + +void GBAWidget::UpdateTitle() +{ + std::string title = fmt::format("GBA{}", m_device_number + 1); + if (!m_netplayer_name.empty()) + title += " " + m_netplayer_name; + + if (!m_game_title.empty()) + title += " | " + m_game_title; + + if (m_muted) + title += " | Muted"; + else + title += fmt::format(" | Volume {}%", m_volume); + + setWindowTitle(QString::fromStdString(title)); +} + +void GBAWidget::UpdateVolume() +{ + int volume = m_muted ? 0 : m_volume * 256 / 100; + g_sound_stream->GetMixer()->SetGBAVolume(m_device_number, volume, volume); + UpdateTitle(); +} + +void GBAWidget::LoadGeometry() +{ + const QSettings& settings = Settings::GetQSettings(); + const QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); + if (settings.contains(key)) + restoreGeometry(settings.value(key).toByteArray()); +} + +void GBAWidget::SaveGeometry() +{ + QSettings& settings = Settings::GetQSettings(); + const QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); + settings.setValue(key, saveGeometry()); +} + +bool GBAWidget::CanControlCore() +{ + return !Movie::IsMovieActive() && !NetPlay::IsNetPlayRunning(); +} + +bool GBAWidget::CanResetCore() +{ + return m_is_local_pad; +} + +void GBAWidget::closeEvent(QCloseEvent* event) +{ + event->ignore(); +} + +void GBAWidget::contextMenuEvent(QContextMenuEvent* event) +{ + auto* menu = new QMenu(this); + connect(menu, &QMenu::triggered, menu, &QMenu::deleteLater); + + auto* disconnect_action = + new QAction(m_force_disconnect ? tr("Dis&connected") : tr("&Connected"), menu); + disconnect_action->setEnabled(CanControlCore()); + disconnect_action->setCheckable(true); + disconnect_action->setChecked(!m_force_disconnect); + connect(disconnect_action, &QAction::triggered, this, &GBAWidget::ToggleDisconnect); + + auto* load_action = new QAction(tr("L&oad ROM"), menu); + load_action->setEnabled(CanControlCore()); + connect(load_action, &QAction::triggered, this, &GBAWidget::LoadROM); + + auto* unload_action = new QAction(tr("&Unload ROM"), menu); + unload_action->setEnabled(CanControlCore() && !m_game_title.empty()); + connect(unload_action, &QAction::triggered, this, &GBAWidget::UnloadROM); + + auto* reset_action = new QAction(tr("&Reset"), menu); + reset_action->setEnabled(CanResetCore()); + connect(reset_action, &QAction::triggered, this, &GBAWidget::ResetCore); + + auto* mute_action = new QAction(tr("&Mute"), menu); + mute_action->setCheckable(true); + mute_action->setChecked(m_muted); + connect(mute_action, &QAction::triggered, this, &GBAWidget::ToggleMute); + + auto* size_menu = new QMenu(tr("Window Size"), menu); + + auto* x1_action = new QAction(tr("&1x"), size_menu); + connect(x1_action, &QAction::triggered, this, [this] { Resize(1); }); + auto* x2_action = new QAction(tr("&2x"), size_menu); + connect(x2_action, &QAction::triggered, this, [this] { Resize(2); }); + auto* x3_action = new QAction(tr("&3x"), size_menu); + connect(x3_action, &QAction::triggered, this, [this] { Resize(3); }); + auto* x4_action = new QAction(tr("&4x"), size_menu); + connect(x4_action, &QAction::triggered, this, [this] { Resize(4); }); + + size_menu->addAction(x1_action); + size_menu->addAction(x2_action); + size_menu->addAction(x3_action); + size_menu->addAction(x4_action); + + auto* state_menu = new QMenu(tr("Save State"), menu); + + auto* import_action = new QAction(tr("&Import State"), state_menu); + import_action->setEnabled(CanControlCore()); + connect(import_action, &QAction::triggered, this, [this] { DoState(false); }); + + auto* export_state = new QAction(tr("&Export State"), state_menu); + connect(export_state, &QAction::triggered, this, [this] { DoState(true); }); + + state_menu->addAction(import_action); + state_menu->addAction(export_state); + + menu->addAction(disconnect_action); + menu->addSeparator(); + menu->addAction(load_action); + menu->addAction(unload_action); + menu->addAction(reset_action); + menu->addSeparator(); + menu->addMenu(state_menu); + menu->addSeparator(); + menu->addAction(mute_action); + menu->addSeparator(); + menu->addMenu(size_menu); + + menu->move(event->globalPos()); + menu->show(); +} + +void GBAWidget::paintEvent(QPaintEvent* event) +{ + QPainter painter(this); + painter.fillRect(QRect(QPoint(), size()), Qt::black); + + if (m_video_buffer.size() == static_cast(m_width * m_height)) + { + QImage image(reinterpret_cast(m_video_buffer.data()), m_width, m_height, + QImage::Format_ARGB32); + image = image.convertToFormat(QImage::Format_RGB32); + image = image.rgbSwapped(); + + QSize widget_size = size(); + if (widget_size == QSize(m_width, m_height)) + { + painter.drawImage(QPoint(), image, QRect(0, 0, m_width, m_height)); + } + else if (static_cast(m_width) / m_height > + static_cast(widget_size.width()) / widget_size.height()) + { + int new_height = widget_size.width() * m_height / m_width; + painter.drawImage( + QRect(0, (widget_size.height() - new_height) / 2, widget_size.width(), new_height), image, + QRect(0, 0, m_width, m_height)); + } + else + { + int new_width = widget_size.height() * m_width / m_height; + painter.drawImage( + QRect((widget_size.width() - new_width) / 2, 0, new_width, widget_size.height()), image, + QRect(0, 0, m_width, m_height)); + } + } +} + +void GBAWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (CanControlCore() && event->mimeData()->hasUrls()) + event->acceptProposedAction(); +} + +void GBAWidget::dropEvent(QDropEvent* event) +{ + if (!CanControlCore()) + return; + + for (const QUrl& url : event->mimeData()->urls()) + { + QFileInfo file_info(url.toLocalFile()); + QString path = file_info.filePath(); + + if (!file_info.isFile()) + continue; + + if (!file_info.exists() || !file_info.isReadable()) + { + ModalMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path)); + continue; + } + + if (file_info.suffix() == QStringLiteral("raw")) + { + Core::RunOnCPUThread( + [core = m_core, card_path = path.toStdString()] { + if (auto core_ptr = core.lock()) + core_ptr->EReaderQueueCard(card_path); + }, + false); + } + else + { + RestartCore(m_core, path.toStdString()); + } + } +} + +GBAWidgetController::~GBAWidgetController() +{ + m_widget->deleteLater(); +} + +void GBAWidgetController::Create(std::weak_ptr core, int device_number, + std::string_view game_title, int width, int height) +{ + m_widget = new GBAWidget(std::move(core), device_number, game_title, width, height); +} + +void GBAWidgetController::GameChanged(std::string_view game_title, int width, int height) +{ + m_widget->GameChanged(game_title, width, height); +} + +void GBAWidgetController::FrameEnded(std::vector video_buffer) +{ + m_widget->SetVideoBuffer(std::move(video_buffer)); +} diff --git a/Source/Core/DolphinQt/GBAWidget.h b/Source/Core/DolphinQt/GBAWidget.h new file mode 100644 index 0000000000..24b2147cd1 --- /dev/null +++ b/Source/Core/DolphinQt/GBAWidget.h @@ -0,0 +1,96 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "Common/CommonTypes.h" + +namespace HW::GBA +{ +class Core; +} // namespace HW::GBA + +class QCloseEvent; +class QContextMenuEvent; +class QDragEnterEvent; +class QDropEvent; +class QPaintEvent; + +class GBAWidget : public QWidget +{ + Q_OBJECT +public: + explicit GBAWidget(std::weak_ptr core, int device_number, + std::string_view game_title, int width, int height, QWidget* parent = nullptr, + Qt::WindowFlags flags = {}); + ~GBAWidget(); + + void GameChanged(std::string_view game_title, int width, int height); + void SetVideoBuffer(std::vector video_buffer); + + void SetVolume(int volume); + void VolumeDown(); + void VolumeUp(); + bool IsMuted(); + void ToggleMute(); + void ToggleDisconnect(); + + void LoadROM(); + void UnloadROM(); + void ResetCore(); + void DoState(bool export_state); + void Resize(int scale); + +private: + void UpdateTitle(); + void UpdateVolume(); + + void LoadGeometry(); + void SaveGeometry(); + + bool CanControlCore(); + bool CanResetCore(); + + void closeEvent(QCloseEvent* event) override; + void contextMenuEvent(QContextMenuEvent* event) override; + void paintEvent(QPaintEvent* event) override; + + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + std::weak_ptr m_core; + std::vector m_video_buffer; + int m_device_number; + int m_local_pad; + std::string m_game_title; + int m_width; + int m_height; + std::string m_netplayer_name; + bool m_is_local_pad; + int m_volume; + bool m_muted; + bool m_force_disconnect; +}; + +class GBAWidgetController : public QObject +{ + Q_OBJECT +public: + explicit GBAWidgetController() = default; + ~GBAWidgetController(); + + void Create(std::weak_ptr core, int device_number, std::string_view game_title, + int width, int height); + void GameChanged(std::string_view game_title, int width, int height); + void FrameEnded(std::vector video_buffer); + +private: + GBAWidget* m_widget{}; +}; diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index 61cbc7036d..bc9d6103b3 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -24,6 +24,9 @@ #include "Core/PowerPC/PowerPC.h" #include "Core/State.h" +#ifdef HAS_LIBMGBA +#include "DolphinQt/GBAWidget.h" +#endif #include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/Settings.h" @@ -115,6 +118,15 @@ void Host::SetRenderFullFocus(bool focus) m_render_full_focus = focus; } +bool Host::GetGBAFocus() +{ +#ifdef HAS_LIBMGBA + return qobject_cast(QApplication::activeWindow()) != nullptr; +#else + return false; +#endif +} + bool Host::GetRenderFullscreen() { return m_render_fullscreen; @@ -167,7 +179,7 @@ void Host_UpdateTitle(const std::string& title) bool Host_RendererHasFocus() { - return Host::GetInstance()->GetRenderFocus(); + return Host::GetInstance()->GetRenderFocus() || Host::GetInstance()->GetGBAFocus(); } bool Host_RendererHasFullFocus() @@ -230,7 +242,9 @@ void Host_TitleChanged() #endif } +#ifndef HAS_LIBMGBA std::unique_ptr Host_CreateGBAHost(std::weak_ptr core) { return nullptr; } +#endif diff --git a/Source/Core/DolphinQt/Host.h b/Source/Core/DolphinQt/Host.h index 287333ac04..6ddc4cb3e4 100644 --- a/Source/Core/DolphinQt/Host.h +++ b/Source/Core/DolphinQt/Host.h @@ -27,6 +27,7 @@ public: bool GetRenderFocus(); bool GetRenderFullFocus(); bool GetRenderFullscreen(); + bool GetGBAFocus(); void SetMainWindowHandle(void* handle); void SetRenderHandle(void* handle);