From 9a22ff653f089057f6990ea32dfd33d96a158698 Mon Sep 17 00:00:00 2001 From: Bonta <40473493+Bonta0@users.noreply.github.com> Date: Sun, 4 Jul 2021 13:13:21 +0200 Subject: [PATCH] Core: Implement GBA Core using libmgba --- Source/Core/Core/CMakeLists.txt | 8 +- Source/Core/Core/HW/GBACore.cpp | 714 ++++++++++++++++++++++++++++++++ Source/Core/Core/HW/GBACore.h | 129 ++++++ Source/Core/DolphinLib.props | 2 + 4 files changed, 851 insertions(+), 2 deletions(-) create mode 100644 Source/Core/Core/HW/GBACore.cpp create mode 100644 Source/Core/Core/HW/GBACore.h 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 + +#define PYCPARSE // Remove static functions from the header +#include +#undef PYCPARSE +#include +#include +#include +#include +#include +#include + +#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(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(vf_archive->size(vf_archive)); + + std::vector 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 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 GetROMHash(VFile* rom) +{ + size_t size = rom->size(rom); + u8* buffer = static_cast(rom->map(rom, size, MAP_READ)); + + std::array 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 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([this] { ThreadLoop(); }); + } + + return true; +} + +void Core::Stop() +{ + if (m_thread) + { + Flush(); + m_exit_loop = true; + { + std::lock_guard 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 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 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(driver)->core->m_link_enabled = true; + return true; + }; + m_sio_driver.unload = [](GBASIODriver* driver) { + static_cast(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(context); + core->m_core->setKeys(core->m_core, core->m_keys); + }; + callbacks.videoFrameEnded = [](void* context) { + auto core = static_cast(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(stream)->core; + core->SetVideoBuffer(); + }; + m_stream.postAudioBuffer = [](mAVStream* stream, blip_t* left, blip_t* right) { + auto core = static_cast(stream)->core; + std::vector 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(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 lock(m_queue_mutex); + m_command_queue.push(command); + m_idle = false; + m_command_cv.notify_one(); + } + else + { + RunCommand(command); + } +} + +std::vector Core::GetJoybusResponse() +{ + if (!IsStarted()) + return {}; + + if (m_thread) + { + std::unique_lock 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 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 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(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 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(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((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(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 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 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 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& 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 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#define PYCPARSE // Remove static functions from the header +#include +#undef PYCPARSE +#include +#include + +#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 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(); + + 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& 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 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 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 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 m_host; + + std::unique_ptr m_thread; + bool m_exit_loop = false; + bool m_idle = false; + std::mutex m_queue_mutex; + std::condition_variable m_command_cv; + std::queue m_command_queue; + + std::mutex m_response_mutex; + std::condition_variable m_response_cv; + bool m_response_ready = false; + std::vector 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 @@ + @@ -846,6 +847,7 @@ +