summaryrefslogtreecommitdiff
path: root/Source/Core
diff options
context:
space:
mode:
authorBonta <40473493+Bonta0@users.noreply.github.com>2021-07-04 13:13:21 +0200
committerBonta <40473493+Bonta0@users.noreply.github.com>2021-07-13 16:42:52 +0200
commit9a22ff653f089057f6990ea32dfd33d96a158698 (patch)
treeed7a9079d42698a9af6d03b6eb622fe4c51698d1 /Source/Core
parent2d744da68c31814ee3d5d3696aaa4d6bf5955a21 (diff)
Core: Implement GBA Core using libmgba
Diffstat (limited to 'Source/Core')
-rw-r--r--Source/Core/Core/CMakeLists.txt8
-rw-r--r--Source/Core/Core/HW/GBACore.cpp714
-rw-r--r--Source/Core/Core/HW/GBACore.h129
-rw-r--r--Source/Core/DolphinLib.props2
4 files changed, 851 insertions, 2 deletions
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 6de7e6dbe2..65eb857982 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -440,8 +440,8 @@ add_library(core
PowerPC/Interpreter/Interpreter_Tables.cpp
PowerPC/Interpreter/Interpreter.cpp
PowerPC/Interpreter/Interpreter.h
- PowerPC/JitCommon/DivUtils.cpp
- PowerPC/JitCommon/DivUtils.h
+ PowerPC/JitCommon/DivUtils.cpp
+ PowerPC/JitCommon/DivUtils.h
PowerPC/JitCommon/JitAsmCommon.cpp
PowerPC/JitCommon/JitAsmCommon.h
PowerPC/JitCommon/JitBase.cpp
@@ -621,6 +621,10 @@ if(ENABLE_VULKAN)
endif()
if(USE_MGBA)
+ target_sources(core PRIVATE
+ HW/GBACore.cpp
+ HW/GBACore.h
+ )
target_link_libraries(core PUBLIC mGBA::mgba)
target_compile_definitions(core PUBLIC -DHAS_LIBMGBA)
endif()
diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp
new file mode 100644
index 0000000000..38ef42f1ba
--- /dev/null
+++ b/Source/Core/Core/HW/GBACore.cpp
@@ -0,0 +1,714 @@
+// Copyright 2021 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/GBACore.h"
+
+#include <mbedtls/sha1.h>
+
+#define PYCPARSE // Remove static functions from the header
+#include <mgba/core/interface.h>
+#undef PYCPARSE
+#include <mgba-util/vfs.h>
+#include <mgba/core/blip_buf.h>
+#include <mgba/core/log.h>
+#include <mgba/core/timing.h>
+#include <mgba/internal/gb/gb.h>
+#include <mgba/internal/gba/gba.h>
+
+#include "AudioCommon/AudioCommon.h"
+#include "Common/ChunkFile.h"
+#include "Common/CommonPaths.h"
+#include "Common/CommonTypes.h"
+#include "Common/Config/Config.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
+#include "Common/MinizipUtil.h"
+#include "Common/ScopeGuard.h"
+#include "Common/Thread.h"
+#include "Core/Config/MainSettings.h"
+#include "Core/ConfigManager.h"
+#include "Core/Core.h"
+#include "Core/HW/SystemTimers.h"
+#include "Core/Host.h"
+#include "Core/NetPlayProto.h"
+
+namespace HW::GBA
+{
+namespace
+{
+mLogger s_stub_logger = {
+ [](mLogger*, int category, mLogLevel level, const char* format, va_list args) {}, nullptr};
+} // namespace
+
+constexpr auto SAMPLES = 512;
+constexpr auto SAMPLE_RATE = 48000;
+
+// libmGBA does not return the correct frequency for some GB models
+static u32 GetCoreFrequency(mCore* core)
+{
+ if (core->platform(core) != mPLATFORM_GB)
+ return static_cast<u32>(core->frequency(core));
+
+ switch (static_cast<::GB*>(core->board)->model)
+ {
+ case GB_MODEL_CGB:
+ case GB_MODEL_SCGB:
+ case GB_MODEL_AGB:
+ return CGB_SM83_FREQUENCY;
+ case GB_MODEL_SGB:
+ return SGB_SM83_FREQUENCY;
+ default:
+ return DMG_SM83_FREQUENCY;
+ }
+}
+
+static VFile* OpenROM_Archive(const char* path)
+{
+ VFile* vf{};
+ VDir* archive = VDirOpenArchive(path);
+ if (!archive)
+ return nullptr;
+ VFile* vf_archive =
+ VDirFindFirst(archive, [](VFile* vf_) { return mCoreIsCompatible(vf_) != mPLATFORM_NONE; });
+ if (vf_archive)
+ {
+ size_t size = static_cast<size_t>(vf_archive->size(vf_archive));
+
+ std::vector<u8> buffer(size);
+ vf_archive->seek(vf_archive, 0, SEEK_SET);
+ vf_archive->read(vf_archive, buffer.data(), size);
+ vf_archive->close(vf_archive);
+
+ vf = VFileMemChunk(buffer.data(), size);
+ }
+ archive->close(archive);
+ return vf;
+}
+
+static VFile* OpenROM_Zip(const char* path)
+{
+ VFile* vf{};
+ unzFile zip = unzOpen(path);
+ if (!zip)
+ return nullptr;
+ do
+ {
+ unz_file_info info{};
+ if (unzGetCurrentFileInfo(zip, &info, nullptr, 0, nullptr, 0, nullptr, 0) != UNZ_OK ||
+ !info.uncompressed_size)
+ continue;
+
+ std::vector<u8> buffer(info.uncompressed_size);
+ if (!Common::ReadFileFromZip(zip, &buffer))
+ continue;
+
+ vf = VFileMemChunk(buffer.data(), info.uncompressed_size);
+ if (mCoreIsCompatible(vf) == mPLATFORM_GBA)
+ {
+ vf->seek(vf, 0, SEEK_SET);
+ break;
+ }
+
+ vf->close(vf);
+ vf = nullptr;
+ } while (unzGoToNextFile(zip) == UNZ_OK);
+ unzClose(zip);
+ return vf;
+}
+
+static VFile* OpenROM(const char* rom_path)
+{
+ VFile* vf{};
+
+ vf = OpenROM_Archive(rom_path);
+ if (!vf)
+ vf = OpenROM_Zip(rom_path);
+ if (!vf)
+ vf = VFileOpen(rom_path, O_RDONLY);
+ if (!vf)
+ return nullptr;
+
+ if (mCoreIsCompatible(vf) == mPLATFORM_NONE)
+ {
+ vf->close(vf);
+ return nullptr;
+ }
+ vf->seek(vf, 0, SEEK_SET);
+
+ return vf;
+}
+
+static std::array<u8, 20> GetROMHash(VFile* rom)
+{
+ size_t size = rom->size(rom);
+ u8* buffer = static_cast<u8*>(rom->map(rom, size, MAP_READ));
+
+ std::array<u8, 20> hash;
+ mbedtls_sha1_ret(buffer, size, hash.data());
+ rom->unmap(rom, buffer, size);
+
+ return hash;
+}
+
+Core::Core(int device_number) : m_device_number(device_number)
+{
+ mLogSetDefaultLogger(&s_stub_logger);
+}
+
+Core::~Core()
+{
+ Stop();
+}
+
+bool Core::Start(u64 gc_ticks)
+{
+ if (IsStarted())
+ return false;
+
+ Common::ScopeGuard start_guard{[&] { Stop(); }};
+
+ VFile* rom{};
+ Common::ScopeGuard rom_guard{[&] {
+ if (rom)
+ rom->close(rom);
+ }};
+
+ m_rom_path = Config::Get(Config::MAIN_GBA_ROM_PATHS[m_device_number]);
+ if (!m_rom_path.empty())
+ {
+ rom = OpenROM(m_rom_path.c_str());
+ if (!rom)
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to open the ROM in {1}", m_device_number + 1,
+ m_rom_path);
+ return false;
+ }
+ m_rom_hash = GetROMHash(rom);
+ }
+
+ m_core = rom ? mCoreFindVF(rom) : mCoreCreate(mPLATFORM_GBA);
+ if (!m_core)
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to create core", m_device_number + 1);
+ return false;
+ }
+ m_core->init(m_core);
+
+ mCoreInitConfig(m_core, "dolphin");
+ mCoreConfigSetValue(&m_core->config, "idleOptimization", "detect");
+ mCoreConfigSetIntValue(&m_core->config, "useBios", 0);
+ mCoreConfigSetIntValue(&m_core->config, "skipBios", 0);
+
+ if (m_core->platform(m_core) == mPLATFORM_GBA &&
+ !LoadBIOS(File::GetUserPath(F_GBABIOS_IDX).c_str()))
+ {
+ return false;
+ }
+
+ if (rom)
+ {
+ if (!m_core->loadROM(m_core, rom))
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to load the ROM in {1}", m_device_number + 1,
+ m_rom_path);
+ return false;
+ }
+ rom_guard.Dismiss();
+
+ std::array<char, 17> game_title{};
+ m_core->getGameTitle(m_core, game_title.data());
+ m_game_title = game_title.data();
+
+ m_save_path = GetSavePath(m_rom_path, m_device_number);
+ if (!m_save_path.empty() && !LoadSave(m_save_path.c_str()))
+ return false;
+ }
+
+ m_last_gc_ticks = gc_ticks;
+ m_gc_ticks_remainder = 0;
+ m_keys = 0;
+
+ SetSIODriver();
+ SetVideoBuffer();
+ SetSampleRates();
+ AddCallbacks();
+ SetAVStream();
+ SetupEvent();
+
+ m_core->reset(m_core);
+ m_started = true;
+ start_guard.Dismiss();
+ // Notify the host and handle a dimension change if that happened after reset()
+ SetVideoBuffer();
+
+ if (Config::Get(Config::MAIN_GBA_THREADS))
+ {
+ m_idle = true;
+ m_exit_loop = false;
+ m_thread = std::make_unique<std::thread>([this] { ThreadLoop(); });
+ }
+
+ return true;
+}
+
+void Core::Stop()
+{
+ if (m_thread)
+ {
+ Flush();
+ m_exit_loop = true;
+ {
+ std::lock_guard<std::mutex> lock(m_queue_mutex);
+ m_command_cv.notify_one();
+ }
+ m_thread->join();
+ m_thread.reset();
+ }
+ if (m_core)
+ {
+ mCoreConfigDeinit(&m_core->config);
+ m_core->deinit(m_core);
+ m_core = nullptr;
+ }
+ m_started = false;
+ m_rom_path = {};
+ m_save_path = {};
+ m_rom_hash = {};
+ m_game_title = {};
+}
+
+void Core::Reset()
+{
+ Flush();
+ if (!IsStarted())
+ return;
+
+ m_core->reset(m_core);
+}
+
+bool Core::IsStarted() const
+{
+ return m_started;
+}
+
+void Core::SetHost(std::weak_ptr<GBAHostInterface> host)
+{
+ m_host = std::move(host);
+}
+
+void Core::SetForceDisconnect(bool force_disconnect)
+{
+ m_force_disconnect = force_disconnect;
+}
+
+void Core::EReaderQueueCard(std::string_view card_path)
+{
+ Flush();
+ if (!IsStarted() || m_core->platform(m_core) != mPlatform::mPLATFORM_GBA)
+ return;
+
+ File::IOFile file(std::string(card_path), "rb");
+ std::vector<u8> core_state(file.GetSize());
+ file.ReadBytes(core_state.data(), core_state.size());
+ GBACartEReaderQueueCard(static_cast<::GBA*>(m_core->board), core_state.data(), core_state.size());
+}
+
+bool Core::LoadBIOS(const char* bios_path)
+{
+ VFile* vf = VFileOpen(bios_path, O_RDONLY);
+ if (!vf)
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to open the BIOS in {1}", m_device_number + 1, bios_path);
+ return false;
+ }
+
+ if (!m_core->loadBIOS(m_core, vf, 0))
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to load the BIOS in {1}", m_device_number + 1, bios_path);
+ vf->close(vf);
+ return false;
+ }
+
+ return true;
+}
+
+bool Core::LoadSave(const char* save_path)
+{
+ VFile* vf = VFileOpen(save_path, O_CREAT | O_RDWR);
+ if (!vf)
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to open the save in {1}", m_device_number + 1, save_path);
+ return false;
+ }
+
+ if (!m_core->loadSave(m_core, vf))
+ {
+ PanicAlertFmtT("Error: GBA{0} failed to load the save in {1}", m_device_number + 1, save_path);
+ vf->close(vf);
+ return false;
+ }
+
+ return true;
+}
+
+void Core::SetSIODriver()
+{
+ if (m_core->platform(m_core) != mPLATFORM_GBA)
+ return;
+
+ GBASIOJOYCreate(&m_sio_driver);
+ GBASIOSetDriver(&static_cast<::GBA*>(m_core->board)->sio, &m_sio_driver, SIO_JOYBUS);
+
+ m_sio_driver.core = this;
+ m_sio_driver.load = [](GBASIODriver* driver) {
+ static_cast<SIODriver*>(driver)->core->m_link_enabled = true;
+ return true;
+ };
+ m_sio_driver.unload = [](GBASIODriver* driver) {
+ static_cast<SIODriver*>(driver)->core->m_link_enabled = false;
+ return true;
+ };
+}
+
+void Core::SetVideoBuffer()
+{
+ u32 width, height;
+ m_core->desiredVideoDimensions(m_core, &width, &height);
+ m_video_buffer.resize(width * height);
+ m_core->setVideoBuffer(m_core, m_video_buffer.data(), width);
+ if (auto host = m_host.lock())
+ host->GameChanged();
+}
+
+void Core::SetSampleRates()
+{
+ m_core->setAudioBufferSize(m_core, SAMPLES);
+ blip_set_rates(m_core->getAudioChannel(m_core, 0), m_core->frequency(m_core), SAMPLE_RATE);
+ blip_set_rates(m_core->getAudioChannel(m_core, 1), m_core->frequency(m_core), SAMPLE_RATE);
+ g_sound_stream->GetMixer()->SetGBAInputSampleRates(m_device_number, SAMPLE_RATE);
+}
+
+void Core::AddCallbacks()
+{
+ mCoreCallbacks callbacks{};
+ callbacks.context = this;
+ callbacks.keysRead = [](void* context) {
+ auto core = static_cast<Core*>(context);
+ core->m_core->setKeys(core->m_core, core->m_keys);
+ };
+ callbacks.videoFrameEnded = [](void* context) {
+ auto core = static_cast<Core*>(context);
+ if (auto host = core->m_host.lock())
+ host->FrameEnded(core->m_video_buffer);
+ };
+ m_core->addCoreCallbacks(m_core, &callbacks);
+}
+
+void Core::SetAVStream()
+{
+ m_stream = {};
+ m_stream.core = this;
+ m_stream.videoDimensionsChanged = [](mAVStream* stream, unsigned width, unsigned height) {
+ auto core = static_cast<AVStream*>(stream)->core;
+ core->SetVideoBuffer();
+ };
+ m_stream.postAudioBuffer = [](mAVStream* stream, blip_t* left, blip_t* right) {
+ auto core = static_cast<AVStream*>(stream)->core;
+ std::vector<s16> buffer(SAMPLES * 2);
+ blip_read_samples(left, &buffer[0], SAMPLES, 1);
+ blip_read_samples(right, &buffer[1], SAMPLES, 1);
+ g_sound_stream->GetMixer()->PushGBASamples(core->m_device_number, &buffer[0], SAMPLES);
+ };
+ m_core->setAVStream(m_core, &m_stream);
+}
+
+void Core::SetupEvent()
+{
+ m_event.context = this;
+ m_event.name = "Dolphin Sync";
+ m_event.callback = [](mTiming* timing, void* context, u32 cycles_late) {
+ Core* core = static_cast<Core*>(context);
+ if (core->m_core->platform(core->m_core) == mPLATFORM_GBA)
+ static_cast<::GBA*>(core->m_core->board)->earlyExit = true;
+ else if (core->m_core->platform(core->m_core) == mPLATFORM_GB)
+ static_cast<::GB*>(core->m_core->board)->earlyExit = true;
+ core->m_waiting_for_event = false;
+ };
+ 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())
+ return;
+
+ Command command{};
+ command.ticks = gc_ticks;
+ command.transfer_time = transfer_time;
+ command.sync_only = buffer == nullptr;
+ if (buffer)
+ std::copy_n(buffer, command.buffer.size(), command.buffer.begin());
+ command.keys = keys;
+
+ if (m_thread)
+ {
+ std::lock_guard<std::mutex> lock(m_queue_mutex);
+ m_command_queue.push(command);
+ m_idle = false;
+ m_command_cv.notify_one();
+ }
+ else
+ {
+ RunCommand(command);
+ }
+}
+
+std::vector<u8> Core::GetJoybusResponse()
+{
+ if (!IsStarted())
+ return {};
+
+ if (m_thread)
+ {
+ std::unique_lock<std::mutex> lock(m_response_mutex);
+ m_response_cv.wait(lock, [&] { return m_response_ready; });
+ }
+ m_response_ready = false;
+ return m_response;
+}
+
+void Core::Flush()
+{
+ if (!IsStarted() || !m_thread)
+ return;
+ std::unique_lock<std::mutex> lock(m_queue_mutex);
+ m_response_cv.wait(lock, [&] { return m_idle; });
+}
+
+void Core::ThreadLoop()
+{
+ Common::SetCurrentThreadName(fmt::format("GBA{}", m_device_number + 1).c_str());
+ std::unique_lock<std::mutex> queue_lock(m_queue_mutex);
+ while (true)
+ {
+ m_command_cv.wait(queue_lock, [&] { return !m_command_queue.empty() || m_exit_loop; });
+ if (m_exit_loop)
+ break;
+ Command command{m_command_queue.front()};
+ m_command_queue.pop();
+ queue_lock.unlock();
+
+ RunCommand(command);
+
+ queue_lock.lock();
+ if (m_command_queue.empty())
+ m_idle = true;
+ m_response_cv.notify_one();
+ }
+}
+
+void Core::RunCommand(Command& command)
+{
+ m_keys = command.keys;
+ RunUntil(command.ticks);
+ if (!command.sync_only)
+ {
+ m_response.clear();
+ if (m_link_enabled && !m_force_disconnect)
+ {
+ int recvd = GBASIOJOYSendCommand(
+ &m_sio_driver, static_cast<GBASIOJOYCommand>(command.buffer[0]), &command.buffer[1]);
+ std::copy(command.buffer.begin() + 1, command.buffer.begin() + 1 + recvd,
+ std::back_inserter(m_response));
+ }
+
+ if (m_thread && !m_response_ready)
+ {
+ std::lock_guard<std::mutex> response_lock(m_response_mutex);
+ m_response_ready = true;
+ m_response_cv.notify_one();
+ }
+ else
+ {
+ m_response_ready = true;
+ }
+ }
+ if (command.transfer_time)
+ RunFor(command.transfer_time);
+}
+
+void Core::RunUntil(u64 gc_ticks)
+{
+ if (static_cast<s64>(gc_ticks - m_last_gc_ticks) <= 0)
+ return;
+
+ const u64 gc_frequency = SystemTimers::GetTicksPerSecond();
+ const u32 core_frequency = GetCoreFrequency(m_core);
+
+ mTimingSchedule(m_core->timing, &m_event,
+ static_cast<s32>((gc_ticks - m_last_gc_ticks) * core_frequency / gc_frequency));
+ m_waiting_for_event = true;
+
+ s32 begin_time = mTimingCurrentTime(m_core->timing);
+ while (m_waiting_for_event)
+ m_core->runLoop(m_core);
+ s32 end_time = mTimingCurrentTime(m_core->timing);
+
+ u64 d = (static_cast<u64>(end_time - begin_time) * gc_frequency) + m_gc_ticks_remainder;
+ m_last_gc_ticks += d / core_frequency;
+ m_gc_ticks_remainder = d % core_frequency;
+}
+
+void Core::RunFor(u64 gc_ticks)
+{
+ RunUntil(m_last_gc_ticks + gc_ticks);
+}
+
+void Core::ImportState(std::string_view state_path)
+{
+ Flush();
+ if (!IsStarted())
+ return;
+
+ std::vector<u8> core_state(m_core->stateSize(m_core));
+ File::IOFile file(std::string(state_path), "rb");
+ if (core_state.size() != file.GetSize())
+ return;
+
+ file.ReadBytes(core_state.data(), core_state.size());
+ m_core->loadState(m_core, core_state.data());
+}
+
+void Core::ExportState(std::string_view state_path)
+{
+ Flush();
+ if (!IsStarted())
+ return;
+
+ std::vector<u8> core_state(m_core->stateSize(m_core));
+ m_core->saveState(m_core, core_state.data());
+
+ File::IOFile file(std::string(state_path), "wb");
+ file.WriteBytes(core_state.data(), core_state.size());
+}
+
+void Core::DoState(PointerWrap& p)
+{
+ Flush();
+ if (!IsStarted())
+ {
+ ::Core::DisplayMessage(fmt::format("GBA{} core not started. Aborting.", m_device_number + 1),
+ 3000);
+ p.SetMode(PointerWrap::MODE_VERIFY);
+ return;
+ }
+
+ bool has_rom = !m_rom_path.empty();
+ p.Do(has_rom);
+ auto old_hash = m_rom_hash;
+ p.Do(m_rom_hash);
+ auto old_title = m_game_title;
+ p.Do(m_game_title);
+
+ if (p.GetMode() == PointerWrap::MODE_READ &&
+ (has_rom != !m_rom_path.empty() ||
+ (has_rom && (old_hash != m_rom_hash || old_title != m_game_title))))
+ {
+ ::Core::DisplayMessage(
+ fmt::format("Incompatible ROM state in GBA{}. Aborting load state.", m_device_number + 1),
+ 3000);
+ p.SetMode(PointerWrap::MODE_VERIFY);
+ return;
+ }
+
+ p.Do(m_video_buffer);
+ p.Do(m_last_gc_ticks);
+ p.Do(m_gc_ticks_remainder);
+ p.Do(m_keys);
+ p.Do(m_link_enabled);
+ p.Do(m_response_ready);
+ p.Do(m_response);
+
+ std::vector<u8> core_state;
+ core_state.resize(m_core->stateSize(m_core));
+
+ if (p.GetMode() == PointerWrap::MODE_WRITE || p.GetMode() == PointerWrap::MODE_VERIFY)
+ {
+ m_core->saveState(m_core, core_state.data());
+ }
+
+ p.Do(core_state);
+
+ if (p.GetMode() == PointerWrap::MODE_READ && m_core->stateSize(m_core) == core_state.size())
+ {
+ m_core->loadState(m_core, core_state.data());
+ if (auto host = m_host.lock())
+ host->FrameEnded(m_video_buffer);
+ }
+}
+
+bool Core::GetRomInfo(const char* rom_path, std::array<u8, 20>& hash, std::string& title)
+{
+ VFile* rom = OpenROM(rom_path);
+ if (!rom)
+ return false;
+
+ hash = GetROMHash(rom);
+
+ mCore* core = mCoreFindVF(rom);
+ if (!core)
+ {
+ rom->close(rom);
+ return false;
+ }
+ core->init(core);
+ if (!core->loadROM(core, rom))
+ {
+ rom->close(rom);
+ return false;
+ }
+
+ std::array<char, 17> game_title{};
+ core->getGameTitle(core, game_title.data());
+ title = game_title.data();
+
+ core->deinit(core);
+ return true;
+}
+
+std::string Core::GetSavePath(std::string_view rom_path, int device_number)
+{
+ std::string save_path =
+ fmt::format("{}-{}.sav", rom_path.substr(0, rom_path.find_last_of('.')), device_number + 1);
+
+ if (!Config::Get(Config::MAIN_GBA_SAVES_IN_ROM_PATH))
+ {
+ save_path =
+ File::GetUserPath(D_GBASAVES_IDX) + save_path.substr(save_path.find_last_of("\\/") + 1);
+ }
+
+ return save_path;
+}
+} // namespace HW::GBA
diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h
new file mode 100644
index 0000000000..1a9f8c4222
--- /dev/null
+++ b/Source/Core/Core/HW/GBACore.h
@@ -0,0 +1,129 @@
+// Copyright 2021 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <vector>
+
+#define PYCPARSE // Remove static functions from the header
+#include <mgba/core/interface.h>
+#undef PYCPARSE
+#include <mgba/core/core.h>
+#include <mgba/gba/interface.h>
+
+#include "Common/CommonTypes.h"
+
+class GBAHostInterface;
+class PointerWrap;
+
+namespace HW::GBA
+{
+class Core;
+struct SIODriver : GBASIODriver
+{
+ Core* core;
+};
+struct AVStream : mAVStream
+{
+ Core* core;
+};
+
+class Core final
+{
+public:
+ explicit Core(int device_number);
+ ~Core();
+
+ bool Start(u64 gc_ticks);
+ void Stop();
+ void Reset();
+ bool IsStarted() const;
+
+ void SetHost(std::weak_ptr<GBAHostInterface> 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<u8> GetJoybusResponse();
+
+ void ImportState(std::string_view state_path);
+ void ExportState(std::string_view state_path);
+ void DoState(PointerWrap& p);
+
+ static bool GetRomInfo(const char* rom_path, std::array<u8, 20>& hash, std::string& title);
+ static std::string GetSavePath(std::string_view rom_path, int device_number);
+
+private:
+ void ThreadLoop();
+ void RunUntil(u64 gc_ticks);
+ void RunFor(u64 gc_ticks);
+ void Flush();
+
+ struct Command
+ {
+ u64 ticks;
+ int transfer_time;
+ bool sync_only;
+ std::array<u8, 6> buffer;
+ u16 keys;
+ };
+ void RunCommand(Command& command);
+
+ bool LoadBIOS(const char* bios_path);
+ bool LoadSave(const char* save_path);
+
+ void SetSIODriver();
+ void SetVideoBuffer();
+ void SetSampleRates();
+ void AddCallbacks();
+ void SetAVStream();
+ void SetupEvent();
+
+ const int m_device_number;
+
+ bool m_started = false;
+ std::string m_rom_path;
+ std::string m_save_path;
+ std::array<u8, 20> m_rom_hash{};
+ std::string m_game_title;
+
+ mCore* m_core{};
+ mTimingEvent m_event{};
+ bool m_waiting_for_event = false;
+ SIODriver m_sio_driver{};
+ AVStream m_stream{};
+ std::vector<u32> m_video_buffer;
+
+ u64 m_last_gc_ticks = 0;
+ u64 m_gc_ticks_remainder = 0;
+ u16 m_keys = 0;
+ bool m_link_enabled = false;
+ bool m_force_disconnect = false;
+
+ std::weak_ptr<GBAHostInterface> m_host;
+
+ std::unique_ptr<std::thread> m_thread;
+ bool m_exit_loop = false;
+ bool m_idle = false;
+ std::mutex m_queue_mutex;
+ std::condition_variable m_command_cv;
+ std::queue<Command> m_command_queue;
+
+ std::mutex m_response_mutex;
+ std::condition_variable m_response_cv;
+ bool m_response_ready = false;
+ std::vector<u8> m_response;
+};
+} // namespace HW::GBA
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 45cc52e6e3..7835689d55 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -264,6 +264,7 @@
<ClInclude Include="Core\HW\EXI\EXI_DeviceMemoryCard.h" />
<ClInclude Include="Core\HW\EXI\EXI_DeviceMic.h" />
<ClInclude Include="Core\HW\EXI\EXI.h" />
+ <ClInclude Include="Core\HW\GBACore.h" />
<ClInclude Include="Core\HW\GBAPad.h" />
<ClInclude Include="Core\HW\GBAPadEmu.h" />
<ClInclude Include="Core\HW\GCKeyboard.h" />
@@ -846,6 +847,7 @@
<ClCompile Include="Core\HW\EXI\EXI_DeviceMemoryCard.cpp" />
<ClCompile Include="Core\HW\EXI\EXI_DeviceMic.cpp" />
<ClCompile Include="Core\HW\EXI\EXI.cpp" />
+ <ClCompile Include="Core\HW\GBACore.cpp" />
<ClCompile Include="Core\HW\GBAPad.cpp" />
<ClCompile Include="Core\HW\GBAPadEmu.cpp" />
<ClCompile Include="Core\HW\GCKeyboard.cpp" />