diff options
23 files changed, 817 insertions, 18 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 2711a222a6..f07566c044 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -733,9 +733,6 @@ if(USE_DISCORD_PRESENCE) include_directories(Externals/discord-rpc/include) endif() -if(NOT ENABLE_QT) - set(USE_MGBA 0) -endif() if(USE_MGBA) dolphin_find_optional_system_library(LIBMGBA Externals/mGBA 0.11) endif() diff --git a/Data/Sys/GameSettings/UGP.ini b/Data/Sys/GameSettings/UGP.ini new file mode 100644 index 0000000000..39d067edd3 --- /dev/null +++ b/Data/Sys/GameSettings/UGP.ini @@ -0,0 +1,8 @@ +# UGPP01, UGPE01, UGPJ01 - Game Boy Player + +[Core] +# Attach Game Boy Player hardware. +HSPDevice = 2 +# Overclock to work around sporadic audio/video stream failures. +Overclock = 1.25 +OverclockEnable = True diff --git a/Externals/mGBA/CMakeLists.txt b/Externals/mGBA/CMakeLists.txt index 122e484b2d..61736a02f1 100644 --- a/Externals/mGBA/CMakeLists.txt +++ b/Externals/mGBA/CMakeLists.txt @@ -11,6 +11,7 @@ if(NOT MSVC) endif() if(ANDROID) + target_compile_definitions(mgba PUBLIC ENABLE_VFS_FD) target_compile_definitions(mgba PRIVATE -Dfutimes=futimens) endif() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index b6a03e6b61..eab79765c6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -145,6 +145,7 @@ enum class BooleanSetting( "EnablePlayTimeTracking", true ), + MAIN_GBA_THREADS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "Threads", true), MAIN_EXPAND_TO_CUTOUT_AREA( Settings.FILE_DOLPHIN, Settings.SECTION_INI_INTERFACE, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt index 9f91a4244c..3a6a097642 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt @@ -112,6 +112,7 @@ class Settings : Closeable { const val SECTION_INI_CORE = "Core" const val SECTION_INI_INTERFACE = "Interface" const val SECTION_INI_DSP = "DSP" + const val SECTION_INI_GBA = "GBA" const val SECTION_LOGGER_LOGS = "Logs" const val SECTION_LOGGER_OPTIONS = "Options" const val SECTION_GFX_HARDWARE = "Hardware" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index c4f1107bcd..512a33ddbb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -69,6 +69,9 @@ enum class StringSetting( "" ), MAIN_WFS_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GENERAL, "WFSPath", ""), + MAIN_GBA_BIOS_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "BIOS", ""), + MAIN_GB_PLAYER_ROM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "GBPlayerRom", ""), + MAIN_GBA_SAVES_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "SavesPath", ""), MAIN_TRIFORCE_IP_REDIRECTIONS( Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityResultLaunchers.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityResultLaunchers.kt index 9c13be1b19..2750bef687 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityResultLaunchers.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityResultLaunchers.kt @@ -34,6 +34,26 @@ class SettingsActivityResultLaunchers( ) } + val requestGbaRomFile = fragment.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + onFileResult( + result, + FileBrowserHelper.GBA_ROM_EXTENSIONS, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + + val requestBinFile = fragment.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + onFileResult( + result, + FileBrowserHelper.BIN_EXTENSION, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + val requestRawFile = fragment.registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 1f68bbbd1a..b11a0323a0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -702,6 +702,46 @@ class SettingsFragmentPresenter( MenuTag.CONFIG_SERIALPORT1 ) ) + + sl.add(HeaderSetting(context, R.string.gba_settings, 0)) + sl.add( + SwitchSetting( + context, + BooleanSetting.MAIN_GBA_THREADS, + R.string.gba_threads, + 0, + ) + ) + sl.add( + FilePicker( + context, + StringSetting.MAIN_GBA_BIOS_PATH, + R.string.gba_bios_path, + 0, + fragmentView.activityResultLaunchers.requestBinFile, + "/GBA/gba_bios.bin" + ) + ) + sl.add( + FilePicker( + context, + StringSetting.MAIN_GB_PLAYER_ROM, + R.string.gb_player_rom, + 0, + fragmentView.activityResultLaunchers.requestGbaRomFile, + null + ) + ) + sl.add( + DirectoryPicker( + context, + StringSetting.MAIN_GBA_SAVES_PATH, + R.string.gba_saves_path, + 0, + fragmentView.activityResultLaunchers.requestDirectory, + "/GBA/Saves/" + ) + ) } private fun addWiiSettings(sl: ArrayList<SettingsItem>) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index 061d102be8..f52a70216f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -39,6 +39,9 @@ public final class FileBrowserHelper GAME_LIKE_EXTENSIONS.add("dff"); } + public static final HashSet<String> GBA_ROM_EXTENSIONS = new HashSet<>(Arrays.asList( + "gba", "gbc", "gb", "agb", "mb", "rom", "bin")); + public static final HashSet<String> BIN_EXTENSION = new HashSet<>(Collections.singletonList( "bin")); diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 855cb56ab8..c1c9ec4b09 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -85,6 +85,11 @@ <string name="slot_a_device">GameCube Slot A Device</string> <string name="slot_b_device">GameCube Slot B Device</string> <string name="serial_port_1_device">GameCube Serial Port 1 Device</string> + <string name="gba_settings">GBA Settings</string> + <string name="gba_threads">Run GBA Cores in Dedicated Threads</string> + <string name="gba_bios_path">BIOS</string> + <string name="gb_player_rom">Game Boy Player ROM</string> + <string name="gba_saves_path">Saves</string> <string name="wii_submenu">Wii</string> <string name="wii_misc_settings">Misc Settings</string> <string name="wii_sd_card_settings">SD Card Settings</string> diff --git a/Source/Core/Common/ChunkFile.h b/Source/Core/Common/ChunkFile.h index f44f08d279..a9737c1e0e 100644 --- a/Source/Core/Common/ChunkFile.h +++ b/Source/Core/Common/ChunkFile.h @@ -300,6 +300,13 @@ public: } } + void DoPointer(void*& x, void* const base) + { + auto* ptr = static_cast<u8*>(x); + DoPointer(ptr, static_cast<u8*>(base)); + x = ptr; + } + void DoMarker(const std::string& prevName, u32 arbitraryNumber = 0x42) { u32 cookie = arbitraryNumber; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 61ae899f00..ba72d80a84 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -250,6 +250,8 @@ add_library(core HW/HSP/HSP_Device.h HW/HSP/HSP_DeviceARAMExpansion.cpp HW/HSP/HSP_DeviceARAMExpansion.h + HW/HSP/HSP_DeviceGBPlayer.cpp + HW/HSP/HSP_DeviceGBPlayer.h HW/HSP/HSP_DeviceNull.cpp HW/HSP/HSP_DeviceNull.h HW/HW.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 147aeedf50..2e1d41a151 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -398,11 +398,13 @@ void SetIsoPaths(const std::vector<std::string>& paths) #ifdef HAS_LIBMGBA const Info<std::string> MAIN_GBA_BIOS_PATH{{System::Main, "GBA", "BIOS"}, ""}; -const std::array<Info<std::string>, 4> MAIN_GBA_ROM_PATHS{ +const std::array<Info<std::string>, 5> MAIN_GBA_ROM_PATHS{ Info<std::string>{{System::Main, "GBA", "Rom1"}, ""}, Info<std::string>{{System::Main, "GBA", "Rom2"}, ""}, Info<std::string>{{System::Main, "GBA", "Rom3"}, ""}, - Info<std::string>{{System::Main, "GBA", "Rom4"}, ""}}; + Info<std::string>{{System::Main, "GBA", "Rom4"}, ""}, + Info<std::string>{{System::Main, "GBA", "GBPlayerRom"}, ""}, +}; const Info<std::string> MAIN_GBA_SAVES_PATH{{System::Main, "GBA", "SavesPath"}, ""}; const Info<bool> MAIN_GBA_SAVES_IN_ROM_PATH{{System::Main, "GBA", "SavesInRomPath"}, false}; const Info<bool> MAIN_GBA_THREADS{{System::Main, "GBA", "Threads"}, true}; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index a1baf26242..45ca6e972b 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -224,10 +224,12 @@ void SetIsoPaths(const std::vector<std::string>& paths); #ifdef HAS_LIBMGBA extern const Info<std::string> MAIN_GBA_BIOS_PATH; -extern const std::array<Info<std::string>, 4> MAIN_GBA_ROM_PATHS; +extern const std::array<Info<std::string>, 5> MAIN_GBA_ROM_PATHS; extern const Info<std::string> MAIN_GBA_SAVES_PATH; extern const Info<bool> MAIN_GBA_SAVES_IN_ROM_PATH; extern const Info<bool> MAIN_GBA_THREADS; + +static constexpr std::size_t GBPLAYER_GBA_INDEX = 4; #endif // Main.Network diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 09f56fed5b..2226610d35 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -39,6 +39,10 @@ #include "Core/NetPlayProto.h" #include "Core/System.h" +#ifdef ANDROID +#include "jni/AndroidCommon/AndroidCommon.h" +#endif + namespace HW::GBA { namespace @@ -126,6 +130,15 @@ static VFile* OpenROM_Zip(const char* path) return vf; } +static VFile* OpenReadOnlyFile(const char* path) +{ +#ifdef ANDROID + if (IsPathAndroidContent(path)) + return VFileFromFD(OpenAndroidContent(path, OpenModeToAndroid("r"))); +#endif + return VFileOpen(path, O_RDONLY); +} + static VFile* OpenROM(const char* rom_path) { VFile* vf{}; @@ -134,7 +147,7 @@ static VFile* OpenROM(const char* rom_path) if (!vf) vf = OpenROM_Zip(rom_path); if (!vf) - vf = VFileOpen(rom_path, O_RDONLY); + vf = OpenReadOnlyFile(rom_path); if (!vf) return nullptr; @@ -238,11 +251,15 @@ bool Core::Start(u64 gc_ticks) m_gc_ticks_remainder = 0; m_keys = 0; - SetSIODriver(); + if (m_device_number != Config::GBPLAYER_GBA_INDEX) + { + SetSIODriver(); + SetAVStream(); + } + SetVideoBuffer(); SetAudioBufferSize(); AddCallbacks(); - SetAVStream(); SetupEvent(); m_core->reset(m_core); @@ -334,7 +351,7 @@ void Core::EReaderQueueCard(std::string_view card_path) bool Core::LoadBIOS(const char* bios_path) { - VFile* vf = VFileOpen(bios_path, O_RDONLY); + VFile* vf = OpenReadOnlyFile(bios_path); if (!vf) { ERROR_LOG_FMT(CORE, "GBA{0} failed to open the BIOS in {1}", m_device_number + 1, bios_path); @@ -394,10 +411,28 @@ void Core::SetSIODriver() void Core::SetVideoBuffer() { - u32 width, height; - m_core->currentVideoSize(m_core, &width, &height); - m_video_buffer.resize(width * height); - m_core->setVideoBuffer(m_core, m_video_buffer.data(), width); + if (m_device_number == Config::GBPLAYER_GBA_INDEX) + { + // GBPlayer expects a GBA-sized video buffer even in GB mode. + // Clear it first to avoid stuck colors from the previous game on switch from GBA->GB. + m_video_buffer.clear(); + m_video_buffer.resize(std::size_t{GBA_VIDEO_HORIZONTAL_PIXELS} * GBA_VIDEO_VERTICAL_PIXELS); + + constexpr size_t GB_VIDEO_OFFSET = 1960; + + m_core->setVideoBuffer( + m_core, m_video_buffer.data() + ((GetPlatform() == mPLATFORM_GBA) ? 0 : GB_VIDEO_OFFSET), + GBA_VIDEO_HORIZONTAL_PIXELS); + } + else + { + u32 width; + u32 height; + m_core->currentVideoSize(m_core, &width, &height); + m_video_buffer.resize(std::size_t{width} * height); + m_core->setVideoBuffer(m_core, m_video_buffer.data(), width); + } + if (auto host = m_host.lock()) host->GameChanged(); } diff --git a/Source/Core/Core/HW/HSP/HSP_Device.cpp b/Source/Core/Core/HW/HSP/HSP_Device.cpp index 069fca3753..7a75afc79c 100644 --- a/Source/Core/Core/HW/HSP/HSP_Device.cpp +++ b/Source/Core/Core/HW/HSP/HSP_Device.cpp @@ -6,7 +6,9 @@ #include <memory> #include "Core/HW/HSP/HSP_DeviceARAMExpansion.h" +#include "Core/HW/HSP/HSP_DeviceGBPlayer.h" #include "Core/HW/HSP/HSP_DeviceNull.h" +#include "Core/System.h" namespace HSP { @@ -26,10 +28,14 @@ void IHSPDevice::DoState(PointerWrap& p) // F A C T O R Y std::unique_ptr<IHSPDevice> HSPDevice_Create(const HSPDeviceType device) { + auto& system = Core::System::GetInstance(); + switch (device) { case HSPDeviceType::ARAMExpansion: return std::make_unique<CHSPDevice_ARAMExpansion>(device); + case HSPDeviceType::GBPlayer: + return std::make_unique<CHSPDevice_GBPlayer>(system, device); case HSPDeviceType::None: default: return std::make_unique<CHSPDevice_Null>(device); diff --git a/Source/Core/Core/HW/HSP/HSP_Device.h b/Source/Core/Core/HW/HSP/HSP_Device.h index b9caa21df8..baf8fc025e 100644 --- a/Source/Core/Core/HW/HSP/HSP_Device.h +++ b/Source/Core/Core/HW/HSP/HSP_Device.h @@ -15,6 +15,7 @@ enum class HSPDeviceType : int { None, ARAMExpansion, + GBPlayer, }; class IHSPDevice diff --git a/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.cpp b/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.cpp new file mode 100644 index 0000000000..470ba38a23 --- /dev/null +++ b/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.cpp @@ -0,0 +1,578 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/HSP/HSP_DeviceGBPlayer.h" + +#include <cstring> +#include <ratio> + +#if defined(HAS_LIBMGBA) +#include <mgba-util/audio-buffer.h> +#include <mgba-util/audio-resampler.h> +#include <mgba/internal/gba/gba.h> +#include <mgba/internal/gba/video.h> + +#include "Core/HW/GBACore.h" +#endif + +#include "Common/ChunkFile.h" +#include "Common/Logging/Log.h" + +#include "Core/Config/MainSettings.h" +#include "Core/CoreTiming.h" +#include "Core/HW/ProcessorInterface.h" +#include "Core/HW/SystemTimers.h" +#include "Core/System.h" + +namespace +{ + +enum class GBPRegister : u8 +{ + Test = 0x10, + Video = 0x11, + Control = 0x14, + SIOControl = 0x15, + Audio = 0x18, + SIO = 0x19, + Keypad = 0x1c, + IRQ = 0x1d, +}; + +constexpr u16 IRQ_ASSERTED = 0x8000; + +constexpr u8 CONTROL_CART_DETECTED = 0x01; +constexpr u8 CONTROL_CART_INSERTED = 0x02; +constexpr u8 CONTROL_3V = 0x04; +constexpr u8 CONTROL_5V = 0x08; +constexpr u8 CONTROL_MASK_IRQ = 0x10; +// constexpr u8 CONTROL_SLEEP = 0x20; +// constexpr u8 CONTROL_LINK_CABLE = 0x40; +// constexpr u8 CONTROL_LINK_ENABLE = 0x80; + +constexpr u32 COMMAND_ADDRESS_MASK = 0x1f; +constexpr u32 AV_ADDRESS_MASK = 0xff8; + +class CGBPlayer_Dummy : public HSP::IGBPlayer +{ +public: + using IGBPlayer::IGBPlayer; + + void Reset() override {} + void Stop() override {} + + bool IsLoaded() const override { return false; } + bool IsGBA() const override { return false; } + + void ReadScanlines(std::span<u32, AV_REGION_SIZE> scanlines) override {} + void ReadAudio(std::span<u8, AV_REGION_SIZE> audio) override {} + + void SetKeys(u16 keys) override {} + + void DoState(PointerWrap& p) override {} +}; + +} // namespace + +namespace HSP +{ + +enum class CHSPDevice_GBPlayer::IRQ : int +{ + Link = 0, + GamePak = 1, + Sleep = 2, + Serial = 3, + Video = 4, + Audio = 5, +}; + +#if defined(HAS_LIBMGBA) + +class CGBPlayer_mGBA : public IGBPlayer +{ +public: + CGBPlayer_mGBA(Core::System&, CHSPDevice_GBPlayer*); + + CGBPlayer_mGBA(const CGBPlayer_mGBA&) = delete; + CGBPlayer_mGBA(CGBPlayer_mGBA&&) = delete; + CGBPlayer_mGBA& operator=(const CGBPlayer_mGBA&) = delete; + CGBPlayer_mGBA& operator=(CGBPlayer_mGBA&&) = delete; + + void Reset() override; + void Stop() override; + + bool IsLoaded() const override; + bool IsGBA() const override; + + void ReadScanlines(std::span<u32, AV_REGION_SIZE> scanlines) override; + void ReadAudio(std::span<u8, AV_REGION_SIZE> audio) override; + + void SetKeys(u16 keys) override; + + void DoState(PointerWrap& p) override; + +private: + static constexpr std::size_t AUDIO_BUFFER_SIZE = 512; + + void UpdateAudio(s64 cycles_late); + void UpdateVideo(u32 next_scanlines, s64 cycles_late); + + HW::GBA::Core m_gba_core; + + mAudioBuffer m_audio_buffer; + mAudioResampler m_audio_resampler; + + CoreTiming::EventType* m_update_audio_event = nullptr; + CoreTiming::EventType* m_update_video_event = nullptr; + + // These are used to calculate timings that don't drift. + u32 m_video_irq_phase = 0; + u16 m_audio_irq_phase = 0; + + u16 m_current_scanline_index = 0; + u16 m_keys = 0; + + // Used to trigger a video IRQ during the next audio IRQ in UpdateAudio. + // Separately timed video IRQs cause the AV stream to enter a bad state. + // It's very fragile for some reason. This might be a CPU timing issue ? + bool m_generate_video_irq = false; +}; + +CGBPlayer_mGBA::CGBPlayer_mGBA(Core::System& system, CHSPDevice_GBPlayer* player) + : IGBPlayer(system, player), m_gba_core{m_system, Config::GBPLAYER_GBA_INDEX} +{ + auto& core_timing = m_system.GetCoreTiming(); + + m_update_audio_event = + core_timing.RegisterEvent("GBPmGBAUpdateAudio", [this](Core::System&, u64, s64 cycles_late) { + UpdateAudio(cycles_late); + }); + m_update_video_event = core_timing.RegisterEvent( + "GBPmGBAUpdateVideo", [this](Core::System&, u64 user_data, s64 cycles_late) { + UpdateVideo(u32(user_data), cycles_late); + }); + + mAudioBufferInit(&m_audio_buffer, AUDIO_BUFFER_SIZE, AUDIO_CHANNEL_COUNT); + mAudioResamplerInit(&m_audio_resampler, mINTERPOLATOR_SINC); + mAudioResamplerSetDestination(&m_audio_resampler, &m_audio_buffer, AUDIO_SAMPLE_RATE); + + // TODO: Does the real hardware boot on power up or wait for the GBPRegister::Control command ? + Reset(); +} + +void CGBPlayer_mGBA::Reset() +{ + Stop(); + + m_gba_core.Start(m_system.GetCoreTiming().GetTicks()); + + // Potentially BIOS or ROM could not be loaded. + if (!m_gba_core.IsStarted()) + return; + + auto& core_timing = m_system.GetCoreTiming(); + + // Start audio/video updates after a delay. This timing isn't critical. + const auto ticks_per_second = m_system.GetSystemTimers().GetTicksPerSecond(); + core_timing.ScheduleEvent(ticks_per_second / 50, m_update_audio_event); + core_timing.ScheduleEvent(ticks_per_second / 50, m_update_video_event, 0); +} + +void CGBPlayer_mGBA::Stop() +{ + if (!m_gba_core.IsStarted()) + return; + + auto& core_timing = m_system.GetCoreTiming(); + + core_timing.RemoveEvent(m_update_audio_event); + core_timing.RemoveEvent(m_update_video_event); + + m_video_irq_phase = 0; + m_audio_irq_phase = 0; + + m_current_scanline_index = 0; + m_keys = 0; + m_generate_video_irq = false; + + m_gba_core.Stop(); + + mAudioBufferClear(&m_audio_buffer); +} + +bool CGBPlayer_mGBA::IsLoaded() const +{ + return true; +} + +bool CGBPlayer_mGBA::IsGBA() const +{ + return m_gba_core.IsStarted() && m_gba_core.GetPlatform() == mPLATFORM_GBA; +} + +void CGBPlayer_mGBA::ReadScanlines(std::span<u32, AV_REGION_SIZE> scanlines) +{ + DEBUG_LOG_FMT(HSP, "GBPlayer: ReadScanlines: {}", m_current_scanline_index); + + if (m_current_scanline_index >= GBA_VIDEO_VERTICAL_PIXELS) + return; + + if (!m_gba_core.IsStarted()) + return; + + // Since the GBA core is idle, this is a convenient time to read and resample some audio. + mAudioResamplerSetSource(&m_audio_resampler, m_gba_core.GetAudioBuffer(), + m_gba_core.GetAudioSampleRate(), true); + mAudioResamplerProcess(&m_audio_resampler); + + const std::span video_buffer = m_gba_core.GetVideoBuffer(); + + for (u32 i = 0; i != GBA_VIDEO_HORIZONTAL_PIXELS * 4; ++i) + { + const u32 color = + M_RGB8_TO_RGB5(video_buffer[(m_current_scanline_index * GBA_VIDEO_HORIZONTAL_PIXELS) + i]); + u32 c = color & 0xFF; + c |= (color & 0xFF00u) << 8; + scanlines[i] = c | (c << 8); // Parity + } + + if (m_current_scanline_index == 0) + scanlines[0] |= 0x80800000; + + m_current_scanline_index += 4; + + if (m_current_scanline_index < GBA_VIDEO_VERTICAL_PIXELS) + m_generate_video_irq = true; +} + +// TODO: Explain the meaning of this audio sample processing. +static constexpr u8 SampleToBits(u16* value) +{ + const auto x = std::min<u32>(*value, 8); + *value -= x; + return u8(0xff00u >> x); +} + +void CGBPlayer_mGBA::ReadAudio(std::span<u8, AV_REGION_SIZE> audio) +{ + std::array<std::array<s16, AUDIO_CHANNEL_COUNT>, AUDIO_READ_SIZE> buffer; + const auto read_count = mAudioBufferRead(&m_audio_buffer, buffer.data()->data(), buffer.size()); + + // Each s16 sample is converted to a u9 which is then converted to 64 u8 values. + // This must have something to do with interpretation by the DSP ? + // TODO: Explain things better. + + constexpr u32 out_bytes_per_sample = AV_REGION_SIZE / AUDIO_READ_SIZE / AUDIO_CHANNEL_COUNT; + static_assert(out_bytes_per_sample == 64); + + auto out_it = audio.begin(); + for (auto [l, r] : std::span{buffer}.first(read_count)) + { + auto l_9bit = u16(u32(l + 0x8000) >> 7u); + auto r_9bit = u16(u32(r + 0x8000) >> 7u); + + for (u32 i = 0; i != out_bytes_per_sample; ++i) + { + *(out_it++) = SampleToBits(&l_9bit); + *(out_it++) = SampleToBits(&r_9bit); + } + } +} + +void CGBPlayer_mGBA::SetKeys(u16 keys) +{ + m_keys = keys; +} + +void CGBPlayer_mGBA::DoState(PointerWrap& p) +{ + m_gba_core.DoState(p); + + p.Do(m_video_irq_phase); + p.Do(m_audio_irq_phase); + + p.Do(m_current_scanline_index); + p.Do(m_keys); + p.Do(m_generate_video_irq); + + // Resampled audio buffer. + p.DoArray(static_cast<u8*>(m_audio_buffer.data.data), u32(m_audio_buffer.data.capacity)); + p.Do(m_audio_buffer.data.size); + p.DoPointer(m_audio_buffer.data.readPtr, m_audio_buffer.data.data); + p.DoPointer(m_audio_buffer.data.writePtr, m_audio_buffer.data.data); +} + +void CGBPlayer_mGBA::UpdateAudio(s64 cycles_late) +{ + m_player->AssertIRQ(CHSPDevice_GBPlayer::IRQ::Audio); + + if (m_generate_video_irq) + { + m_player->AssertIRQ(CHSPDevice_GBPlayer::IRQ::Video); + m_generate_video_irq = false; + } + + constexpr u32 audio_irq_freq = AUDIO_SAMPLE_RATE / AUDIO_READ_SIZE; + + const s64 ticks_per_second = m_system.GetSystemTimers().GetTicksPerSecond(); + + // Calculate a non-drifting time for the next IRQ. + const s64 this_irq_ticks = ticks_per_second * m_audio_irq_phase / audio_irq_freq; + + ++m_audio_irq_phase; + const s64 next_irq_ticks = ticks_per_second * m_audio_irq_phase / audio_irq_freq; + + m_audio_irq_phase %= audio_irq_freq; + + m_system.GetCoreTiming().ScheduleEvent(next_irq_ticks - this_irq_ticks - cycles_late, + m_update_audio_event); +} + +void CGBPlayer_mGBA::UpdateVideo(u32 scanline_index, s64 cycles_late) +{ + // The ~59.7 GBA framerate. + constexpr std::ratio<GBA_ARM7TDMI_FREQUENCY, VIDEO_TOTAL_LENGTH> gba_framerate; + + if (scanline_index == 0) + { + DEBUG_LOG_FMT(HSP, "GBPlayer: Frame start"); + + m_current_scanline_index = 0; + + m_generate_video_irq = true; + + auto& core_timing = m_system.GetCoreTiming(); + + const s64 ticks_per_second = m_system.GetSystemTimers().GetTicksPerSecond(); + + // Schedule an event to execute the GBA on vblank. + const s64 visible_scanlines_ticks = ticks_per_second * GBA_VIDEO_VERTICAL_PIXELS * + VIDEO_HORIZONTAL_LENGTH / GBA_ARM7TDMI_FREQUENCY; + core_timing.ScheduleEvent(visible_scanlines_ticks - cycles_late, m_update_video_event, + GBA_VIDEO_VERTICAL_PIXELS); + + // Calculate a non-drifting time for the start of the next frame. + const s64 this_frame_ticks = + ticks_per_second * m_video_irq_phase * gba_framerate.den / gba_framerate.num; + + ++m_video_irq_phase; + const s64 next_frame_ticks = + ticks_per_second * m_video_irq_phase * gba_framerate.den / gba_framerate.num; + + m_video_irq_phase %= gba_framerate.num; + + core_timing.ScheduleEvent(next_frame_ticks - this_frame_ticks - cycles_late, + m_update_video_event, 0); + + // Ensure the GBA frame emulation is complete. + m_gba_core.Flush(); + } + else if (scanline_index == GBA_VIDEO_VERTICAL_PIXELS) + { + DEBUG_LOG_FMT(HSP, "GBPlayer: Frame end"); + + if (m_current_scanline_index != GBA_VIDEO_VERTICAL_PIXELS) + { + // This happens when the game doesn't read the entire frame in time. + // Lowering the CPU clock override will cause this to happen repeatedly. + WARN_LOG_FMT(HSP, "GBPlayer: Frame end while reading scanlines: {}", + m_current_scanline_index); + } + + // Emulate a single frame during vblank. + m_gba_core.RunFrame(m_keys); + + m_current_scanline_index = scanline_index; + } +} + +#endif + +CHSPDevice_GBPlayer::CHSPDevice_GBPlayer(Core::System& system, HSPDeviceType device) + : IHSPDevice(device), m_system{system} +{ +#if defined(HAS_LIBMGBA) + m_gbp = std::make_unique<CGBPlayer_mGBA>(m_system, this); +#else + m_gbp = std::make_unique<CGBPlayer_Dummy>(m_system, this); +#endif +} + +u64 CHSPDevice_GBPlayer::Read(u32 address) +{ + u64 value = 0; + switch (GBPRegister(address >> 20)) + { + case GBPRegister::Test: + { + std::memcpy(&value, &m_test[address & COMMAND_ADDRESS_MASK], sizeof(value)); + break; + } + case GBPRegister::Control: + { + if (m_gbp->IsLoaded()) + { + m_control |= CONTROL_CART_INSERTED; + if ((m_control & CONTROL_3V) && (m_gbp->IsGBA() || (m_control & CONTROL_5V))) + m_control |= CONTROL_CART_DETECTED; + else + m_control &= ~CONTROL_CART_DETECTED; + } + else + { + m_control &= ~(CONTROL_CART_DETECTED | CONTROL_CART_INSERTED); + } + + value = m_control * 0x0101010101010101ULL; + break; + } + case GBPRegister::IRQ: + { + value = m_irq * 0x0000000100000001ULL; + value |= (m_irq & 0xFF00) * 0x0001010000010100ULL; + value &= 0xFFFF7FFFFFFF7FFFULL; + break; + } + case GBPRegister::Video: + { + const u32 offset = address & AV_ADDRESS_MASK; + if (offset == 0) + m_gbp->ReadScanlines(m_scanlines); + + const u32 scanlines_pos = offset / 4; + + value = u64(m_scanlines[scanlines_pos]) << 32; + value |= m_scanlines[scanlines_pos + 1]; + break; + } + case GBPRegister::Audio: + { + const u32 offset = address & AV_ADDRESS_MASK; + if (offset == 0) + m_gbp->ReadAudio(m_audio); + + const u32 audio_pos = offset / 4; + + value = 0x0101010100000000ULL * m_audio[audio_pos]; + value |= 0x01010101ULL * m_audio[audio_pos + 1]; + break; + } + default: + { + WARN_LOG_FMT(HSP, "GBPlayer: Unknown read from 0x{:08x}", address); + break; + } + } + return value; +} + +void CHSPDevice_GBPlayer::Write(u32 address, u64 value) +{ + // This seems to compensate for DSP.cpp turning 32byte writes into 4 x 8byte writes. + // TODO: Rectify that and adjust this. + constexpr u32 last_chunk = 0x18; + + switch (GBPRegister(address >> 20)) + { + case GBPRegister::Test: + { + value = ~value; + std::memcpy(&m_test[address & COMMAND_ADDRESS_MASK], &value, sizeof(value)); + break; + } + case GBPRegister::Control: + { + if ((address & COMMAND_ADDRESS_MASK) != last_chunk) + break; + + if (!(m_control & (CONTROL_3V | CONTROL_5V)) && (value & (CONTROL_3V | CONTROL_5V))) + { + INFO_LOG_FMT(HSP, "GBPlayer: Reset"); + m_gbp->Reset(); + } + + if ((m_control & (CONTROL_3V | CONTROL_5V)) && !(value & (CONTROL_3V | CONTROL_5V))) + { + INFO_LOG_FMT(HSP, "GBPlayer: Stop"); + m_gbp->Stop(); + + m_scanlines.fill(0); + m_audio.fill(0); + } + + m_control = value & 0xFC; + UpdateInterrupts(); + break; + } + case GBPRegister::IRQ: + { + if ((address & COMMAND_ADDRESS_MASK) != last_chunk) + break; + + m_irq &= ~value; + UpdateInterrupts(); + break; + } + case GBPRegister::Keypad: + { + if ((address & COMMAND_ADDRESS_MASK) != last_chunk) + break; + + m_gbp->SetKeys(value & 0x3FF); + break; + } + default: + { + WARN_LOG_FMT(HSP, "GBPlayer: Unknown write to 0x{:08x}: 0x{:016x}", address, value); + break; + } + } +} + +void CHSPDevice_GBPlayer::DoState(PointerWrap& p) +{ + // Fail when trying to load a state with a different HAS_LIBMGBA value. +#if defined(HAS_LIBMGBA) + p.DoMarker("Have mGBA", 1); +#else + p.DoMarker("Have mGBA", 0); +#endif + + p.Do(m_test); + p.Do(m_control); + p.Do(m_irq); + + p.Do(m_scanlines); + p.Do(m_audio); + + m_gbp->DoState(p); +} + +void CHSPDevice_GBPlayer::AssertIRQ(IRQ irq) +{ + // The 6 IRQ types set the even bits: 0x555. + // FYI: GBPlayer software appears to use the odd bits to mask the even bits. + const u32 irq_bit_value = 1u << (2u * u32(irq)); + + if ((m_irq & irq_bit_value) != 0) + WARN_LOG_FMT(HSP, "GBPlayer: IRQ {} is already set.", u32(irq)); + + m_irq |= irq_bit_value | IRQ_ASSERTED; + UpdateInterrupts(); +} + +void CHSPDevice_GBPlayer::UpdateInterrupts() +{ + const bool set_interrupt = ((m_control & CONTROL_MASK_IRQ) == 0) && ((m_irq & IRQ_ASSERTED) != 0); + m_system.GetProcessorInterface().SetInterrupt(ProcessorInterface::INT_CAUSE_HSP, set_interrupt); +} + +IGBPlayer::IGBPlayer(Core::System& system, CHSPDevice_GBPlayer* player) + : m_system{system}, m_player{player} +{ +} + +IGBPlayer::~IGBPlayer() = default; + +} // namespace HSP diff --git a/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.h b/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.h new file mode 100644 index 0000000000..ba93412de0 --- /dev/null +++ b/Source/Core/Core/HW/HSP/HSP_DeviceGBPlayer.h @@ -0,0 +1,82 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <memory> +#include <span> + +#include "Common/CommonTypes.h" +#include "Core/HW/HSP/HSP_Device.h" + +namespace Core +{ +class System; +} + +namespace HSP +{ +class CHSPDevice_GBPlayer; + +class IGBPlayer +{ +public: + // Audio/Video data is read in chunks. + static constexpr std::size_t AV_REGION_SIZE = 0x400; + + static constexpr std::size_t AUDIO_SAMPLE_RATE = 0x8000; + static constexpr std::size_t AUDIO_READ_SIZE = 8; + static constexpr std::size_t AUDIO_CHANNEL_COUNT = 2; + + IGBPlayer(Core::System&, CHSPDevice_GBPlayer*); + virtual ~IGBPlayer(); + + virtual void Reset() = 0; + virtual void Stop() = 0; + + virtual bool IsLoaded() const = 0; + virtual bool IsGBA() const = 0; + + virtual void ReadScanlines(std::span<u32, AV_REGION_SIZE>) = 0; + virtual void ReadAudio(std::span<u8, AV_REGION_SIZE>) = 0; + + virtual void SetKeys(u16) = 0; + + virtual void DoState(PointerWrap& p) = 0; + +protected: + Core::System& m_system; + CHSPDevice_GBPlayer* const m_player; +}; + +class CHSPDevice_GBPlayer : public IHSPDevice +{ +public: + enum class IRQ : int; + + CHSPDevice_GBPlayer(Core::System& system, HSPDeviceType device); + + void Write(u32 address, u64 value) override; + u64 Read(u32 address) override; + + void DoState(PointerWrap& p) override; + + void AssertIRQ(IRQ); + +private: + void UpdateInterrupts(); + + Core::System& m_system; + + std::array<u8, 0x20> m_test{}; + u8 m_control{0xCC}; + u16 m_irq{0}; + + std::array<u32, IGBPlayer::AV_REGION_SIZE> m_scanlines{}; + std::array<u8, IGBPlayer::AV_REGION_SIZE> m_audio{}; + + std::unique_ptr<IGBPlayer> m_gbp; +}; + +} // namespace HSP diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index e121dcfb7b..a234b4f919 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 185; // Last changed in PR 14526 +constexpr u32 STATE_VERSION = 186; // Last changed in PR 14535 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index ea1679a743..d50fbcdc4d 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -317,6 +317,7 @@ <ClInclude Include="Core\HW\HSP\HSP.h" /> <ClInclude Include="Core\HW\HSP\HSP_Device.h" /> <ClInclude Include="Core\HW\HSP\HSP_DeviceARAMExpansion.h" /> + <ClInclude Include="Core\HW\HSP\HSP_DeviceGBPlayer.h" /> <ClInclude Include="Core\HW\HSP\HSP_DeviceNull.h" /> <ClInclude Include="Core\HW\HW.h" /> <ClInclude Include="Core\HW\MagCard\C1231BR.h" /> @@ -1026,6 +1027,7 @@ <ClCompile Include="Core\HW\HSP\HSP.cpp" /> <ClCompile Include="Core\HW\HSP\HSP_Device.cpp" /> <ClCompile Include="Core\HW\HSP\HSP_DeviceARAMExpansion.cpp" /> + <ClCompile Include="Core\HW\HSP\HSP_DeviceGBPlayer.cpp" /> <ClCompile Include="Core\HW\HSP\HSP_DeviceNull.cpp" /> <ClCompile Include="Core\HW\HW.cpp" /> <ClCompile Include="Core\HW\MagCard\C1231BR.cpp" /> diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp index b4ee1f25ad..178a26d64b 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp +++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp @@ -220,7 +220,10 @@ void GameCubePane::CreateWidgets() { m_gba_rom_edits[i] = new ConfigText(Config::MAIN_GBA_ROM_PATHS[i]); m_gba_browse_roms[i] = new NonDefaultQPushButton(QStringLiteral("...")); - gba_layout->addWidget(new QLabel(tr("Port %1 ROM:").arg(i + 1)), gba_row, 0); + auto* const label = + new QLabel((i == Config::GBPLAYER_GBA_INDEX) ? tr("Game Boy Player ROM:") : + tr("Port %1 ROM:").arg(i + 1)); + gba_layout->addWidget(label, gba_row, 0); gba_layout->addWidget(m_gba_rom_edits[i], gba_row, 1); gba_layout->addWidget(m_gba_browse_roms[i], gba_row, 2); gba_row++; diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.h b/Source/Core/DolphinQt/Settings/GameCubePane.h index a97e82af3f..3a339496a6 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.h +++ b/Source/Core/DolphinQt/Settings/GameCubePane.h @@ -85,8 +85,8 @@ private: ConfigBool* m_gba_save_rom_path; QPushButton* m_gba_browse_bios; ConfigUserPath* m_gba_bios_edit; - std::array<QPushButton*, 4> m_gba_browse_roms; - std::array<ConfigText*, 4> m_gba_rom_edits; + std::array<QPushButton*, 5> m_gba_browse_roms; + std::array<ConfigText*, 5> m_gba_rom_edits; QPushButton* m_gba_browse_saves; ConfigUserPath* m_gba_saves_edit; }; |
