diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 7916cbfdc0..5b65e82a3d 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -292,6 +292,25 @@ bool Core::IsStarted() const return m_started; } +CoreInfo Core::GetCoreInfo() const +{ + CoreInfo info{}; + info.device_number = m_device_number; + info.width = GBA_VIDEO_HORIZONTAL_PIXELS; + info.height = GBA_VIDEO_VERTICAL_PIXELS; + + if (!IsStarted()) + return info; + + info.is_gba = m_core->platform(m_core) == mPlatform::mPLATFORM_GBA; + info.has_rom = !m_rom_path.empty(); + info.has_ereader = + info.is_gba && static_cast<::GBA*>(m_core->board)->memory.hw.devices & HW_EREADER; + m_core->desiredVideoDimensions(m_core, &info.width, &info.height); + info.game_title = m_game_title; + return info; +} + void Core::SetHost(std::weak_ptr host) { m_host = std::move(host); @@ -305,7 +324,7 @@ void Core::SetForceDisconnect(bool force_disconnect) void Core::EReaderQueueCard(std::string_view card_path) { Flush(); - if (!IsStarted() || m_core->platform(m_core) != mPlatform::mPLATFORM_GBA) + if (!GetCoreInfo().has_ereader) return; File::IOFile file(std::string(card_path), "rb"); @@ -438,28 +457,6 @@ void Core::SetupEvent() m_event.priority = 0x80; } -int Core::GetDeviceNumber() const -{ - return m_device_number; -} - -void Core::GetVideoDimensions(u32* width, u32* height) const -{ - if (!IsStarted()) - { - *width = GBA_VIDEO_HORIZONTAL_PIXELS; - *height = GBA_VIDEO_VERTICAL_PIXELS; - return; - } - - m_core->desiredVideoDimensions(m_core, width, height); -} - -std::string Core::GetGameTitle() const -{ - return m_game_title; -} - void Core::SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys) { if (!IsStarted()) diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h index 1a9f8c4222..51fcdf0d10 100644 --- a/Source/Core/Core/HW/GBACore.h +++ b/Source/Core/Core/HW/GBACore.h @@ -36,6 +36,17 @@ struct AVStream : mAVStream Core* core; }; +struct CoreInfo +{ + int device_number; + bool is_gba; + bool has_rom; + bool has_ereader; + u32 width; + u32 height; + std::string game_title; +}; + class Core final { public: @@ -46,15 +57,12 @@ public: void Stop(); void Reset(); bool IsStarted() const; + CoreInfo GetCoreInfo() const; void SetHost(std::weak_ptr host); void SetForceDisconnect(bool force_disconnect); void EReaderQueueCard(std::string_view card_path); - int GetDeviceNumber() const; - void GetVideoDimensions(u32* width, u32* height) const; - std::string GetGameTitle() const; - void SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys); std::vector GetJoybusResponse(); diff --git a/Source/Core/DolphinQt/GBAHost.cpp b/Source/Core/DolphinQt/GBAHost.cpp index 918426e320..048574ee5f 100644 --- a/Source/Core/DolphinQt/GBAHost.cpp +++ b/Source/Core/DolphinQt/GBAHost.cpp @@ -15,16 +15,9 @@ GBAHost::GBAHost(std::weak_ptr core) 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); - + HW::GBA::CoreInfo info = core_ptr->GetCoreInfo(); 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); - }); + info] { widget_controller->Create(core, info); }); } GBAHost::~GBAHost() @@ -37,15 +30,10 @@ 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); - }); + HW::GBA::CoreInfo info = core_ptr->GetCoreInfo(); + QueueOnObject(m_widget_controller, [widget_controller = m_widget_controller, info] { + widget_controller->GameChanged(info); + }); } void GBAHost::FrameEnded(const std::vector& video_buffer) diff --git a/Source/Core/DolphinQt/GBAWidget.cpp b/Source/Core/DolphinQt/GBAWidget.cpp index 7d625c687d..5112b88397 100644 --- a/Source/Core/DolphinQt/GBAWidget.cpp +++ b/Source/Core/DolphinQt/GBAWidget.cpp @@ -12,16 +12,15 @@ #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" @@ -38,7 +37,7 @@ static void RestartCore(const std::weak_ptr& core, std::string_vi [core, rom_path = std::string(rom_path)] { if (auto core_ptr = core.lock()) { - auto& info = Config::MAIN_GBA_ROM_PATHS[core_ptr->GetDeviceNumber()]; + auto& info = Config::MAIN_GBA_ROM_PATHS[core_ptr->GetCoreInfo().device_number]; core_ptr->Stop(); Config::SetCurrent(info, rom_path); if (core_ptr->Start(CoreTiming::GetTicks())) @@ -60,56 +59,62 @@ static void QueueEReaderCard(const std::weak_ptr& core, std::stri 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) +GBAWidget::GBAWidget(std::weak_ptr core, const HW::GBA::CoreInfo& info, + const std::optional& netplay_pad) + : QWidget(nullptr, LoadWindowFlags(netplay_pad ? netplay_pad->local_pad : info.device_number)), + m_core(std::move(core)), m_core_info(info), m_local_pad(info.device_number), + m_is_local_pad(true), m_volume(0), m_muted(false), m_force_disconnect(false), m_moving(false), + m_interframe_blending(false) { bool visible = true; - if (NetPlay::IsNetPlayRunning()) + if (netplay_pad) { - NetPlay::PadDetails details = NetPlay::GetPadDetails(m_device_number); - if (details.local_pad < 4) - { - m_netplayer_name = details.player_name; - m_is_local_pad = details.is_local; - m_local_pad = details.local_pad; - visible = !details.hide_gba; - } + m_netplayer_name = netplay_pad->player_name; + m_is_local_pad = netplay_pad->is_local; + m_local_pad = netplay_pad->local_pad; + visible = !netplay_pad->hide_gba; } setWindowIcon(Resources::GetAppIcon()); setAcceptDrops(true); - resize(m_width, m_height); + resize(m_core_info.width, m_core_info.height); setVisible(visible); SetVolume(100); if (!visible) ToggleMute(); - LoadGeometry(); + LoadSettings(); UpdateTitle(); } GBAWidget::~GBAWidget() { - SaveGeometry(); + SaveSettings(); } -void GBAWidget::GameChanged(std::string_view game_title, int width, int height) +void GBAWidget::GameChanged(const HW::GBA::CoreInfo& info) { - m_game_title = game_title; - m_width = width; - m_height = height; + m_core_info = info; + m_previous_frame = QImage(); UpdateTitle(); update(); } void GBAWidget::SetVideoBuffer(std::vector video_buffer) { - m_video_buffer = std::move(video_buffer); + m_previous_frame = std::move(m_last_frame); + if (video_buffer.size() == static_cast(m_core_info.width * m_core_info.height)) + { + m_last_frame = QImage(reinterpret_cast(video_buffer.data()), m_core_info.width, + m_core_info.height, QImage::Format_ARGB32) + .convertToFormat(QImage::Format_RGB32) + .rgbSwapped(); + } + else + { + m_last_frame = QImage(); + } update(); } @@ -170,7 +175,7 @@ void GBAWidget::LoadROM() void GBAWidget::UnloadROM() { - if (!CanControlCore() || m_game_title.empty()) + if (!CanControlCore() || !m_core_info.has_rom) return; RestartCore(m_core); @@ -224,17 +229,57 @@ void GBAWidget::DoState(bool export_state) void GBAWidget::Resize(int scale) { - resize(m_width * scale, m_height * scale); + resize(m_core_info.width * scale, m_core_info.height * scale); +} + +bool GBAWidget::IsBorderless() const +{ + return windowFlags().testFlag(Qt::FramelessWindowHint) || + windowState().testFlag(Qt::WindowFullScreen); +} + +void GBAWidget::SetBorderless(bool enable) +{ + if (windowState().testFlag(Qt::WindowFullScreen)) + { + if (!enable) + setWindowState((windowState() ^ Qt::WindowFullScreen) | Qt::WindowMaximized); + } + else if (windowState().testFlag(Qt::WindowMaximized)) + { + if (enable) + setWindowState((windowState() ^ Qt::WindowMaximized) | Qt::WindowFullScreen); + } + else if (windowFlags().testFlag(Qt::FramelessWindowHint) != enable) + { + QRect saved_geometry = geometry(); + setWindowFlag(Qt::FramelessWindowHint, enable); + setGeometry(saved_geometry); + show(); + } +} + +bool GBAWidget::IsAlwaysOnTop() const +{ + return windowFlags().testFlag(Qt::WindowStaysOnTopHint); +} + +void GBAWidget::SetAlwaysOnTop(bool enable) +{ + if (windowFlags().testFlag(Qt::WindowStaysOnTopHint) == enable) + return; + setWindowFlag(Qt::WindowStaysOnTopHint, enable); + show(); } void GBAWidget::UpdateTitle() { - std::string title = fmt::format("GBA{}", m_device_number + 1); + std::string title = fmt::format("GBA{}", m_core_info.device_number + 1); if (!m_netplayer_name.empty()) title += " " + m_netplayer_name; - if (!m_game_title.empty()) - title += " | " + m_game_title; + if (!m_core_info.game_title.empty()) + title += " | " + m_core_info.game_title; if (m_muted) title += " | Muted"; @@ -247,23 +292,37 @@ void GBAWidget::UpdateTitle() void GBAWidget::UpdateVolume() { int volume = m_muted ? 0 : m_volume * 256 / 100; - g_sound_stream->GetMixer()->SetGBAVolume(m_device_number, volume, volume); + g_sound_stream->GetMixer()->SetGBAVolume(m_core_info.device_number, volume, volume); UpdateTitle(); } -void GBAWidget::LoadGeometry() +Qt::WindowFlags GBAWidget::LoadWindowFlags(int device_number) { 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()); + const QString key = QStringLiteral("gbawidget/flags%1").arg(device_number + 1); + return settings.contains(key) ? Qt::WindowFlags{settings.value(key).toInt()} : Qt::WindowFlags{}; } -void GBAWidget::SaveGeometry() +void GBAWidget::LoadSettings() +{ + const QSettings& settings = Settings::GetQSettings(); + QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); + if (settings.contains(key)) + restoreGeometry(settings.value(key).toByteArray()); + + key = QStringLiteral("gbawidget/interframeblending%1").arg(m_local_pad + 1); + if (settings.contains(key)) + m_interframe_blending = settings.value(key).toBool(); +} + +void GBAWidget::SaveSettings() { QSettings& settings = Settings::GetQSettings(); - const QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); - settings.setValue(key, saveGeometry()); + settings.setValue(QStringLiteral("gbawidget/flags%1").arg(m_local_pad + 1), + static_cast(windowFlags())); + settings.setValue(QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1), saveGeometry()); + settings.setValue(QStringLiteral("gbawidget/interframeblending%1").arg(m_local_pad + 1), + m_interframe_blending); } bool GBAWidget::CanControlCore() @@ -298,24 +357,32 @@ void GBAWidget::contextMenuEvent(QContextMenuEvent* event) 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()); + unload_action->setEnabled(CanControlCore() && m_core_info.has_rom); connect(unload_action, &QAction::triggered, this, &GBAWidget::UnloadROM); auto* card_action = new QAction(tr("&Scan e-Reader Card(s)"), menu); - card_action->setEnabled(CanControlCore() && !m_game_title.empty()); + card_action->setEnabled(CanControlCore() && m_core_info.has_ereader); connect(card_action, &QAction::triggered, this, &GBAWidget::PromptForEReaderCards); auto* reset_action = new QAction(tr("&Reset"), menu); reset_action->setEnabled(CanResetCore()); connect(reset_action, &QAction::triggered, this, &GBAWidget::ResetCore); + 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); }); + 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* options_menu = new QMenu(tr("Options"), menu); + auto* size_menu = new QMenu(tr("Window Size"), options_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); @@ -325,22 +392,21 @@ void GBAWidget::contextMenuEvent(QContextMenuEvent* event) 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* borderless_action = new QAction(tr("&Borderless Window"), options_menu); + borderless_action->setCheckable(true); + borderless_action->setChecked(IsBorderless()); + connect(borderless_action, &QAction::triggered, this, [this] { SetBorderless(!IsBorderless()); }); - auto* state_menu = new QMenu(tr("Save State"), menu); + auto* topmost_action = new QAction(tr("Always on &Top"), options_menu); + topmost_action->setCheckable(true); + topmost_action->setChecked(IsAlwaysOnTop()); + connect(topmost_action, &QAction::triggered, this, [this] { SetAlwaysOnTop(!IsAlwaysOnTop()); }); - 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); + auto* blending_action = new QAction(tr("&Interframe Blending"), options_menu); + blending_action->setCheckable(true); + blending_action->setChecked(m_interframe_blending); + connect(blending_action, &QAction::triggered, this, + [this] { m_interframe_blending = !m_interframe_blending; }); menu->addAction(disconnect_action); menu->addSeparator(); @@ -353,44 +419,85 @@ void GBAWidget::contextMenuEvent(QContextMenuEvent* event) menu->addSeparator(); menu->addAction(mute_action); menu->addSeparator(); - menu->addMenu(size_menu); + menu->addMenu(options_menu); + + state_menu->addAction(import_action); + state_menu->addAction(export_state); + + options_menu->addMenu(size_menu); + options_menu->addSeparator(); + options_menu->addAction(borderless_action); + options_menu->addAction(topmost_action); + options_menu->addAction(blending_action); + + size_menu->addAction(x1_action); + size_menu->addAction(x2_action); + size_menu->addAction(x3_action); + size_menu->addAction(x4_action); menu->move(event->globalPos()); menu->show(); } +void GBAWidget::mouseDoubleClickEvent(QMouseEvent* event) +{ + SetBorderless(!IsBorderless()); +} + +void GBAWidget::mousePressEvent(QMouseEvent* event) +{ + if (event->button() != Qt::MouseButton::LeftButton || + !windowFlags().testFlag(Qt::FramelessWindowHint)) + return; + m_moving = true; + m_move_pos = event->pos(); +} + +void GBAWidget::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() != Qt::MouseButton::LeftButton) + return; + m_moving = false; +} + +void GBAWidget::mouseMoveEvent(QMouseEvent* event) +{ + if (!m_moving) + return; + move(event->globalPos() - m_move_pos - (geometry().topLeft() - pos())); +} + 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)) + const QRect src_rect(0, 0, m_core_info.width, m_core_info.height); + QRect target_rect{}; + if (size() == QSize(m_core_info.width, m_core_info.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(); + target_rect = QRect(0, 0, m_core_info.width, m_core_info.height); + } + else if (static_cast(m_core_info.width) / m_core_info.height > + static_cast(width()) / height()) + { + int new_height = width() * m_core_info.height / m_core_info.width; + target_rect = QRect(0, (height() - new_height) / 2, width(), new_height); + } + else + { + int new_width = height() * m_core_info.width / m_core_info.height; + target_rect = QRect((width() - new_width) / 2, 0, new_width, height()); + } - 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)); - } + if (m_interframe_blending && m_previous_frame.size() == src_rect.size()) + { + painter.drawImage(target_rect, m_previous_frame, src_rect); + painter.setOpacity(0.5); + } + if (m_last_frame.size() == src_rect.size()) + { + painter.drawImage(target_rect, m_last_frame, src_rect); } } @@ -431,15 +538,21 @@ GBAWidgetController::~GBAWidgetController() m_widget->deleteLater(); } -void GBAWidgetController::Create(std::weak_ptr core, int device_number, - std::string_view game_title, int width, int height) +void GBAWidgetController::Create(std::weak_ptr core, const HW::GBA::CoreInfo& info) { - m_widget = new GBAWidget(std::move(core), device_number, game_title, width, height); + std::optional netplay_pad; + if (NetPlay::IsNetPlayRunning()) + { + const NetPlay::PadDetails details = NetPlay::GetPadDetails(info.device_number); + if (details.local_pad < 4) + netplay_pad = details; + } + m_widget = new GBAWidget(std::move(core), info, netplay_pad); } -void GBAWidgetController::GameChanged(std::string_view game_title, int width, int height) +void GBAWidgetController::GameChanged(const HW::GBA::CoreInfo& info) { - m_widget->GameChanged(game_title, width, height); + m_widget->GameChanged(info); } void GBAWidgetController::FrameEnded(std::vector video_buffer) diff --git a/Source/Core/DolphinQt/GBAWidget.h b/Source/Core/DolphinQt/GBAWidget.h index 1ca585f299..949858a4b7 100644 --- a/Source/Core/DolphinQt/GBAWidget.h +++ b/Source/Core/DolphinQt/GBAWidget.h @@ -4,35 +4,39 @@ #pragma once #include +#include #include #include #include +#include +#include #include #include "Common/CommonTypes.h" - -namespace HW::GBA -{ -class Core; -} // namespace HW::GBA +#include "Core/HW/GBACore.h" class QCloseEvent; class QContextMenuEvent; class QDragEnterEvent; class QDropEvent; +class QMouseEvent; class QPaintEvent; +namespace NetPlay +{ +struct PadDetails; +} // namespace NetPlay + 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 = {}); + explicit GBAWidget(std::weak_ptr core, const HW::GBA::CoreInfo& info, + const std::optional& netplay_pad); ~GBAWidget(); - void GameChanged(std::string_view game_title, int width, int height); + void GameChanged(const HW::GBA::CoreInfo& info); void SetVideoBuffer(std::vector video_buffer); void SetVolume(int volume); @@ -49,35 +53,47 @@ public: void DoState(bool export_state); void Resize(int scale); + bool IsBorderless() const; + void SetBorderless(bool enable); + + bool IsAlwaysOnTop() const; + void SetAlwaysOnTop(bool enable); + private: void UpdateTitle(); void UpdateVolume(); - void LoadGeometry(); - void SaveGeometry(); + static Qt::WindowFlags LoadWindowFlags(int device_number); + void LoadSettings(); + void SaveSettings(); bool CanControlCore(); bool CanResetCore(); void closeEvent(QCloseEvent* event) override; void contextMenuEvent(QContextMenuEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* 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; + HW::GBA::CoreInfo m_core_info; + QImage m_last_frame; + QImage m_previous_frame; 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; + std::string m_netplayer_name; int m_volume; bool m_muted; bool m_force_disconnect; + bool m_moving; + QPoint m_move_pos; + bool m_interframe_blending; }; class GBAWidgetController : public QObject @@ -87,9 +103,8 @@ 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 Create(std::weak_ptr core, const HW::GBA::CoreInfo& info); + void GameChanged(const HW::GBA::CoreInfo& info); void FrameEnded(std::vector video_buffer); private: