From c197f18a466381a1fe53d6578f004f50ca1413f8 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 28 Mar 2026 14:08:39 -0500 Subject: [PATCH 1/5] HW/GBACore: Enable GB/GBC ROM loading. --- Source/Core/Core/HW/GBACore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 6d1d14b239..6f02313e0b 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -114,7 +114,7 @@ static VFile* OpenROM_Zip(const char* path) continue; vf = VFileMemChunk(buffer.data(), info->uncompressed_size); - if (mCoreIsCompatible(vf) == mPLATFORM_GBA) + if (mCoreIsCompatible(vf) != mPLATFORM_NONE) { vf->seek(vf, 0, SEEK_SET); break; From 1ab57199e615f971732eeb0f1dd88b3a13ce94af Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 29 Mar 2026 10:52:04 -0500 Subject: [PATCH 2/5] HW/GBACore: Allow running without the GBA bios. --- Source/Core/Core/HW/GBACore.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 6f02313e0b..5eaf204b21 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -27,9 +27,9 @@ #include "Common/Crypto/SHA1.h" #include "Common/FileUtil.h" #include "Common/IOFile.h" +#include "Common/Logging/Log.h" #include "Common/MinizipUtil.h" #include "Common/ScopeGuard.h" -#include "Common/Thread.h" #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" @@ -209,10 +209,9 @@ bool Core::Start(u64 gc_ticks) 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())) + if (m_core->platform(m_core) == mPLATFORM_GBA) { - return false; + LoadBIOS(File::GetUserPath(F_GBABIOS_IDX).c_str()); } if (rom) @@ -338,13 +337,13 @@ 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); + ERROR_LOG_FMT(CORE, "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); + ERROR_LOG_FMT(CORE, "GBA{0} failed to load the BIOS in {1}", m_device_number + 1, bios_path); vf->close(vf); return false; } From f103155658e1041bed914abd67327396ebf58fad Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 28 Mar 2026 14:03:34 -0500 Subject: [PATCH 3/5] HW/GBACore: Expose the ability to run for a single frame. --- Source/Core/Core/HW/GBACore.cpp | 32 +++++++++++++++++++++++++------- Source/Core/Core/HW/GBACore.h | 18 ++++++++++-------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 5eaf204b21..5f2c3ff5b6 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -470,12 +470,20 @@ void Core::SetupEvent() m_event.priority = 0x80; } +void Core::RunFrame(u16 keys) +{ + PushEvent({ + .event_type = SyncEventType::RunFrame, + .keys = keys, + }); +} + void Core::SyncJoybus(u64 gc_ticks, u16 keys) { PushEvent({ - .run_until_ticks = gc_ticks, + .event_type = SyncEventType::TimeSync, .keys = keys, - .event_type = JoybusEventType::TimeSync, + .run_until_ticks = gc_ticks, }); } @@ -491,9 +499,9 @@ void Core::SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 ke m_command_pending.store(true, std::memory_order_relaxed); PushEvent({ - .run_until_ticks = gc_ticks, + .event_type = SyncEventType::RunCommand, .keys = keys, - .event_type = JoybusEventType::RunCommand, + .run_until_ticks = gc_ticks, }); } @@ -510,7 +518,7 @@ void Core::Flush() m_event_thread.WaitForCompletion(); } -void Core::PushEvent(JoybusEvent event) +void Core::PushEvent(SyncEvent event) { if (m_event_thread.IsRunning()) m_event_thread.Push(event); @@ -518,12 +526,22 @@ void Core::PushEvent(JoybusEvent event) HandleEvent(event); } -void Core::HandleEvent(JoybusEvent event) +void Core::HandleEvent(SyncEvent event) { m_keys = event.keys; + + if (event.event_type == SyncEventType::RunFrame) + { + m_last_gc_ticks = m_system.GetCoreTiming().GetTicks(); + m_gc_ticks_remainder = 0; + + m_core->runFrame(m_core); + return; + } + RunUntil(event.run_until_ticks); - if (event.event_type != JoybusEventType::RunCommand) + if (event.event_type != SyncEventType::RunCommand) return; if (m_link_enabled && !m_force_disconnect) diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h index 64f61a0eb7..110b79a16a 100644 --- a/Source/Core/Core/HW/GBACore.h +++ b/Source/Core/Core/HW/GBACore.h @@ -72,6 +72,7 @@ public: void SetForceDisconnect(bool force_disconnect); void EReaderQueueCard(std::string_view card_path); + void RunFrame(u16 keys); void SyncJoybus(u64 gc_ticks, u16 keys); void SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys); int GetJoybusResponse(u8* data_out); @@ -90,19 +91,20 @@ private: void RunFor(u64 gc_ticks); void Flush(); - enum class JoybusEventType : u8 + enum class SyncEventType : u8 { TimeSync, RunCommand, + RunFrame, }; - struct JoybusEvent + struct SyncEvent { - u64 run_until_ticks{}; + SyncEventType event_type{}; u16 keys{}; - JoybusEventType event_type{}; + u64 run_until_ticks{}; // Not used by SyncEventType::RunFrame. }; - void PushEvent(JoybusEvent event); - void HandleEvent(JoybusEvent event); + void PushEvent(SyncEvent event); + void HandleEvent(SyncEvent event); bool LoadBIOS(const char* bios_path); bool LoadSave(const char* save_path); @@ -137,7 +139,7 @@ private: std::weak_ptr m_host; - // Set by the GC thread before issuing a JoybusEventType::RunCommand. + // Set by the GC thread before issuing a SyncEventType::RunCommand. int m_joybus_command_transfer_time{}; GBASIOJOYCommand m_joybus_command{}; @@ -149,7 +151,7 @@ private: std::atomic_bool m_command_pending{}; // The entire threaded GBA runs within events pushed to this queue. - Common::WorkQueueThreadSP m_event_thread; + Common::WorkQueueThreadSP m_event_thread; ::Core::System& m_system; }; From de0999a19c164d2055fc5ece338e4ad2208999ca Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 28 Mar 2026 14:07:41 -0500 Subject: [PATCH 4/5] HW/GBACore: Make the Flush function public and add additional getters. --- Source/Core/Core/HW/GBACore.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h index 110b79a16a..47f5a3102e 100644 --- a/Source/Core/Core/HW/GBACore.h +++ b/Source/Core/Core/HW/GBACore.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -77,6 +78,15 @@ public: void SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys); int GetJoybusResponse(u8* data_out); + // Wait for requested GBA emulation to complete. + void Flush(); + + mAudioBuffer* GetAudioBuffer() { return m_core->getAudioBuffer(m_core); } + std::span GetVideoBuffer() const { return m_video_buffer; } + + mPlatform GetPlatform() const { return m_core->platform(m_core); } + u32 GetAudioSampleRate() const { return m_core->audioSampleRate(m_core); } + void ImportState(std::string_view state_path); void ExportState(std::string_view state_path); void ImportSave(std::string_view save_path); @@ -89,7 +99,6 @@ public: private: void RunUntil(u64 gc_ticks); void RunFor(u64 gc_ticks); - void Flush(); enum class SyncEventType : u8 { From 84dd2304d60a47816bee348272b4dc0946dbbfea Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 29 Mar 2026 11:12:22 -0500 Subject: [PATCH 5/5] HW/GBACore: Allow save states to work when the GBA core is stopped. --- Source/Core/Core/HW/GBACore.cpp | 12 ++++++++---- Source/Core/Core/State.cpp | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 5f2c3ff5b6..d95617b9b3 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -650,11 +650,13 @@ void Core::ExportSave(std::string_view save_path) void Core::DoState(PointerWrap& p) { Flush(); - if (!IsStarted()) + + bool is_started = IsStarted(); + p.Do(is_started); + + if (!is_started) { - ::Core::DisplayMessage(fmt::format("GBA{} core not started. Aborting.", m_device_number + 1), - 3000); - p.SetVerifyMode(); + Stop(); return; } @@ -675,6 +677,8 @@ void Core::DoState(PointerWrap& p) return; } + Start(0); + p.Do(m_video_buffer); p.Do(m_last_gc_ticks); p.Do(m_gc_ticks_remainder); diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index b9f655e578..e121dcfb7b 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP s_compress_and_dump_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 184; // Last changed in PR 14110 +constexpr u32 STATE_VERSION = 185; // Last changed in PR 14526 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217