From f665eaad5fb038762fcf4de7b950362e23647aa2 Mon Sep 17 00:00:00 2001 From: y21 <30553356+y21@users.noreply.github.com> Date: Thu, 1 May 2025 23:55:29 +0200 Subject: [PATCH 001/123] Check buffer out and size in HID_USBv5/USB_VEN GetVersion Ioctl --- Source/Core/Core/IOS/USB/USBV5.cpp | 16 ++++++++++++++++ Source/Core/Core/IOS/USB/USBV5.h | 1 + Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp | 8 +------- Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp | 8 +------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Source/Core/Core/IOS/USB/USBV5.cpp b/Source/Core/Core/IOS/USB/USBV5.cpp index e43e59dfaf2d..afc29a252a46 100644 --- a/Source/Core/Core/IOS/USB/USBV5.cpp +++ b/Source/Core/Core/IOS/USB/USBV5.cpp @@ -204,6 +204,22 @@ std::optional USBV5ResourceManager::HandleDeviceIOCtl(const IOCtlReque return handler(*device); } +IPCReply USBV5ResourceManager::GetUSBVersion(const IOCtlRequest& request) const +{ + static constexpr u32 VERSION = 0x50001; + + if (request.buffer_in != 0 || request.buffer_in_size != 0 || request.buffer_out == 0 || + request.buffer_out_size != 0x20) + { + return IPCReply(IPC_EINVAL); + } + + auto& system = GetSystem(); + auto& memory = system.GetMemory(); + memory.Write_U32(VERSION, request.buffer_out); + return IPCReply(IPC_SUCCESS); +} + void USBV5ResourceManager::OnDeviceChange(const ChangeEvent event, std::shared_ptr device) { diff --git a/Source/Core/Core/IOS/USB/USBV5.h b/Source/Core/Core/IOS/USB/USBV5.h index 8c722ff72386..059092c1a93d 100644 --- a/Source/Core/Core/IOS/USB/USBV5.h +++ b/Source/Core/Core/IOS/USB/USBV5.h @@ -83,6 +83,7 @@ class USBV5ResourceManager : public USBHost using Handler = std::function(USBV5Device&)>; std::optional HandleDeviceIOCtl(const IOCtlRequest& request, Handler handler); + IPCReply GetUSBVersion(const IOCtlRequest& request) const; void OnDeviceChange(ChangeEvent event, std::shared_ptr device) override; void OnDeviceChangeEnd() override; diff --git a/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp b/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp index e193c498f5b2..ef9e64ebb9b9 100644 --- a/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp +++ b/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp @@ -16,21 +16,15 @@ namespace IOS::HLE { -constexpr u32 USBV5_VERSION = 0x50001; - USB_HIDv5::~USB_HIDv5() = default; std::optional USB_HIDv5::IOCtl(const IOCtlRequest& request) { - auto& system = GetSystem(); - auto& memory = system.GetMemory(); - request.Log(GetDeviceName(), Common::Log::LogType::IOS_USB); switch (request.request) { case USB::IOCTL_USBV5_GETVERSION: - memory.Write_U32(USBV5_VERSION, request.buffer_out); - return IPCReply(IPC_SUCCESS); + return GetUSBVersion(request); case USB::IOCTL_USBV5_GETDEVICECHANGE: return GetDeviceChange(request); case USB::IOCTL_USBV5_SHUTDOWN: diff --git a/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp b/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp index d8d624e89896..c8ccbbbcd68a 100644 --- a/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp +++ b/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp @@ -16,21 +16,15 @@ namespace IOS::HLE { -constexpr u32 USBV5_VERSION = 0x50001; - USB_VEN::~USB_VEN() = default; std::optional USB_VEN::IOCtl(const IOCtlRequest& request) { - auto& system = GetSystem(); - auto& memory = system.GetMemory(); - request.Log(GetDeviceName(), Common::Log::LogType::IOS_USB); switch (request.request) { case USB::IOCTL_USBV5_GETVERSION: - memory.Write_U32(USBV5_VERSION, request.buffer_out); - return IPCReply(IPC_SUCCESS); + return GetUSBVersion(request); case USB::IOCTL_USBV5_GETDEVICECHANGE: return GetDeviceChange(request); case USB::IOCTL_USBV5_SHUTDOWN: From 7b496b2f5bc60fe968a23a8d0053d4838ea9e7d5 Mon Sep 17 00:00:00 2001 From: "Dr. Dystopia" Date: Sat, 19 Apr 2025 20:07:43 +0200 Subject: [PATCH 002/123] DiscIO: Remove redundant casts --- Source/Core/DiscIO/WIABlob.cpp | 2 +- Source/Core/DiscIO/WIABlob.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/DiscIO/WIABlob.cpp b/Source/Core/DiscIO/WIABlob.cpp index 9ee84702947e..69c4f5450838 100644 --- a/Source/Core/DiscIO/WIABlob.cpp +++ b/Source/Core/DiscIO/WIABlob.cpp @@ -1445,7 +1445,7 @@ WIARVZFileReader::ProcessAndCompress(CompressThreadState* state, CompressPa ASSERT(hash_offset <= std::numeric_limits::max()); HashExceptionEntry& exception = exception_lists[exception_list_index].emplace_back(); - exception.offset = static_cast(Common::swap16(hash_offset)); + exception.offset = Common::swap16(static_cast(hash_offset)); std::memcpy(exception.hash.data(), desired_hash, Common::SHA1::DIGEST_LEN); } }; diff --git a/Source/Core/DiscIO/WIABlob.h b/Source/Core/DiscIO/WIABlob.h index e0d6a66d8798..07f1a51857f4 100644 --- a/Source/Core/DiscIO/WIABlob.h +++ b/Source/Core/DiscIO/WIABlob.h @@ -60,7 +60,7 @@ class WIARVZFileReader final : public BlobReader std::string GetCompressionMethod() const override; std::optional GetCompressionLevel() const override { - return static_cast(static_cast(Common::swap32(m_header_2.compression_level))); + return static_cast(Common::swap32(m_header_2.compression_level)); } bool Read(u64 offset, u64 size, u8* out_ptr) override; From 3994a9446f52c62b747d9f9e9bdd001431d4c2f1 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 9 Jun 2025 13:11:18 -0700 Subject: [PATCH 003/123] AutoUpdateChecker: Delete old Updater.log file Delete old Updater.log file from GetExeDirectory(). The log was moved to GetUserPath(D_LOGS_IDX) in 5.0-14529, but users looking to make a bug report might find the old one instead so let's delete it to prevent any confusion. --- Source/Core/UICommon/AutoUpdate.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/Core/UICommon/AutoUpdate.cpp b/Source/Core/UICommon/AutoUpdate.cpp index e1d6b7732c37..20b2d0f95f1d 100644 --- a/Source/Core/UICommon/AutoUpdate.cpp +++ b/Source/Core/UICommon/AutoUpdate.cpp @@ -79,9 +79,9 @@ std::string MakeUpdaterCommandLine(const std::map& fla return cmdline; } -#ifdef __APPLE__ void CleanupFromPreviousUpdate() { +#ifdef __APPLE__ // Remove the relocated updater file. File::DeleteDirRecursively(UpdaterPath(true)); @@ -90,9 +90,12 @@ void CleanupFromPreviousUpdate() // version with an embedded updater, it won't delete the folder structure of the bundle, so // we should clean those leftovers up. File::DeleteDirRecursively(File::GetExeDirectory() + DIR_SEP + "Dolphin Updater.app"); -} #endif + // Updater.log was moved from GetExeDirectory() to GetUserPath(D_LOGS_IDX) in 5.0-14529. + File::Delete(File::GetExeDirectory() + DIR_SEP + "Updater.log", + File::IfAbsentBehavior::NoConsoleWarning); +} #endif // This ignores i18n because most of the text in there (change descriptions) is only going to be @@ -190,7 +193,7 @@ void AutoUpdateChecker::CheckForUpdate(std::string_view update_track, if (!SystemSupportsAutoUpdates() || update_track.empty()) return; -#ifdef __APPLE__ +#ifdef OS_SUPPORTS_UPDATER CleanupFromPreviousUpdate(); #endif From f8cddf344d2eda6f2bce6292c3af7a1496687055 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 6 Jun 2025 18:46:15 +0200 Subject: [PATCH 004/123] Android: Clear listener in SwitchSettingViewHolder If bind was called more than once for a SwitchSettingViewHolder, the line `binding.settingSwitch.isChecked = setting.isChecked` would accidentally trigger the listener registered during the previous bind call. --- .../features/settings/ui/viewholder/SwitchSettingViewHolder.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index ddcf1d39b5c2..2907b948b686 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -32,6 +32,9 @@ class SwitchSettingViewHolder( binding.textSettingName.text = item.name binding.textSettingDescription.text = item.description + // Make sure we don't trigger any listener set earlier + binding.settingSwitch.setOnCheckedChangeListener(null) + binding.settingSwitch.isChecked = setting.isChecked binding.settingSwitch.isEnabled = setting.isEditable From cff0ba76c1cfd8150b3f4ff2082579459829353a Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Thu, 24 Apr 2025 07:50:11 -0400 Subject: [PATCH 005/123] Init achievement manager in Android startup --- Source/Android/jni/MainAndroid.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 707e81e336a8..7c78f49045b2 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -33,6 +33,7 @@ #include "Common/Version.h" #include "Common/WindowSystemInfo.h" +#include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" @@ -559,6 +560,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Initialize(J WiimoteReal::InitAdapterClass(); UICommon::Init(); UICommon::InitControllers(WindowSystemInfo(WindowSystemType::Android, nullptr, nullptr, nullptr)); + + AchievementManager::GetInstance().Init(nullptr); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReportStartToAnalytics(JNIEnv*, From 458bb05af910323e98a29638377fbe868a4d58c4 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 19 May 2025 11:35:50 +0200 Subject: [PATCH 006/123] Core: Let any thread call previously host-thread-only functions By letting threads other than the host thread use things like CPUThreadGuard, we can do a significant cleanup in AchievementsManager in a later commit of this pull request. Note: Some functions still can't be called from the CPU thread (or threads the CPU thread might block on, like the GPU thread), but can be called from any other thread. --- Source/Core/Core/Core.cpp | 130 +++++++++++++++++++++------------ Source/Core/Core/Core.h | 14 ++-- Source/Core/DolphinQt/Host.cpp | 4 +- 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index f026cb939d05..ac745a22a431 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -105,6 +105,11 @@ static bool s_is_throttler_temp_disabled = false; static bool s_frame_step = false; static std::atomic s_stop_frame_step; +// Threads other than the CPU thread must hold this when taking on the role of the CPU thread. +// The CPU thread is not required to hold this when doing normal work, but must hold it if writing +// to s_state. +static std::recursive_mutex s_core_mutex; + // The value Paused is never stored in this variable. The core is considered to be in // the Paused state if this variable is Running and the CPU reports that it's stepping. static std::atomic s_state = State::Uninitialized; @@ -221,6 +226,8 @@ bool WantsDeterminism() // BootManager.cpp bool Init(Core::System& system, std::unique_ptr boot, const WindowSystemInfo& wsi) { + std::lock_guard lock(s_core_mutex); + if (s_emu_thread.joinable()) { if (!IsUninitialized(system)) @@ -266,16 +273,20 @@ static void ResetRumble() // Called from GUI thread void Stop(Core::System& system) // - Hammertime! { - const State state = s_state.load(); - if (state == State::Stopping || state == State::Uninitialized) - return; + { + std::lock_guard lock(s_core_mutex); - AchievementManager::GetInstance().CloseGame(); + const State state = s_state.load(); + if (state == State::Stopping || state == State::Uninitialized) + return; - s_state.store(State::Stopping); + s_state.store(State::Stopping); + } NotifyStateChanged(State::Stopping); + AchievementManager::GetInstance().CloseGame(); + // Dump left over jobs HostDispatchJobs(system); @@ -322,7 +333,7 @@ void UndeclareAsHostThread() static void CPUSetInitialExecutionState(bool force_paused = false) { // The CPU starts in stepping state, and will wait until a new state is set before executing. - // SetState must be called on the host thread, so we defer it for later. + // SetState isn't safe to call from the CPU thread, so we ask the host thread to call it. QueueHostJob([force_paused](Core::System& system) { bool paused = SConfig::GetInstance().bBootToPause || force_paused; SetState(system, paused ? State::Paused : State::Running, true, true); @@ -362,10 +373,14 @@ static void CpuThread(Core::System& system, const std::optional& sa File::Delete(*savestate_path); } - // If s_state is Starting, change it to Running. But if it's already been set to Stopping - // by the host thread, don't change it. - State expected = State::Starting; - s_state.compare_exchange_strong(expected, State::Running); + { + std::unique_lock core_lock(s_core_mutex); + + // If s_state is Starting, change it to Running. But if it's already been set to Stopping + // because another thread called Stop, don't change it. + State expected = State::Starting; + s_state.compare_exchange_strong(expected, State::Running); + } { #ifndef _WIN32 @@ -423,12 +438,17 @@ static void FifoPlayerThread(Core::System& system, const std::optional boot { NotifyStateChanged(State::Starting); Common::ScopeGuard flag_guard{[] { - s_state.store(State::Uninitialized); + { + std::lock_guard lock(s_core_mutex); + s_state.store(State::Uninitialized); + } NotifyStateChanged(State::Uninitialized); @@ -682,35 +705,39 @@ static void EmuThread(Core::System& system, std::unique_ptr boot void SetState(Core::System& system, State state, bool report_state_change, bool override_achievement_restrictions) { - // State cannot be controlled until the CPU Thread is operational - if (s_state.load() != State::Running) - return; - - switch (state) { - case State::Paused: -#ifdef USE_RETRO_ACHIEVEMENTS - if (!override_achievement_restrictions && !AchievementManager::GetInstance().CanPause()) + std::lock_guard lock(s_core_mutex); + + // State cannot be controlled until the CPU Thread is operational + if (s_state.load() != State::Running) return; + + switch (state) + { + case State::Paused: +#ifdef USE_RETRO_ACHIEVEMENTS + if (!override_achievement_restrictions && !AchievementManager::GetInstance().CanPause()) + return; #endif // USE_RETRO_ACHIEVEMENTS - // NOTE: GetState() will return State::Paused immediately, even before anything has - // stopped (including the CPU). - system.GetCPU().SetStepping(true); // Break - Wiimote::Pause(); - ResetRumble(); + // NOTE: GetState() will return State::Paused immediately, even before anything has + // stopped (including the CPU). + system.GetCPU().SetStepping(true); // Break + Wiimote::Pause(); + ResetRumble(); #ifdef USE_RETRO_ACHIEVEMENTS - AchievementManager::GetInstance().DoIdle(); + AchievementManager::GetInstance().DoIdle(); #endif // USE_RETRO_ACHIEVEMENTS - break; - case State::Running: - { - system.GetCPU().SetStepping(false); - Wiimote::Resume(); - break; - } - default: - PanicAlertFmt("Invalid state"); - break; + break; + case State::Running: + { + system.GetCPU().SetStepping(false); + Wiimote::Resume(); + break; + } + default: + PanicAlertFmt("Invalid state"); + break; + } } // Certain callers only change the state momentarily. Sending a callback for them causes @@ -781,7 +808,7 @@ void SaveScreenShot(std::string_view name) static bool PauseAndLock(Core::System& system) { - // WARNING: PauseAndLock is not fully threadsafe so is only valid on the Host Thread + s_core_mutex.lock(); if (!IsRunning(system)) return true; @@ -804,7 +831,7 @@ static bool PauseAndLock(Core::System& system) static void RestoreStateAndUnlock(Core::System& system, const bool unpause_on_unlock) { - // WARNING: RestoreStateAndUnlock is not fully threadsafe so is only valid on the Host Thread + Common::ScopeGuard scope_guard([] { s_core_mutex.unlock(); }); if (!IsRunning(system)) return; @@ -824,8 +851,7 @@ static void RestoreStateAndUnlock(Core::System& system, const bool unpause_on_un void RunOnCPUThread(Core::System& system, Common::MoveOnlyFunction function, bool wait_for_completion) { - // If the CPU thread is not running, assume there is no active CPU thread we can race against. - if (!IsRunning(system) || IsCPUThread()) + if (IsCPUThread()) { function(); return; @@ -834,10 +860,15 @@ void RunOnCPUThread(Core::System& system, Common::MoveOnlyFunction funct // Pause the CPU (set it to stepping mode). const bool was_running = PauseAndLock(system); - // Queue the job function. - if (wait_for_completion) + if (!IsRunning(system)) { - // Trigger the event after executing the function. + // If the core hasn't been started, there is no active CPU thread we can race against. + function(); + wait_for_completion = false; + } + else if (wait_for_completion) + { + // Queue the job function followed by triggering the event. s_cpu_thread_job_finished.Reset(); system.GetCPU().AddCPUThreadJob([&function] { function(); @@ -846,6 +877,7 @@ void RunOnCPUThread(Core::System& system, Common::MoveOnlyFunction funct } else { + // Queue the job function. system.GetCPU().AddCPUThreadJob(std::move(function)); } @@ -967,6 +999,8 @@ void NotifyStateChanged(Core::State state) void UpdateWantDeterminism(Core::System& system, bool initial) { + const Core::CPUThreadGuard guard(system); + // For now, this value is not itself configurable. Instead, individual // settings that depend on it, such as GPU determinism mode. should have // override options for testing, @@ -975,7 +1009,6 @@ void UpdateWantDeterminism(Core::System& system, bool initial) { NOTICE_LOG_FMT(COMMON, "Want determinism <- {}", new_want_determinism ? "true" : "false"); - const Core::CPUThreadGuard guard(system); s_wants_determinism = new_want_determinism; const auto ios = system.GetIOS(); if (ios) @@ -1037,6 +1070,9 @@ void DoFrameStep(Core::System& system) OSD::AddMessage("Frame stepping is disabled in RetroAchievements hardcore mode"); return; } + + std::lock_guard lock(s_core_mutex); + if (GetState(system) == State::Paused) { // if already paused, frame advance for 1 frame diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h index 68a402772a94..72627d0038d8 100644 --- a/Source/Core/Core/Core.h +++ b/Source/Core/Core/Core.h @@ -91,10 +91,9 @@ enum class ConsoleType : u32 ReservedTDEVSystem = 0x20000007, }; -// This is an RAII alternative to using PauseAndLock. If constructed from the host thread, the CPU -// thread is paused, and the current thread temporarily becomes the CPU thread. If constructed from -// the CPU thread, nothing special happens. This should only be constructed on the CPU thread or the -// host thread. +// This is an RAII alternative to using PauseAndLock. If constructed from any thread other than the +// CPU thread, the CPU thread is paused, and the current thread temporarily becomes the CPU thread. +// If constructed from the CPU thread, nothing special happens. // // Some functions use a parameter of this type to indicate that the function should only be called // from the CPU thread. If the parameter is a pointer, the function has a fallback for being called @@ -118,6 +117,8 @@ class CPUThreadGuard final bool m_was_unpaused = false; }; +// These three are normally called from the Host thread. However, they can be called from any thread +// that isn't launched by the emulator core. bool Init(Core::System& system, std::unique_ptr boot, const WindowSystemInfo& wsi); void Stop(Core::System& system); void Shutdown(Core::System& system); @@ -144,7 +145,8 @@ bool IsHostThread(); bool WantsDeterminism(); -// [NOT THREADSAFE] For use by Host only +// SetState can't be called by the CPU thread, but can be called by any thread that isn't launched +// by the emulator core. void SetState(Core::System& system, State state, bool report_state_change = true, bool override_achievement_restrictions = false); State GetState(Core::System& system); @@ -159,7 +161,6 @@ void FrameUpdateOnCPUThread(); void OnFrameEnd(Core::System& system); // Run a function on the CPU thread, asynchronously. -// This is only valid to call from the host thread, since it uses PauseAndLock() internally. void RunOnCPUThread(Core::System& system, Common::MoveOnlyFunction function, bool wait_for_completion); @@ -171,7 +172,6 @@ int AddOnStateChangedCallback(StateChangedCallbackFunc callback); bool RemoveOnStateChangedCallback(int* handle); void NotifyStateChanged(Core::State state); -// Run on the Host thread when the factors change. [NOT THREADSAFE] void UpdateWantDeterminism(Core::System& system, bool initial = false); // Queue an arbitrary function to asynchronously run once on the Host thread later. diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index 57586300cfdc..9407e5424b8b 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -245,7 +246,8 @@ bool Host_TASInputHasFocus() void Host_YieldToUI() { - qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + if (qApp->thread() == QThread::currentThread()) + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } void Host_UpdateDisasmDialog() From 1bba42de45b50638b7907f816c669eff7a52ad6e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 19 May 2025 11:46:43 +0200 Subject: [PATCH 007/123] RetroAchievements: Remove MemoryPeeker/MemoryPoker's copying approach This makes the code simpler, and saves us from the slow operation of copying the all of RAM on every frame when RAIntegration is enabled. --- Source/Core/Core/AchievementManager.cpp | 80 +------------------------ Source/Core/Core/AchievementManager.h | 3 - 2 files changed, 2 insertions(+), 81 deletions(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 5423bb29e03c..7a41d57c84f1 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -65,7 +65,7 @@ void AchievementManager::Init(void* hwnd) { { std::lock_guard lg{m_lock}; - m_client = rc_client_create(MemoryVerifier, Request); + m_client = rc_client_create(MemoryPeeker, Request); } std::string host_url = Config::Get(Config::RA_HOST_URL); if (!host_url.empty()) @@ -170,7 +170,6 @@ void AchievementManager::LoadGame(const DiscIO::Volume* volume) } else { - rc_client_set_read_memory_function(m_client, MemoryVerifier); rc_client_begin_load_game(m_client, "", LoadGameCallback, NULL); } return; @@ -210,7 +209,6 @@ void AchievementManager::LoadGame(const DiscIO::Volume* volume) else { u32 console_id = FindConsoleID(volume->GetVolumeType()); - rc_client_set_read_memory_function(m_client, MemoryVerifier); rc_client_begin_identify_and_load_game(m_client, console_id, "", NULL, 0, LoadGameCallback, NULL); } @@ -320,22 +318,6 @@ void AchievementManager::DoFrame() if (!(IsGameLoaded() || m_dll_found) || !Core::IsCPUThread()) return; { -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - if (m_dll_found) - { - std::lock_guard lg{m_memory_lock}; - Core::System* system = m_system.load(std::memory_order_acquire); - if (!system) - return; - Core::CPUThreadGuard thread_guard(*system); - u32 mem2_size = (system->IsWii()) ? system->GetMemory().GetExRamSizeReal() : 0; - if (m_cloned_memory.size() != MEM1_SIZE + mem2_size) - m_cloned_memory.resize(MEM1_SIZE + mem2_size); - system->GetMemory().CopyFromEmu(m_cloned_memory.data(), 0, MEM1_SIZE); - if (mem2_size > 0) - system->GetMemory().CopyFromEmu(m_cloned_memory.data() + MEM1_SIZE, MEM2_START, mem2_size); - } -#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION std::lock_guard lg{m_lock}; rc_client_do_frame(m_client); } @@ -1061,7 +1043,6 @@ void AchievementManager::LoadGameCallback(int result, const char* error_message, if (game == nullptr) return; - rc_client_set_read_memory_function(instance.m_client, MemoryPeeker); instance.FetchGameBadges(); instance.m_system.store(&Core::System::GetInstance(), std::memory_order_release); instance.update_event.Trigger({.all = true}); @@ -1337,53 +1318,11 @@ void AchievementManager::Request(const rc_api_request_t* request, }); } -// Currently, when rc_client calls the memory peek method provided in its constructor (or in -// rc_client_set_read_memory_function) it will do so on the thread that calls DoFrame, which is -// currently the host thread, with one exception: an asynchronous callback in the load game process. -// This is done to validate/invalidate each memory reference in the downloaded assets, mark assets -// as unsupported, and notify the player upon startup that there are unsupported assets and how -// many. As such, all that call needs to do is return the number of bytes that can be read with this -// call. As only the CPU and host threads are allowed to read from memory, I provide a separate -// method for this verification. In lieu of a more convenient set of steps, I provide MemoryVerifier -// to rc_client at construction, and in the Load Game callback, after the verification has been -// complete, I call rc_client_set_read_memory_function to switch to the usual MemoryPeeker for all -// future synchronous calls. -u32 AchievementManager::MemoryVerifier(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client) -{ - auto& system = Core::System::GetInstance(); - u32 mem2_size = system.GetMemory().GetExRamSizeReal(); - if (address < MEM1_SIZE + mem2_size) - return std::min(MEM1_SIZE + mem2_size - address, num_bytes); - return 0; -} - u32 AchievementManager::MemoryPeeker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client) { if (buffer == nullptr) return 0u; -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - auto& instance = AchievementManager::GetInstance(); - if (instance.m_dll_found) - { - std::lock_guard lg{instance.m_memory_lock}; - if (u64(address) + num_bytes > instance.m_cloned_memory.size()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Attempt to read past memory size: size {} address {} write length {}", - instance.m_cloned_memory.size(), address, num_bytes); - return 0; - } - std::copy(instance.m_cloned_memory.begin() + address, - instance.m_cloned_memory.begin() + address + num_bytes, buffer); - return num_bytes; - } -#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION auto& system = Core::System::GetInstance(); - if (!(Core::IsHostThread() || Core::IsCPUThread())) - { - ASSERT_MSG(ACHIEVEMENTS, false, "MemoryPeeker called from wrong thread"); - return 0; - } Core::CPUThreadGuard thread_guard(system); if (address > MEM1_SIZE) address += (MEM2_START - MEM1_SIZE); @@ -1607,32 +1546,17 @@ void AchievementManager::MemoryPoker(u32 address, u8* buffer, u32 num_bytes, rc_ { if (buffer == nullptr) return; - if (!(Core::IsHostThread() || Core::IsCPUThread())) - { - Core::QueueHostJob([address, buffer, num_bytes, client](Core::System& system) { - MemoryPoker(address, buffer, num_bytes, client); - }); - return; - } auto& instance = AchievementManager::GetInstance(); - if (u64(address) + num_bytes >= instance.m_cloned_memory.size()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Attempt to write past memory size: size {} address {} write length {}", - instance.m_cloned_memory.size(), address, num_bytes); - return; - } Core::System* system = instance.m_system.load(std::memory_order_acquire); if (!system) return; Core::CPUThreadGuard thread_guard(*system); - std::lock_guard lg{instance.m_memory_lock}; if (address < MEM1_SIZE) system->GetMemory().CopyToEmu(address, buffer, num_bytes); else system->GetMemory().CopyToEmu(address - MEM1_SIZE + MEM2_START, buffer, num_bytes); - std::copy(buffer, buffer + num_bytes, instance.m_cloned_memory.begin() + address); } + void AchievementManager::GameTitleEstimateHandler(char* buffer, u32 buffer_size, rc_client_t* client) { diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 225bcb1ddbd0..57bdb8206c64 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -251,7 +251,6 @@ class AchievementManager static void Request(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); - static u32 MemoryVerifier(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client); static u32 MemoryPeeker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client); void FetchBadge(Badge* badge, u32 badge_type, const BadgeNameFunction function, const UpdatedItems callback_data); @@ -293,8 +292,6 @@ class AchievementManager bool m_dll_found = false; #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - std::vector m_cloned_memory; - std::recursive_mutex m_memory_lock; std::string m_title_estimate; #endif // RC_CLIENT_SUPPORTS_RAINTEGRATION From 068947e2b613d15f724c87c501949c9105cc7f7f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 19 May 2025 12:08:10 +0200 Subject: [PATCH 008/123] Core: Remove IsHostThread The core no longer cares which thread is the host thread. Cleaning up Android's HostThreadLock is left for another PR, in part because the HostThreadLock in NativeConfig.cpp still serves a purpose, and in part to make any issues easier to bisect. --- Source/Android/jni/Host.h | 25 ++++++------------------- Source/Core/Core/Core.cpp | 16 ---------------- Source/Core/Core/Core.h | 3 --- Source/Core/DolphinNoGUI/MainNoGUI.cpp | 2 -- Source/Core/DolphinQt/Main.cpp | 2 -- Source/Core/DolphinQt/Settings.cpp | 3 ++- Source/Core/DolphinTool/ToolMain.cpp | 2 -- Source/UnitTests/UnitTestsMain.cpp | 1 - 8 files changed, 8 insertions(+), 46 deletions(-) diff --git a/Source/Android/jni/Host.h b/Source/Android/jni/Host.h index 3a7a30482578..0183038d9a24 100644 --- a/Source/Android/jni/Host.h +++ b/Source/Android/jni/Host.h @@ -5,37 +5,24 @@ #include -#include "Core/Core.h" - // The Core only supports using a single Host thread. // If multiple threads want to call host functions then they need to queue // sequentially for access. +// TODO: The above isn't true anymore, so we should get rid of this class. struct HostThreadLock { - explicit HostThreadLock() : m_lock(s_host_identity_mutex) { Core::DeclareAsHostThread(); } + explicit HostThreadLock() : m_lock(s_host_identity_mutex) {} - ~HostThreadLock() - { - if (m_lock.owns_lock()) - Core::UndeclareAsHostThread(); - } + ~HostThreadLock() = default; HostThreadLock(const HostThreadLock& other) = delete; HostThreadLock(HostThreadLock&& other) = delete; HostThreadLock& operator=(const HostThreadLock& other) = delete; HostThreadLock& operator=(HostThreadLock&& other) = delete; - void Lock() - { - m_lock.lock(); - Core::DeclareAsHostThread(); - } - - void Unlock() - { - m_lock.unlock(); - Core::UndeclareAsHostThread(); - } + void Lock() { m_lock.lock(); } + + void Unlock() { m_lock.unlock(); } private: static std::mutex s_host_identity_mutex; diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index ac745a22a431..6ff85a9002c2 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -131,7 +131,6 @@ static Common::Event s_cpu_thread_job_finished; static thread_local bool tls_is_cpu_thread = false; static thread_local bool tls_is_gpu_thread = false; -static thread_local bool tls_is_host_thread = false; static void EmuThread(Core::System& system, std::unique_ptr boot, WindowSystemInfo wsi); @@ -212,11 +211,6 @@ bool IsGPUThread() return tls_is_gpu_thread; } -bool IsHostThread() -{ - return tls_is_host_thread; -} - bool WantsDeterminism() { return s_wants_determinism; @@ -319,16 +313,6 @@ void UndeclareAsGPUThread() tls_is_gpu_thread = false; } -void DeclareAsHostThread() -{ - tls_is_host_thread = true; -} - -void UndeclareAsHostThread() -{ - tls_is_host_thread = false; -} - // For the CPU Thread only. static void CPUSetInitialExecutionState(bool force_paused = false) { diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h index 72627d0038d8..462a49fa278b 100644 --- a/Source/Core/Core/Core.h +++ b/Source/Core/Core/Core.h @@ -127,8 +127,6 @@ void DeclareAsCPUThread(); void UndeclareAsCPUThread(); void DeclareAsGPUThread(); void UndeclareAsGPUThread(); -void DeclareAsHostThread(); -void UndeclareAsHostThread(); std::string StopMessage(bool main_thread, std::string_view message); @@ -141,7 +139,6 @@ bool IsUninitialized(Core::System& system); bool IsCPUThread(); // this tells us whether we are the CPU thread. bool IsGPUThread(); -bool IsHostThread(); bool WantsDeterminism(); diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp index 669daabec00a..8abc7746c645 100644 --- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp +++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp @@ -189,8 +189,6 @@ static std::unique_ptr GetPlatform(const optparse::Values& options) int main(const int argc, char* argv[]) { - Core::DeclareAsHostThread(); - const auto parser = CommandLineParse::CreateParser(CommandLineParse::ParserOptions::OmitGUIOptions); parser->add_option("-p", "--platform") diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index 49436475ae9d..ce85ee1f0deb 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -127,8 +127,6 @@ int main(int argc, char* argv[]) } #endif - Core::DeclareAsHostThread(); - #ifdef __APPLE__ // On macOS, a command line option matching the format "-psn_X_XXXXXX" is passed when // the application is launched for the first time. This is to set the "ProcessSerialNumber", diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp index 2767badbec46..24bcb367b508 100644 --- a/Source/Core/DolphinQt/Settings.cpp +++ b/Source/Core/DolphinQt/Settings.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "AudioCommon/AudioCommon.h" @@ -76,7 +77,7 @@ Settings::Settings() }); m_hotplug_event_hook = g_controller_interface.RegisterDevicesChangedCallback([this] { - if (Core::IsHostThread()) + if (qApp->thread() == QThread::currentThread()) { emit DevicesChanged(); } diff --git a/Source/Core/DolphinTool/ToolMain.cpp b/Source/Core/DolphinTool/ToolMain.cpp index 224ead3d7b22..058bccc7b5bf 100644 --- a/Source/Core/DolphinTool/ToolMain.cpp +++ b/Source/Core/DolphinTool/ToolMain.cpp @@ -32,8 +32,6 @@ static void PrintUsage() int main(int argc, char* argv[]) { - Core::DeclareAsHostThread(); - if (argc < 2) { PrintUsage(); diff --git a/Source/UnitTests/UnitTestsMain.cpp b/Source/UnitTests/UnitTestsMain.cpp index 4eed0c60993e..3edb1c190929 100644 --- a/Source/UnitTests/UnitTestsMain.cpp +++ b/Source/UnitTests/UnitTestsMain.cpp @@ -25,7 +25,6 @@ int main(int argc, char** argv) { fmt::print(stderr, "Running main() from UnitTestsMain.cpp\n"); Common::RegisterMsgAlertHandler(TestMsgHandler); - Core::DeclareAsHostThread(); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From cdc21490e48b7974c68b30bd52dc160c4f0c09cf Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sun, 9 Nov 2025 19:24:26 -0500 Subject: [PATCH 009/123] Android: Convert NetworkHelper to Kotlin --- .../dolphinemu/utils/NetworkHelper.java | 127 ------------------ .../dolphinemu/utils/NetworkHelper.kt | 84 ++++++++++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 6 +- 3 files changed, 87 insertions(+), 130 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.java deleted file mode 100644 index 380abc949e14..000000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils; - -import android.app.Service; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.LinkAddress; -import android.net.LinkProperties; -import android.net.Network; -import android.net.RouteInfo; -import android.os.Build; - -import androidx.annotation.Keep; -import androidx.annotation.RequiresApi; - -import org.dolphinemu.dolphinemu.DolphinApplication; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.UnknownHostException; - -public class NetworkHelper -{ - private static ConnectivityManager GetConnectivityManager() - { - Context context = DolphinApplication.getAppContext(); - ConnectivityManager manager = - (ConnectivityManager) context.getSystemService(Service.CONNECTIVITY_SERVICE); - if (manager == null) - Log.warning("Cannot get Network link as ConnectivityManager is null."); - return manager; - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private static LinkAddress GetIPv4Link() - { - ConnectivityManager manager = GetConnectivityManager(); - if (manager == null) - return null; - Network active_network = manager.getActiveNetwork(); - if (active_network == null) - { - Log.warning("Active network is null."); - return null; - } - LinkProperties properties = manager.getLinkProperties(active_network); - if (properties == null) - { - Log.warning("Link properties is null."); - return null; - } - for (LinkAddress link : properties.getLinkAddresses()) - { - InetAddress address = link.getAddress(); - if (address instanceof Inet4Address) - return link; - } - Log.warning("No IPv4 link found."); - return null; - } - - private static int InetAddressToInt(InetAddress address) - { - byte[] net_addr = address.getAddress(); - int result = 0; - // Convert address to little endian - for (int i = 0; i < net_addr.length; i++) - { - result |= (net_addr[i] & 0xFF) << (8 * i); - } - return result; - } - - @Keep - public static int GetNetworkIpAddress() - { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) - return 0; - LinkAddress link = GetIPv4Link(); - if (link == null) - return 0; - return InetAddressToInt(link.getAddress()); - } - - @Keep - public static int GetNetworkPrefixLength() - { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) - return 0; - LinkAddress link = GetIPv4Link(); - if (link == null) - return 0; - return link.getPrefixLength(); - } - - @Keep - public static int GetNetworkGateway() - { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) - return 0; - ConnectivityManager manager = GetConnectivityManager(); - if (manager == null) - return 0; - Network active_network = manager.getActiveNetwork(); - if (active_network == null) - return 0; - LinkProperties properties = manager.getLinkProperties(active_network); - if (properties == null) - return 0; - try - { - InetAddress addr_out = InetAddress.getByName("8.8.8.8"); - for (RouteInfo route : properties.getRoutes()) - { - if (!route.matches(addr_out)) - continue; - return InetAddressToInt(route.getGateway()); - } - } - catch (UnknownHostException ignore) - { - } - Log.warning("No valid gateway found."); - return 0; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt new file mode 100644 index 000000000000..1b3140bf10de --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.LinkAddress +import android.os.Build +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import org.dolphinemu.dolphinemu.DolphinApplication +import java.net.Inet4Address +import java.net.InetAddress +import java.net.UnknownHostException + +@Keep +object NetworkHelper { + private fun getConnectivityManager(): ConnectivityManager? { + val context = DolphinApplication.getAppContext() + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + if (manager == null) { + Log.warning("Cannot get network link as ConnectivityManager is null.") + } + return manager + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getIPv4Link(): LinkAddress? { + val manager = getConnectivityManager() ?: return null + val activeNetwork = manager.activeNetwork + if (activeNetwork == null) { + Log.warning("Active network is null.") + return null + } + val properties = manager.getLinkProperties(activeNetwork) + if (properties == null) { + Log.warning("Link properties is null.") + return null + } + return properties.linkAddresses.firstOrNull { it.address is Inet4Address } ?: run { + Log.warning("No IPv4 link found.") + null + } + } + + private fun InetAddress.inetAddressToInt(): Int { + return address.foldIndexed(0) { index, acc, byte -> + acc or ((byte.toInt() and 0xFF) shl (8 * index)) + } + } + + @Keep + @JvmStatic + fun getNetworkIpAddress(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return 0 + return getIPv4Link()?.address?.inetAddressToInt() ?: 0 + } + + @Keep + @JvmStatic + fun getNetworkPrefixLength(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return 0 + return getIPv4Link()?.prefixLength ?: 0 + } + + @Keep + @JvmStatic + fun getNetworkGateway(): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return 0 + val manager = getConnectivityManager() ?: return 0 + val activeNetwork = manager.activeNetwork ?: return 0 + val properties = manager.getLinkProperties(activeNetwork) ?: return 0 + val gatewayAddress = try { + val target = InetAddress.getByName("8.8.8.8") + properties.routes.firstOrNull { it.matches(target) }?.gateway + } catch (ignored: UnknownHostException) { + null + } + return gatewayAddress?.inetAddressToInt() ?: run { + Log.warning("No valid gateway found.") + 0 + } + } +} diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 67ce1dc554dc..18b486023a98 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -662,11 +662,11 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper"); s_network_helper_class = reinterpret_cast(env->NewGlobalRef(network_helper_class)); s_network_helper_get_network_ip_address = - env->GetStaticMethodID(s_network_helper_class, "GetNetworkIpAddress", "()I"); + env->GetStaticMethodID(s_network_helper_class, "getNetworkIpAddress", "()I"); s_network_helper_get_network_prefix_length = - env->GetStaticMethodID(s_network_helper_class, "GetNetworkPrefixLength", "()I"); + env->GetStaticMethodID(s_network_helper_class, "getNetworkPrefixLength", "()I"); s_network_helper_get_network_gateway = - env->GetStaticMethodID(s_network_helper_class, "GetNetworkGateway", "()I"); + env->GetStaticMethodID(s_network_helper_class, "getNetworkGateway", "()I"); env->DeleteLocalRef(network_helper_class); const jclass boolean_supplier_class = From 220315737f688206c3d62a90a201bdcb1687b110 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Tue, 11 Nov 2025 16:48:12 -0500 Subject: [PATCH 010/123] Android: Update dependencies Note: This also updates Kotlin to 2.2.21 which requires small adjustments in our Kotlin code. --- Source/Android/app/build.gradle.kts | 33 ++++++++++--------- .../dolphinemu/adapters/GameRowPresenter.kt | 2 +- .../adapters/SettingsRowPresenter.kt | 2 +- .../viewholders/TvGameViewHolder.kt | 2 +- Source/Android/benchmark/build.gradle.kts | 6 ++-- Source/Android/build.gradle.kts | 8 ++--- .../gradle/wrapper/gradle-wrapper.properties | 3 +- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 54a12a6058c8..dff980e170de 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -122,35 +122,36 @@ android { dependencies { baselineProfile(project(":benchmark")) - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") - implementation("androidx.core:core-ktx:1.13.0") - implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.core:core-ktx:1.17.0") + implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.cardview:cardview:1.0.0") - implementation("androidx.recyclerview:recyclerview:1.3.2") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.recyclerview:recyclerview:1.4.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") + implementation("androidx.fragment:fragment-ktx:1.8.9") implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") - implementation("com.google.android.material:material:1.11.0") - implementation("androidx.core:core-splashscreen:1.0.1") + implementation("com.google.android.material:material:1.13.0") + implementation("androidx.core:core-splashscreen:1.2.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("androidx.profileinstaller:profileinstaller:1.3.1") + implementation("androidx.profileinstaller:profileinstaller:1.4.1") // Kotlin extensions for lifecycle components - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4") // Android TV UI libraries. - implementation("androidx.leanback:leanback:1.0.0") - implementation("androidx.tvprovider:tvprovider:1.0.0") + implementation("androidx.leanback:leanback:1.2.0") + implementation("androidx.tvprovider:tvprovider:1.1.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") // For loading game covers from disk and GameTDB - implementation("io.coil-kt:coil:2.6.0") + implementation("io.coil-kt:coil:2.7.0") // For loading custom GPU drivers - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") implementation("com.nononsenseapps:filepicker:4.2.1") } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt index 754c08987691..4f43601e6851 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt @@ -41,7 +41,7 @@ class GameRowPresenter : Presenter() { return TvGameViewHolder(gameCard) } - override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { + override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { val holder = viewHolder as TvGameViewHolder val context = holder.cardParent.context val gameFile = item as GameFile diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt index c52039c5b269..f67a23e2da04 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt @@ -25,7 +25,7 @@ class SettingsRowPresenter : Presenter() { return TvSettingsViewHolder(settingsCard) } - override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { + override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { val holder = viewHolder as TvSettingsViewHolder val context = holder.cardParent.context val settingsItem = item as TvSettingsItem diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt index 6fc5adbfa75e..9c868eeca9e7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt @@ -22,6 +22,6 @@ class TvGameViewHolder(itemView: View) : Presenter.ViewHolder(itemView) { init { itemView.tag = this cardParent = itemView as ImageCardView - imageScreenshot = cardParent.mainImageView + imageScreenshot = cardParent.mainImageView!! } } diff --git a/Source/Android/benchmark/build.gradle.kts b/Source/Android/benchmark/build.gradle.kts index fbc4f1105a5a..06267c42e565 100644 --- a/Source/Android/benchmark/build.gradle.kts +++ b/Source/Android/benchmark/build.gradle.kts @@ -45,8 +45,8 @@ baselineProfile { } dependencies { - implementation("androidx.test.ext:junit:1.1.5") - implementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.test.ext:junit:1.3.0") + implementation("androidx.test.espresso:espresso-core:3.7.0") implementation("androidx.test.uiautomator:uiautomator:2.3.0") - implementation("androidx.benchmark:benchmark-macro-junit4:1.2.4") + implementation("androidx.benchmark:benchmark-macro-junit4:1.4.1") } diff --git a/Source/Android/build.gradle.kts b/Source/Android/build.gradle.kts index f4e5a5f41fdc..2c39f29bd210 100644 --- a/Source/Android/build.gradle.kts +++ b/Source/Android/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.21" apply false - id("com.android.test") version "8.13.0" apply false + id("com.android.application") version "8.13.1" apply false + id("com.android.library") version "8.13.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false + id("com.android.test") version "8.13.1" apply false id("androidx.baselineprofile") version "1.3.3" apply false } diff --git a/Source/Android/gradle/wrapper/gradle-wrapper.properties b/Source/Android/gradle/wrapper/gradle-wrapper.properties index 37f853b1c84d..f186804f35bf 100644 --- a/Source/Android/gradle/wrapper/gradle-wrapper.properties +++ b/Source/Android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ +#Tue Nov 11 16:01:52 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 16260040e0d585f529abcde3820e2a203d1710eb Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 27 Oct 2025 18:18:38 -0500 Subject: [PATCH 011/123] CoreTiming: Add "Rush Frame Presentation" setting to throttle only once after each presentation for lower input latency. --- Source/Core/Core/Config/MainSettings.cpp | 2 ++ Source/Core/Core/Config/MainSettings.h | 1 + Source/Core/Core/CoreTiming.cpp | 29 +++++++++++++++++-- Source/Core/Core/CoreTiming.h | 6 ++++ .../Core/DolphinQt/Settings/AdvancedPane.cpp | 13 +++++++++ Source/Core/VideoCommon/Present.cpp | 9 +++++- 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 2a9bad56e03c..70838b0fb697 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -47,6 +47,8 @@ const Info MAIN_DSP_HLE{{System::Main, "Core", "DSPHLE"}, true}; const Info MAIN_MAX_FALLBACK{{System::Main, "Core", "MaxFallback"}, 100}; const Info MAIN_TIMING_VARIANCE{{System::Main, "Core", "TimingVariance"}, 40}; const Info MAIN_CORRECT_TIME_DRIFT{{System::Main, "Core", "CorrectTimeDrift"}, false}; +const Info MAIN_RUSH_FRAME_PRESENTATION{{System::Main, "Core", "RushFramePresentation"}, + false}; #if defined(ANDROID) // Currently enabled by default on Android because the performance boost is really needed. constexpr bool DEFAULT_CPU_THREAD = true; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 2355508f9f41..bbe56556ac98 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -65,6 +65,7 @@ extern const Info MAIN_DSP_HLE; extern const Info MAIN_MAX_FALLBACK; extern const Info MAIN_TIMING_VARIANCE; extern const Info MAIN_CORRECT_TIME_DRIFT; +extern const Info MAIN_RUSH_FRAME_PRESENTATION; extern const Info MAIN_CPU_THREAD; extern const Info MAIN_SYNC_ON_SKIP_IDLE; extern const Info MAIN_DEFAULT_ISO; diff --git a/Source/Core/Core/CoreTiming.cpp b/Source/Core/Core/CoreTiming.cpp index e16db6a7ee12..af5bc4731cee 100644 --- a/Source/Core/Core/CoreTiming.cpp +++ b/Source/Core/Core/CoreTiming.cpp @@ -29,6 +29,7 @@ #include "VideoCommon/PerformanceMetrics.h" #include "VideoCommon/VideoBackendBase.h" #include "VideoCommon/VideoConfig.h" +#include "VideoCommon/VideoEvents.h" namespace CoreTiming { @@ -113,6 +114,11 @@ void CoreTimingManager::Init() ResetThrottle(GetTicks()); } }); + + m_throttled_after_presentation = false; + m_frame_hook = m_system.GetVideoEvents().after_present_event.Register([this](const PresentInfo&) { + m_throttled_after_presentation.store(false, std::memory_order_relaxed); + }); } void CoreTimingManager::Shutdown() @@ -124,6 +130,7 @@ void CoreTimingManager::Shutdown() ClearPendingEvents(); UnregisterAllEvents(); CPUThreadConfigCallback::RemoveConfigChangedCallback(m_registered_config_callback_id); + m_frame_hook.reset(); } void CoreTimingManager::RefreshConfig() @@ -134,6 +141,11 @@ void CoreTimingManager::RefreshConfig() 1.0f); m_config_oc_inv_factor = 1.0f / m_config_oc_factor; m_config_sync_on_skip_idle = Config::Get(Config::MAIN_SYNC_ON_SKIP_IDLE); + m_config_rush_frame_presentation = Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION); + + // We don't want to skip so much throttling that the audio buffer overfills. + m_max_throttle_skip_time = + std::chrono::milliseconds{Config::Get(Config::MAIN_AUDIO_BUFFER_SIZE)} / 2; // A maximum fallback is used to prevent the system from sleeping for // too long or going full speed in an attempt to catch up to timings. @@ -422,6 +434,21 @@ void CoreTimingManager::SleepUntil(TimePoint time_point) void CoreTimingManager::Throttle(const s64 target_cycle) { + const TimePoint time = Clock::now(); + + const bool already_throttled = + m_throttled_after_presentation.exchange(true, std::memory_order_relaxed); + + // If RushFramePresentation is enabled, try to Throttle just once after each presentation. + // This lowers input latency by speeding through to presentation after grabbing input. + // Make sure we don't get too far ahead of proper timing though, + // otherwise the emulator unreasonably speeds through loading screens that don't have XFB copies, + // making audio sound terrible. + const bool skip_throttle = already_throttled && m_config_rush_frame_presentation && + ((GetTargetHostTime(target_cycle) - time) < m_max_throttle_skip_time); + if (skip_throttle) + return; + if (IsSpeedUnlimited()) { ResetThrottle(target_cycle); @@ -441,8 +468,6 @@ void CoreTimingManager::Throttle(const s64 target_cycle) TimePoint target_time = CalculateTargetHostTimeInternal(target_cycle); - const TimePoint time = Clock::now(); - const TimePoint min_target = time - m_max_fallback; // "Correct Time Drift" setting prevents timing relaxing. diff --git a/Source/Core/Core/CoreTiming.h b/Source/Core/Core/CoreTiming.h index 3a522b8deaae..68d1fd4fd292 100644 --- a/Source/Core/Core/CoreTiming.h +++ b/Source/Core/Core/CoreTiming.h @@ -207,6 +207,7 @@ class CoreTimingManager float m_config_oc_factor = 1.0f; float m_config_oc_inv_factor = 1.0f; bool m_config_sync_on_skip_idle = false; + bool m_config_rush_frame_presentation = false; s64 m_throttle_reference_cycle = 0; TimePoint m_throttle_reference_time = Clock::now(); @@ -232,6 +233,11 @@ class CoreTimingManager Common::PrecisionTimer m_precision_gpu_timer; Common::EventHook m_core_state_changed_hook; + Common::EventHook m_frame_hook; + + // Used to optionally minimize throttling for improving input latency. + std::atomic_bool m_throttled_after_presentation = false; + DT m_max_throttle_skip_time{}; }; } // namespace CoreTiming diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index 399c46c486ab..347310c0c7f3 100644 --- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp +++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp @@ -114,6 +114,19 @@ void AdvancedPane::CreateLayout() "

If unsure, leave this unchecked.")); timing_group_layout->addWidget(correct_time_drift); + auto* const rush_frame_presentation = + // i18n: "Rush" is a verb + new ConfigBool{tr("Rush Frame Presentation"), Config::MAIN_RUSH_FRAME_PRESENTATION}; + rush_frame_presentation->SetDescription( + tr("Limits throttling between input and frame output," + " speeding through emulation to reach presentation," + " displaying sooner, and thus reducing input latency." + "

This will generally make frame pacing worse." + "
This setting can work either with or without Immediately Present XFB." + "
An Audio Buffer Size of at least 80 ms is recommended to ensure full effect." + "

If unsure, leave this unchecked.")); + timing_group_layout->addWidget(rush_frame_presentation); + // Make all labels the same width, so that the sliders are aligned. const QFontMetrics font_metrics{font()}; const int label_width = font_metrics.boundingRect(QStringLiteral(" 500% (000.00 VPS)")).width(); diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index d2ba5f22118c..ce4b62a85b79 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -5,6 +5,7 @@ #include "Common/ChunkFile.h" #include "Core/Config/GraphicsSettings.h" +#include "Core/Config/MainSettings.h" #include "Core/CoreTiming.h" #include "Core/HW/VideoInterface.h" #include "Core/Host.h" @@ -201,7 +202,13 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, if (!is_duplicate || !g_ActiveConfig.bSkipPresentingDuplicateXFBs) { - Present(presentation_time); + // If RushFramePresentation is enabled, ignore the proper time to present as soon as possible. + // The goal is to achieve the lowest possible input latency. + if (Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION)) + Present(); + else + Present(presentation_time); + ProcessFrameDumping(ticks); video_events.after_present_event.Trigger(present_info); From d4f68cb1647ca87e5364431f09ab38b3178ed3cd Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 27 Oct 2025 05:25:25 -0500 Subject: [PATCH 012/123] HW/VideoInterface: Selectively throttle on "VBlank" based on "Immediate XFB" being enabled. --- Source/Core/Core/HW/VideoInterface.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/HW/VideoInterface.cpp b/Source/Core/Core/HW/VideoInterface.cpp index c0f46bed3586..f97852df7aa9 100644 --- a/Source/Core/Core/HW/VideoInterface.cpp +++ b/Source/Core/Core/HW/VideoInterface.cpp @@ -886,10 +886,10 @@ void VideoInterfaceManager::EndField(FieldType field, u64 ticks) // Note: OutputField above doesn't present when using GPU-on-Thread or Early/Immediate XFB, // giving "VBlank" measurements here poor pacing without a Throttle call. - // If the user actually wants the data, we'll Throttle to make the numbers nice. - const bool is_vblank_data_wanted = g_ActiveConfig.bShowVPS || g_ActiveConfig.bShowVTimes || - g_ActiveConfig.bLogRenderTimeToFile || - g_ActiveConfig.bShowGraphs; + // We'll throttle so long as Immediate XFB isn't enabled. + // That setting intends to minimize input latency and throttling would be counterproductive. + // The Rush Frame Presentation setting is handled by Throttle itself. + const bool is_vblank_data_wanted = !g_ActiveConfig.bImmediateXFB; if (is_vblank_data_wanted) m_system.GetCoreTiming().Throttle(ticks); From cc331feb020d71f109d41b7f841bd69caa96bbb3 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 27 Oct 2025 18:14:57 -0500 Subject: [PATCH 013/123] VideoCommon: Make Presenter aware of the next swap time to eliminate unsafe usage of GetTicks() with ImmediateXFB + DualCore. --- Source/Core/VideoCommon/BPStructs.cpp | 9 +------ Source/Core/VideoCommon/FrameDumpFFMpeg.cpp | 2 ++ Source/Core/VideoCommon/Present.cpp | 18 ++++++++++--- Source/Core/VideoCommon/Present.h | 9 ++++++- Source/Core/VideoCommon/VideoBackendBase.cpp | 27 +++++++++++++++++--- Source/Core/VideoCommon/VideoEvents.h | 1 - 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp index 1ece9f278ee1..9518fd044683 100644 --- a/Source/Core/VideoCommon/BPStructs.cpp +++ b/Source/Core/VideoCommon/BPStructs.cpp @@ -13,7 +13,6 @@ #include "Common/EnumMap.h" #include "Common/Logging/Log.h" -#include "Core/CoreTiming.h" #include "Core/DolphinAnalytics.h" #include "Core/FifoPlayer/FifoPlayer.h" #include "Core/FifoPlayer/FifoRecorder.h" @@ -359,14 +358,8 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager& if (g_ActiveConfig.bImmediateXFB) { - // TODO: GetTicks is not sane from the GPU thread. - // This value is currently used for frame dumping and the custom shader "time_ms" value. - // Frame dumping has more calls that aren't sane from the GPU thread. - // i.e. Frame dumping is not sane in "Dual Core" mode in general. - const u64 ticks = system.GetCoreTiming().GetTicks(); - // below div two to convert from bytes to pixels - it expects width, not stride - g_presenter->ImmediateSwap(destAddr, destStride / 2, destStride, height, ticks); + g_presenter->ImmediateSwap(destAddr, destStride / 2, destStride, height); } else { diff --git a/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp b/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp index e0e61529f732..52d41549dd8e 100644 --- a/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp +++ b/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp @@ -65,6 +65,7 @@ namespace { AVRational GetTimeBaseForCurrentRefreshRate(s64 max_denominator) { + // TODO: GetTargetRefreshRate* are not safe from GPU thread. auto& vi = Core::System::GetInstance().GetVideoInterface(); int num; int den; @@ -368,6 +369,7 @@ void FFMpegFrameDump::AddFrame(const FrameData& frame) // Calculate presentation timestamp from ticks since start. const s64 pts = av_rescale_q( frame.state.ticks - m_context->start_ticks, + // TODO: GetTicksPerSecond is not safe from GPU thread. AVRational{1, int(Core::System::GetInstance().GetSystemTimers().GetTicksPerSecond())}, m_context->codec->time_base); diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index ce4b62a85b79..bedc1c128f61 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -215,12 +215,14 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, } } -void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, u64 ticks) +void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height) { + const u64 ticks = m_next_swap_estimated_ticks; + FetchXFB(xfb_addr, fb_width, fb_stride, fb_height, ticks); PresentInfo present_info; - present_info.emulated_timestamp = ticks; // TODO: This should be the time of the next VI field + present_info.emulated_timestamp = ticks; present_info.frame_count = m_frame_count++; present_info.reason = PresentInfo::PresentReason::Immediate; present_info.present_count = m_present_count++; @@ -235,6 +237,12 @@ void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_ video_events.after_present_event.Trigger(present_info); } +void Presenter::SetNextSwapEstimatedTime(u64 ticks, TimePoint host_time) +{ + m_next_swap_estimated_ticks = ticks; + m_next_swap_estimated_time = host_time; +} + void Presenter::ProcessFrameDumping(u64 ticks) const { if (g_frame_dumper->IsFrameDumping() && m_xfb_entry) @@ -938,8 +946,10 @@ void Presenter::DoState(PointerWrap& p) // This technically counts as the end of the frame GetVideoEvents().after_frame_event.Trigger(Core::System::GetInstance()); - ImmediateSwap(m_last_xfb_addr, m_last_xfb_width, m_last_xfb_stride, m_last_xfb_height, - m_last_xfb_ticks); + m_next_swap_estimated_ticks = m_last_xfb_ticks; + m_next_swap_estimated_time = Clock::now(); + + ImmediateSwap(m_last_xfb_addr, m_last_xfb_width, m_last_xfb_stride, m_last_xfb_height); } } diff --git a/Source/Core/VideoCommon/Present.h b/Source/Core/VideoCommon/Present.h index a355af43856b..d6662f97f13c 100644 --- a/Source/Core/VideoCommon/Present.h +++ b/Source/Core/VideoCommon/Present.h @@ -37,7 +37,9 @@ class Presenter void ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, u64 ticks, TimePoint presentation_time); - void ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, u64 ticks); + void ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height); + + void SetNextSwapEstimatedTime(u64 ticks, TimePoint host_time); void Present(std::optional presentation_time = std::nullopt); void ClearLastXfbId() { m_last_xfb_id = std::numeric_limits::max(); } @@ -167,6 +169,11 @@ class Presenter u32 m_last_xfb_height = MAX_XFB_HEIGHT; Common::EventHook m_config_changed; + + // Calculated from the previous swap time and current refresh rate. + // Can be used for presentation of ImmediateXFB swaps which don't have timing information. + u64 m_next_swap_estimated_ticks = 0; + TimePoint m_next_swap_estimated_time{Clock::now()}; }; } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/VideoBackendBase.cpp b/Source/Core/VideoCommon/VideoBackendBase.cpp index 676349b65bb5..23d9d6e7f42a 100644 --- a/Source/Core/VideoCommon/VideoBackendBase.cpp +++ b/Source/Core/VideoCommon/VideoBackendBase.cpp @@ -21,6 +21,8 @@ #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/DolphinAnalytics.h" +#include "Core/HW/SystemTimers.h" +#include "Core/HW/VideoInterface.h" #include "Core/System.h" // TODO: ugly @@ -93,16 +95,35 @@ std::string VideoBackendBase::BadShaderFilename(const char* shader_stage, int co void VideoBackendBase::Video_OutputXFB(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, u64 ticks) { - if (m_initialized && g_presenter && !g_ActiveConfig.bImmediateXFB) + if (!m_initialized || !g_presenter) + return; + + auto& system = Core::System::GetInstance(); + auto& core_timing = system.GetCoreTiming(); + + if (!g_ActiveConfig.bImmediateXFB) { - auto& system = Core::System::GetInstance(); system.GetFifo().SyncGPU(Fifo::SyncGPUReason::Swap); - const TimePoint presentation_time = system.GetCoreTiming().GetTargetHostTime(ticks); + const TimePoint presentation_time = core_timing.GetTargetHostTime(ticks); AsyncRequests::GetInstance()->PushEvent([=] { g_presenter->ViSwap(xfb_addr, fb_width, fb_stride, fb_height, ticks, presentation_time); }); } + + // Inform the Presenter of the next estimated swap time. + + auto& vi = system.GetVideoInterface(); + const s64 refresh_rate_den = vi.GetTargetRefreshRateDenominator(); + const s64 refresh_rate_num = vi.GetTargetRefreshRateNumerator(); + + const auto next_swap_estimated_ticks = + ticks + (system.GetSystemTimers().GetTicksPerSecond() * refresh_rate_den / refresh_rate_num); + const auto next_swap_estimated_time = core_timing.GetTargetHostTime(next_swap_estimated_ticks); + + AsyncRequests::GetInstance()->PushEvent([=] { + g_presenter->SetNextSwapEstimatedTime(next_swap_estimated_ticks, next_swap_estimated_time); + }); } u32 VideoBackendBase::Video_GetQueryResult(PerfQueryType type) diff --git a/Source/Core/VideoCommon/VideoEvents.h b/Source/Core/VideoCommon/VideoEvents.h index ccee5bc3bdaf..b7ecf0fca58f 100644 --- a/Source/Core/VideoCommon/VideoEvents.h +++ b/Source/Core/VideoCommon/VideoEvents.h @@ -34,7 +34,6 @@ struct PresentInfo PresentReason reason = PresentReason::Immediate; // The exact emulated time of the when real hardware would have presented this frame - // FIXME: Immediate should predict the timestamp of this present u64 emulated_timestamp = 0; // TODO: From c2a1dce246e27f1ac9dc421387e4d70402bb96f4 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 27 Oct 2025 18:57:07 -0500 Subject: [PATCH 014/123] VideoCommon: Add "Smooth Early Presentation" setting to improve frame pacing with ImmediateXFB and/or RushFramePresentation. --- Source/Core/Core/Config/MainSettings.cpp | 2 + Source/Core/Core/Config/MainSettings.h | 1 + .../Core/DolphinQt/Settings/AdvancedPane.cpp | 11 +++ Source/Core/VideoCommon/Present.cpp | 73 ++++++++++++++----- Source/Core/VideoCommon/Present.h | 9 ++- Source/Core/VideoCommon/VideoEvents.h | 5 +- 6 files changed, 78 insertions(+), 23 deletions(-) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 70838b0fb697..b06fb5b49639 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -49,6 +49,8 @@ const Info MAIN_TIMING_VARIANCE{{System::Main, "Core", "TimingVariance"}, 4 const Info MAIN_CORRECT_TIME_DRIFT{{System::Main, "Core", "CorrectTimeDrift"}, false}; const Info MAIN_RUSH_FRAME_PRESENTATION{{System::Main, "Core", "RushFramePresentation"}, false}; +const Info MAIN_SMOOTH_EARLY_PRESENTATION{{System::Main, "Core", "SmoothEarlyPresentation"}, + false}; #if defined(ANDROID) // Currently enabled by default on Android because the performance boost is really needed. constexpr bool DEFAULT_CPU_THREAD = true; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index bbe56556ac98..cc64c80d69c1 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -66,6 +66,7 @@ extern const Info MAIN_MAX_FALLBACK; extern const Info MAIN_TIMING_VARIANCE; extern const Info MAIN_CORRECT_TIME_DRIFT; extern const Info MAIN_RUSH_FRAME_PRESENTATION; +extern const Info MAIN_SMOOTH_EARLY_PRESENTATION; extern const Info MAIN_CPU_THREAD; extern const Info MAIN_SYNC_ON_SKIP_IDLE; extern const Info MAIN_DEFAULT_ISO; diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index 347310c0c7f3..b96d2b24eaa3 100644 --- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp +++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp @@ -127,6 +127,17 @@ void AdvancedPane::CreateLayout() "

If unsure, leave this unchecked.")); timing_group_layout->addWidget(rush_frame_presentation); + auto* const smooth_early_presentation = + // i18n: "Smooth" is a verb + new ConfigBool{tr("Smooth Early Presentation"), Config::MAIN_SMOOTH_EARLY_PRESENTATION}; + smooth_early_presentation->SetDescription( + tr("Adaptively adjusts the timing of early frame presentation." + "

This can improve frame pacing with Immediately Present XFB" + " and/or Rush Frame Presentation," + " while still maintaining most of the input latency benefits." + "

If unsure, leave this unchecked.")); + timing_group_layout->addWidget(smooth_early_presentation); + // Make all labels the same width, so that the sliders are aligned. const QFontMetrics font_metrics{font()}; const int label_width = font_metrics.boundingRect(QStringLiteral(" 500% (000.00 VPS)")).width(); diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index bedc1c128f61..ea7cb65f95d2 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -163,9 +163,12 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, { bool is_duplicate = FetchXFB(xfb_addr, fb_width, fb_stride, fb_height, ticks); - PresentInfo present_info; - present_info.emulated_timestamp = ticks; - present_info.present_count = m_present_count++; + PresentInfo present_info{ + .present_count = m_present_count++, + .emulated_timestamp = ticks, + .intended_present_time = presentation_time, + }; + if (is_duplicate) { present_info.frame_count = m_frame_count - 1; // Previous frame @@ -202,13 +205,7 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, if (!is_duplicate || !g_ActiveConfig.bSkipPresentingDuplicateXFBs) { - // If RushFramePresentation is enabled, ignore the proper time to present as soon as possible. - // The goal is to achieve the lowest possible input latency. - if (Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION)) - Present(); - else - Present(presentation_time); - + Present(&present_info); ProcessFrameDumping(ticks); video_events.after_present_event.Trigger(present_info); @@ -221,17 +218,19 @@ void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_ FetchXFB(xfb_addr, fb_width, fb_stride, fb_height, ticks); - PresentInfo present_info; - present_info.emulated_timestamp = ticks; - present_info.frame_count = m_frame_count++; - present_info.reason = PresentInfo::PresentReason::Immediate; - present_info.present_count = m_present_count++; + PresentInfo present_info{ + .frame_count = m_frame_count++, + .present_count = m_present_count++, + .reason = PresentInfo::PresentReason::Immediate, + .emulated_timestamp = ticks, + .intended_present_time = m_next_swap_estimated_time, + }; auto& video_events = GetVideoEvents(); video_events.before_present_event.Trigger(present_info); - Present(); + Present(&present_info); ProcessFrameDumping(ticks); video_events.after_present_event.Trigger(present_info); @@ -834,7 +833,7 @@ void Presenter::RenderXFBToScreen(const MathUtil::Rectangle& target_rc, } } -void Presenter::Present(std::optional presentation_time) +void Presenter::Present(PresentInfo* present_info) { m_present_count++; @@ -888,8 +887,16 @@ void Presenter::Present(std::optional presentation_time) { std::lock_guard guard(m_swap_mutex); - if (presentation_time.has_value()) - Core::System::GetInstance().GetCoreTiming().SleepUntil(*presentation_time); + if (present_info != nullptr) + { + const auto present_time = GetUpdatedPresentationTime(present_info->intended_present_time); + + Core::System::GetInstance().GetCoreTiming().SleepUntil(present_time); + + // Perhaps in the future a more accurate time can be acquired from the various backends. + present_info->actual_present_time = Clock::now(); + present_info->present_time_accuracy = PresentInfo::PresentTimeAccuracy::PresentInProgress; + } g_gfx->PresentBackbuffer(); } @@ -907,6 +914,34 @@ void Presenter::Present(std::optional presentation_time) g_gfx->EndUtilityDrawing(); } +TimePoint Presenter::GetUpdatedPresentationTime(TimePoint intended_presentation_time) +{ + const auto now = Clock::now(); + const auto arrival_offset = std::min(now - intended_presentation_time, DT{}); + + if (!Config::Get(Config::MAIN_SMOOTH_EARLY_PRESENTATION)) + { + m_presentation_time_offset = arrival_offset; + + // When SmoothEarlyPresentation is off and ImmediateXFB or RushFramePresentation are on, + // present as soon as possible as the goal is to achieve low input latency. + if (g_ActiveConfig.bImmediateXFB || Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION)) + return now; + + return intended_presentation_time; + } + + // Adjust slowly backward in time but quickly forward in time. + // This keeps the pacing moderately smooth even if games produce regular sporadic bumps. + // This was tuned to handle the terrible pacing in Brawl with "Immediate XFB". + // Super Mario Galaxy 1 + 2 still perform poorly here in SingleCore mode. + const auto adjustment_divisor = (arrival_offset < m_presentation_time_offset) ? 100 : 2; + + m_presentation_time_offset += (arrival_offset - m_presentation_time_offset) / adjustment_divisor; + + return intended_presentation_time + m_presentation_time_offset; +} + void Presenter::SetKeyMap(const DolphinKeyMap& key_map) { if (m_onscreen_ui) diff --git a/Source/Core/VideoCommon/Present.h b/Source/Core/VideoCommon/Present.h index d6662f97f13c..e9ad21dbd937 100644 --- a/Source/Core/VideoCommon/Present.h +++ b/Source/Core/VideoCommon/Present.h @@ -41,7 +41,7 @@ class Presenter void SetNextSwapEstimatedTime(u64 ticks, TimePoint host_time); - void Present(std::optional presentation_time = std::nullopt); + void Present(PresentInfo* present_info = nullptr); void ClearLastXfbId() { m_last_xfb_id = std::numeric_limits::max(); } bool Initialize(); @@ -170,6 +170,13 @@ class Presenter Common::EventHook m_config_changed; + // Updates state for the SmoothEarlyPresentation setting if enabled. + // Returns the desired presentation time regardless. + TimePoint GetUpdatedPresentationTime(TimePoint intended_presentation_time); + + // Used by the SmoothEarlyPresentation setting. + DT m_presentation_time_offset{}; + // Calculated from the previous swap time and current refresh rate. // Can be used for presentation of ImmediateXFB swaps which don't have timing information. u64 m_next_swap_estimated_ticks = 0; diff --git a/Source/Core/VideoCommon/VideoEvents.h b/Source/Core/VideoCommon/VideoEvents.h index b7ecf0fca58f..fe30aa251b8d 100644 --- a/Source/Core/VideoCommon/VideoEvents.h +++ b/Source/Core/VideoCommon/VideoEvents.h @@ -36,11 +36,10 @@ struct PresentInfo // The exact emulated time of the when real hardware would have presented this frame u64 emulated_timestamp = 0; - // TODO: - // u64 intended_present_time = 0; + TimePoint intended_present_time{}; // AfterPresent only: The actual time the frame was presented - u64 actual_present_time = 0; + TimePoint actual_present_time{}; enum class PresentTimeAccuracy { From bf61c890cafc494def3a30ec06b742571b20b8d9 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 27 Oct 2025 23:59:31 -0500 Subject: [PATCH 015/123] VideoCommon/PerformanceMetrics: Display current offset between the latest frame presentation time and the intended presentation time in the "Show Frame Times" box. --- Source/Core/Core/Core.cpp | 4 ++++ Source/Core/VideoCommon/PerformanceMetrics.cpp | 11 +++++++++++ Source/Core/VideoCommon/PerformanceMetrics.h | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 5241e40a29de..4da1c94d1210 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -868,6 +868,10 @@ void Callback_FramePresented(const PresentInfo& present_info) { g_perf_metrics.CountFrame(); + const auto presentation_offset = + present_info.actual_present_time - present_info.intended_present_time; + g_perf_metrics.SetLatestFramePresentationOffset(presentation_offset); + if (present_info.reason == PresentInfo::PresentReason::VideoInterfaceDuplicate) return; diff --git a/Source/Core/VideoCommon/PerformanceMetrics.cpp b/Source/Core/VideoCommon/PerformanceMetrics.cpp index 5a38e1e3c66b..d00e5dbbba9e 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.cpp +++ b/Source/Core/VideoCommon/PerformanceMetrics.cpp @@ -23,6 +23,8 @@ void PerformanceMetrics::Reset() m_speed = 0; m_max_speed = 0; + + m_frame_presentation_offset = DT{}; } void PerformanceMetrics::CountFrame() @@ -98,6 +100,11 @@ double PerformanceMetrics::GetMaxSpeed() const return m_max_speed.load(std::memory_order_relaxed); } +void PerformanceMetrics::SetLatestFramePresentationOffset(DT offset) +{ + m_frame_presentation_offset.store(offset, std::memory_order_relaxed); +} + void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) { m_vps_counter.UpdateStats(); @@ -293,6 +300,10 @@ void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) DT_ms(m_fps_counter.GetDtAvg()).count()); ImGui::TextColored(ImVec4(r, g, b, 1.0f), " ±:%6.2lfms", DT_ms(m_fps_counter.GetDtStd()).count()); + + const auto offset = + DT_ms(m_frame_presentation_offset.load(std::memory_order_relaxed)).count(); + ImGui::TextColored(ImVec4(r, g, b, 1.0f), "ofs:%5.1lfms", offset); } } ImGui::End(); diff --git a/Source/Core/VideoCommon/PerformanceMetrics.h b/Source/Core/VideoCommon/PerformanceMetrics.h index bca6372d181e..914e6bc82f56 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.h +++ b/Source/Core/VideoCommon/PerformanceMetrics.h @@ -43,6 +43,9 @@ class PerformanceMetrics double GetSpeed() const; double GetMaxSpeed() const; + // Call from any thread. + void SetLatestFramePresentationOffset(DT offset); + // ImGui Functions void DrawImGuiStats(const float backbuffer_scale); @@ -55,6 +58,8 @@ class PerformanceMetrics std::atomic m_speed{}; std::atomic m_max_speed{}; + std::atomic
m_frame_presentation_offset{}; + struct PerfSample { TimePoint clock_time; From cd0e40ba973d8add843c2cabb24769523bd3c7de Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Wed, 12 Nov 2025 08:13:30 -0500 Subject: [PATCH 016/123] Android: Update Gradle Wrapper --- .../Android/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 1 - Source/Android/gradlew | 12 ++++-------- Source/Android/gradlew.bat | 3 +-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Source/Android/gradle/wrapper/gradle-wrapper.jar b/Source/Android/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..f8e1ee3125fe0768e9a76ee977ac089eb657005e 100644 GIT binary patch delta 37538 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 }= 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 F{{RkpxF!Gq diff --git a/Source/Android/gradle/wrapper/gradle-wrapper.properties b/Source/Android/gradle/wrapper/gradle-wrapper.properties index f186804f35bf..bad7c2462f5a 100644 --- a/Source/Android/gradle/wrapper/gradle-wrapper.properties +++ b/Source/Android/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Tue Nov 11 16:01:52 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip diff --git a/Source/Android/gradlew b/Source/Android/gradlew index f5feea6d6b11..adff685a0348 100755 --- a/Source/Android/gradlew +++ b/Source/Android/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/Source/Android/gradlew.bat b/Source/Android/gradlew.bat index 9b42019c7915..e509b2dd8fe5 100644 --- a/Source/Android/gradlew.bat +++ b/Source/Android/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell From 6d5e8b9ed8ced1ca180b4634fd2d36f35a34d240 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 11 Nov 2025 16:19:33 -0600 Subject: [PATCH 017/123] Externals: Update mGBA to latest master. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joshua Vandaële --- CMakeLists.txt | 2 +- Externals/mGBA/CMakeLists.txt | 2 +- Externals/mGBA/exports.props | 1 + Externals/mGBA/mgba | 2 +- Externals/mGBA/mgba.vcxproj | 15 ++++--- Source/Core/Core/HW/GBACore.cpp | 71 ++++++++++++++++++--------------- Source/Core/Core/HW/GBACore.h | 4 +- 7 files changed, 56 insertions(+), 41 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index da1a9eec10c7..2fa3769f8fde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -756,7 +756,7 @@ if(NOT ENABLE_QT) set(USE_MGBA 0) endif() if(USE_MGBA) - dolphin_find_optional_system_library(LIBMGBA Externals/mGBA) + dolphin_find_optional_system_library(LIBMGBA Externals/mGBA 0.11) endif() find_package(SYSTEMD) diff --git a/Externals/mGBA/CMakeLists.txt b/Externals/mGBA/CMakeLists.txt index f48231e35212..122e484b2db2 100644 --- a/Externals/mGBA/CMakeLists.txt +++ b/Externals/mGBA/CMakeLists.txt @@ -3,7 +3,7 @@ set(USE_LZMA ON) add_subdirectory(mgba EXCLUDE_FROM_ALL) dolphin_disable_warnings(mgba) -target_compile_definitions(mgba PUBLIC HAVE_CRC32) +target_compile_definitions(mgba PUBLIC HAVE_CRC32 ENABLE_VFS ENABLE_DIRECTORIES) target_link_libraries(mgba ZLIB::ZLIB) if(NOT MSVC) diff --git a/Externals/mGBA/exports.props b/Externals/mGBA/exports.props index 34e6c4f02d4a..ddc8446c6d8b 100644 --- a/Externals/mGBA/exports.props +++ b/Externals/mGBA/exports.props @@ -3,6 +3,7 @@ $(ExternalsDir)mGBA\mgba\include;%(AdditionalIncludeDirectories) + ENABLE_VFS;ENABLE_DIRECTORIES;%(PreprocessorDefinitions) diff --git a/Externals/mGBA/mgba b/Externals/mGBA/mgba index 8739b22fbc90..0b40863f64d0 160000 --- a/Externals/mGBA/mgba +++ b/Externals/mGBA/mgba @@ -1 +1 @@ -Subproject commit 8739b22fbc90fdf0b4f6612ef9c0520f0ba44a51 +Subproject commit 0b40863f64d0940f333fa1c638e75f86f8a26a33 diff --git a/Externals/mGBA/mgba.vcxproj b/Externals/mGBA/mgba.vcxproj index d1235246e367..026b9b9267ce 100644 --- a/Externals/mGBA/mgba.vcxproj +++ b/Externals/mGBA/mgba.vcxproj @@ -18,7 +18,7 @@ mgba\include;mgba\src;mgba\src\third-party\lzma;%(AdditionalIncludeDirectories) - BUILD_STATIC;M_CORE_GB;M_CORE_GBA;USE_LZMA;_7ZIP_PPMD_SUPPPORT;HAVE_STRDUP;HAVE_SETLOCALE;HAVE_CHMOD;HAVE_UMASK;HAVE_CRC32;%(PreprocessorDefinitions) + BUILD_STATIC;M_CORE_GB;M_CORE_GBA;USE_LZMA;_7ZIP_PPMD_SUPPPORT;HAVE_STRDUP;HAVE_SETLOCALE;HAVE_CHMOD;HAVE_UMASK;HAVE_CRC32;ENABLE_VFS;ENABLE_VFS_FD;ENABLE_DIRECTORIES;%(PreprocessorDefinitions) "$(CScript)" /nologo /E:JScript "make_version.c.js" @@ -67,7 +67,9 @@ - + + $(IntDir)obj2\ + @@ -95,6 +97,7 @@ + $(IntDir)obj3\ @@ -138,13 +141,14 @@ $(IntDir)obj2\ - $(IntDir)obj2\ $(IntDir)obj2\ + + @@ -153,6 +157,9 @@ + + + @@ -177,7 +184,6 @@ $(IntDir)obj3\ - @@ -206,7 +212,6 @@ $(IntDir)obj3\ - diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index b25414f3b1ac..272b458fc565 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -9,19 +9,19 @@ #include #undef PYCPARSE #include -#include #include #include #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/Crypto/SHA1.h" @@ -30,6 +30,7 @@ #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" @@ -46,8 +47,7 @@ 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; +constexpr size_t AUDIO_BUFFER_SIZE = 512; // libmGBA does not return the correct frequency for some GB models static u32 GetCoreFrequency(mCore* core) @@ -225,9 +225,9 @@ bool Core::Start(u64 gc_ticks) } rom_guard.Dismiss(); - std::array game_title{}; - m_core->getGameTitle(m_core, game_title.data()); - m_game_title = game_title.data(); + mGameInfo info; + m_core->getGameInfo(m_core, &info); + m_game_title = info.title; m_save_path = NetPlay::IsNetPlayRunning() ? NetPlay::GetGBASavePath(m_device_number) : GetSavePath(m_rom_path, m_device_number); @@ -241,7 +241,7 @@ bool Core::Start(u64 gc_ticks) SetSIODriver(); SetVideoBuffer(); - SetSampleRates(); + SetAudioBufferSize(); AddCallbacks(); SetAVStream(); SetupEvent(); @@ -386,18 +386,19 @@ 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) { + m_sio_driver.init = [](GBASIODriver* driver) { static_cast(driver)->core->m_link_enabled = true; return true; }; - m_sio_driver.unload = [](GBASIODriver* driver) { + m_sio_driver.handlesMode = [](GBASIODriver* driver, GBASIOMode mode) { + return mode == GBA_SIO_JOYBUS; + }; + m_sio_driver.deinit = [](GBASIODriver* driver) { static_cast(driver)->core->m_link_enabled = false; - return true; }; + + GBASIOSetDriver(&static_cast<::GBA*>(m_core->board)->sio, &m_sio_driver); } void Core::SetVideoBuffer() @@ -410,15 +411,9 @@ void Core::SetVideoBuffer() host->GameChanged(); } -void Core::SetSampleRates() +void Core::SetAudioBufferSize() { - 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); - - SoundStream* sound_stream = m_system.GetSoundStream(); - sound_stream->GetMixer()->SetGBAInputSampleRateDivisors( - m_device_number, Mixer::FIXED_SAMPLE_RATE_DIVIDEND / SAMPLE_RATE); + m_core->setAudioBufferSize(m_core, AUDIO_BUFFER_SIZE); } void Core::AddCallbacks() @@ -445,14 +440,26 @@ void Core::SetAVStream() 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); + m_stream.audioRateChanged = [](mAVStream* stream, unsigned rate) { + auto* core = static_cast(stream)->core; + auto* const sound_stream = core->m_system.GetSoundStream(); + sound_stream->GetMixer()->SetGBAInputSampleRateDivisors( + core->m_device_number, Mixer::FIXED_SAMPLE_RATE_DIVIDEND / rate); + }; + m_stream.postAudioBuffer = [](mAVStream* stream, mAudioBuffer* audio_buffer) { + size_t sample_count = mAudioBufferAvailable(audio_buffer); + const size_t required_buffer_size = sample_count * audio_buffer->channels; + + auto* const av_stream = static_cast(stream); + if (required_buffer_size > av_stream->sample_buffer.size()) + av_stream->sample_buffer.reset(required_buffer_size); + + sample_count = mAudioBufferRead(audio_buffer, av_stream->sample_buffer.data(), sample_count); - SoundStream* sound_stream = core->m_system.GetSoundStream(); - sound_stream->GetMixer()->PushGBASamples(core->m_device_number, &buffer[0], SAMPLES); + auto* const core = av_stream->core; + auto* const sound_stream = core->m_system.GetSoundStream(); + sound_stream->GetMixer()->PushGBASamples(core->m_device_number, av_stream->sample_buffer.data(), + sample_count); }; m_core->setAVStream(m_core, &m_stream); } @@ -733,9 +740,9 @@ bool Core::GetRomInfo(const char* rom_path, std::array& hash, std::strin return false; } - std::array game_title{}; - core->getGameTitle(core, game_title.data()); - title = game_title.data(); + mGameInfo info; + core->getGameInfo(core, &info); + title = info.title; core->deinit(core); return true; diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h index 6fd4b86e0c21..72faa1bec50d 100644 --- a/Source/Core/Core/HW/GBACore.h +++ b/Source/Core/Core/HW/GBACore.h @@ -21,6 +21,7 @@ #include #include +#include "Common/Buffer.h" #include "Common/CommonTypes.h" class GBAHostInterface; @@ -40,6 +41,7 @@ struct SIODriver : GBASIODriver struct AVStream : mAVStream { Core* core; + Common::UniqueBuffer sample_buffer; }; struct CoreInfo @@ -106,7 +108,7 @@ class Core final void SetSIODriver(); void SetVideoBuffer(); - void SetSampleRates(); + void SetAudioBufferSize(); void AddCallbacks(); void SetAVStream(); void SetupEvent(); From b7e447e21258f103cbaa1bda1ec6b7a4ce3d4e97 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 11 Nov 2025 01:08:34 -0600 Subject: [PATCH 018/123] GameINI: Add patches to limit internal framerate in Driver: San Francisco. --- Data/Sys/GameSettings/SDVE41.ini | 9 +++++++++ Data/Sys/GameSettings/SDVP41.ini | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 Data/Sys/GameSettings/SDVE41.ini create mode 100644 Data/Sys/GameSettings/SDVP41.ini diff --git a/Data/Sys/GameSettings/SDVE41.ini b/Data/Sys/GameSettings/SDVE41.ini new file mode 100644 index 000000000000..2ba3036de037 --- /dev/null +++ b/Data/Sys/GameSettings/SDVE41.ini @@ -0,0 +1,9 @@ +# SDVE41 - Driver: San Francisco + +[OnFrame] +$Limit internal frame rate (speed hack) +# This NOPs a branch that skips a VIWaitForRetrace. +0x803b40b8:dword:0x60000000 + +[OnFrame_Enabled] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/SDVP41.ini b/Data/Sys/GameSettings/SDVP41.ini new file mode 100644 index 000000000000..2df36f893ac9 --- /dev/null +++ b/Data/Sys/GameSettings/SDVP41.ini @@ -0,0 +1,9 @@ +# SDVP41 - Driver: San Francisco + +[OnFrame] +$Limit internal frame rate (speed hack) +# This NOPs a branch that skips a VIWaitForRetrace. +0x803b2cdc:dword:0x60000000 + +[OnFrame_Enabled] +$Limit internal frame rate (speed hack) From 9f66cc5c8a24697db27a81b71f76b336866d8e35 Mon Sep 17 00:00:00 2001 From: da pwo <111203195+da-pwo@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:30:30 -0500 Subject: [PATCH 019/123] Flatpak: Allow access to native discord IPC socket for rich presence this line: - --filesystem=xdg-run/app/com.discordapp.Discord:create specifically targets the flatpak version of discord, so anyone using the native package wouldnt get rich presence --- Flatpak/org.DolphinEmu.dolphin-emu.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Flatpak/org.DolphinEmu.dolphin-emu.yml b/Flatpak/org.DolphinEmu.dolphin-emu.yml index 1bbee5dd53e9..0bece4769bab 100644 --- a/Flatpak/org.DolphinEmu.dolphin-emu.yml +++ b/Flatpak/org.DolphinEmu.dolphin-emu.yml @@ -14,6 +14,7 @@ finish-args: # required for the emulated bluetooth adapter feature to work. - --allow=bluetooth - --filesystem=xdg-run/app/com.discordapp.Discord:create + - --filesystem=xdg-run/discord-ipc-0:create # required to disable the screensaver in various desktops - --talk-name=org.freedesktop.ScreenSaver - --talk-name=org.xfce.ScreenSaver From cd45dcea94746aecf195c1aadb6c6386c1238ec1 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 14 Nov 2025 23:01:46 -0600 Subject: [PATCH 020/123] HW/SI: Fix CMD_STATUS response lengths. They are supposed to be 3, not 4. --- Source/Core/Core/HW/SI/SI_Device.cpp | 10 ++++++++++ Source/Core/Core/HW/SI/SI_Device.h | 8 ++++++++ Source/Core/Core/HW/SI/SI_DeviceDanceMat.cpp | 8 ++------ Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp | 1 - Source/Core/Core/HW/SI/SI_DeviceGCController.cpp | 4 +--- Source/Core/Core/HW/SI/SI_DeviceGCSteeringWheel.cpp | 4 +--- Source/Core/Core/HW/SI/SI_DeviceKeyboard.cpp | 5 +---- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Source/Core/Core/HW/SI/SI_Device.cpp b/Source/Core/Core/HW/SI/SI_Device.cpp index 8ea3e75d0248..451523624b16 100644 --- a/Source/Core/Core/HW/SI/SI_Device.cpp +++ b/Source/Core/Core/HW/SI/SI_Device.cpp @@ -11,6 +11,7 @@ #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" +#include "Common/Swap.h" #include "Core/HW/SI/SI_DeviceDanceMat.h" #include "Core/HW/SI/SI_DeviceGBA.h" #ifdef HAS_LIBMGBA @@ -87,6 +88,15 @@ void ISIDevice::OnEvent(u64 userdata, s64 cycles_late) { } +int ISIDevice::CreateStatusResponse(u32 si_device_id, u8* buffer) +{ + constexpr int RESPONSE_LENGTH = 3; + + Common::BigEndianValue id(si_device_id); + std::memcpy(buffer, &id, RESPONSE_LENGTH); + return RESPONSE_LENGTH; +} + int SIDevice_GetGBATransferTime(const SystemTimers::SystemTimersManager& timers, EBufferCommands cmd) { diff --git a/Source/Core/Core/HW/SI/SI_Device.h b/Source/Core/Core/HW/SI/SI_Device.h index ebab7e323575..234c475e5f3e 100644 --- a/Source/Core/Core/HW/SI/SI_Device.h +++ b/Source/Core/Core/HW/SI/SI_Device.h @@ -123,7 +123,12 @@ class ISIDevice SIDevices GetDeviceType() const; // Run the SI Buffer + // Return value: + // positive: The response length. + // 0: Response not ready, we will try again `TransferInterval()` cycles later. + // -1: No response. virtual int RunBuffer(u8* buffer, int request_length); + virtual int TransferInterval(); virtual DataResponse GetData(u32& hi, u32& low) = 0; @@ -138,6 +143,9 @@ class ISIDevice virtual void OnEvent(u64 userdata, s64 cycles_late); protected: + // Only the three high bytes of `si_device_id` are used. + static int CreateStatusResponse(u32 si_device_id, u8* buffer); + Core::System& m_system; int m_device_number; diff --git a/Source/Core/Core/HW/SI/SI_DeviceDanceMat.cpp b/Source/Core/Core/HW/SI/SI_DeviceDanceMat.cpp index d7ca1c8a3a21..10cf79c0eca8 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceDanceMat.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceDanceMat.cpp @@ -3,10 +3,7 @@ #include "Core/HW/SI/SI_DeviceDanceMat.h" -#include - #include "Common/CommonTypes.h" -#include "Common/Swap.h" #include "InputCommon/GCPadStatus.h" namespace SerialInterface @@ -22,11 +19,10 @@ int CSIDevice_DanceMat::RunBuffer(u8* buffer, int request_length) const auto command = static_cast(buffer[0]); if (command == EBufferCommands::CMD_STATUS) { + // Only used for logging. ISIDevice::RunBuffer(buffer, request_length); - const u32 id = Common::swap32(SI_DANCEMAT); - std::memcpy(buffer, &id, sizeof(id)); - return sizeof(id); + return CreateStatusResponse(SI_DANCEMAT, buffer); } return CSIDevice_GCController::RunBuffer(buffer, request_length); } diff --git a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp index b9c22f1d5854..eb7c06343dd7 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp @@ -10,7 +10,6 @@ #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" -#include "Common/Swap.h" #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/HW/GBACore.h" diff --git a/Source/Core/Core/HW/SI/SI_DeviceGCController.cpp b/Source/Core/Core/HW/SI/SI_DeviceGCController.cpp index beb633f49929..270e75f56ad6 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGCController.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceGCController.cpp @@ -57,9 +57,7 @@ int CSIDevice_GCController::RunBuffer(u8* buffer, int request_length) case EBufferCommands::CMD_STATUS: case EBufferCommands::CMD_RESET: { - const u32 id = Common::swap32(SI_GC_CONTROLLER); - std::memcpy(buffer, &id, sizeof(id)); - return sizeof(id); + return CreateStatusResponse(SI_GC_CONTROLLER, buffer); } case EBufferCommands::CMD_DIRECT: diff --git a/Source/Core/Core/HW/SI/SI_DeviceGCSteeringWheel.cpp b/Source/Core/Core/HW/SI/SI_DeviceGCSteeringWheel.cpp index cb639f415e2a..a3ca7e9ebefe 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGCSteeringWheel.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceGCSteeringWheel.cpp @@ -34,9 +34,7 @@ int CSIDevice_GCSteeringWheel::RunBuffer(u8* buffer, int request_length) case EBufferCommands::CMD_STATUS: case EBufferCommands::CMD_RESET: { - const u32 id = Common::swap32(SI_GC_STEERING); - std::memcpy(buffer, &id, sizeof(id)); - return sizeof(id); + return CreateStatusResponse(SI_GC_STEERING, buffer); } default: return CSIDevice_GCController::RunBuffer(buffer, request_length); diff --git a/Source/Core/Core/HW/SI/SI_DeviceKeyboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceKeyboard.cpp index 28ffc13f6118..e3655bc904ed 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceKeyboard.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceKeyboard.cpp @@ -9,7 +9,6 @@ #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" -#include "Common/Swap.h" #include "Core/HW/GCKeyboard.h" #include "InputCommon/KeyboardStatus.h" @@ -35,9 +34,7 @@ int CSIDevice_Keyboard::RunBuffer(u8* buffer, int request_length) case EBufferCommands::CMD_STATUS: case EBufferCommands::CMD_RESET: { - const u32 id = Common::swap32(SI_GC_KEYBOARD); - std::memcpy(buffer, &id, sizeof(id)); - return sizeof(id); + return CreateStatusResponse(SI_GC_KEYBOARD, buffer); } case EBufferCommands::CMD_DIRECT_KB: From a3b1445e04931eea2fcf9f5cf7e78e0f92084591 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 15 Nov 2025 15:55:49 -0600 Subject: [PATCH 021/123] HW/SI: Adjust logging verbosity in RunSIBuffer and minor cleanups. --- Source/Core/Core/HW/SI/SI.cpp | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Source/Core/Core/HW/SI/SI.cpp b/Source/Core/Core/HW/SI/SI.cpp index c869ed0e3dfb..f451be328dab 100644 --- a/Source/Core/Core/HW/SI/SI.cpp +++ b/Source/Core/Core/HW/SI/SI.cpp @@ -3,13 +3,15 @@ #include "Core/HW/SI/SI.h" -#include #include -#include #include -#include #include -#include + +#if defined(_DEBUG) +#include + +#include "Common/StringUtil.h" +#endif #include "Common/BitField.h" #include "Common/ChunkFile.h" @@ -147,9 +149,12 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late) { const s32 request_length = ConvertSILengthField(m_com_csr.OUTLNGTH); const s32 expected_response_length = ConvertSILengthField(m_com_csr.INLNGTH); + +#if defined(_DEBUG) const std::vector request_copy(m_si_buffer.data(), m_si_buffer.data() + request_length); +#endif - const std::unique_ptr& device = m_channel[m_com_csr.CHANNEL].device; + auto* const device = m_channel[m_com_csr.CHANNEL].device.get(); const s32 actual_response_length = device->RunBuffer(m_si_buffer.data(), request_length); DEBUG_LOG_FMT(SERIALINTERFACE, @@ -159,15 +164,16 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late) actual_response_length); if (actual_response_length > 0 && expected_response_length != actual_response_length) { - std::ostringstream ss; - for (const u8 b : request_copy) - { - ss << std::hex << std::setw(2) << std::setfill('0') << (int)b << ' '; - } - DEBUG_LOG_FMT( +#if defined(_DEBUG) + WARN_LOG_FMT( SERIALINTERFACE, "RunSIBuffer: expected_response_length({}) != actual_response_length({}): request: {}", - expected_response_length, actual_response_length, ss.str()); + expected_response_length, actual_response_length, Common::BytesToHexString(request_copy)); +#else + WARN_LOG_FMT(SERIALINTERFACE, + "RunSIBuffer: expected_response_length({}) != actual_response_length({})", + expected_response_length, actual_response_length); +#endif } // TODO: From e630b0692e76fe4771a90a921ca22124d1b0100e Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 15 Nov 2025 20:25:36 -0600 Subject: [PATCH 022/123] VideoCommon/FramebufferManager: Silence warning: warning: virtual method '~FramebufferManager' is inside a 'final' class and can never be overridden [-Wunnecessary-virtual-specifier] --- Source/Core/VideoCommon/FramebufferManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoCommon/FramebufferManager.h b/Source/Core/VideoCommon/FramebufferManager.h index 765469807001..9f10918ac227 100644 --- a/Source/Core/VideoCommon/FramebufferManager.h +++ b/Source/Core/VideoCommon/FramebufferManager.h @@ -49,7 +49,7 @@ class FramebufferManager final { public: FramebufferManager(); - virtual ~FramebufferManager(); + ~FramebufferManager(); // Does not require the framebuffer to be created. Slower than direct queries. static AbstractTextureFormat GetEFBColorFormat(); From 9dea0859eb1ab34cbf8e25d6f99f1bd7dd28b097 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 15 Nov 2025 20:30:24 -0600 Subject: [PATCH 023/123] Common/BitField: Silence two warnings: warning: definition of implicit copy constructor for 'BitField<2, 2, ColorChannel>' is deprecated because it has a user-declared copy assignment operator [-Wdeprecated-copy] Redundant access specifier has the same accessibility as the previous access specifier. --- Source/Core/Common/BitField.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Core/Common/BitField.h b/Source/Core/Common/BitField.h index 9eef80e03295..35f8871b12b7 100644 --- a/Source/Core/Common/BitField.h +++ b/Source/Core/Common/BitField.h @@ -129,6 +129,9 @@ struct BitField // so that we can use this within unions constexpr BitField() = default; + // Allow copy construction. + constexpr BitField(const BitField&) = default; + // We explicitly delete the copy assignment operator here, because the // default copy assignment would copy the full storage value, rather than // just the bits relevant to this particular bit field. @@ -382,7 +385,6 @@ class BitFieldArrayIterator constexpr BitFieldArrayIterator(BitFieldArrayIterator&& other) = default; BitFieldArrayIterator& operator=(BitFieldArrayIterator&& other) = default; -public: BitFieldArrayIterator& operator++() { m_index++; From a026a0d5e49627b28d3a960915e7ccbba399b1a5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 17 Oct 2025 22:38:52 +0200 Subject: [PATCH 024/123] Android: Treat EmulationActivity dialog dismiss the same as OK In the dialog where you can choose what controller the input overlay should be controlling, there's an OK button. If you change controller but don't press OK, your selection will be saved, but the input overlay won't refresh to show the new controller unless you perform some other action that would cause it to refresh. This is not good UX. This commit changes the behavior not only of this dialog but also other dialogs spawned by EmulationActivity so that everything is properly updated when dismissing a dialog, as if you had pressed OK. --- .../activities/EmulationActivity.kt | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 184392f75f6f..60d4311ee5db 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -561,6 +561,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(gcSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } InputOverlay.OVERLAY_WIIMOTE_CLASSIC -> { @@ -575,6 +576,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(classicSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } InputOverlay.OVERLAY_WIIMOTE_NUNCHUK -> { @@ -596,6 +598,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(nunchukSettingBase + translateToSettingsIndex(indexSelected)) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else -> { @@ -611,14 +614,13 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(wiimoteSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } } builder - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> - emulationFragment?.refreshInputOverlay() - } + .setPositiveButton(R.string.ok, null) .show() } @@ -640,6 +642,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(gcSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else if (currentController == InputOverlay.OVERLAY_WIIMOTE_CLASSIC) { val wiiClassicEnabledButtons = BooleanArray(14) @@ -653,6 +656,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(classicSettingBase + indexSelected) .setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else { val wiiEnabledButtons = BooleanArray(11) @@ -667,6 +671,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } else { builder.setMultiChoiceItems( @@ -674,6 +679,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting .valueOf(wiiSettingBase + indexSelected).setBoolean(settings, isChecked) + emulationFragment?.refreshInputOverlay() } } } @@ -682,7 +688,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { .setNeutralButton(R.string.emulation_toggle_all) { _: DialogInterface?, _: Int -> emulationFragment!!.toggleInputOverlayVisibility(settings) } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshInputOverlay() } + .setPositiveButton(R.string.ok, null) .show() } @@ -707,10 +713,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { settings, InputOverlayPointer.DOUBLE_TAP_OPTIONS[which] ) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.initInputPointer() } + .setPositiveButton(R.string.ok, null) .show() } @@ -721,9 +726,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { valueTo = 150f value = IntSetting.MAIN_CONTROL_SCALE.int.toFloat() stepSize = 1f - addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> - dialogBinding.inputScaleValue.text = "${(progress.toInt() + 50)}%" - }) + addOnChangeListener { _: Slider?, value: Float, _: Boolean -> + dialogBinding.inputScaleValue.text = "${(value.toInt() + 50)}%" + IntSetting.MAIN_CONTROL_SCALE.setInt(settings, value.toInt()) + emulationFragment?.refreshInputOverlay() + } } inputScaleValue.text = "${(dialogBinding.inputScaleSlider.value.toInt() + 50)}%" @@ -732,9 +739,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { valueTo = 100f value = IntSetting.MAIN_CONTROL_OPACITY.int.toFloat() stepSize = 1f - addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean -> - inputOpacityValue.text = progress.toInt().toString() + "%" - }) + addOnChangeListener { _: Slider?, value: Float, _: Boolean -> + inputOpacityValue.text = value.toInt().toString() + "%" + IntSetting.MAIN_CONTROL_OPACITY.setInt(settings, value.toInt()) + emulationFragment?.refreshInputOverlay() + } } inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%" } @@ -742,17 +751,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { MaterialAlertDialogBuilder(this) .setTitle(R.string.emulation_control_adjustments) .setView(dialogBinding.root) - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> - IntSetting.MAIN_CONTROL_SCALE.setInt( - settings, - dialogBinding.inputScaleSlider.value.toInt() - ) - IntSetting.MAIN_CONTROL_OPACITY.setInt( - settings, - dialogBinding.inputOpacitySlider.value.toInt() - ) - emulationFragment?.refreshInputOverlay() - } + .setPositiveButton(R.string.ok, null) .setNeutralButton(R.string.default_values) { _: DialogInterface?, _: Int -> IntSetting.MAIN_CONTROL_SCALE.delete(settings) IntSetting.MAIN_CONTROL_OPACITY.delete(settings) @@ -858,10 +857,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { entries.toArray(arrayOf()), checkedItem ) { _: DialogInterface?, indexSelected: Int -> controllerSetting.setInt(settings, values[indexSelected]) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshInputOverlay() } + .setPositiveButton(R.string.ok, null) .setNeutralButton( R.string.emulation_more_controller_settings ) { _: DialogInterface?, _: Int -> SettingsActivity.launch(this, MenuTag.SETTINGS) } @@ -876,10 +874,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { IntSetting.MAIN_IR_MODE.int ) { _: DialogInterface?, indexSelected: Int -> IntSetting.MAIN_IR_MODE.setInt(settings, indexSelected) - } - .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment?.refreshOverlayPointer() } + .setPositiveButton(R.string.ok, null) .show() } From 060f7925603440f36e59ffbb355143be80f15add Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 9 Nov 2025 14:25:42 +0100 Subject: [PATCH 025/123] Android: Rate limit refreshInputOverlay calls from sliders Sliders can trigger change listeners very rapidly, so let's add some rate limiting so dragging a slider doesn't cause the whole UI to lag. (Now the input overlay looks laggy when dragging a slider, though.) --- .../activities/EmulationActivity.kt | 11 +++++-- .../dolphinemu/utils/RateLimiter.kt | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 60d4311ee5db..1b23118de392 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -8,6 +8,8 @@ import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.SparseIntArray import android.view.KeyEvent import android.view.MenuItem @@ -61,6 +63,7 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner import org.dolphinemu.dolphinemu.utils.DirectoryInitialization import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.RateLimiter import org.dolphinemu.dolphinemu.utils.ThemeHelper import kotlin.math.roundToInt @@ -88,6 +91,10 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityEmulationBinding + private val refreshInputOverlayRateLimiter = RateLimiter(Handler(Looper.getMainLooper()), 100) { + emulationFragment?.refreshInputOverlay() + } + private val requestChangeDisc = registerForActivityResult( ActivityResultContracts.GetContent() ) { uri: Uri? -> @@ -729,7 +736,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { addOnChangeListener { _: Slider?, value: Float, _: Boolean -> dialogBinding.inputScaleValue.text = "${(value.toInt() + 50)}%" IntSetting.MAIN_CONTROL_SCALE.setInt(settings, value.toInt()) - emulationFragment?.refreshInputOverlay() + refreshInputOverlayRateLimiter.run() } } inputScaleValue.text = @@ -742,7 +749,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { addOnChangeListener { _: Slider?, value: Float, _: Boolean -> inputOpacityValue.text = value.toInt().toString() + "%" IntSetting.MAIN_CONTROL_OPACITY.setInt(settings, value.toInt()) - emulationFragment?.refreshInputOverlay() + refreshInputOverlayRateLimiter.run() } } inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt new file mode 100644 index 000000000000..498e128875cd --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/RateLimiter.kt @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.os.Handler +import android.os.SystemClock + +class RateLimiter( + private val handler: Handler, + private val delayBetweenRunsMs: Int, + private val runnable: Runnable +) { + private var nextAllowedRun = 0L + private var pendingRun = false + + fun run() { + if (!pendingRun) { + val time = SystemClock.uptimeMillis() + if (time >= nextAllowedRun) { + runnable.run() + nextAllowedRun = time + delayBetweenRunsMs + } else { + pendingRun = true + handler.postAtTime({ + runnable.run() + nextAllowedRun = SystemClock.uptimeMillis() + delayBetweenRunsMs + pendingRun = false + }, nextAllowedRun) + } + } + } +} From 2dead5009b593bc17f23eabf62e717be0d7bd01d Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 22 Aug 2023 18:49:28 +0200 Subject: [PATCH 026/123] Jit64: Extract handling of immediate Rc --- Source/Core/Core/PowerPC/Jit64/Jit.h | 1 + .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h index e72397ccc520..5ce409cbaf1c 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/PowerPC/Jit64/Jit.h @@ -122,6 +122,7 @@ class Jit64 : public JitBase, public QuantizedMemoryRoutines void FinalizeCarry(Gen::CCFlags cond); void FinalizeCarry(bool ca); void ComputeRC(preg_t preg, bool needs_test = true, bool needs_sext = true); + void FinalizeImmediateRC(s32 value); void AndWithMask(Gen::X64Reg reg, u32 mask); void RotateLeft(int bits, Gen::X64Reg regOp, const Gen::OpArg& arg, u8 rotate); diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 65d2a4296b29..03d8e2af7a82 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -150,7 +150,10 @@ void Jit64::ComputeRC(preg_t preg, bool needs_test, bool needs_sext) if (arg.IsImm()) { - MOV(64, PPCSTATE_CR(0), Imm32(arg.SImm32())); + const s32 value = arg.SImm32(); + arg.Unlock(); + FinalizeImmediateRC(value); + return; } else if (needs_sext) { @@ -164,33 +167,32 @@ void Jit64::ComputeRC(preg_t preg, bool needs_test, bool needs_sext) if (CheckMergedBranch(0)) { - if (arg.IsImm()) + if (needs_test) { - s32 offset = arg.SImm32(); + TEST(32, arg, arg); arg.Unlock(); - DoMergedBranchImmediate(offset); } else { - if (needs_test) - { - TEST(32, arg, arg); - arg.Unlock(); - } - else - { - // If an operand to the cmp/rc op we're merging with the branch isn't used anymore, it'd be - // better to flush it here so that we don't have to flush it on both sides of the branch. - // We don't want to do this if a test is needed though, because it would interrupt macro-op - // fusion. - arg.Unlock(); - gpr.Flush(~js.op->gprInUse); - } - DoMergedBranchCondition(); + // If an operand to the cmp/rc op we're merging with the branch isn't used anymore, it'd be + // better to flush it here so that we don't have to flush it on both sides of the branch. + // We don't want to do this if a test is needed though, because it would interrupt macro-op + // fusion. + arg.Unlock(); + gpr.Flush(~js.op->gprInUse); } + DoMergedBranchCondition(); } } +void Jit64::FinalizeImmediateRC(s32 value) +{ + MOV(64, PPCSTATE_CR(0), Imm32(value)); + + if (CheckMergedBranch(0)) + DoMergedBranchImmediate(value); +} + // we can't do this optimization in the emitter because MOVZX and AND have different effects on // flags. void Jit64::AndWithMask(X64Reg reg, u32 mask) From e8060bd169b6ef17f3a4e197f1b7296af328ecb5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 23 Aug 2023 22:41:03 +0200 Subject: [PATCH 027/123] JitArm64: Add function for setting constant overflow --- Source/Core/Core/PowerPC/JitArm64/Jit.h | 1 + .../PowerPC/JitArm64/JitArm64_Integer.cpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index 77ba6d0bb350..2d14d634dfa2 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -376,6 +376,7 @@ class JitArm64 : public JitBase, public Arm64Gen::ARM64CodeBlock, public CommonA void ComputeRC0(Arm64Gen::ARM64Reg reg); void ComputeRC0(u32 imm); + void GenerateConstantOverflow(bool overflow); void ComputeCarry(Arm64Gen::ARM64Reg reg); // reg must contain 0 or 1 void ComputeCarry(bool carry); void ComputeCarry(); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 087a4cd90778..fc12676043bc 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -39,6 +39,25 @@ void JitArm64::ComputeRC0(u32 imm) MOVI2R(gpr.CR(0), s64(s32(imm))); } +void JitArm64::GenerateConstantOverflow(bool overflow) +{ + ARM64Reg WA = gpr.GetReg(); + + if (overflow) + { + MOVI2R(WA, XER_OV_MASK | XER_SO_MASK); + STRB(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF(xer_so_ov)); + } + else + { + LDRB(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF(xer_so_ov)); + AND(WA, WA, LogicalImm(~XER_OV_MASK, GPRSize::B32)); + STRB(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF(xer_so_ov)); + } + + gpr.Unlock(WA); +} + void JitArm64::ComputeCarry(ARM64Reg reg) { js.carryFlag = CarryFlag::InPPCState; From f9601dc38c78151fe1a8f95128a07fc821e50664 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 22 Aug 2023 17:44:35 +0200 Subject: [PATCH 028/123] Jit: Extract immediate handling to separate ConstantPropagation class Restructuring things in this way brings two immediate benefits: * Code is deduplicated between Jit64 and JitArm64. * Materializing an immediate value in a register no longer results in us forgetting what the immediate value was. As a more long-term benefit, this lets us also run constant propagation as part of PPCAnalyst, which could let us do cool stuff in the future like statically determining whether a conditional branch will be taken. But I have nothing concrete planned for that right now. --- Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/PowerPC/Jit64/Jit.cpp | 60 ++++++++++--- Source/Core/Core/PowerPC/Jit64/Jit.h | 3 + Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 35 +++++++- Source/Core/Core/PowerPC/JitArm64/Jit.h | 3 + .../PowerPC/JitCommon/ConstantPropagation.cpp | 19 ++++ .../PowerPC/JitCommon/ConstantPropagation.h | 86 +++++++++++++++++++ Source/Core/DolphinLib.props | 2 + 8 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp create mode 100644 Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index c898c8f693e7..7fc503e2f979 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -508,6 +508,8 @@ add_library(core PowerPC/Interpreter/Interpreter_Tables.cpp PowerPC/Interpreter/Interpreter.cpp PowerPC/Interpreter/Interpreter.h + PowerPC/JitCommon/ConstantPropagation.cpp + PowerPC/JitCommon/ConstantPropagation.h PowerPC/JitCommon/DivUtils.cpp PowerPC/JitCommon/DivUtils.h PowerPC/JitCommon/JitAsmCommon.cpp diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 377b4388fb40..6c161bd8b4fb 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -42,6 +42,7 @@ #include "Core/PowerPC/Jit64Common/Jit64Constants.h" #include "Core/PowerPC/Jit64Common/Jit64PowerPCState.h" #include "Core/PowerPC/Jit64Common/TrampolineCache.h" +#include "Core/PowerPC/JitCommon/ConstantPropagation.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PPCAnalyst.h" @@ -921,6 +922,8 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) gpr.Start(); fpr.Start(); + m_constant_propagation.Clear(); + js.downcountAmount = 0; js.skipInstructions = 0; js.carryFlag = CarryFlag::InPPCState; @@ -1105,21 +1108,56 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) { gpr.Flush(); fpr.Flush(); + m_constant_propagation.Clear(); + + CompileInstruction(op); } else { - // If we have an input register that is going to be used again, load it pre-emptively, - // even if the instruction doesn't strictly need it in a register, to avoid redundant - // loads later. Of course, don't do this if we're already out of registers. - // As a bit of a heuristic, make sure we have at least one register left over for the - // output, which needs to be bound in the actual instruction compilation. - // TODO: make this smarter in the case that we're actually register-starved, i.e. - // prioritize the more important registers. - gpr.PreloadRegisters(op.regsIn & op.gprInUse & ~op.gprDiscardable); - fpr.PreloadRegisters(op.fregsIn & op.fprInXmm & ~op.fprDiscardable); - } + const JitCommon::ConstantPropagationResult constant_propagation_result = + m_constant_propagation.EvaluateInstruction(op.inst); + + if (!constant_propagation_result.instruction_fully_executed) + { + if (!bJITRegisterCacheOff) + { + // If we have an input register that is going to be used again, load it pre-emptively, + // even if the instruction doesn't strictly need it in a register, to avoid redundant + // loads later. Of course, don't do this if we're already out of registers. + // As a bit of a heuristic, make sure we have at least one register left over for the + // output, which needs to be bound in the actual instruction compilation. + // TODO: make this smarter in the case that we're actually register-starved, i.e. + // prioritize the more important registers. + gpr.PreloadRegisters(op.regsIn & op.gprInUse & ~op.gprDiscardable); + fpr.PreloadRegisters(op.fregsIn & op.fprInXmm & ~op.fprDiscardable); + } + + CompileInstruction(op); + + m_constant_propagation.ClearGPRs(op.regsOut); + } - CompileInstruction(op); + m_constant_propagation.Apply(constant_propagation_result); + + if (constant_propagation_result.gpr >= 0) + { + gpr.SetImmediate32(constant_propagation_result.gpr, + constant_propagation_result.gpr_value); + } + + if (constant_propagation_result.instruction_fully_executed) + { + if (constant_propagation_result.carry) + FinalizeCarry(*constant_propagation_result.carry); + + if (constant_propagation_result.overflow) + GenerateConstantOverflow(*constant_propagation_result.overflow); + + // FinalizeImmediateRC is called last, because it may trigger branch merging + if (constant_propagation_result.compute_rc) + FinalizeImmediateRC(constant_propagation_result.gpr_value); + } + } js.fpr_is_store_safe = op.fprIsStoreSafeAfterInst; diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h index 5ce409cbaf1c..189f0c2b4bb9 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/PowerPC/Jit64/Jit.h @@ -31,6 +31,7 @@ #include "Core/PowerPC/Jit64Common/BlockCache.h" #include "Core/PowerPC/Jit64Common/Jit64AsmCommon.h" #include "Core/PowerPC/Jit64Common/TrampolineCache.h" +#include "Core/PowerPC/JitCommon/ConstantPropagation.h" #include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/JitCommon/JitCache.h" @@ -289,6 +290,8 @@ class Jit64 : public JitBase, public QuantizedMemoryRoutines GPRRegCache gpr{*this}; FPURegCache fpr{*this}; + JitCommon::ConstantPropagation m_constant_propagation; + Jit64AsmRoutineManager asm_routines{*this}; HyoutaUtilities::RangeSizeSet m_free_ranges_near; diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index 1eda45c58e34..f968ef5bdf1b 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -33,6 +33,7 @@ #include "Core/PatchEngine.h" #include "Core/PowerPC/Interpreter/Interpreter.h" #include "Core/PowerPC/JitArm64/JitArm64_RegCache.h" +#include "Core/PowerPC/JitCommon/ConstantPropagation.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/PowerPC.h" #include "Core/System.h" @@ -1169,6 +1170,8 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) gpr.Start(js.gpa); fpr.Start(js.fpa); + m_constant_propagation.Clear(); + if (!js.noSpeculativeConstantsAddresses.contains(js.blockStart)) { IntializeSpeculativeConstants(); @@ -1341,9 +1344,39 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) FlushCarry(); gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + m_constant_propagation.Clear(); + + CompileInstruction(op); } + else + { + const JitCommon::ConstantPropagationResult constant_propagation_result = + m_constant_propagation.EvaluateInstruction(op.inst); + + if (!constant_propagation_result.instruction_fully_executed) + { + CompileInstruction(op); + + m_constant_propagation.ClearGPRs(op.regsOut); + } - CompileInstruction(op); + m_constant_propagation.Apply(constant_propagation_result); + + if (constant_propagation_result.gpr >= 0) + gpr.SetImmediate(constant_propagation_result.gpr, constant_propagation_result.gpr_value); + + if (constant_propagation_result.instruction_fully_executed) + { + if (constant_propagation_result.carry) + ComputeCarry(*constant_propagation_result.carry); + + if (constant_propagation_result.overflow) + GenerateConstantOverflow(*constant_propagation_result.overflow); + + if (constant_propagation_result.compute_rc) + ComputeRC0(constant_propagation_result.gpr_value); + } + } js.fpr_is_store_safe = op.fprIsStoreSafeAfterInst; diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index 2d14d634dfa2..df44b9b6793c 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -16,6 +16,7 @@ #include "Core/PowerPC/JitArm64/JitArm64Cache.h" #include "Core/PowerPC/JitArm64/JitArm64_RegCache.h" #include "Core/PowerPC/JitArmCommon/BackPatch.h" +#include "Core/PowerPC/JitCommon/ConstantPropagation.h" #include "Core/PowerPC/JitCommon/JitAsmCommon.h" #include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/PPCAnalyst.h" @@ -397,6 +398,8 @@ class JitArm64 : public JitBase, public Arm64Gen::ARM64CodeBlock, public CommonA Arm64GPRCache gpr; Arm64FPRCache fpr; + JitCommon::ConstantPropagation m_constant_propagation; + JitArm64BlockCache blocks{*this}; Arm64Gen::ARM64FloatEmitter m_float_emit; diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp new file mode 100644 index 000000000000..b4afeed5b7da --- /dev/null +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -0,0 +1,19 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/PowerPC/JitCommon/ConstantPropagation.h" + +namespace JitCommon +{ +ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruction inst) const +{ + return {}; +} + +void ConstantPropagation::Apply(ConstantPropagationResult result) +{ + if (result.gpr >= 0) + SetGPR(result.gpr, result.gpr_value); +} + +} // namespace JitCommon diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h new file mode 100644 index 000000000000..2a24b9e71054 --- /dev/null +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -0,0 +1,86 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Common/BitSet.h" +#include "Common/CommonTypes.h" +#include "Core/PowerPC/PowerPC.h" + +#include +#include +#include + +namespace JitCommon +{ +struct ConstantPropagationResult final +{ + constexpr ConstantPropagationResult() = default; + + constexpr ConstantPropagationResult(s8 gpr_, u32 gpr_value_, bool compute_rc_ = false) + : gpr_value(gpr_value_), gpr(gpr_), instruction_fully_executed(true), compute_rc(compute_rc_) + { + } + + // If gpr is non-negative, this is the value the instruction writes to that GPR. + u32 gpr_value = 0; + + // If the instruction couldn't be evaluated or doesn't output to a GPR, this is -1. + // Otherwise, this is the GPR that the instruction writes to. + s8 gpr = -1; + + // Whether the instruction was able to be fully evaluated with no side effects unaccounted for, + // or in other words, whether the JIT can skip emitting code for this instruction. + bool instruction_fully_executed = false; + + // If true, CR0 needs to be set based on gpr_value. + bool compute_rc = false; + + // If not std::nullopt, the instruction writes this to the carry flag. + std::optional carry = std::nullopt; + + // If not std::nullopt, the instruction writes this to the overflow flag. + std::optional overflow = std::nullopt; +}; + +class ConstantPropagation final +{ +public: + ConstantPropagationResult EvaluateInstruction(UGeckoInstruction inst) const; + + void Apply(ConstantPropagationResult result); + + template + bool HasGPR(Args... gprs) const + { + return HasGPRs(BitSet32{static_cast(gprs)...}); + } + + bool HasGPRs(BitSet32 gprs) const { return (m_gpr_values_known & gprs) == gprs; } + + u32 GetGPR(size_t gpr) const { return m_gpr_values[gpr]; } + + void SetGPR(size_t gpr, u32 value) + { + m_gpr_values_known[gpr] = true; + m_gpr_values[gpr] = value; + } + + template + void ClearGPR(Args... gprs) + { + ClearGPRs(BitSet32{static_cast(gprs)...}); + } + + void ClearGPRs(BitSet32 gprs) { m_gpr_values_known &= ~gprs; } + + void Clear() { m_gpr_values_known = BitSet32{}; } + +private: + static constexpr size_t GPR_COUNT = 32; + + std::array m_gpr_values; + BitSet32 m_gpr_values_known{}; +}; + +} // namespace JitCommon diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 17e44675ac2c..3cfac5838c4c 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -455,6 +455,7 @@ + @@ -1139,6 +1140,7 @@ + From 20332f441b7ff5ea799bf9ffc55e1ec438fd513f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 22 Aug 2023 18:40:55 +0200 Subject: [PATCH 029/123] Jit: Move reg_imm to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 15 ----- Source/Core/Core/PowerPC/JitArm64/Jit.h | 2 +- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 57 ++++--------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 45 ++++++++++++++- .../PowerPC/JitCommon/ConstantPropagation.h | 9 +++ 5 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 03d8e2af7a82..ce53909f8da8 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -327,14 +327,6 @@ void Jit64::reg_imm(UGeckoInstruction inst) case 24: // ori case 25: // oris { - // check for nop - if (a == s && inst.UIMM == 0) - { - // Make the nop visible in the generated code. not much use but interesting if we see one. - NOP(); - return; - } - const u32 immediate = inst.OPCD == 24 ? inst.UIMM : inst.UIMM << 16; regimmop(a, s, true, immediate, Or, &XEmitter::OR); break; @@ -348,13 +340,6 @@ void Jit64::reg_imm(UGeckoInstruction inst) case 26: // xori case 27: // xoris { - if (s == a && inst.UIMM == 0) - { - // Make the nop visible in the generated code. - NOP(); - return; - } - const u32 immediate = inst.OPCD == 26 ? inst.UIMM : inst.UIMM << 16; regimmop(a, s, true, immediate, Xor, &XEmitter::XOR, false); break; diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index df44b9b6793c..1b738061b22b 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -384,7 +384,7 @@ class JitArm64 : public JitBase, public Arm64Gen::ARM64CodeBlock, public CommonA void LoadCarry(); void FlushCarry(); - void reg_imm(u32 d, u32 a, u32 value, u32 (*do_op)(u32, u32), + void reg_imm(u32 d, u32 a, u32 value, void (ARM64XEmitter::*op)(Arm64Gen::ARM64Reg, Arm64Gen::ARM64Reg, u64, Arm64Gen::ARM64Reg), bool Rc = false); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index fc12676043bc..25d6ecbb631c 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -155,41 +155,17 @@ void JitArm64::FlushCarry() js.carryFlag = CarryFlag::InPPCState; } -void JitArm64::reg_imm(u32 d, u32 a, u32 value, u32 (*do_op)(u32, u32), +void JitArm64::reg_imm(u32 d, u32 a, u32 value, void (ARM64XEmitter::*op)(ARM64Reg, ARM64Reg, u64, ARM64Reg), bool Rc) { - if (gpr.IsImm(a)) + gpr.BindToRegister(d, d == a); { - gpr.SetImmediate(d, do_op(gpr.GetImm(a), value)); - if (Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a); - { - auto WA = gpr.GetScopedReg(); - (this->*op)(gpr.R(d), gpr.R(a), value, WA); - } - - if (Rc) - ComputeRC0(gpr.R(d)); + auto WA = gpr.GetScopedReg(); + (this->*op)(gpr.R(d), gpr.R(a), value, WA); } -} -static constexpr u32 BitOR(u32 a, u32 b) -{ - return a | b; -} - -static constexpr u32 BitAND(u32 a, u32 b) -{ - return a & b; -} - -static constexpr u32 BitXOR(u32 a, u32 b) -{ - return a ^ b; + if (Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::arith_imm(UGeckoInstruction inst) @@ -203,34 +179,21 @@ void JitArm64::arith_imm(UGeckoInstruction inst) case 24: // ori case 25: // oris { - // check for nop - if (a == s && inst.UIMM == 0) - { - // NOP - return; - } - const u32 immediate = inst.OPCD == 24 ? inst.UIMM : inst.UIMM << 16; - reg_imm(a, s, immediate, BitOR, &ARM64XEmitter::ORRI2R); + reg_imm(a, s, immediate, &ARM64XEmitter::ORRI2R); break; } case 28: // andi - reg_imm(a, s, inst.UIMM, BitAND, &ARM64XEmitter::ANDI2R, true); + reg_imm(a, s, inst.UIMM, &ARM64XEmitter::ANDI2R, true); break; case 29: // andis - reg_imm(a, s, inst.UIMM << 16, BitAND, &ARM64XEmitter::ANDI2R, true); + reg_imm(a, s, inst.UIMM << 16, &ARM64XEmitter::ANDI2R, true); break; case 26: // xori case 27: // xoris { - if (a == s && inst.UIMM == 0) - { - // NOP - return; - } - const u32 immediate = inst.OPCD == 26 ? inst.UIMM : inst.UIMM << 16; - reg_imm(a, s, immediate, BitXOR, &ARM64XEmitter::EORI2R); + reg_imm(a, s, immediate, &ARM64XEmitter::EORI2R); break; } } diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index b4afeed5b7da..1b09f42c8021 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -5,9 +5,52 @@ namespace JitCommon { +static constexpr u32 BitOR(u32 a, u32 b) +{ + return a | b; +} + +static constexpr u32 BitAND(u32 a, u32 b) +{ + return a & b; +} + +static constexpr u32 BitXOR(u32 a, u32 b) +{ + return a ^ b; +} + ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruction inst) const { - return {}; + switch (inst.OPCD) + { + case 24: // ori + case 25: // oris + return EvaluateBitwiseImm(inst, BitOR); + case 26: // xori + case 27: // xoris + return EvaluateBitwiseImm(inst, BitXOR); + case 28: // andi + case 29: // andis + return EvaluateBitwiseImm(inst, BitAND); + default: + return {}; + } +} + +ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruction inst, + u32 (*do_op)(u32, u32)) const +{ + const bool is_and = do_op == &BitAND; + const u32 immediate = inst.OPCD & 1 ? inst.UIMM << 16 : inst.UIMM; + + if (inst.UIMM == 0 && !is_and && inst.RA == inst.RS) + return DO_NOTHING; + + if (!HasGPR(inst.RS)) + return {}; + + return ConstantPropagationResult(inst.RA, do_op(m_gpr_values[inst.RS], immediate), is_and); } void ConstantPropagation::Apply(ConstantPropagationResult result) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 2a24b9e71054..176ee3d513be 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -77,6 +77,15 @@ class ConstantPropagation final void Clear() { m_gpr_values_known = BitSet32{}; } private: + ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, + u32 (*do_op)(u32, u32)) const; + + static constexpr ConstantPropagationResult DO_NOTHING = [] { + ConstantPropagationResult result; + result.instruction_fully_executed = true; + return result; + }(); + static constexpr size_t GPR_COUNT = 32; std::array m_gpr_values; From 3a6eea74dd1fd345d409455a7471764abc4207e6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 22 Aug 2023 19:50:38 +0200 Subject: [PATCH 030/123] Jit: Move addix to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 31 ++++++------------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 13 ++------ .../PowerPC/JitCommon/ConstantPropagation.cpp | 16 ++++++++++ .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index ce53909f8da8..c8aea9863e96 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -260,25 +260,18 @@ void Jit64::regimmop(int d, int a, bool binary, u32 value, Operation doop, if (a || binary || carry) { carry &= js.op->wantsCA; - if (gpr.IsImm(a) && !carry) + RCOpArg Ra = gpr.Use(a, RCMode::Read); + RCX64Reg Rd = gpr.Bind(d, RCMode::Write); + RegCache::Realize(Ra, Rd); + if (doop == Add && Ra.IsSimpleReg() && !carry && d != a) { - gpr.SetImmediate32(d, doop(gpr.Imm32(a), value)); + LEA(32, Rd, MDisp(Ra.GetSimpleReg(), value)); } else { - RCOpArg Ra = gpr.Use(a, RCMode::Read); - RCX64Reg Rd = gpr.Bind(d, RCMode::Write); - RegCache::Realize(Ra, Rd); - if (doop == Add && Ra.IsSimpleReg() && !carry && d != a) - { - LEA(32, Rd, MDisp(Ra.GetSimpleReg(), value)); - } - else - { - if (d != a) - MOV(32, Rd, Ra); - (this->*op)(32, Rd, Imm32(value)); // m_GPR[d] = m_GPR[_inst.RA] + _inst.SIMM_16; - } + if (d != a) + MOV(32, Rd, Ra); + (this->*op)(32, Rd, Imm32(value)); // m_GPR[d] = m_GPR[_inst.RA] + _inst.SIMM_16; } if (carry) FinalizeCarry(CC_C); @@ -304,12 +297,8 @@ void Jit64::reg_imm(UGeckoInstruction inst) switch (inst.OPCD) { case 14: // addi - // occasionally used as MOV - emulate, with immediate propagation - if (a != 0 && d != a && gpr.IsImm(a)) - { - gpr.SetImmediate32(d, gpr.Imm32(a) + (u32)(s32)inst.SIMM_16); - } - else if (a != 0 && d != a && inst.SIMM_16 == 0) + // occasionally used as MOV + if (a != 0 && d != a && inst.SIMM_16 == 0) { RCOpArg Ra = gpr.Use(a, RCMode::Read); RCX64Reg Rd = gpr.Bind(d, RCMode::Write); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 25d6ecbb631c..1628fe04c4da 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -213,17 +213,10 @@ void JitArm64::addix(UGeckoInstruction inst) if (a) { - if (gpr.IsImm(a)) - { - gpr.SetImmediate(d, gpr.GetImm(a) + imm); - } - else - { - gpr.BindToRegister(d, d == a); + gpr.BindToRegister(d, d == a); - auto WA = gpr.GetScopedReg(); - ADDI2R(gpr.R(d), gpr.R(a), imm, WA); - } + auto WA = gpr.GetScopedReg(); + ADDI2R(gpr.R(d), gpr.R(a), imm, WA); } else { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 1b09f42c8021..34771366f3df 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -24,6 +24,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc { switch (inst.OPCD) { + case 14: // addi + case 15: // addis + return EvaluateAddImm(inst); case 24: // ori case 25: // oris return EvaluateBitwiseImm(inst, BitOR); @@ -38,6 +41,19 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc } } +ConstantPropagationResult ConstantPropagation::EvaluateAddImm(UGeckoInstruction inst) const +{ + const s32 immediate = inst.OPCD & 1 ? inst.SIMM_16 << 16 : inst.SIMM_16; + + if (inst.RA == 0) + return ConstantPropagationResult(inst.RD, immediate); + + if (!HasGPR(inst.RA)) + return {}; + + return ConstantPropagationResult(inst.RD, m_gpr_values[inst.RA] + immediate); +} + ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 176ee3d513be..797c78392543 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -77,6 +77,7 @@ class ConstantPropagation final void Clear() { m_gpr_values_known = BitSet32{}; } private: + ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; From 77b46c30aded7b562566483ab8d94c24c3461499 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 23 Aug 2023 15:22:34 +0200 Subject: [PATCH 031/123] Jit: Move boolX to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 87 ++++-------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 24 +--- .../PowerPC/JitCommon/ConstantPropagation.cpp | 128 ++++++++++++++++++ .../PowerPC/JitCommon/ConstantPropagation.h | 4 + 4 files changed, 162 insertions(+), 81 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index c8aea9863e96..d9b5cb360b3f 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -680,29 +680,7 @@ void Jit64::boolX(UGeckoInstruction inst) bool needs_test = false; DEBUG_ASSERT_MSG(DYNA_REC, inst.OPCD == 31, "Invalid boolX"); - if (gpr.IsImm(s, b)) - { - const u32 rs_offset = gpr.Imm32(s); - const u32 rb_offset = gpr.Imm32(b); - - if (inst.SUBOP10 == 28) // andx - gpr.SetImmediate32(a, rs_offset & rb_offset); - else if (inst.SUBOP10 == 476) // nandx - gpr.SetImmediate32(a, ~(rs_offset & rb_offset)); - else if (inst.SUBOP10 == 60) // andcx - gpr.SetImmediate32(a, rs_offset & (~rb_offset)); - else if (inst.SUBOP10 == 444) // orx - gpr.SetImmediate32(a, rs_offset | rb_offset); - else if (inst.SUBOP10 == 124) // norx - gpr.SetImmediate32(a, ~(rs_offset | rb_offset)); - else if (inst.SUBOP10 == 412) // orcx - gpr.SetImmediate32(a, rs_offset | (~rb_offset)); - else if (inst.SUBOP10 == 316) // xorx - gpr.SetImmediate32(a, rs_offset ^ rb_offset); - else if (inst.SUBOP10 == 284) // eqvx - gpr.SetImmediate32(a, ~(rs_offset ^ rb_offset)); - } - else if (gpr.IsImm(s) || gpr.IsImm(b)) + if (gpr.IsImm(s) || gpr.IsImm(b)) { const auto [i, j] = gpr.IsImm(s) ? std::pair(s, b) : std::pair(b, s); u32 imm = gpr.Imm32(i); @@ -756,53 +734,46 @@ void Jit64::boolX(UGeckoInstruction inst) } else if (is_and) { - if (imm == 0) + RCOpArg Rj = gpr.Use(j, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::Write); + RegCache::Realize(Rj, Ra); + + if (imm == 0xFFFFFFFF) + { + if (a != j) + MOV(32, Ra, Rj); + if (final_not || complement_b) + NOT(32, Ra); + needs_test = true; + } + else if (complement_b) { - gpr.SetImmediate32(a, final_not ? 0xFFFFFFFF : 0); + if (a != j) + MOV(32, Ra, Rj); + NOT(32, Ra); + AND(32, Ra, Imm32(imm)); } else { - RCOpArg Rj = gpr.Use(j, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RegCache::Realize(Rj, Ra); - - if (imm == 0xFFFFFFFF) + if (a == j) { - if (a != j) - MOV(32, Ra, Rj); - if (final_not || complement_b) - NOT(32, Ra); - needs_test = true; + AND(32, Ra, Imm32(imm)); } - else if (complement_b) + else if (s32(imm) >= -128 && s32(imm) <= 127) { - if (a != j) - MOV(32, Ra, Rj); - NOT(32, Ra); + MOV(32, Ra, Rj); AND(32, Ra, Imm32(imm)); } else { - if (a == j) - { - AND(32, Ra, Imm32(imm)); - } - else if (s32(imm) >= -128 && s32(imm) <= 127) - { - MOV(32, Ra, Rj); - AND(32, Ra, Imm32(imm)); - } - else - { - MOV(32, Ra, Imm32(imm)); - AND(32, Ra, Rj); - } + MOV(32, Ra, Imm32(imm)); + AND(32, Ra, Rj); + } - if (final_not) - { - NOT(32, Ra); - needs_test = true; - } + if (final_not) + { + NOT(32, Ra); + needs_test = true; } } } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 1628fe04c4da..9ab44b66aedb 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -231,29 +231,7 @@ void JitArm64::boolX(UGeckoInstruction inst) JITDISABLE(bJITIntegerOff); int a = inst.RA, s = inst.RS, b = inst.RB; - if (gpr.IsImm(s) && gpr.IsImm(b)) - { - if (inst.SUBOP10 == 28) // andx - gpr.SetImmediate(a, (u32)gpr.GetImm(s) & (u32)gpr.GetImm(b)); - else if (inst.SUBOP10 == 476) // nandx - gpr.SetImmediate(a, ~((u32)gpr.GetImm(s) & (u32)gpr.GetImm(b))); - else if (inst.SUBOP10 == 60) // andcx - gpr.SetImmediate(a, (u32)gpr.GetImm(s) & (~(u32)gpr.GetImm(b))); - else if (inst.SUBOP10 == 444) // orx - gpr.SetImmediate(a, (u32)gpr.GetImm(s) | (u32)gpr.GetImm(b)); - else if (inst.SUBOP10 == 124) // norx - gpr.SetImmediate(a, ~((u32)gpr.GetImm(s) | (u32)gpr.GetImm(b))); - else if (inst.SUBOP10 == 412) // orcx - gpr.SetImmediate(a, (u32)gpr.GetImm(s) | (~(u32)gpr.GetImm(b))); - else if (inst.SUBOP10 == 316) // xorx - gpr.SetImmediate(a, (u32)gpr.GetImm(s) ^ (u32)gpr.GetImm(b)); - else if (inst.SUBOP10 == 284) // eqvx - gpr.SetImmediate(a, ~((u32)gpr.GetImm(s) ^ (u32)gpr.GetImm(b))); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else if (s == b) + if (s == b) { if ((inst.SUBOP10 == 28 /* andx */) || (inst.SUBOP10 == 444 /* orx */)) { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 34771366f3df..f29ce2a759e2 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -36,6 +36,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc case 28: // andi case 29: // andis return EvaluateBitwiseImm(inst, BitAND); + case 31: + return EvaluateTable31(inst); default: return {}; } @@ -69,6 +71,132 @@ ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruct return ConstantPropagationResult(inst.RA, do_op(m_gpr_values[inst.RS], immediate), is_and); } +ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction inst) const +{ + const bool has_s = HasGPR(inst.RS); + const bool has_b = HasGPR(inst.RB); + if (!has_s || !has_b) + { + if (has_s) + return EvaluateTable31OneRegisterKnown(inst, GetGPR(inst.RS), false); + else if (has_b) + return EvaluateTable31OneRegisterKnown(inst, GetGPR(inst.RB), true); + else if (inst.RS == inst.RB) + return EvaluateTable31IdenticalRegisters(inst); + else + return {}; + } + + u32 a; + const u32 s = GetGPR(inst.RS); + const u32 b = GetGPR(inst.RB); + + switch (inst.SUBOP10) + { + case 28: // andx + a = s & b; + break; + case 60: // andcx + a = s & (~b); + break; + case 124: // norx + a = ~(s | b); + break; + case 284: // eqvx + a = ~(s ^ b); + break; + case 316: // xorx + a = s ^ b; + break; + case 412: // orcx + a = s | (~b); + break; + case 444: // orx + a = s | b; + break; + case 476: // nandx + a = ~(s & b); + break; + default: + return {}; + } + + return ConstantPropagationResult(inst.RA, a, inst.Rc); +} + +ConstantPropagationResult +ConstantPropagation::EvaluateTable31OneRegisterKnown(UGeckoInstruction inst, u32 value, + bool known_reg_is_b) const +{ + u32 a; + + switch (inst.SUBOP10) + { + case 60: // andcx + if (known_reg_is_b) + value = ~value; + [[fallthrough]]; + case 28: // andx + if (value == 0) + a = 0; + else + return {}; + break; + case 124: // norx + if (value == 0xFFFFFFFF) + a = 0; + else + return {}; + break; + case 412: // orcx + if (known_reg_is_b) + value = ~value; + [[fallthrough]]; + case 444: // orx + if (value == 0xFFFFFFFF) + a = 0xFFFFFFFF; + else + return {}; + break; + case 476: // nandx + if (value == 0) + a = 0xFFFFFFFF; + else + return {}; + break; + default: + return {}; + } + + return ConstantPropagationResult(inst.RA, a, inst.Rc); +} + +ConstantPropagationResult +ConstantPropagation::EvaluateTable31IdenticalRegisters(UGeckoInstruction inst) const +{ + u32 a; + + switch (inst.SUBOP10) + { + case 60: // andcx + a = 0; + break; + case 284: // eqvx + a = 0xFFFFFFFF; + break; + case 316: // xorx + a = 0; + break; + case 412: // orcx + a = 0xFFFFFFFF; + break; + default: + return {}; + } + + return ConstantPropagationResult(inst.RA, a, inst.Rc); +} + void ConstantPropagation::Apply(ConstantPropagationResult result) { if (result.gpr >= 0) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 797c78392543..5560ffdb8080 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -80,6 +80,10 @@ class ConstantPropagation final ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; + ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateTable31OneRegisterKnown(UGeckoInstruction inst, u32 value, + bool known_reg_is_b) const; + ConstantPropagationResult EvaluateTable31IdenticalRegisters(UGeckoInstruction inst) const; static constexpr ConstantPropagationResult DO_NOTHING = [] { ConstantPropagationResult result; From f04417eb5a117dfe6552f3203d39697b4ef0d7d5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 24 Aug 2023 13:16:08 +0200 Subject: [PATCH 032/123] Jit: Move addx to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit.cpp | 2 +- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 11 +-- Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 2 +- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 32 ++------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 68 ++++++++++++++++--- .../PowerPC/JitCommon/ConstantPropagation.h | 12 ++-- 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 6c161bd8b4fb..024b4c6e4af7 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -1115,7 +1115,7 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) else { const JitCommon::ConstantPropagationResult constant_propagation_result = - m_constant_propagation.EvaluateInstruction(op.inst); + m_constant_propagation.EvaluateInstruction(op.inst, opinfo->flags); if (!constant_propagation_result.instruction_fully_executed) { diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index d9b5cb360b3f..1b5674f65b93 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1826,16 +1826,7 @@ void Jit64::addx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; bool carry = !(inst.SUBOP10 & (1 << 8)); - if (gpr.IsImm(a, b)) - { - const s32 i = gpr.SImm32(a), j = gpr.SImm32(b); - gpr.SetImmediate32(d, i + j); - if (carry) - FinalizeCarry(Interpreter::Helper_Carry(i, j)); - if (inst.OE) - GenerateConstantOverflow((s64)i + (s64)j); - } - else if (gpr.IsImm(a) || gpr.IsImm(b)) + if (gpr.IsImm(a) || gpr.IsImm(b)) { const auto [i, j] = gpr.IsImm(a) ? std::pair(a, b) : std::pair(b, a); const s32 imm = gpr.SImm32(i); diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index f968ef5bdf1b..ba30b0bc8185 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -1351,7 +1351,7 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) else { const JitCommon::ConstantPropagationResult constant_propagation_result = - m_constant_propagation.EvaluateInstruction(op.inst); + m_constant_propagation.EvaluateInstruction(op.inst, opinfo->flags); if (!constant_propagation_result.instruction_fully_executed) { diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 9ab44b66aedb..6739de443992 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -476,14 +476,7 @@ void JitArm64::addx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - s32 i = (s32)gpr.GetImm(a), j = (s32)gpr.GetImm(b); - gpr.SetImmediate(d, i + j); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else if (gpr.IsImm(a) || gpr.IsImm(b)) + if (gpr.IsImm(a) || gpr.IsImm(b)) { int imm_reg = gpr.IsImm(a) ? a : b; int in_reg = gpr.IsImm(a) ? b : a; @@ -1679,25 +1672,12 @@ void JitArm64::addcx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - u32 i = gpr.GetImm(a), j = gpr.GetImm(b); - gpr.SetImmediate(d, i + j); + gpr.BindToRegister(d, d == a || d == b); + CARRY_IF_NEEDED(ADD, ADDS, gpr.R(d), gpr.R(a), gpr.R(b)); - bool has_carry = Interpreter::Helper_Carry(i, j); - ComputeCarry(has_carry); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a || d == b); - CARRY_IF_NEEDED(ADD, ADDS, gpr.R(d), gpr.R(a), gpr.R(b)); - - ComputeCarry(); - if (inst.Rc) - ComputeRC0(gpr.R(d)); - } + ComputeCarry(); + if (inst.Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::divwux(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index f29ce2a759e2..84a37efa16da 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -3,6 +3,8 @@ #include "Core/PowerPC/JitCommon/ConstantPropagation.h" +#include "Core/PowerPC/PPCTables.h" + namespace JitCommon { static constexpr u32 BitOR(u32 a, u32 b) @@ -20,7 +22,8 @@ static constexpr u32 BitXOR(u32 a, u32 b) return a ^ b; } -ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruction inst) const +ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruction inst, + u64 flags) const { switch (inst.OPCD) { @@ -37,7 +40,7 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc case 29: // andis return EvaluateBitwiseImm(inst, BitAND); case 31: - return EvaluateTable31(inst); + return EvaluateTable31(inst, flags); default: return {}; } @@ -71,18 +74,65 @@ ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruct return ConstantPropagationResult(inst.RA, do_op(m_gpr_values[inst.RS], immediate), is_and); } -ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction inst) const +ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction inst, + u64 flags) const +{ + if (flags & FL_OUT_D) + { + // input a, b -> output d + return EvaluateTable31AB(inst, flags); + } + else + { + // input s, b -> output a + return EvaluateTable31SB(inst); + } +} + +ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstruction inst, + u64 flags) const +{ + if (!HasGPR(inst.RA, inst.RB)) + return {}; + + u64 d; + s64 d_overflow; + const u32 a = GetGPR(inst.RA); + const u32 b = GetGPR(inst.RB); + + switch (inst.SUBOP10) + { + case 10: // addcx + case 522: // addcox + case 266: // addx + case 778: // addox + d = u64(a) + u64(b); + d_overflow = s64(s32(a)) + s64(s32(b)); + break; + default: + return {}; + } + + ConstantPropagationResult result(inst.RD, u32(d), inst.Rc); + if (flags & FL_SET_CA) + result.carry = (d >> 32 != 0); + if (flags & FL_SET_OE) + result.overflow = (d_overflow != s64(s32(d_overflow))); + return result; +} + +ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstruction inst) const { const bool has_s = HasGPR(inst.RS); const bool has_b = HasGPR(inst.RB); if (!has_s || !has_b) { if (has_s) - return EvaluateTable31OneRegisterKnown(inst, GetGPR(inst.RS), false); + return EvaluateTable31SBOneRegisterKnown(inst, GetGPR(inst.RS), false); else if (has_b) - return EvaluateTable31OneRegisterKnown(inst, GetGPR(inst.RB), true); + return EvaluateTable31SBOneRegisterKnown(inst, GetGPR(inst.RB), true); else if (inst.RS == inst.RB) - return EvaluateTable31IdenticalRegisters(inst); + return EvaluateTable31SBIdenticalRegisters(inst); else return {}; } @@ -125,8 +175,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction } ConstantPropagationResult -ConstantPropagation::EvaluateTable31OneRegisterKnown(UGeckoInstruction inst, u32 value, - bool known_reg_is_b) const +ConstantPropagation::EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u32 value, + bool known_reg_is_b) const { u32 a; @@ -172,7 +222,7 @@ ConstantPropagation::EvaluateTable31OneRegisterKnown(UGeckoInstruction inst, u32 } ConstantPropagationResult -ConstantPropagation::EvaluateTable31IdenticalRegisters(UGeckoInstruction inst) const +ConstantPropagation::EvaluateTable31SBIdenticalRegisters(UGeckoInstruction inst) const { u32 a; diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 5560ffdb8080..a7f5b27fd306 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -46,7 +46,7 @@ struct ConstantPropagationResult final class ConstantPropagation final { public: - ConstantPropagationResult EvaluateInstruction(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateInstruction(UGeckoInstruction inst, u64 flags) const; void Apply(ConstantPropagationResult result); @@ -80,10 +80,12 @@ class ConstantPropagation final ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; - ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst) const; - ConstantPropagationResult EvaluateTable31OneRegisterKnown(UGeckoInstruction inst, u32 value, - bool known_reg_is_b) const; - ConstantPropagationResult EvaluateTable31IdenticalRegisters(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst, u64 flags) const; + ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; + ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u32 value, + bool known_reg_is_b) const; + ConstantPropagationResult EvaluateTable31SBIdenticalRegisters(UGeckoInstruction inst) const; static constexpr ConstantPropagationResult DO_NOTHING = [] { ConstantPropagationResult result; From b506cb2ad878a0f2560cb6a3ad1b35c2e60f9e67 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 24 Aug 2023 14:17:36 +0200 Subject: [PATCH 033/123] Jit: Move extsXx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 7 +--- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 17 ++------ .../PowerPC/JitCommon/ConstantPropagation.cpp | 41 ++++++++++++++++--- .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 1b5674f65b93..91e879fd69db 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1026,13 +1026,8 @@ void Jit64::extsXx(UGeckoInstruction inst) int a = inst.RA, s = inst.RS; int size = inst.SUBOP10 == 922 ? 16 : 8; - if (gpr.IsImm(s)) - { - gpr.SetImmediate32(a, (u32)(s32)(size == 16 ? (s16)gpr.Imm32(s) : (s8)gpr.Imm32(s))); - } - else { - RCOpArg Rs = gpr.Use(s, RCMode::Read); + RCOpArg Rs = gpr.UseNoImm(s, RCMode::Read); RCX64Reg Ra = gpr.Bind(a, RCMode::Write); RegCache::Realize(Rs, Ra); MOVSX(32, size, Ra, Rs); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 6739de443992..bfab22171218 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -506,19 +506,10 @@ void JitArm64::extsXx(UGeckoInstruction inst) int a = inst.RA, s = inst.RS; int size = inst.SUBOP10 == 922 ? 16 : 8; - if (gpr.IsImm(s)) - { - gpr.SetImmediate(a, (u32)(s32)(size == 16 ? (s16)gpr.GetImm(s) : (s8)gpr.GetImm(s))); - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else - { - gpr.BindToRegister(a, a == s); - SBFM(gpr.R(a), gpr.R(s), 0, size - 1); - if (inst.Rc) - ComputeRC0(gpr.R(a)); - } + gpr.BindToRegister(a, a == s); + SBFM(gpr.R(a), gpr.R(s), 0, size - 1); + if (inst.Rc) + ComputeRC0(gpr.R(a)); } void JitArm64::cntlzwx(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 84a37efa16da..3aecc0f5316c 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -77,18 +77,49 @@ ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruct ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction inst, u64 flags) const { - if (flags & FL_OUT_D) + if (flags & FL_IN_B) { - // input a, b -> output d - return EvaluateTable31AB(inst, flags); + if (flags & FL_OUT_D) + { + // input a, b -> output d + return EvaluateTable31AB(inst, flags); + } + else + { + // input s, b -> output a + return EvaluateTable31SB(inst); + } } else { - // input s, b -> output a - return EvaluateTable31SB(inst); + // input s -> output a + return EvaluateTable31S(inst); } } +ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstruction inst) const +{ + if (!HasGPR(inst.RS)) + return {}; + + u32 a; + const u32 s = GetGPR(inst.RS); + + switch (inst.SUBOP10) + { + case 922: // extshx + a = s32(s16(s)); + break; + case 954: // extsbx + a = s32(s8(s)); + break; + default: + return {}; + } + + return ConstantPropagationResult(inst.RA, a, inst.Rc); +} + ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index a7f5b27fd306..9dfc6e602e8c 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -81,6 +81,7 @@ class ConstantPropagation final ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst, u64 flags) const; + ConstantPropagationResult EvaluateTable31S(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u32 value, From 92a5a46b2c6ac8849b22cb931d5e37ecce036503 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 24 Aug 2023 14:31:42 +0200 Subject: [PATCH 034/123] Jit: Move cntlzwx to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 7 +------ .../Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 17 ++++------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 5 +++++ 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 91e879fd69db..040444c72113 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2649,14 +2649,9 @@ void Jit64::cntlzwx(UGeckoInstruction inst) int s = inst.RS; bool needs_test = false; - if (gpr.IsImm(s)) - { - gpr.SetImmediate32(a, static_cast(std::countl_zero(gpr.Imm32(s)))); - } - else { RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RCOpArg Rs = gpr.Use(s, RCMode::Read); + RCOpArg Rs = gpr.UseNoImm(s, RCMode::Read); RegCache::Realize(Ra, Rs); if (cpu_info.bLZCNT) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index bfab22171218..1e080817a34a 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -519,19 +519,10 @@ void JitArm64::cntlzwx(UGeckoInstruction inst) int a = inst.RA; int s = inst.RS; - if (gpr.IsImm(s)) - { - gpr.SetImmediate(a, static_cast(std::countl_zero(gpr.GetImm(s)))); - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else - { - gpr.BindToRegister(a, a == s); - CLZ(gpr.R(a), gpr.R(s)); - if (inst.Rc) - ComputeRC0(gpr.R(a)); - } + gpr.BindToRegister(a, a == s); + CLZ(gpr.R(a), gpr.R(s)); + if (inst.Rc) + ComputeRC0(gpr.R(a)); } void JitArm64::negx(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 3aecc0f5316c..630929efbcf1 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -3,6 +3,8 @@ #include "Core/PowerPC/JitCommon/ConstantPropagation.h" +#include + #include "Core/PowerPC/PPCTables.h" namespace JitCommon @@ -107,6 +109,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstructio switch (inst.SUBOP10) { + case 26: // cntlzwx + a = std::countl_zero(s); + break; case 922: // extshx a = s32(s16(s)); break; From 4c8995fae565357979e40576d7ece73b264afecd Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 24 Aug 2023 14:45:39 +0200 Subject: [PATCH 035/123] Jit: Move negx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 9 +------ .../PowerPC/JitArm64/JitArm64_Integer.cpp | 17 +++--------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 26 +++++++++++++++++-- .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 040444c72113..5f3b5e179920 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2283,15 +2283,8 @@ void Jit64::negx(UGeckoInstruction inst) int a = inst.RA; int d = inst.RD; - if (gpr.IsImm(a)) { - gpr.SetImmediate32(d, ~(gpr.Imm32(a)) + 1); - if (inst.OE) - GenerateConstantOverflow(gpr.Imm32(d) == 0x80000000); - } - else - { - RCOpArg Ra = gpr.Use(a, RCMode::Read); + RCOpArg Ra = gpr.UseNoImm(a, RCMode::Read); RCX64Reg Rd = gpr.Bind(d, RCMode::Write); RegCache::Realize(Ra, Rd); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 1e080817a34a..a0d13c6a2674 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -534,19 +534,10 @@ void JitArm64::negx(UGeckoInstruction inst) FALLBACK_IF(inst.OE); - if (gpr.IsImm(a)) - { - gpr.SetImmediate(d, ~((u32)gpr.GetImm(a)) + 1); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a); - SUB(gpr.R(d), ARM64Reg::WSP, gpr.R(a)); - if (inst.Rc) - ComputeRC0(gpr.R(d)); - } + gpr.BindToRegister(d, d == a); + SUB(gpr.R(d), ARM64Reg::WSP, gpr.R(a)); + if (inst.Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::cmp(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 630929efbcf1..a79d8f00fa14 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -94,11 +94,33 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31(UGeckoInstruction } else { - // input s -> output a - return EvaluateTable31S(inst); + switch (inst.SUBOP10) + { + case 104: // negx + case 616: // negox + // input a -> output d + return EvaluateTable31Negx(inst, flags); + default: + // input s -> output a + return EvaluateTable31S(inst); + } } } +ConstantPropagationResult ConstantPropagation::EvaluateTable31Negx(UGeckoInstruction inst, + u64 flags) const +{ + if (!HasGPR(inst.RA)) + return {}; + + const s64 out = -s64(s32(GetGPR(inst.RA))); + + ConstantPropagationResult result(inst.RD, u32(out), inst.Rc); + if (flags & FL_SET_OE) + result.overflow = (out != s64(s32(out))); + return result; +} + ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstruction inst) const { if (!HasGPR(inst.RS)) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 9dfc6e602e8c..097932e1bbcf 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -81,6 +81,7 @@ class ConstantPropagation final ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst, u64 flags) const; + ConstantPropagationResult EvaluateTable31Negx(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31S(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; From 1a22bda0a7bb3a0cc5a7335ce3756d7eed346851 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 24 May 2024 19:49:53 +0200 Subject: [PATCH 036/123] Jit: Move rlwinmx and rlwnmx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 169 ++++++++---------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 8 - .../PowerPC/JitCommon/ConstantPropagation.cpp | 18 ++ .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 95 insertions(+), 101 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 5f3b5e179920..206e62cc4829 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1979,112 +1979,99 @@ void Jit64::rlwinmx(UGeckoInstruction inst) int a = inst.RA; int s = inst.RS; - if (gpr.IsImm(s)) + const bool left_shift = inst.SH && inst.MB == 0 && inst.ME == 31 - inst.SH; + const bool right_shift = inst.SH && inst.ME == 31 && inst.MB == 32 - inst.SH; + const bool field_extract = inst.SH && inst.ME == 31 && inst.MB > 32 - inst.SH; + const u32 mask = MakeRotationMask(inst.MB, inst.ME); + const u32 prerotate_mask = std::rotr(mask, inst.SH); + const bool simple_mask = mask == 0xff || mask == 0xffff; + const bool simple_prerotate_mask = prerotate_mask == 0xff || prerotate_mask == 0xffff; + // In case of a merged branch, track whether or not we've set flags. + // If not, we need to do a test later to get them. + bool needs_test = true; + // If we know the high bit can't be set, we can avoid doing a sign extend for flag storage. + bool needs_sext = true; + int mask_size = inst.ME - inst.MB + 1; + + if (simple_mask && !(inst.SH & (mask_size - 1)) && !gpr.IsBound(s) && !gpr.IsImm(s)) { - u32 result = gpr.Imm32(s); - if (inst.SH != 0) - result = std::rotl(result, inst.SH); - result &= MakeRotationMask(inst.MB, inst.ME); - gpr.SetImmediate32(a, result); - if (inst.Rc) - ComputeRC(a); + // optimized case: byte/word extract from m_ppc_state + + // Note: If a == s, calling Realize(Ra) will allocate a host register for Rs, + // so we have to get mem_source from Rs before calling Realize(Ra) + + RCOpArg Rs = gpr.Use(s, RCMode::Read); + RegCache::Realize(Rs); + OpArg mem_source = Rs.Location(); + if (inst.SH) + mem_source.AddMemOffset((32 - inst.SH) >> 3); + Rs.Unlock(); + + RCX64Reg Ra = gpr.Bind(a, RCMode::Write); + RegCache::Realize(Ra); + MOVZX(32, mask_size, Ra, mem_source); + + needs_sext = false; } else { - const bool left_shift = inst.SH && inst.MB == 0 && inst.ME == 31 - inst.SH; - const bool right_shift = inst.SH && inst.ME == 31 && inst.MB == 32 - inst.SH; - const bool field_extract = inst.SH && inst.ME == 31 && inst.MB > 32 - inst.SH; - const u32 mask = MakeRotationMask(inst.MB, inst.ME); - const u32 prerotate_mask = std::rotr(mask, inst.SH); - const bool simple_mask = mask == 0xff || mask == 0xffff; - const bool simple_prerotate_mask = prerotate_mask == 0xff || prerotate_mask == 0xffff; - // In case of a merged branch, track whether or not we've set flags. - // If not, we need to do a test later to get them. - bool needs_test = true; - // If we know the high bit can't be set, we can avoid doing a sign extend for flag storage. - bool needs_sext = true; - int mask_size = inst.ME - inst.MB + 1; + RCOpArg Rs = gpr.UseNoImm(s, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::Write); + RegCache::Realize(Rs, Ra); - if (simple_mask && !(inst.SH & (mask_size - 1)) && !gpr.IsBound(s)) + if (a != s && left_shift && Rs.IsSimpleReg() && inst.SH <= 3) { - // optimized case: byte/word extract from m_ppc_state - - // Note: If a == s, calling Realize(Ra) will allocate a host register for Rs, - // so we have to get mem_source from Rs before calling Realize(Ra) - - RCOpArg Rs = gpr.Use(s, RCMode::Read); - RegCache::Realize(Rs); - OpArg mem_source = Rs.Location(); + LEA(32, Ra, MScaled(Rs.GetSimpleReg(), SCALE_1 << inst.SH, 0)); + } + // optimized case: byte/word extract plus rotate + else if (simple_prerotate_mask && !left_shift) + { + MOVZX(32, prerotate_mask == 0xff ? 8 : 16, Ra, Rs); if (inst.SH) - mem_source.AddMemOffset((32 - inst.SH) >> 3); - Rs.Unlock(); + ROL(32, Ra, Imm8(inst.SH)); + needs_sext = (mask & 0x80000000) != 0; + } + // Use BEXTR where possible: Only AMD implements this in one uop + else if (field_extract && cpu_info.bBMI1 && cpu_info.vendor == CPUVendor::AMD) + { + MOV(32, R(RSCRATCH), Imm32((mask_size << 8) | (32 - inst.SH))); + BEXTR(32, Ra, Rs, RSCRATCH); + needs_sext = false; + } + else if (left_shift) + { + if (a != s) + MOV(32, Ra, Rs); - RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RegCache::Realize(Ra); - MOVZX(32, mask_size, Ra, mem_source); + SHL(32, Ra, Imm8(inst.SH)); + } + else if (right_shift) + { + if (a != s) + MOV(32, Ra, Rs); + SHR(32, Ra, Imm8(inst.MB)); needs_sext = false; } else { - RCOpArg Rs = gpr.Use(s, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RegCache::Realize(Rs, Ra); - - if (a != s && left_shift && Rs.IsSimpleReg() && inst.SH <= 3) - { - LEA(32, Ra, MScaled(Rs.GetSimpleReg(), SCALE_1 << inst.SH, 0)); - } - // optimized case: byte/word extract plus rotate - else if (simple_prerotate_mask && !left_shift) - { - MOVZX(32, prerotate_mask == 0xff ? 8 : 16, Ra, Rs); - if (inst.SH) - ROL(32, Ra, Imm8(inst.SH)); - needs_sext = (mask & 0x80000000) != 0; - } - // Use BEXTR where possible: Only AMD implements this in one uop - else if (field_extract && cpu_info.bBMI1 && cpu_info.vendor == CPUVendor::AMD) - { - MOV(32, R(RSCRATCH), Imm32((mask_size << 8) | (32 - inst.SH))); - BEXTR(32, Ra, Rs, RSCRATCH); - needs_sext = false; - } - else if (left_shift) - { - if (a != s) - MOV(32, Ra, Rs); + RotateLeft(32, Ra, Rs, inst.SH); - SHL(32, Ra, Imm8(inst.SH)); - } - else if (right_shift) + if (!(inst.MB == 0 && inst.ME == 31)) { - if (a != s) - MOV(32, Ra, Rs); - - SHR(32, Ra, Imm8(inst.MB)); - needs_sext = false; - } - else - { - RotateLeft(32, Ra, Rs, inst.SH); - - if (!(inst.MB == 0 && inst.ME == 31)) - { - // we need flags if we're merging the branch - if (inst.Rc && CheckMergedBranch(0)) - AND(32, Ra, Imm32(mask)); - else - AndWithMask(Ra, mask); - needs_sext = inst.MB == 0; - needs_test = false; - } + // we need flags if we're merging the branch + if (inst.Rc && CheckMergedBranch(0)) + AND(32, Ra, Imm32(mask)); + else + AndWithMask(Ra, mask); + needs_sext = inst.MB == 0; + needs_test = false; } } - - if (inst.Rc) - ComputeRC(a, needs_test, needs_sext); } + + if (inst.Rc) + ComputeRC(a, needs_test, needs_sext); } void Jit64::rlwimix(UGeckoInstruction inst) @@ -2233,11 +2220,7 @@ void Jit64::rlwnmx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, s = inst.RS; const u32 mask = MakeRotationMask(inst.MB, inst.ME); - if (gpr.IsImm(b, s)) - { - gpr.SetImmediate32(a, std::rotl(gpr.Imm32(s), gpr.Imm32(b) & 0x1F) & mask); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 amount = gpr.Imm32(b) & 0x1f; RCX64Reg Ra = gpr.Bind(a, RCMode::Write); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index a0d13c6a2674..b1d1994ac4e0 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -719,15 +719,7 @@ void JitArm64::cmpli(UGeckoInstruction inst) void JitArm64::rlwinmx_internal(UGeckoInstruction inst, u32 sh) { u32 a = inst.RA, s = inst.RS; - const u32 mask = MakeRotationMask(inst.MB, inst.ME); - if (gpr.IsImm(inst.RS)) - { - gpr.SetImmediate(a, std::rotl(gpr.GetImm(s), sh) & mask); - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - return; - } if (mask == 0) { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index a79d8f00fa14..833ff1b04023 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -5,6 +5,7 @@ #include +#include "Core/PowerPC/Gekko.h" #include "Core/PowerPC/PPCTables.h" namespace JitCommon @@ -32,6 +33,13 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc case 14: // addi case 15: // addis return EvaluateAddImm(inst); + case 21: // rlwinmx + return EvaluateRlwinmxRlwnmx(inst, inst.SH); + case 23: // rlwnmx + if (HasGPR(inst.RB)) + return EvaluateRlwinmxRlwnmx(inst, GetGPR(inst.RB) & 0x1F); + else + return {}; case 24: // ori case 25: // oris return EvaluateBitwiseImm(inst, BitOR); @@ -61,6 +69,16 @@ ConstantPropagationResult ConstantPropagation::EvaluateAddImm(UGeckoInstruction return ConstantPropagationResult(inst.RD, m_gpr_values[inst.RA] + immediate); } +ConstantPropagationResult ConstantPropagation::EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, + u32 shift) const +{ + if (!HasGPR(inst.RS)) + return {}; + + const u32 mask = MakeRotationMask(inst.MB, inst.ME); + return ConstantPropagationResult(inst.RA, std::rotl(GetGPR(inst.RS), shift) & mask, inst.Rc); +} + ConstantPropagationResult ConstantPropagation::EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 097932e1bbcf..e003d2e41951 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -78,6 +78,7 @@ class ConstantPropagation final private: ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; ConstantPropagationResult EvaluateTable31(UGeckoInstruction inst, u64 flags) const; From a3797778ffac9c6be5780e0cebc976239fbca74f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 09:55:26 +0200 Subject: [PATCH 037/123] Jit: Move srawix to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 8 +------- .../Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 12 +----------- .../Core/PowerPC/JitCommon/ConstantPropagation.cpp | 9 ++++++++- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 206e62cc4829..935c212bbc3d 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2561,13 +2561,7 @@ void Jit64::srawix(UGeckoInstruction inst) int s = inst.RS; int amount = inst.SH; - if (gpr.IsImm(s)) - { - s32 imm = gpr.SImm32(s); - gpr.SetImmediate32(a, imm >> amount); - FinalizeCarry(amount != 0 && imm < 0 && (u32(imm) << (32 - amount))); - } - else if (amount != 0) + if (amount != 0) { RCX64Reg Ra = gpr.Bind(a, RCMode::Write); RCOpArg Rs = gpr.Use(s, RCMode::Read); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index b1d1994ac4e0..229bb9c4713f 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -810,17 +810,7 @@ void JitArm64::srawix(UGeckoInstruction inst) int amount = inst.SH; bool inplace_carry = CanMergeNextInstructions(1) && js.op[1].wantsCAInFlags; - if (gpr.IsImm(s)) - { - s32 imm = (s32)gpr.GetImm(s); - gpr.SetImmediate(a, imm >> amount); - - ComputeCarry(amount != 0 && (imm < 0) && (u32(imm) << (32 - amount))); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else if (amount == 0) + if (amount == 0) { gpr.BindToRegister(a, a == s); ARM64Reg RA = gpr.R(a); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 833ff1b04023..cffa4f77901f 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -144,6 +144,7 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstructio if (!HasGPR(inst.RS)) return {}; + std::optional carry; u32 a; const u32 s = GetGPR(inst.RS); @@ -152,6 +153,10 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstructio case 26: // cntlzwx a = std::countl_zero(s); break; + case 824: // srawix + a = s32(s) >> inst.SH; + carry = inst.SH != 0 && s32(s) < 0 && (s << (32 - inst.SH)); + break; case 922: // extshx a = s32(s16(s)); break; @@ -162,7 +167,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstructio return {}; } - return ConstantPropagationResult(inst.RA, a, inst.Rc); + ConstantPropagationResult result(ConstantPropagationResult(inst.RA, a, inst.Rc)); + result.carry = carry; + return result; } ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstruction inst, From 1eea6103755c98a5c398e210067a9e053d66823e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 10:23:14 +0200 Subject: [PATCH 038/123] Jit: Move addicx to ConstantPropagation Note: Jit64 didn't support immediate handling for addic before. --- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 26 +++++-------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 17 ++++++++++++ .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 229bb9c4713f..b78f1a838b52 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -871,30 +871,16 @@ void JitArm64::addic(UGeckoInstruction inst) int a = inst.RA, d = inst.RD; bool rc = inst.OPCD == 13; s32 simm = inst.SIMM_16; - u32 imm = (u32)simm; - if (gpr.IsImm(a)) + gpr.BindToRegister(d, d == a); { - u32 i = gpr.GetImm(a); - gpr.SetImmediate(d, i + imm); - - bool has_carry = Interpreter::Helper_Carry(i, imm); - ComputeCarry(has_carry); - if (rc) - ComputeRC0(gpr.GetImm(d)); + auto WA = gpr.GetScopedReg(); + CARRY_IF_NEEDED(ADDI2R, ADDSI2R, gpr.R(d), gpr.R(a), simm, WA); } - else - { - gpr.BindToRegister(d, d == a); - { - auto WA = gpr.GetScopedReg(); - CARRY_IF_NEEDED(ADDI2R, ADDSI2R, gpr.R(d), gpr.R(a), simm, WA); - } - ComputeCarry(); - if (rc) - ComputeRC0(gpr.R(d)); - } + ComputeCarry(); + if (rc) + ComputeRC0(gpr.R(d)); } bool JitArm64::MultiplyImmediate(u32 imm, int a, int d, bool rc) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index cffa4f77901f..d5f05b6f2b05 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -6,6 +6,7 @@ #include #include "Core/PowerPC/Gekko.h" +#include "Core/PowerPC/Interpreter/Interpreter.h" #include "Core/PowerPC/PPCTables.h" namespace JitCommon @@ -30,6 +31,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc { switch (inst.OPCD) { + case 12: // addic + case 13: // addic. + return EvaluateAddImmCarry(inst); case 14: // addi case 15: // addis return EvaluateAddImm(inst); @@ -69,6 +73,19 @@ ConstantPropagationResult ConstantPropagation::EvaluateAddImm(UGeckoInstruction return ConstantPropagationResult(inst.RD, m_gpr_values[inst.RA] + immediate); } +ConstantPropagationResult ConstantPropagation::EvaluateAddImmCarry(UGeckoInstruction inst) const +{ + if (!HasGPR(inst.RA)) + return {}; + + const u32 a = m_gpr_values[inst.RA]; + const bool rc = inst.OPCD & 1; + + ConstantPropagationResult result(inst.RD, a + inst.SIMM_16, rc); + result.carry = Interpreter::Helper_Carry(a, inst.SIMM_16); + return result; +} + ConstantPropagationResult ConstantPropagation::EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index e003d2e41951..b0718688ce55 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -78,6 +78,7 @@ class ConstantPropagation final private: ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateAddImmCarry(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; From b469981c7259a84608b951ca36d064eca6392667 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 12:04:01 +0200 Subject: [PATCH 039/123] Jit: Move mulli to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 9 +-------- Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 7 +------ .../Core/PowerPC/JitCommon/ConstantPropagation.cpp | 10 ++++++++++ .../Core/Core/PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 935c212bbc3d..b4ac8df181a9 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1262,14 +1262,7 @@ void Jit64::mulli(UGeckoInstruction inst) int a = inst.RA, d = inst.RD; u32 imm = inst.SIMM_16; - if (gpr.IsImm(a)) - { - gpr.SetImmediate32(d, gpr.Imm32(a) * imm); - } - else - { - MultiplyImmediate(imm, a, d, false); - } + MultiplyImmediate(imm, a, d, false); } void Jit64::mullwx(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index b78f1a838b52..96dc459057dc 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -959,12 +959,7 @@ void JitArm64::mulli(UGeckoInstruction inst) int a = inst.RA, d = inst.RD; - if (gpr.IsImm(a)) - { - s32 i = (s32)gpr.GetImm(a); - gpr.SetImmediate(d, i * inst.SIMM_16); - } - else if (MultiplyImmediate((u32)(s32)inst.SIMM_16, a, d, false)) + if (MultiplyImmediate((u32)(s32)inst.SIMM_16, a, d, false)) { // Code is generated inside MultiplyImmediate, nothing to be done here. } diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index d5f05b6f2b05..f3de311dee60 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -31,6 +31,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc { switch (inst.OPCD) { + case 7: // mulli + return EvaluateMulImm(inst); case 12: // addic case 13: // addic. return EvaluateAddImmCarry(inst); @@ -60,6 +62,14 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc } } +ConstantPropagationResult ConstantPropagation::EvaluateMulImm(UGeckoInstruction inst) const +{ + if (!HasGPR(inst.RA)) + return {}; + + return ConstantPropagationResult(inst.RD, m_gpr_values[inst.RA] * inst.SIMM_16); +} + ConstantPropagationResult ConstantPropagation::EvaluateAddImm(UGeckoInstruction inst) const { const s32 immediate = inst.OPCD & 1 ? inst.SIMM_16 << 16 : inst.SIMM_16; diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index b0718688ce55..9b8070caf1b2 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -77,6 +77,7 @@ class ConstantPropagation final void Clear() { m_gpr_values_known = BitSet32{}; } private: + ConstantPropagationResult EvaluateMulImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImmCarry(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const; From 7456ba3d3dacedf4e1f14775d08e3f09488d4731 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 11:33:23 +0200 Subject: [PATCH 040/123] Jit: Move mullwx, mulhwx, mulhwux to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 18 +------ .../PowerPC/JitArm64/JitArm64_Integer.cpp | 51 +++++-------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 10 ++++ 3 files changed, 24 insertions(+), 55 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index b4ac8df181a9..af6723c8214c 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1271,14 +1271,7 @@ void Jit64::mullwx(UGeckoInstruction inst) JITDISABLE(bJITIntegerOff); int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a, b)) - { - s32 i = gpr.SImm32(a), j = gpr.SImm32(b); - gpr.SetImmediate32(d, i * j); - if (inst.OE) - GenerateConstantOverflow((s64)i * (s64)j); - } - else if (gpr.IsImm(a) || gpr.IsImm(b)) + if (gpr.IsImm(a) || gpr.IsImm(b)) { u32 imm = gpr.IsImm(a) ? gpr.Imm32(a) : gpr.Imm32(b); int src = gpr.IsImm(a) ? b : a; @@ -1320,14 +1313,7 @@ void Jit64::mulhwXx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; bool sign = inst.SUBOP10 == 75; - if (gpr.IsImm(a, b)) - { - if (sign) - gpr.SetImmediate32(d, (u32)((u64)(((s64)gpr.SImm32(a) * (s64)gpr.SImm32(b))) >> 32)); - else - gpr.SetImmediate32(d, (u32)(((u64)gpr.Imm32(a) * (u64)gpr.Imm32(b)) >> 32)); - } - else if (sign) + if (sign) { RCOpArg Ra = gpr.Use(a, RCMode::Read); RCOpArg Rb = gpr.UseNoImm(b, RCMode::Read); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 96dc459057dc..ebd3dd273aa4 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -984,15 +984,8 @@ void JitArm64::mullwx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - s32 i = (s32)gpr.GetImm(a), j = (s32)gpr.GetImm(b); - gpr.SetImmediate(d, i * j); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else if ((gpr.IsImm(a) && MultiplyImmediate(gpr.GetImm(a), b, d, inst.Rc)) || - (gpr.IsImm(b) && MultiplyImmediate(gpr.GetImm(b), a, d, inst.Rc))) + if ((gpr.IsImm(a) && MultiplyImmediate(gpr.GetImm(a), b, d, inst.Rc)) || + (gpr.IsImm(b) && MultiplyImmediate(gpr.GetImm(b), a, d, inst.Rc))) { // Code is generated inside MultiplyImmediate, nothing to be done here. } @@ -1012,22 +1005,12 @@ void JitArm64::mulhwx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - s32 i = (s32)gpr.GetImm(a), j = (s32)gpr.GetImm(b); - gpr.SetImmediate(d, (u32)((u64)(((s64)i * (s64)j)) >> 32)); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a || d == b); - SMULL(EncodeRegTo64(gpr.R(d)), gpr.R(a), gpr.R(b)); - LSR(EncodeRegTo64(gpr.R(d)), EncodeRegTo64(gpr.R(d)), 32); + gpr.BindToRegister(d, d == a || d == b); + SMULL(EncodeRegTo64(gpr.R(d)), gpr.R(a), gpr.R(b)); + LSR(EncodeRegTo64(gpr.R(d)), EncodeRegTo64(gpr.R(d)), 32); - if (inst.Rc) - ComputeRC0(gpr.R(d)); - } + if (inst.Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::mulhwux(UGeckoInstruction inst) @@ -1037,22 +1020,12 @@ void JitArm64::mulhwux(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - u32 i = gpr.GetImm(a), j = gpr.GetImm(b); - gpr.SetImmediate(d, (u32)(((u64)i * (u64)j) >> 32)); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a || d == b); - UMULL(EncodeRegTo64(gpr.R(d)), gpr.R(a), gpr.R(b)); - LSR(EncodeRegTo64(gpr.R(d)), EncodeRegTo64(gpr.R(d)), 32); + gpr.BindToRegister(d, d == a || d == b); + UMULL(EncodeRegTo64(gpr.R(d)), gpr.R(a), gpr.R(b)); + LSR(EncodeRegTo64(gpr.R(d)), EncodeRegTo64(gpr.R(d)), 32); - if (inst.Rc) - ComputeRC0(gpr.R(d)); - } + if (inst.Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::addzex(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index f3de311dee60..78facf40bd24 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -219,6 +219,16 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi d = u64(a) + u64(b); d_overflow = s64(s32(a)) + s64(s32(b)); break; + case 11: // mulhwux + d = d_overflow = (u64(a) * u64(b)) >> 32; + break; + case 75: // mulhwx + d = d_overflow = u64(s64(s32(a)) * s64(s32(b))) >> 32; + break; + case 235: // mullwx + case 747: // mullwox + d = d_overflow = s64(s32(a)) * s64(s32(b)); + break; default: return {}; } From 2134991be8300eb454ab2e779973017ef29f6c46 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 12:20:40 +0200 Subject: [PATCH 041/123] Jit: Move multiplication by 0 optimization to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 7 ---- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 9 +---- .../PowerPC/JitCommon/ConstantPropagation.cpp | 39 ++++++++++++++++++- .../PowerPC/JitCommon/ConstantPropagation.h | 2 + 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index af6723c8214c..cdca3274af08 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1201,13 +1201,6 @@ void Jit64::MultiplyImmediate(u32 imm, int a, int d, bool overflow) RCX64Reg Rd = gpr.Bind(d, RCMode::Write); RegCache::Realize(Ra, Rd); - // simplest cases first - if (imm == 0) - { - XOR(32, Rd, Rd); - return; - } - if (imm == (u32)-1) { if (d != a) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index ebd3dd273aa4..1a4027059cd8 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -885,14 +885,7 @@ void JitArm64::addic(UGeckoInstruction inst) bool JitArm64::MultiplyImmediate(u32 imm, int a, int d, bool rc) { - if (imm == 0) - { - // Multiplication by zero (0). - gpr.SetImmediate(d, 0); - if (rc) - ComputeRC0(gpr.GetImm(d)); - } - else if (imm == 1) + if (imm == 1) { // Multiplication by one (1). if (d != a) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 78facf40bd24..980347062b42 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -64,6 +64,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc ConstantPropagationResult ConstantPropagation::EvaluateMulImm(UGeckoInstruction inst) const { + if (inst.SIMM_16 == 0) + return ConstantPropagationResult(inst.RD, 0); + if (!HasGPR(inst.RA)) return {}; @@ -202,8 +205,17 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31S(UGeckoInstructio ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const { - if (!HasGPR(inst.RA, inst.RB)) - return {}; + const bool has_a = HasGPR(inst.RA); + const bool has_b = HasGPR(inst.RB); + if (!has_a || !has_b) + { + if (has_a) + return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RA)); + else if (has_b) + return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RB)); + else + return {}; + } u64 d; s64 d_overflow; @@ -241,6 +253,29 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi return result; } +ConstantPropagationResult +ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, + u32 value) const +{ + switch (inst.SUBOP10) + { + case 11: // mulhwux + case 75: // mulhwx + case 235: // mullwx + case 747: // mullwox + if (value == 0) + { + ConstantPropagationResult result(inst.RD, 0, inst.Rc); + if (flags & FL_SET_OE) + result.overflow = false; + return result; + } + break; + } + + return {}; +} + ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstruction inst) const { const bool has_s = HasGPR(inst.RS); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 9b8070caf1b2..4ff7823aa44f 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -87,6 +87,8 @@ class ConstantPropagation final ConstantPropagationResult EvaluateTable31Negx(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31S(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; + ConstantPropagationResult EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, + u32 value) const; ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u32 value, bool known_reg_is_b) const; From 204a8fbd5348ae2b072bca63697a72adb4c357ae Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 16:03:02 +0200 Subject: [PATCH 042/123] Jit: Move subfx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 19 +--------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 36 +++---------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 31 ++++++++++++++++ .../PowerPC/JitCommon/ConstantPropagation.h | 2 ++ 4 files changed, 39 insertions(+), 49 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index cdca3274af08..f10bca3cf3de 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1097,24 +1097,7 @@ void Jit64::subfx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; const bool carry = !(inst.SUBOP10 & (1 << 5)); - if (a == b) - { - gpr.SetImmediate32(d, 0); - if (carry) - FinalizeCarry(true); - if (inst.OE) - GenerateConstantOverflow(false); - } - else if (gpr.IsImm(a, b)) - { - s32 i = gpr.SImm32(b), j = gpr.SImm32(a); - gpr.SetImmediate32(d, i - j); - if (carry) - FinalizeCarry(j == 0 || Interpreter::Helper_Carry((u32)i, 0u - (u32)j)); - if (inst.OE) - GenerateConstantOverflow((s64)i - (s64)j); - } - else if (gpr.IsImm(a)) + if (gpr.IsImm(a)) { s32 j = gpr.SImm32(a); RCOpArg Rb = gpr.Use(b, RCMode::Read); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 1a4027059cd8..fe5a401b6a9d 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1122,26 +1122,10 @@ void JitArm64::subfx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (a == b) - { - gpr.SetImmediate(d, 0); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else if (gpr.IsImm(a) && gpr.IsImm(b)) - { - u32 i = gpr.GetImm(a), j = gpr.GetImm(b); - gpr.SetImmediate(d, j - i); - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else - { - gpr.BindToRegister(d, d == a || d == b); - SUB(gpr.R(d), gpr.R(b), gpr.R(a)); - if (inst.Rc) - ComputeRC0(gpr.R(d)); - } + gpr.BindToRegister(d, d == a || d == b); + SUB(gpr.R(d), gpr.R(b), gpr.R(a)); + if (inst.Rc) + ComputeRC0(gpr.R(d)); } void JitArm64::subfex(UGeckoInstruction inst) @@ -1283,17 +1267,7 @@ void JitArm64::subfcx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - u32 a_imm = gpr.GetImm(a), b_imm = gpr.GetImm(b); - - gpr.SetImmediate(d, b_imm - a_imm); - ComputeCarry(a_imm == 0 || Interpreter::Helper_Carry(b_imm, 0u - a_imm)); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else if (gpr.IsImm(a, 0)) + if (gpr.IsImm(a, 0)) { if (d != b) { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 980347062b42..f794af1653c4 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -213,6 +213,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RA)); else if (has_b) return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RB)); + else if (inst.RA == inst.RB) + return EvaluateTable31ABIdenticalRegisters(inst, flags); else return {}; } @@ -224,6 +226,13 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi switch (inst.SUBOP10) { + case 8: // subfcx + case 40: // subfx + case 520: // subfcox + case 552: // subfox + d = u64(u32(~a)) + u64(b) + 1; + d_overflow = s64(s32(b)) - s64(s32(a)); + break; case 10: // addcx case 522: // addcox case 266: // addx @@ -276,6 +285,28 @@ ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u return {}; } +ConstantPropagationResult +ConstantPropagation::EvaluateTable31ABIdenticalRegisters(UGeckoInstruction inst, u64 flags) const +{ + switch (inst.SUBOP10) + { + case 8: // subfcx + case 40: // subfx + case 520: // subfcox + case 552: // subfox + { + ConstantPropagationResult result(inst.RD, 0, inst.Rc); + if (flags & FL_SET_CA) + result.carry = true; + if (flags & FL_SET_OE) + result.overflow = false; + return result; + } + default: + return {}; + } +} + ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstruction inst) const { const bool has_s = HasGPR(inst.RS); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 4ff7823aa44f..7b4759c0aeb3 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -89,6 +89,8 @@ class ConstantPropagation final ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, u32 value) const; + ConstantPropagationResult EvaluateTable31ABIdenticalRegisters(UGeckoInstruction inst, + u64 flags) const; ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u32 value, bool known_reg_is_b) const; From c7d8a0b2767849a7f18f78b22acfbb3e476c4dbc Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 31 Aug 2024 16:29:29 +0200 Subject: [PATCH 043/123] Jit: Move subfic to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 8 ---- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 46 ++++++++----------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 15 ++++++ .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index f10bca3cf3de..4187e9f34680 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1042,14 +1042,6 @@ void Jit64::subfic(UGeckoInstruction inst) JITDISABLE(bJITIntegerOff); int a = inst.RA, d = inst.RD, imm = inst.SIMM_16; - if (gpr.IsImm(a)) - { - u32 i = imm, j = gpr.Imm32(a); - gpr.SetImmediate32(d, i - j); - FinalizeCarry(j == 0 || (i > j - 1)); - return; - } - RCOpArg Ra = gpr.Use(a, RCMode::Read); RCX64Reg Rd = gpr.Bind(d, RCMode::Write); RegCache::Realize(Ra, Rd); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index fe5a401b6a9d..b36d35b2063a 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1381,44 +1381,34 @@ void JitArm64::subfic(UGeckoInstruction inst) int a = inst.RA, d = inst.RD; s32 imm = inst.SIMM_16; - if (gpr.IsImm(a)) - { - u32 a_imm = gpr.GetImm(a); + const bool will_read = d == a; + gpr.BindToRegister(d, will_read); + ARM64Reg RD = gpr.R(d); - gpr.SetImmediate(d, imm - a_imm); - ComputeCarry(a_imm == 0 || Interpreter::Helper_Carry(imm, 0u - a_imm)); + if (imm == -1) + { + // d = -1 - a = ~a + MVN(RD, gpr.R(a)); + // CA is always set in this case + ComputeCarry(true); } else { - const bool will_read = d == a; - gpr.BindToRegister(d, will_read); - ARM64Reg RD = gpr.R(d); + const bool is_zero = imm == 0; - if (imm == -1) + // d = imm - a { - // d = -1 - a = ~a - MVN(RD, gpr.R(a)); - // CA is always set in this case - ComputeCarry(true); - } - else - { - const bool is_zero = imm == 0; - - // d = imm - a + Arm64GPRCache::ScopedARM64Reg WA(ARM64Reg::WZR); + if (!is_zero) { - Arm64GPRCache::ScopedARM64Reg WA(ARM64Reg::WZR); - if (!is_zero) - { - WA = will_read ? gpr.GetScopedReg() : Arm64GPRCache::ScopedARM64Reg(RD); - MOVI2R(WA, imm); - } - - CARRY_IF_NEEDED(SUB, SUBS, RD, WA, gpr.R(a)); + WA = will_read ? gpr.GetScopedReg() : Arm64GPRCache::ScopedARM64Reg(RD); + MOVI2R(WA, imm); } - ComputeCarry(); + CARRY_IF_NEEDED(SUB, SUBS, RD, WA, gpr.R(a)); } + + ComputeCarry(); } } diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index f794af1653c4..1f6526679675 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -33,6 +33,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc { case 7: // mulli return EvaluateMulImm(inst); + case 8: // subfic + return EvaluateSubImmCarry(inst); case 12: // addic case 13: // addic. return EvaluateAddImmCarry(inst); @@ -73,6 +75,19 @@ ConstantPropagationResult ConstantPropagation::EvaluateMulImm(UGeckoInstruction return ConstantPropagationResult(inst.RD, m_gpr_values[inst.RA] * inst.SIMM_16); } +ConstantPropagationResult ConstantPropagation::EvaluateSubImmCarry(UGeckoInstruction inst) const +{ + if (!HasGPR(inst.RA)) + return {}; + + const u32 a = GetGPR(inst.RA); + const u32 imm = s32(inst.SIMM_16); + + ConstantPropagationResult result(inst.RD, imm - a); + result.carry = imm >= a; + return result; +} + ConstantPropagationResult ConstantPropagation::EvaluateAddImm(UGeckoInstruction inst) const { const s32 immediate = inst.OPCD & 1 ? inst.SIMM_16 << 16 : inst.SIMM_16; diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 7b4759c0aeb3..50b1de58ae57 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -78,6 +78,7 @@ class ConstantPropagation final private: ConstantPropagationResult EvaluateMulImm(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateSubImmCarry(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImmCarry(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const; From fc6c278007f4412cd14473dbea5e985e3f18d621 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 09:31:34 +0200 Subject: [PATCH 044/123] Jit: Move divwux to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 17 +----------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 10 +------ .../PowerPC/JitCommon/ConstantPropagation.cpp | 26 ++++++++++++++++--- .../PowerPC/JitCommon/ConstantPropagation.h | 2 +- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 4187e9f34680..d73b20bc9e89 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1321,22 +1321,7 @@ void Jit64::divwux(UGeckoInstruction inst) JITDISABLE(bJITIntegerOff); int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a, b)) - { - if (gpr.Imm32(b) == 0) - { - gpr.SetImmediate32(d, 0); - if (inst.OE) - GenerateConstantOverflow(true); - } - else - { - gpr.SetImmediate32(d, gpr.Imm32(a) / gpr.Imm32(b)); - if (inst.OE) - GenerateConstantOverflow(false); - } - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 divisor = gpr.Imm32(b); if (divisor == 0) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index b36d35b2063a..129445fff943 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1554,15 +1554,7 @@ void JitArm64::divwux(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - u32 i = gpr.GetImm(a), j = gpr.GetImm(b); - gpr.SetImmediate(d, j == 0 ? 0 : i / j); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(d)); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { const u32 divisor = gpr.GetImm(b); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 1f6526679675..e67e247b43c3 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -225,9 +225,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi if (!has_a || !has_b) { if (has_a) - return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RA)); + return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RA), false); else if (has_b) - return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RB)); + return EvaluateTable31ABOneRegisterKnown(inst, flags, GetGPR(inst.RB), true); else if (inst.RA == inst.RB) return EvaluateTable31ABIdenticalRegisters(inst, flags); else @@ -265,6 +265,10 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi case 747: // mullwox d = d_overflow = s64(s32(a)) * s64(s32(b)); break; + case 459: // divwux + case 971: // divwuox + d = d_overflow = b == 0 ? 0x1'0000'0000 : u64(a / b); + break; default: return {}; } @@ -278,8 +282,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi } ConstantPropagationResult -ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, - u32 value) const +ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, u32 value, + bool known_reg_is_b) const { switch (inst.SUBOP10) { @@ -295,6 +299,20 @@ ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u return result; } break; + case 459: // divwux + case 971: // divwuox + if (known_reg_is_b && value == 0) + { + ConstantPropagationResult result(inst.RD, 0, inst.Rc); + if (flags & FL_SET_OE) + result.overflow = true; + return result; + } + if (!known_reg_is_b && value == 0 && !(flags & FL_SET_OE)) + { + return ConstantPropagationResult(inst.RD, 0, inst.Rc); + } + break; } return {}; diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 50b1de58ae57..467944a658ca 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -89,7 +89,7 @@ class ConstantPropagation final ConstantPropagationResult EvaluateTable31S(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateTable31AB(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u64 flags, - u32 value) const; + u32 value, bool known_reg_is_b) const; ConstantPropagationResult EvaluateTable31ABIdenticalRegisters(UGeckoInstruction inst, u64 flags) const; ConstantPropagationResult EvaluateTable31SB(UGeckoInstruction inst) const; From 45760841b21f96fcedc325c6121b4446a55f5cb7 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 10:13:57 +0200 Subject: [PATCH 045/123] Jit: Move divwx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 19 +-------------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 23 +------------------ .../PowerPC/JitCommon/ConstantPropagation.cpp | 9 ++++++++ 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index d73b20bc9e89..458ecdc2e556 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1433,24 +1433,7 @@ void Jit64::divwx(UGeckoInstruction inst) JITDISABLE(bJITIntegerOff); int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a, b)) - { - s32 i = gpr.SImm32(a), j = gpr.SImm32(b); - if (j == 0 || (i == (s32)0x80000000 && j == -1)) - { - const u32 result = i < 0 ? 0xFFFFFFFF : 0x00000000; - gpr.SetImmediate32(d, result); - if (inst.OE) - GenerateConstantOverflow(true); - } - else - { - gpr.SetImmediate32(d, i / j); - if (inst.OE) - GenerateConstantOverflow(false); - } - } - else if (gpr.IsImm(a)) + if (gpr.IsImm(a)) { // Constant dividend const u32 dividend = gpr.Imm32(a); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 129445fff943..a2b4297738b1 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1625,28 +1625,7 @@ void JitArm64::divwx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, d = inst.RD; - if (gpr.IsImm(a) && gpr.IsImm(b)) - { - s32 imm_a = gpr.GetImm(a); - s32 imm_b = gpr.GetImm(b); - u32 imm_d; - if (imm_b == 0 || (static_cast(imm_a) == 0x80000000 && imm_b == -1)) - { - if (imm_a < 0) - imm_d = 0xFFFFFFFF; - else - imm_d = 0; - } - else - { - imm_d = static_cast(imm_a / imm_b); - } - gpr.SetImmediate(d, imm_d); - - if (inst.Rc) - ComputeRC0(imm_d); - } - else if (gpr.IsImm(a, 0)) + if (gpr.IsImm(a, 0)) { // Zero divided by anything is always zero gpr.SetImmediate(d, 0); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index e67e247b43c3..4484fd8c329a 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -269,6 +269,12 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31AB(UGeckoInstructi case 971: // divwuox d = d_overflow = b == 0 ? 0x1'0000'0000 : u64(a / b); break; + case 491: // divwx + case 1003: // divwox + d = d_overflow = b == 0 || (a == 0x80000000 && b == 0xFFFFFFFF) ? + (s32(a) < 0 ? 0xFFFFFFFF : 0x1'0000'0000) : + s32(a) / s32(b); + break; default: return {}; } @@ -308,6 +314,9 @@ ConstantPropagation::EvaluateTable31ABOneRegisterKnown(UGeckoInstruction inst, u result.overflow = true; return result; } + [[fallthrough]]; + case 491: // divwx + case 1003: // divwox if (!known_reg_is_b && value == 0 && !(flags & FL_SET_OE)) { return ConstantPropagationResult(inst.RD, 0, inst.Rc); From bb645e6cbbeece0669e23f82872b1f0f0f36ecfd Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 10:49:25 +0200 Subject: [PATCH 046/123] Jit: Move slwx to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 15 +-------------- .../Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 16 +--------------- .../PowerPC/JitCommon/ConstantPropagation.cpp | 11 +++++++++++ 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 458ecdc2e556..e38c9e63c31f 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2266,14 +2266,7 @@ void Jit64::slwx(UGeckoInstruction inst) int b = inst.RB; int s = inst.RS; - if (gpr.IsImm(b, s)) - { - u32 amount = gpr.Imm32(b); - gpr.SetImmediate32(a, (amount & 0x20) ? 0 : gpr.Imm32(s) << (amount & 0x1f)); - if (inst.Rc) - ComputeRC(a); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 amount = gpr.Imm32(b); if (amount & 0x20) @@ -2297,12 +2290,6 @@ void Jit64::slwx(UGeckoInstruction inst) if (inst.Rc) ComputeRC(a); } - else if (gpr.IsImm(s) && gpr.Imm32(s) == 0) - { - gpr.SetImmediate32(a, 0); - if (inst.Rc) - ComputeRC(a); - } else if (cpu_info.bBMI2) { RCX64Reg Ra = gpr.Bind(a, RCMode::Write); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index a2b4297738b1..6ee99a898cb1 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1799,21 +1799,7 @@ void JitArm64::slwx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, s = inst.RS; - if (gpr.IsImm(b) && gpr.IsImm(s)) - { - u32 i = gpr.GetImm(s), j = gpr.GetImm(b); - gpr.SetImmediate(a, (j & 0x20) ? 0 : i << (j & 0x1F)); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else if (gpr.IsImm(s, 0)) - { - gpr.SetImmediate(a, 0); - if (inst.Rc) - ComputeRC0(0); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 i = gpr.GetImm(b); if (i & 0x20) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 4484fd8c329a..615141273ed2 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -371,6 +371,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstructi switch (inst.SUBOP10) { + case 24: // slwx + a = u32(u64(s) << (b & 0x3f)); + break; case 28: // andx a = s & b; break; @@ -410,6 +413,14 @@ ConstantPropagation::EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u switch (inst.SUBOP10) { + case 24: // slwx + if (!known_reg_is_b && value == 0) + a = 0; + else if (known_reg_is_b && (value & 0x20)) + a = 0; + else + return {}; + break; case 60: // andcx if (known_reg_is_b) value = ~value; From c136fd9807bb68a008fc69261259efbf461729d6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 10:54:12 +0200 Subject: [PATCH 047/123] Jit: Move srwx to ConstantPropagation --- Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 7 +------ Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 10 +--------- .../Core/PowerPC/JitCommon/ConstantPropagation.cpp | 6 +++++- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index e38c9e63c31f..d0d29b5e8165 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2204,12 +2204,7 @@ void Jit64::srwx(UGeckoInstruction inst) int b = inst.RB; int s = inst.RS; - if (gpr.IsImm(b, s)) - { - u32 amount = gpr.Imm32(b); - gpr.SetImmediate32(a, (amount & 0x20) ? 0 : (gpr.Imm32(s) >> (amount & 0x1f))); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 amount = gpr.Imm32(b); if (amount & 0x20) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 6ee99a898cb1..8bed1f548bec 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1836,15 +1836,7 @@ void JitArm64::srwx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, s = inst.RS; - if (gpr.IsImm(b) && gpr.IsImm(s)) - { - u32 i = gpr.GetImm(s), amount = gpr.GetImm(b); - gpr.SetImmediate(a, (amount & 0x20) ? 0 : i >> (amount & 0x1F)); - - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 amount = gpr.GetImm(b); if (amount & 0x20) diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 615141273ed2..2aea6c887e18 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -398,6 +398,9 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstructi case 476: // nandx a = ~(s & b); break; + case 536: // srwx + a = u32(u64(s) >> (b & 0x3f)); + break; default: return {}; } @@ -413,7 +416,8 @@ ConstantPropagation::EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u switch (inst.SUBOP10) { - case 24: // slwx + case 24: // slwx + case 536: // srwx if (!known_reg_is_b && value == 0) a = 0; else if (known_reg_is_b && (value & 0x20)) From bac911aac472f5c1b8cd793769915754772447db Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 11:28:37 +0200 Subject: [PATCH 048/123] Jit: Move srawx to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 22 +------------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 29 +------------------ .../PowerPC/JitCommon/ConstantPropagation.cpp | 21 ++++++++++++++ 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index d0d29b5e8165..eb0a6086a01a 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -2338,22 +2338,7 @@ void Jit64::srawx(UGeckoInstruction inst) int b = inst.RB; int s = inst.RS; - if (gpr.IsImm(b, s)) - { - s32 i = gpr.SImm32(s), amount = gpr.SImm32(b); - if (amount & 0x20) - { - gpr.SetImmediate32(a, i & 0x80000000 ? 0xFFFFFFFF : 0); - FinalizeCarry(i & 0x80000000 ? true : false); - } - else - { - amount &= 0x1F; - gpr.SetImmediate32(a, i >> amount); - FinalizeCarry(amount != 0 && i < 0 && (u32(i) << (32 - amount))); - } - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { u32 amount = gpr.Imm32(b); RCX64Reg Ra = gpr.Bind(a, RCMode::Write); @@ -2389,11 +2374,6 @@ void Jit64::srawx(UGeckoInstruction inst) FinalizeCarry(CC_NZ); } } - else if (gpr.IsImm(s) && gpr.Imm32(s) == 0) - { - gpr.SetImmediate32(a, 0); - FinalizeCarry(false); - } else if (cpu_info.bBMI2) { RCX64Reg Ra = gpr.Bind(a, RCMode::Write); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 8bed1f548bec..55eddf11c03c 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1871,34 +1871,7 @@ void JitArm64::srawx(UGeckoInstruction inst) int a = inst.RA, b = inst.RB, s = inst.RS; - if (gpr.IsImm(b) && gpr.IsImm(s)) - { - s32 i = gpr.GetImm(s), amount = gpr.GetImm(b); - if (amount & 0x20) - { - gpr.SetImmediate(a, i & 0x80000000 ? 0xFFFFFFFF : 0); - ComputeCarry(i & 0x80000000 ? true : false); - } - else - { - amount &= 0x1F; - gpr.SetImmediate(a, i >> amount); - ComputeCarry(amount != 0 && i < 0 && (u32(i) << (32 - amount))); - } - - if (inst.Rc) - ComputeRC0(gpr.GetImm(a)); - return; - } - else if (gpr.IsImm(s, 0)) - { - gpr.SetImmediate(a, 0); - ComputeCarry(false); - if (inst.Rc) - ComputeRC0(0); - return; - } - else if (gpr.IsImm(b)) + if (gpr.IsImm(b)) { int amount = gpr.GetImm(b); diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 2aea6c887e18..5efd3b55e144 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -401,6 +401,15 @@ ConstantPropagationResult ConstantPropagation::EvaluateTable31SB(UGeckoInstructi case 536: // srwx a = u32(u64(s) >> (b & 0x3f)); break; + case 792: // srawx + { + const u64 temp = (s64(s32(s)) << 32) >> (b & 0x3f); + a = u32(temp >> 32); + + ConstantPropagationResult result(inst.RA, a, inst.Rc); + result.carry = (temp & a) != 0; + return result; + } default: return {}; } @@ -457,6 +466,18 @@ ConstantPropagation::EvaluateTable31SBOneRegisterKnown(UGeckoInstruction inst, u else return {}; break; + case 792: // srawx + if (!known_reg_is_b && value == 0) + { + ConstantPropagationResult result(inst.RA, 0, inst.Rc); + result.carry = false; + return result; + } + else + { + return {}; + } + break; default: return {}; } From 502317a4851141a53cc0d94cfecff4c578dafb79 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 11:44:34 +0200 Subject: [PATCH 049/123] Jit: Move rlwimix to ConstantPropagation --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 191 ++++++++---------- .../PowerPC/JitArm64/JitArm64_Integer.cpp | 106 +++++----- .../PowerPC/JitCommon/ConstantPropagation.cpp | 18 ++ .../PowerPC/JitCommon/ConstantPropagation.h | 1 + 4 files changed, 154 insertions(+), 162 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index eb0a6086a01a..03bf2fc7867f 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -1997,135 +1997,118 @@ void Jit64::rlwimix(UGeckoInstruction inst) int s = inst.RS; const u32 mask = MakeRotationMask(inst.MB, inst.ME); + const bool left_shift = mask == 0U - (1U << inst.SH); + const bool right_shift = mask == (1U << inst.SH) - 1; + bool needs_test = false; - if (gpr.IsImm(a, s)) + if (mask == 0 || (a == s && inst.SH == 0)) { - gpr.SetImmediate32(a, (gpr.Imm32(a) & ~mask) | (std::rotl(gpr.Imm32(s), inst.SH) & mask)); - if (inst.Rc) - ComputeRC(a); + needs_test = true; } - else if (gpr.IsImm(s) && mask == 0xFFFFFFFF) + else if (mask == 0xFFFFFFFF) { - gpr.SetImmediate32(a, std::rotl(gpr.Imm32(s), inst.SH)); - - if (inst.Rc) - ComputeRC(a); + RCOpArg Rs = gpr.Use(s, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::Write); + RegCache::Realize(Rs, Ra); + RotateLeft(32, Ra, Rs, inst.SH); + needs_test = true; } - else + else if (gpr.IsImm(s)) + { + RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); + RegCache::Realize(Ra); + AndWithMask(Ra, ~mask); + OR(32, Ra, Imm32(std::rotl(gpr.Imm32(s), inst.SH) & mask)); + } + else if (gpr.IsImm(a)) { - const bool left_shift = mask == 0U - (1U << inst.SH); - const bool right_shift = mask == (1U << inst.SH) - 1; - bool needs_test = false; + const u32 maskA = gpr.Imm32(a) & ~mask; - if (mask == 0 || (a == s && inst.SH == 0)) + RCOpArg Rs = gpr.Use(s, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::Write); + RegCache::Realize(Rs, Ra); + + if (inst.SH == 0) { - needs_test = true; + MOV(32, Ra, Rs); + AndWithMask(Ra, mask); } - else if (mask == 0xFFFFFFFF) + else if (left_shift) { - RCOpArg Rs = gpr.Use(s, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RegCache::Realize(Rs, Ra); - RotateLeft(32, Ra, Rs, inst.SH); - needs_test = true; + MOV(32, Ra, Rs); + SHL(32, Ra, Imm8(inst.SH)); } - else if (gpr.IsImm(s)) + else if (right_shift) { - RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); - RegCache::Realize(Ra); - AndWithMask(Ra, ~mask); - OR(32, Ra, Imm32(std::rotl(gpr.Imm32(s), inst.SH) & mask)); + MOV(32, Ra, Rs); + SHR(32, Ra, Imm8(32 - inst.SH)); } - else if (gpr.IsImm(a)) + else { - const u32 maskA = gpr.Imm32(a) & ~mask; - - RCOpArg Rs = gpr.Use(s, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::Write); - RegCache::Realize(Rs, Ra); + RotateLeft(32, Ra, Rs, inst.SH); + AndWithMask(Ra, mask); + } - if (inst.SH == 0) - { - MOV(32, Ra, Rs); - AndWithMask(Ra, mask); - } - else if (left_shift) - { - MOV(32, Ra, Rs); - SHL(32, Ra, Imm8(inst.SH)); - } - else if (right_shift) - { - MOV(32, Ra, Rs); - SHR(32, Ra, Imm8(32 - inst.SH)); - } - else - { - RotateLeft(32, Ra, Rs, inst.SH); - AndWithMask(Ra, mask); - } + if (maskA) + OR(32, Ra, Imm32(maskA)); + else + needs_test = true; + } + else if (inst.SH) + { + // TODO: perhaps consider pinsrb or abuse of AH + RCOpArg Rs = gpr.Use(s, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); + RegCache::Realize(Rs, Ra); - if (maskA) - OR(32, Ra, Imm32(maskA)); - else - needs_test = true; + if (left_shift) + { + MOV(32, R(RSCRATCH), Rs); + SHL(32, R(RSCRATCH), Imm8(inst.SH)); } - else if (inst.SH) + else if (right_shift) { - // TODO: perhaps consider pinsrb or abuse of AH - RCOpArg Rs = gpr.Use(s, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); - RegCache::Realize(Rs, Ra); - - if (left_shift) - { - MOV(32, R(RSCRATCH), Rs); - SHL(32, R(RSCRATCH), Imm8(inst.SH)); - } - else if (right_shift) - { - MOV(32, R(RSCRATCH), Rs); - SHR(32, R(RSCRATCH), Imm8(32 - inst.SH)); - } - else - { - RotateLeft(32, RSCRATCH, Rs, inst.SH); - } + MOV(32, R(RSCRATCH), Rs); + SHR(32, R(RSCRATCH), Imm8(32 - inst.SH)); + } + else + { + RotateLeft(32, RSCRATCH, Rs, inst.SH); + } - if (mask == 0xFF || mask == 0xFFFF) - { - MOV(mask == 0xFF ? 8 : 16, Ra, R(RSCRATCH)); - needs_test = true; - } - else - { - if (!left_shift && !right_shift) - AndWithMask(RSCRATCH, mask); - AndWithMask(Ra, ~mask); - OR(32, Ra, R(RSCRATCH)); - } + if (mask == 0xFF || mask == 0xFFFF) + { + MOV(mask == 0xFF ? 8 : 16, Ra, R(RSCRATCH)); + needs_test = true; } else { - RCX64Reg Rs = gpr.Bind(s, RCMode::Read); - RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); - RegCache::Realize(Rs, Ra); + if (!left_shift && !right_shift) + AndWithMask(RSCRATCH, mask); + AndWithMask(Ra, ~mask); + OR(32, Ra, R(RSCRATCH)); + } + } + else + { + RCX64Reg Rs = gpr.Bind(s, RCMode::Read); + RCX64Reg Ra = gpr.Bind(a, RCMode::ReadWrite); + RegCache::Realize(Rs, Ra); - if (mask == 0xFF || mask == 0xFFFF) - { - MOV(mask == 0xFF ? 8 : 16, Ra, Rs); - needs_test = true; - } - else - { - XOR(32, Ra, Rs); - AndWithMask(Ra, ~mask); - XOR(32, Ra, Rs); - } + if (mask == 0xFF || mask == 0xFFFF) + { + MOV(mask == 0xFF ? 8 : 16, Ra, Rs); + needs_test = true; + } + else + { + XOR(32, Ra, Rs); + AndWithMask(Ra, ~mask); + XOR(32, Ra, Rs); } - if (inst.Rc) - ComputeRC(a, needs_test); } + if (inst.Rc) + ComputeRC(a, needs_test); } void Jit64::rlwnmx(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 55eddf11c03c..dd01d6dbddb0 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1975,74 +1975,64 @@ void JitArm64::rlwimix(UGeckoInstruction inst) const u32 width = inst.ME - inst.MB + 1; const u32 rot_dist = inst.SH ? 32 - inst.SH : 0; - if (gpr.IsImm(a) && gpr.IsImm(s)) + if (mask == 0 || (a == s && inst.SH == 0)) { - u32 res = (gpr.GetImm(a) & ~mask) | (std::rotl(gpr.GetImm(s), inst.SH) & mask); - gpr.SetImmediate(a, res); - if (inst.Rc) - ComputeRC0(res); + // Do Nothing } - else + else if (mask == 0xFFFFFFFF) { - if (mask == 0 || (a == s && inst.SH == 0)) - { - // Do Nothing - } - else if (mask == 0xFFFFFFFF) - { - if (inst.SH || a != s) - gpr.BindToRegister(a, a == s); + if (inst.SH || a != s) + gpr.BindToRegister(a, a == s); - if (inst.SH) - ROR(gpr.R(a), gpr.R(s), rot_dist); - else if (a != s) - MOV(gpr.R(a), gpr.R(s)); - } - else if (lsb == 0 && inst.MB <= inst.ME && rot_dist + width <= 32) - { - // Destination is in least significant position - // No mask inversion - // Source field pre-rotation is contiguous - gpr.BindToRegister(a, true); - BFXIL(gpr.R(a), gpr.R(s), rot_dist, width); - } - else if (inst.SH == 0 && inst.MB <= inst.ME) - { - // No rotation - // No mask inversion - gpr.BindToRegister(a, true); - auto WA = gpr.GetScopedReg(); - UBFX(WA, gpr.R(s), lsb, width); - BFI(gpr.R(a), WA, lsb, width); - } - else if (inst.SH && inst.MB <= inst.ME) + if (inst.SH) + ROR(gpr.R(a), gpr.R(s), rot_dist); + else if (a != s) + MOV(gpr.R(a), gpr.R(s)); + } + else if (lsb == 0 && inst.MB <= inst.ME && rot_dist + width <= 32) + { + // Destination is in least significant position + // No mask inversion + // Source field pre-rotation is contiguous + gpr.BindToRegister(a, true); + BFXIL(gpr.R(a), gpr.R(s), rot_dist, width); + } + else if (inst.SH == 0 && inst.MB <= inst.ME) + { + // No rotation + // No mask inversion + gpr.BindToRegister(a, true); + auto WA = gpr.GetScopedReg(); + UBFX(WA, gpr.R(s), lsb, width); + BFI(gpr.R(a), WA, lsb, width); + } + else if (inst.SH && inst.MB <= inst.ME) + { + // No mask inversion + gpr.BindToRegister(a, true); + if ((rot_dist + lsb) % 32 == 0) { - // No mask inversion - gpr.BindToRegister(a, true); - if ((rot_dist + lsb) % 32 == 0) - { - BFI(gpr.R(a), gpr.R(s), lsb, width); - } - else - { - auto WA = gpr.GetScopedReg(); - ROR(WA, gpr.R(s), (rot_dist + lsb) % 32); - BFI(gpr.R(a), WA, lsb, width); - } + BFI(gpr.R(a), gpr.R(s), lsb, width); } else { - gpr.BindToRegister(a, true); - ARM64Reg RA = gpr.R(a); auto WA = gpr.GetScopedReg(); - const u32 inverted_mask = ~mask; - - AND(WA, gpr.R(s), LogicalImm(std::rotl(mask, rot_dist), GPRSize::B32)); - AND(RA, RA, LogicalImm(inverted_mask, GPRSize::B32)); - ORR(RA, RA, WA, ArithOption(WA, ShiftType::ROR, rot_dist)); + ROR(WA, gpr.R(s), (rot_dist + lsb) % 32); + BFI(gpr.R(a), WA, lsb, width); } + } + else + { + gpr.BindToRegister(a, true); + ARM64Reg RA = gpr.R(a); + auto WA = gpr.GetScopedReg(); + const u32 inverted_mask = ~mask; - if (inst.Rc) - ComputeRC0(gpr.R(a)); + AND(WA, gpr.R(s), LogicalImm(std::rotl(mask, rot_dist), GPRSize::B32)); + AND(RA, RA, LogicalImm(inverted_mask, GPRSize::B32)); + ORR(RA, RA, WA, ArithOption(WA, ShiftType::ROR, rot_dist)); } + + if (inst.Rc) + ComputeRC0(gpr.R(a)); } diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp index 5efd3b55e144..b632cd3fc4e3 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.cpp @@ -41,6 +41,8 @@ ConstantPropagationResult ConstantPropagation::EvaluateInstruction(UGeckoInstruc case 14: // addi case 15: // addis return EvaluateAddImm(inst); + case 20: // rlwimix + return EvaluateRlwimix(inst); case 21: // rlwinmx return EvaluateRlwinmxRlwnmx(inst, inst.SH); case 23: // rlwnmx @@ -114,6 +116,22 @@ ConstantPropagationResult ConstantPropagation::EvaluateAddImmCarry(UGeckoInstruc return result; } +ConstantPropagationResult ConstantPropagation::EvaluateRlwimix(UGeckoInstruction inst) const +{ + if (!HasGPR(inst.RS)) + return {}; + + const u32 mask = MakeRotationMask(inst.MB, inst.ME); + if (mask == 0xFFFFFFFF) + return ConstantPropagationResult(inst.RA, std::rotl(GetGPR(inst.RS), inst.SH), inst.Rc); + + if (!HasGPR(inst.RA)) + return {}; + + return ConstantPropagationResult( + inst.RA, (GetGPR(inst.RA) & ~mask) | (std::rotl(GetGPR(inst.RS), inst.SH) & mask), inst.Rc); +} + ConstantPropagationResult ConstantPropagation::EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const { diff --git a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h index 467944a658ca..ffbf543ed6d5 100644 --- a/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h +++ b/Source/Core/Core/PowerPC/JitCommon/ConstantPropagation.h @@ -81,6 +81,7 @@ class ConstantPropagation final ConstantPropagationResult EvaluateSubImmCarry(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImm(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateAddImmCarry(UGeckoInstruction inst) const; + ConstantPropagationResult EvaluateRlwimix(UGeckoInstruction inst) const; ConstantPropagationResult EvaluateRlwinmxRlwnmx(UGeckoInstruction inst, u32 shift) const; ConstantPropagationResult EvaluateBitwiseImm(UGeckoInstruction inst, u32 (*do_op)(u32, u32)) const; From 7065b93ba557513ecf9365eff8e1db9aaa41c77e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 15:45:23 +0200 Subject: [PATCH 050/123] JitArm64: Pass index to more Arm64GPRCache functions This refactorization is needed for upcoming commits. --- .../PowerPC/JitArm64/JitArm64_RegCache.cpp | 19 +++++++---------- .../Core/PowerPC/JitArm64/JitArm64_RegCache.h | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index 20a86c0389d4..43631b2d9afd 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -137,12 +137,6 @@ void Arm64RegCache::DiscardRegister(size_t preg) UnlockRegister(host_reg); } -// GPR Cache -constexpr size_t GUEST_GPR_COUNT = 32; -constexpr size_t GUEST_CR_COUNT = 8; -constexpr size_t GUEST_GPR_OFFSET = 0; -constexpr size_t GUEST_CR_OFFSET = GUEST_GPR_COUNT; - Arm64GPRCache::Arm64GPRCache() : Arm64RegCache(GUEST_GPR_COUNT + GUEST_CR_COUNT) { } @@ -273,8 +267,8 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r const size_t ppc_offset = GetGuestByIndex(i).ppc_offset; if (ppc_offset <= 252) { - ARM64Reg RX1 = reg1_zero ? ARM64Reg::WZR : R(GetGuestByIndex(i)); - ARM64Reg RX2 = reg2_zero ? ARM64Reg::WZR : R(GetGuestByIndex(i + 1)); + ARM64Reg RX1 = reg1_zero ? ARM64Reg::WZR : BindForRead(i); + ARM64Reg RX2 = reg2_zero ? ARM64Reg::WZR : BindForRead(i + 1); m_emit->STP(IndexType::Signed, RX1, RX2, PPC_REG, u32(ppc_offset)); if (flush_all) { @@ -335,8 +329,9 @@ void Arm64GPRCache::Flush(FlushMode mode, ARM64Reg tmp_reg, FlushCRRegisters(BitSet8(0xFF), mode, tmp_reg, ignore_discarded_registers); } -ARM64Reg Arm64GPRCache::R(const GuestRegInfo& guest_reg) +ARM64Reg Arm64GPRCache::BindForRead(size_t index) { + GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; size_t bitsize = guest_reg.bitsize; @@ -378,8 +373,9 @@ ARM64Reg Arm64GPRCache::R(const GuestRegInfo& guest_reg) return ARM64Reg::INVALID_REG; } -void Arm64GPRCache::SetImmediate(const GuestRegInfo& guest_reg, u32 imm, bool dirty) +void Arm64GPRCache::SetImmediateInternal(size_t index, u32 imm, bool dirty) { + GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; if (reg.GetType() == RegType::Register) UnlockRegister(EncodeRegTo32(reg.GetReg())); @@ -387,8 +383,9 @@ void Arm64GPRCache::SetImmediate(const GuestRegInfo& guest_reg, u32 imm, bool di reg.SetDirty(dirty); } -void Arm64GPRCache::BindToRegister(const GuestRegInfo& guest_reg, bool will_read, bool will_write) +void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) { + GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; const size_t bitsize = guest_reg.bitsize; diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h index 990b2dcee79c..c973e3bc285a 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h @@ -328,15 +328,15 @@ class Arm64GPRCache : public Arm64RegCache // Returns a guest GPR inside of a host register. // Will dump an immediate to the host register as well. - Arm64Gen::ARM64Reg R(size_t preg) { return R(GetGuestGPR(preg)); } + Arm64Gen::ARM64Reg R(size_t preg) { return BindForRead(GUEST_GPR_OFFSET + preg); } // Returns a guest CR inside of a host register. - Arm64Gen::ARM64Reg CR(size_t preg) { return R(GetGuestCR(preg)); } + Arm64Gen::ARM64Reg CR(size_t preg) { return BindForRead(GUEST_CR_OFFSET + preg); } // Set a register to an immediate. Only valid for guest GPRs. void SetImmediate(size_t preg, u32 imm, bool dirty = true) { - SetImmediate(GetGuestGPR(preg), imm, dirty); + SetImmediateInternal(GUEST_GPR_OFFSET + preg, imm, dirty); } // Returns if a register is set as an immediate. Only valid for guest GPRs. @@ -374,14 +374,14 @@ class Arm64GPRCache : public Arm64RegCache // flushed. Just remember to call this function again with will_write = true after the Flush call. void BindToRegister(size_t preg, bool will_read, bool will_write = true) { - BindToRegister(GetGuestGPR(preg), will_read, will_write); + BindForWrite(GUEST_GPR_OFFSET + preg, will_read, will_write); } // Binds a guest CR to a host register, optionally loading its value. // The description of BindToRegister above applies to this function as well. void BindCRToRegister(size_t preg, bool will_read, bool will_write = true) { - BindToRegister(GetGuestCR(preg), will_read, will_write); + BindForWrite(GUEST_CR_OFFSET + preg, will_read, will_write); } BitSet32 GetCallerSavedUsed() const override; @@ -428,14 +428,19 @@ class Arm64GPRCache : public Arm64RegCache GuestRegInfo GetGuestCR(size_t preg); GuestRegInfo GetGuestByIndex(size_t index); - Arm64Gen::ARM64Reg R(const GuestRegInfo& guest_reg); - void SetImmediate(const GuestRegInfo& guest_reg, u32 imm, bool dirty); - void BindToRegister(const GuestRegInfo& guest_reg, bool will_read, bool will_write = true); + Arm64Gen::ARM64Reg BindForRead(size_t index); + void SetImmediateInternal(size_t index, u32 imm, bool dirty); + void BindForWrite(size_t index, bool will_read, bool will_write = true); void FlushRegisters(BitSet32 regs, FlushMode mode, Arm64Gen::ARM64Reg tmp_reg, IgnoreDiscardedRegisters ignore_discarded_registers); void FlushCRRegisters(BitSet8 regs, FlushMode mode, Arm64Gen::ARM64Reg tmp_reg, IgnoreDiscardedRegisters ignore_discarded_registers); + + static constexpr size_t GUEST_GPR_COUNT = 32; + static constexpr size_t GUEST_CR_COUNT = 8; + static constexpr size_t GUEST_GPR_OFFSET = 0; + static constexpr size_t GUEST_CR_OFFSET = GUEST_GPR_COUNT; }; class Arm64FPRCache : public Arm64RegCache From 502b48a690ece557c1c2c6eeff746d11ab0f6534 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 2 Nov 2024 15:46:40 +0100 Subject: [PATCH 051/123] JitArm64: Make FlushRegisters unlock condition more robust To find out whether a host register needs to be unlocked, FlushRegisters checks if the guest register is known to be a zero immediate. This works right now, but it will stop working correctly once we gain the ability to have a guest register be a known immediate and be in a host register at the same time, because a register that's known to be a zero immediate may have had a host register allocated prior to the call to FlushRegisters. Instead, we should check whether the register is RegType::Register after we're done calling BindForRead. --- Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index 43631b2d9afd..786f5be4a324 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -272,10 +272,10 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r m_emit->STP(IndexType::Signed, RX1, RX2, PPC_REG, u32(ppc_offset)); if (flush_all) { - if (!reg1_zero) - UnlockRegister(EncodeRegTo32(RX1)); - if (!reg2_zero) - UnlockRegister(EncodeRegTo32(RX2)); + if (reg1.GetType() == RegType::Register) + UnlockRegister(reg1.GetReg()); + if (reg2.GetType() == RegType::Register) + UnlockRegister(reg2.GetReg()); reg1.Flush(); reg2.Flush(); } From 4114a0b50660e0821f4f54ddd597372a99c75a48 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 16:10:17 +0200 Subject: [PATCH 052/123] Jit: Update constant propagation during instruction This commit makes the JIT set/clear the individual registers of ConstantPropagation immediately instead of at the end of the instruction. This is needed to prevent Jit64::ComputeRC, which reads from a register written to earlier during the same instruction, from reading back stale register values from ConstantPropagation in the next commit. --- Source/Core/Core/PowerPC/Jit64/Jit.cpp | 5 +++-- Source/Core/Core/PowerPC/Jit64/Jit.h | 2 ++ Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp | 5 +++++ Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h | 1 + Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp | 6 ++++++ Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h | 1 + Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp | 3 +++ Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h | 1 + Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 7 +++---- Source/Core/Core/PowerPC/JitArm64/Jit.h | 2 ++ Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp | 9 ++++++--- 11 files changed, 33 insertions(+), 9 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 024b4c6e4af7..bdbb023c258a 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -370,6 +370,9 @@ void Jit64::FallBackToInterpreter(UGeckoInstruction inst) gpr.Reset(js.op->regsOut); fpr.Reset(js.op->GetFregsOut()); + // We must also update constant propagation + m_constant_propagation.ClearGPRs(js.op->regsOut); + if (js.op->opinfo->flags & FL_SET_MSR) EmitUpdateMembase(); @@ -1133,8 +1136,6 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) } CompileInstruction(op); - - m_constant_propagation.ClearGPRs(op.regsOut); } m_constant_propagation.Apply(constant_propagation_result); diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h index 189f0c2b4bb9..8b96fda107ab 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/PowerPC/Jit64/Jit.h @@ -84,6 +84,8 @@ class Jit64 : public JitBase, public QuantizedMemoryRoutines void FlushRegistersBeforeSlowAccess(); + JitCommon::ConstantPropagation& GetConstantPropagation() { return m_constant_propagation; } + JitBlockCache* GetBlockCache() override { return &blocks; } void Trace(); diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp index df210f80c5bc..64870ec026a8 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp @@ -25,6 +25,11 @@ void FPURegCache::LoadRegister(preg_t preg, X64Reg new_loc) m_emitter->MOVAPD(new_loc, m_regs[preg].Location().value()); } +void FPURegCache::DiscardImm(preg_t preg) +{ + // FPURegCache doesn't support immediates, so no need to do anything +} + std::span FPURegCache::GetAllocationOrder() const { static constexpr X64Reg allocation_order[] = {XMM6, XMM7, XMM8, XMM9, XMM10, XMM11, XMM12, diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h index f7d81663b6da..f34db9d08864 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h @@ -16,6 +16,7 @@ class FPURegCache final : public RegCache Gen::OpArg GetDefaultLocation(preg_t preg) const override; void StoreRegister(preg_t preg, const Gen::OpArg& newLoc) override; void LoadRegister(preg_t preg, Gen::X64Reg newLoc) override; + void DiscardImm(preg_t preg) override; std::span GetAllocationOrder() const override; BitSet32 GetRegUtilization() const override; BitSet32 CountRegsIn(preg_t preg, u32 lookahead) const override; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp index ca30e15784a2..b44382ba447f 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp @@ -25,6 +25,11 @@ void GPRRegCache::LoadRegister(preg_t preg, X64Reg new_loc) m_emitter->MOV(32, ::Gen::R(new_loc), m_regs[preg].Location().value()); } +void GPRRegCache::DiscardImm(preg_t preg) +{ + m_jit.GetConstantPropagation().ClearGPR(preg); +} + OpArg GPRRegCache::GetDefaultLocation(preg_t preg) const { return PPCSTATE_GPR(preg); @@ -50,6 +55,7 @@ void GPRRegCache::SetImmediate32(preg_t preg, u32 imm_value, bool dirty) // processing speculative constants. DiscardRegContentsIfCached(preg); m_regs[preg].SetToImm32(imm_value, dirty); + m_jit.GetConstantPropagation().SetGPR(preg, imm_value); } BitSet32 GPRRegCache::GetRegUtilization() const diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h index 60985e196071..a5bf52426942 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h @@ -17,6 +17,7 @@ class GPRRegCache final : public RegCache Gen::OpArg GetDefaultLocation(preg_t preg) const override; void StoreRegister(preg_t preg, const Gen::OpArg& new_loc) override; void LoadRegister(preg_t preg, Gen::X64Reg new_loc) override; + void DiscardImm(preg_t preg) override; std::span GetAllocationOrder() const override; BitSet32 GetRegUtilization() const override; BitSet32 CountRegsIn(preg_t preg, u32 lookahead) const override; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp index d59d4f0a01df..d1e06661709d 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp @@ -536,6 +536,9 @@ void RegCache::BindToRegister(preg_t i, bool doLoad, bool makeDirty) m_xregs[RX(i)].MakeDirty(); } + if (makeDirty) + DiscardImm(i); + ASSERT_MSG(DYNA_REC, !m_xregs[RX(i)].IsLocked(), "WTF, this reg ({} -> {}) should have been flushed", i, Common::ToUnderlying(RX(i))); } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h index 3677d2b42b7e..1ff4e27ea785 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h @@ -193,6 +193,7 @@ class RegCache virtual Gen::OpArg GetDefaultLocation(preg_t preg) const = 0; virtual void StoreRegister(preg_t preg, const Gen::OpArg& new_loc) = 0; virtual void LoadRegister(preg_t preg, Gen::X64Reg new_loc) = 0; + virtual void DiscardImm(preg_t preg) = 0; virtual std::span GetAllocationOrder() const = 0; diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index ba30b0bc8185..975f5f95f7d1 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -279,6 +279,9 @@ void JitArm64::FallBackToInterpreter(UGeckoInstruction inst) fpr.ResetRegisters(js.op->GetFregsOut()); gpr.ResetCRRegisters(js.op->crOut); + // We must also update constant propagation + m_constant_propagation.ClearGPRs(js.op->regsOut); + if (js.op->opinfo->flags & FL_SET_MSR) EmitUpdateMembase(); @@ -1354,12 +1357,8 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) m_constant_propagation.EvaluateInstruction(op.inst, opinfo->flags); if (!constant_propagation_result.instruction_fully_executed) - { CompileInstruction(op); - m_constant_propagation.ClearGPRs(op.regsOut); - } - m_constant_propagation.Apply(constant_propagation_result); if (constant_propagation_result.gpr >= 0) diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index 1b738061b22b..e98f950d42fd 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -36,6 +36,8 @@ class JitArm64 : public JitBase, public Arm64Gen::ARM64CodeBlock, public CommonA void Init() override; void Shutdown() override; + JitCommon::ConstantPropagation& GetConstantPropagation() { return m_constant_propagation; } + JitBaseBlockCache* GetBlockCache() override { return &blocks; } bool IsInCodeSpace(const u8* ptr) const { return IsInSpace(ptr); } bool HandleFault(uintptr_t access_address, SContext* ctx) override; diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index 786f5be4a324..d0c9ac71bde4 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -381,6 +381,7 @@ void Arm64GPRCache::SetImmediateInternal(size_t index, u32 imm, bool dirty) UnlockRegister(EncodeRegTo32(reg.GetReg())); reg.LoadToImm(imm); reg.SetDirty(dirty); + m_jit->GetConstantPropagation().SetGPR(index - GUEST_GPR_OFFSET, imm); } void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) @@ -388,6 +389,7 @@ void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; const size_t bitsize = guest_reg.bitsize; + const bool is_gpr = index >= GUEST_GPR_OFFSET && index < GUEST_GPR_OFFSET + GUEST_GPR_COUNT; reg.ResetLastUsed(); @@ -414,12 +416,13 @@ void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) m_emit->MOVI2R(host_reg, reg.GetImm()); } reg.Load(host_reg); - if (will_write) - reg.SetDirty(true); } - else if (will_write) + + if (will_write) { reg.SetDirty(true); + if (is_gpr) + m_jit->GetConstantPropagation().ClearGPR(index - GUEST_GPR_OFFSET); } } From 817bb9d94c43ca6be8862e2a09159f9b67836807 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 25 Aug 2023 15:28:07 +0200 Subject: [PATCH 053/123] Jit64: Don't store immediate values in register cache They're now stored in ConstantPropagation instead. I've also removed the LocationType enum. The location of each guest register is now tracked using three booleans: Whether it is in ppcState, whether it is in a host register, and whether it is a known immediate. The first two of these booleans are stored in the register cache, and the last one is stored in ConstantPropagation. This new model allows us to handle the combination of a value simultaneously being in a host register and being a known immediate. It also keeps track of which registers are dirty, which was previously kept track of in X64CachedReg. The old model maps to the new model as follows: default host_reg immediate Default true false false Discarded false false false Bound (!dirty) true false Immediate false false true SpeculativeImmediate true false true [previously unrepresentable] (!dirty) true true --- Source/Core/Core/PowerPC/Jit64/Jit.cpp | 1 + .../Core/PowerPC/Jit64/RegCache/CachedReg.h | 118 +++++-------- .../PowerPC/Jit64/RegCache/FPURegCache.cpp | 48 +++++- .../Core/PowerPC/Jit64/RegCache/FPURegCache.h | 8 +- .../PowerPC/Jit64/RegCache/GPRRegCache.cpp | 69 +++++++- .../Core/PowerPC/Jit64/RegCache/GPRRegCache.h | 9 +- .../PowerPC/Jit64/RegCache/JitRegCache.cpp | 156 ++++++------------ .../Core/PowerPC/Jit64/RegCache/JitRegCache.h | 21 ++- 8 files changed, 220 insertions(+), 210 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index bdbb023c258a..6cee5b971e34 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -1142,6 +1142,7 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) if (constant_propagation_result.gpr >= 0) { + // Mark the GPR as dirty in the register cache gpr.SetImmediate32(constant_propagation_result.gpr, constant_propagation_result.gpr_value); } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/CachedReg.h b/Source/Core/Core/PowerPC/Jit64/RegCache/CachedReg.h index acf5480abb7a..99a6e472254f 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/CachedReg.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/CachedReg.h @@ -16,111 +16,79 @@ using preg_t = size_t; class PPCCachedReg { public: - enum class LocationType - { - /// Value is currently at its default location - Default, - /// Value is not stored anywhere because we know it won't be read before the next write - Discarded, - /// Value is currently bound to a x64 register - Bound, - /// Value is known as an immediate and has not been written back to its default location - Immediate, - /// Value is known as an immediate and is already present at its default location - SpeculativeImmediate, - }; - PPCCachedReg() = default; - explicit PPCCachedReg(Gen::OpArg default_location_) - : default_location(default_location_), location(default_location_) - { - } + explicit PPCCachedReg(Gen::OpArg default_location) : m_default_location(default_location) {} - const std::optional& Location() const { return location; } + Gen::OpArg GetDefaultLocation() const { return m_default_location; } - LocationType GetLocationType() const + Gen::X64Reg GetHostRegister() const { - if (!location.has_value()) - return LocationType::Discarded; - - if (!away) - { - ASSERT(!revertable); - - if (location->IsImm()) - return LocationType::SpeculativeImmediate; - - ASSERT(*location == default_location); - return LocationType::Default; - } - - ASSERT(location->IsImm() || location->IsSimpleReg()); - return location->IsImm() ? LocationType::Immediate : LocationType::Bound; + ASSERT(m_in_host_register); + return m_host_register; } - bool IsAway() const { return away; } - bool IsDiscarded() const { return !location.has_value(); } - bool IsBound() const { return GetLocationType() == LocationType::Bound; } + bool IsInDefaultLocation() const { return m_in_default_location; } + bool IsInHostRegister() const { return m_in_host_register; } - void SetBoundTo(Gen::X64Reg xreg) + void SetFlushed(bool maintain_host_register) { - away = true; - location = Gen::R(xreg); + ASSERT(!m_revertable); + if (!maintain_host_register) + m_in_host_register = false; + m_in_default_location = true; } - void SetDiscarded() + void SetInHostRegister(Gen::X64Reg xreg, bool dirty) { - ASSERT(!revertable); - away = false; - location = std::nullopt; + if (dirty) + m_in_default_location = false; + m_in_host_register = true; + m_host_register = xreg; } - void SetFlushed() - { - ASSERT(!revertable); - away = false; - location = default_location; - } + void SetDirty() { m_in_default_location = false; } - void SetToImm32(u32 imm32, bool dirty = true) + void SetDiscarded() { - away |= dirty; - location = Gen::Imm32(imm32); + ASSERT(!m_revertable); + m_in_default_location = false; + m_in_host_register = false; } - bool IsRevertable() const { return revertable; } + bool IsRevertable() const { return m_revertable; } void SetRevertable() { - ASSERT(IsBound()); - revertable = true; + ASSERT(m_in_host_register); + m_revertable = true; } void SetRevert() { - ASSERT(revertable); - revertable = false; - SetFlushed(); + ASSERT(m_revertable); + m_revertable = false; + SetFlushed(false); } void SetCommit() { - ASSERT(revertable); - revertable = false; + ASSERT(m_revertable); + m_revertable = false; } - bool IsLocked() const { return locked > 0; } - void Lock() { locked++; } + bool IsLocked() const { return m_locked > 0; } + void Lock() { m_locked++; } void Unlock() { ASSERT(IsLocked()); - locked--; + m_locked--; } private: - Gen::OpArg default_location{}; - std::optional location{}; - bool away = false; // value not in source register - bool revertable = false; - size_t locked = 0; + Gen::OpArg m_default_location{}; + Gen::X64Reg m_host_register{}; + bool m_in_default_location = true; + bool m_in_host_register = false; + bool m_revertable = false; + size_t m_locked = 0; }; class X64CachedReg @@ -128,25 +96,20 @@ class X64CachedReg public: preg_t Contents() const { return ppcReg; } - void SetBoundTo(preg_t ppcReg_, bool dirty_) + void SetBoundTo(preg_t ppcReg_) { free = false; ppcReg = ppcReg_; - dirty = dirty_; } void Unbind() { ppcReg = static_cast(Gen::INVALID_REG); free = true; - dirty = false; } bool IsFree() const { return free && !locked; } - bool IsDirty() const { return dirty; } - void MakeDirty() { dirty = true; } - bool IsLocked() const { return locked > 0; } void Lock() { locked++; } void Unlock() @@ -158,7 +121,6 @@ class X64CachedReg private: preg_t ppcReg = static_cast(Gen::INVALID_REG); bool free = true; - bool dirty = false; size_t locked = 0; }; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp index 64870ec026a8..3e089ffe7ddc 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.cpp @@ -13,16 +13,54 @@ FPURegCache::FPURegCache(Jit64& jit) : RegCache{jit} { } -void FPURegCache::StoreRegister(preg_t preg, const OpArg& new_loc) +bool FPURegCache::IsImm(preg_t preg) const { - ASSERT_MSG(DYNA_REC, m_regs[preg].IsBound(), "Unbound register - {}", preg); - m_emitter->MOVAPD(new_loc, m_regs[preg].Location()->GetSimpleReg()); + return false; +} + +u32 FPURegCache::Imm32(preg_t preg) const +{ + ASSERT_MSG(DYNA_REC, false, "FPURegCache doesn't support immediates"); + return 0; +} + +s32 FPURegCache::SImm32(preg_t preg) const +{ + ASSERT_MSG(DYNA_REC, false, "FPURegCache doesn't support immediates"); + return 0; +} + +OpArg FPURegCache::R(preg_t preg) const +{ + if (m_regs[preg].IsInHostRegister()) + { + return ::Gen::R(m_regs[preg].GetHostRegister()); + } + else + { + ASSERT_MSG(DYNA_REC, m_regs[preg].IsInDefaultLocation(), "FPR {} missing!", preg); + return m_regs[preg].GetDefaultLocation(); + } +} + +void FPURegCache::StoreRegister(preg_t preg, const OpArg& new_loc, + IgnoreDiscardedRegisters ignore_discarded_registers) +{ + if (m_regs[preg].IsInHostRegister()) + { + m_emitter->MOVAPD(new_loc, m_regs[preg].GetHostRegister()); + } + else + { + ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No, + "FPR {} not in host register", preg); + } } void FPURegCache::LoadRegister(preg_t preg, X64Reg new_loc) { - ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - {}", preg); - m_emitter->MOVAPD(new_loc, m_regs[preg].Location().value()); + ASSERT_MSG(DYNA_REC, m_regs[preg].IsInDefaultLocation(), "FPR {} not in default location", preg); + m_emitter->MOVAPD(new_loc, m_regs[preg].GetDefaultLocation()); } void FPURegCache::DiscardImm(preg_t preg) diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h index f34db9d08864..76cad940aecd 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/FPURegCache.h @@ -12,9 +12,15 @@ class FPURegCache final : public RegCache public: explicit FPURegCache(Jit64& jit); + bool IsImm(preg_t preg) const override; + u32 Imm32(preg_t preg) const override; + s32 SImm32(preg_t preg) const override; + protected: + Gen::OpArg R(preg_t preg) const override; Gen::OpArg GetDefaultLocation(preg_t preg) const override; - void StoreRegister(preg_t preg, const Gen::OpArg& newLoc) override; + void StoreRegister(preg_t preg, const Gen::OpArg& newLoc, + IgnoreDiscardedRegisters ignore_discarded_registers) override; void LoadRegister(preg_t preg, Gen::X64Reg newLoc) override; void DiscardImm(preg_t preg) override; std::span GetAllocationOrder() const override; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp index b44382ba447f..a740d76e3dc9 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp @@ -13,16 +13,71 @@ GPRRegCache::GPRRegCache(Jit64& jit) : RegCache{jit} { } -void GPRRegCache::StoreRegister(preg_t preg, const OpArg& new_loc) +bool GPRRegCache::IsImm(preg_t preg) const { - ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - {}", preg); - m_emitter->MOV(32, new_loc, m_regs[preg].Location().value()); + return m_jit.GetConstantPropagation().HasGPR(preg); +} + +u32 GPRRegCache::Imm32(preg_t preg) const +{ + ASSERT(m_jit.GetConstantPropagation().HasGPR(preg)); + return m_jit.GetConstantPropagation().GetGPR(preg); +} + +s32 GPRRegCache::SImm32(preg_t preg) const +{ + ASSERT(m_jit.GetConstantPropagation().HasGPR(preg)); + return m_jit.GetConstantPropagation().GetGPR(preg); +} + +OpArg GPRRegCache::R(preg_t preg) const +{ + if (m_regs[preg].IsInHostRegister()) + { + return ::Gen::R(m_regs[preg].GetHostRegister()); + } + else if (m_jit.GetConstantPropagation().HasGPR(preg)) + { + return ::Gen::Imm32(m_jit.GetConstantPropagation().GetGPR(preg)); + } + else + { + ASSERT_MSG(DYNA_REC, m_regs[preg].IsInDefaultLocation(), "GPR {} missing!", preg); + return m_regs[preg].GetDefaultLocation(); + } +} + +void GPRRegCache::StoreRegister(preg_t preg, const OpArg& new_loc, + IgnoreDiscardedRegisters ignore_discarded_registers) +{ + if (m_regs[preg].IsInHostRegister()) + { + m_emitter->MOV(32, new_loc, ::Gen::R(m_regs[preg].GetHostRegister())); + } + else if (m_jit.GetConstantPropagation().HasGPR(preg)) + { + m_emitter->MOV(32, new_loc, ::Gen::Imm32(m_jit.GetConstantPropagation().GetGPR(preg))); + } + else + { + ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No, + "GPR {} not in host register or constant propagation", preg); + } } void GPRRegCache::LoadRegister(preg_t preg, X64Reg new_loc) { - ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - {}", preg); - m_emitter->MOV(32, ::Gen::R(new_loc), m_regs[preg].Location().value()); + const JitCommon::ConstantPropagation& constant_propagation = m_jit.GetConstantPropagation(); + if (constant_propagation.HasGPR(preg)) + { + m_emitter->MOV(32, ::Gen::R(new_loc), ::Gen::Imm32(constant_propagation.GetGPR(preg))); + } + else + { + ASSERT_MSG(DYNA_REC, m_regs[preg].IsInDefaultLocation(), "GPR {} not in default location", + preg); + m_emitter->MOV(32, ::Gen::R(new_loc), m_regs[preg].GetDefaultLocation()); + } } void GPRRegCache::DiscardImm(preg_t preg) @@ -53,8 +108,8 @@ void GPRRegCache::SetImmediate32(preg_t preg, u32 imm_value, bool dirty) { // "dirty" can be false to avoid redundantly flushing an immediate when // processing speculative constants. - DiscardRegContentsIfCached(preg); - m_regs[preg].SetToImm32(imm_value, dirty); + if (dirty) + DiscardRegister(preg); m_jit.GetConstantPropagation().SetGPR(preg, imm_value); } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h index a5bf52426942..9c0b394bad9e 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.h @@ -11,11 +11,18 @@ class GPRRegCache final : public RegCache { public: explicit GPRRegCache(Jit64& jit); + + bool IsImm(preg_t preg) const override; + u32 Imm32(preg_t preg) const override; + s32 SImm32(preg_t preg) const override; + void SetImmediate32(preg_t preg, u32 imm_value, bool dirty = true); protected: + Gen::OpArg R(preg_t preg) const override; Gen::OpArg GetDefaultLocation(preg_t preg) const override; - void StoreRegister(preg_t preg, const Gen::OpArg& new_loc) override; + void StoreRegister(preg_t preg, const Gen::OpArg& new_loc, + IgnoreDiscardedRegisters ignore_discarded_registers) override; void LoadRegister(preg_t preg, Gen::X64Reg new_loc) override; void DiscardImm(preg_t preg) override; std::span GetAllocationOrder() const override; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp index d1e06661709d..2a787f312074 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp @@ -136,7 +136,7 @@ bool RCOpArg::IsImm() const { if (const preg_t* preg = std::get_if(&contents)) { - return rc->R(*preg).IsImm(); + return rc->IsImm(*preg); } else if (std::holds_alternative(contents)) { @@ -149,7 +149,7 @@ s32 RCOpArg::SImm32() const { if (const preg_t* preg = std::get_if(&contents)) { - return rc->R(*preg).SImm32(); + return rc->SImm32(*preg); } else if (const u32* imm = std::get_if(&contents)) { @@ -163,7 +163,7 @@ u32 RCOpArg::Imm32() const { if (const preg_t* preg = std::get_if(&contents)) { - return rc->R(*preg).Imm32(); + return rc->Imm32(*preg); } else if (const u32* imm = std::get_if(&contents)) { @@ -297,25 +297,16 @@ bool RegCache::SanityCheck() const { for (size_t i = 0; i < m_regs.size(); i++) { - switch (m_regs[i].GetLocationType()) - { - case PPCCachedReg::LocationType::Default: - case PPCCachedReg::LocationType::Discarded: - case PPCCachedReg::LocationType::SpeculativeImmediate: - case PPCCachedReg::LocationType::Immediate: - break; - case PPCCachedReg::LocationType::Bound: + if (m_regs[i].IsInHostRegister()) { if (m_regs[i].IsLocked() || m_regs[i].IsRevertable()) return false; - Gen::X64Reg xr = m_regs[i].Location()->GetSimpleReg(); + Gen::X64Reg xr = m_regs[i].GetHostRegister(); if (m_xregs[xr].IsLocked()) return false; if (m_xregs[xr].Contents() != i) return false; - break; - } } } return true; @@ -379,13 +370,7 @@ void RegCache::Discard(BitSet32 pregs) ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction is in progress for {}!", i); - if (m_regs[i].IsBound()) - { - X64Reg xr = RX(i); - m_xregs[xr].Unbind(); - } - - m_regs[i].SetDiscarded(); + DiscardRegister(i); } } @@ -401,25 +386,7 @@ void RegCache::Flush(BitSet32 pregs, IgnoreDiscardedRegisters ignore_discarded_r ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction is in progress for {}!", i); - switch (m_regs[i].GetLocationType()) - { - case PPCCachedReg::LocationType::Default: - break; - case PPCCachedReg::LocationType::Discarded: - ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No, - "Attempted to flush discarded PPC reg {}", i); - break; - case PPCCachedReg::LocationType::SpeculativeImmediate: - // We can have a cached value without a host register through speculative constants. - // It must be cleared when flushing, otherwise it may be out of sync with PPCSTATE, - // if PPCSTATE is modified externally (e.g. fallback to interpreter). - m_regs[i].SetFlushed(); - break; - case PPCCachedReg::LocationType::Bound: - case PPCCachedReg::LocationType::Immediate: - StoreFromRegister(i); - break; - } + StoreFromRegister(i, FlushMode::Full, ignore_discarded_registers); } } @@ -427,9 +394,9 @@ void RegCache::Reset(BitSet32 pregs) { for (preg_t i : pregs) { - ASSERT_MSG(DYNA_REC, !m_regs[i].IsAway(), + ASSERT_MSG(DYNA_REC, !m_regs[i].IsInHostRegister(), "Attempted to reset a loaded register (did you mean to flush it?)"); - m_regs[i].SetFlushed(); + m_regs[i].SetFlushed(false); } } @@ -465,7 +432,7 @@ void RegCache::PreloadRegisters(BitSet32 to_preload) { if (NumFreeRegisters() < 2) return; - if (!R(preg).IsImm()) + if (!IsImm(preg)) BindToRegister(preg, true, false); } } @@ -492,48 +459,46 @@ void RegCache::FlushX(X64Reg reg) } } -void RegCache::DiscardRegContentsIfCached(preg_t preg) +void RegCache::DiscardRegister(preg_t preg) { - if (m_regs[preg].IsBound()) + if (m_regs[preg].IsInHostRegister()) { - X64Reg xr = m_regs[preg].Location()->GetSimpleReg(); + X64Reg xr = m_regs[preg].GetHostRegister(); m_xregs[xr].Unbind(); - m_regs[preg].SetFlushed(); } + + m_regs[preg].SetDiscarded(); } void RegCache::BindToRegister(preg_t i, bool doLoad, bool makeDirty) { - if (!m_regs[i].IsBound()) + if (!m_regs[i].IsInHostRegister()) { X64Reg xr = GetFreeXReg(); - ASSERT_MSG(DYNA_REC, !m_xregs[xr].IsDirty(), "Xreg {} already dirty", Common::ToUnderlying(xr)); ASSERT_MSG(DYNA_REC, !m_xregs[xr].IsLocked(), "GetFreeXReg returned locked register"); ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Invalid transaction state"); - m_xregs[xr].SetBoundTo(i, makeDirty || m_regs[i].IsAway()); + m_xregs[xr].SetBoundTo(i); if (doLoad) - { - ASSERT_MSG(DYNA_REC, !m_regs[i].IsDiscarded(), "Attempted to load a discarded value"); LoadRegister(i, xr); - } ASSERT_MSG(DYNA_REC, - std::ranges::none_of( - m_regs, [xr](const auto& l) { return l.has_value() && l->IsSimpleReg(xr); }, - &PPCCachedReg::Location), + std::ranges::none_of(m_regs, + [xr](const auto& r) { + return r.IsInHostRegister() && r.GetHostRegister() == xr; + }), "Xreg {} already bound", Common::ToUnderlying(xr)); - m_regs[i].SetBoundTo(xr); + m_regs[i].SetInHostRegister(xr, makeDirty); } else { // reg location must be simplereg; memory locations // and immediates are taken care of above. if (makeDirty) - m_xregs[RX(i)].MakeDirty(); + m_regs[i].SetDirty(); } if (makeDirty) @@ -543,36 +508,19 @@ void RegCache::BindToRegister(preg_t i, bool doLoad, bool makeDirty) "WTF, this reg ({} -> {}) should have been flushed", i, Common::ToUnderlying(RX(i))); } -void RegCache::StoreFromRegister(preg_t i, FlushMode mode) +void RegCache::StoreFromRegister(preg_t i, FlushMode mode, + IgnoreDiscardedRegisters ignore_discarded_registers) { // When a transaction is in progress, allowing the store would overwrite the old value. ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction on {} is in progress!", i); - bool doStore = false; + if (!m_regs[i].IsInDefaultLocation()) + StoreRegister(i, GetDefaultLocation(i), ignore_discarded_registers); - switch (m_regs[i].GetLocationType()) - { - case PPCCachedReg::LocationType::Default: - case PPCCachedReg::LocationType::Discarded: - case PPCCachedReg::LocationType::SpeculativeImmediate: - return; - case PPCCachedReg::LocationType::Bound: - { - X64Reg xr = RX(i); - doStore = m_xregs[xr].IsDirty(); - if (mode == FlushMode::Full) - m_xregs[xr].Unbind(); - break; - } - case PPCCachedReg::LocationType::Immediate: - doStore = true; - break; - } + if (mode == FlushMode::Full && m_regs[i].IsInHostRegister()) + m_xregs[m_regs[i].GetHostRegister()].Unbind(); - if (doStore) - StoreRegister(i, GetDefaultLocation(i)); - if (mode == FlushMode::Full) - m_regs[i].SetFlushed(); + m_regs[i].SetFlushed(mode != FlushMode::Full); } X64Reg RegCache::GetFreeXReg() @@ -637,7 +585,7 @@ float RegCache::ScoreRegister(X64Reg xreg) const // bias a bit against dirty registers. Testing shows that a bias of 2 seems roughly // right: 3 causes too many extra clobbers, while 1 saves very few clobbers relative // to the number of extra stores it causes. - if (m_xregs[xreg].IsDirty()) + if (!m_regs[preg].IsInDefaultLocation()) score += 2; // If the register isn't actually needed in a physical register for a later instruction, @@ -658,16 +606,10 @@ float RegCache::ScoreRegister(X64Reg xreg) const return score; } -const OpArg& RegCache::R(preg_t preg) const -{ - ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - {}", preg); - return m_regs[preg].Location().value(); -} - X64Reg RegCache::RX(preg_t preg) const { - ASSERT_MSG(DYNA_REC, m_regs[preg].IsBound(), "Unbound register - {}", preg); - return m_regs[preg].Location()->GetSimpleReg(); + ASSERT_MSG(DYNA_REC, m_regs[preg].IsInHostRegister(), "Not in host register - {}", preg); + return m_regs[preg].GetHostRegister(); } void RegCache::Lock(preg_t preg) @@ -723,29 +665,23 @@ void RegCache::Realize(preg_t preg) return; } - switch (m_regs[preg].GetLocationType()) + if (IsImm(preg)) + { + if (dirty || kill_imm) + do_bind(); + else + m_constraints[preg].Realized(RCConstraint::RealizedLoc::Imm); + } + else if (!m_regs[preg].IsInHostRegister()) { - case PPCCachedReg::LocationType::Default: if (kill_mem) - { do_bind(); - return; - } - m_constraints[preg].Realized(RCConstraint::RealizedLoc::Mem); - return; - case PPCCachedReg::LocationType::Discarded: - case PPCCachedReg::LocationType::Bound: + else + m_constraints[preg].Realized(RCConstraint::RealizedLoc::Mem); + } + else + { do_bind(); - return; - case PPCCachedReg::LocationType::Immediate: - case PPCCachedReg::LocationType::SpeculativeImmediate: - if (dirty || kill_imm) - { - do_bind(); - return; - } - m_constraints[preg].Realized(RCConstraint::RealizedLoc::Imm); - break; } } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h index 1ff4e27ea785..0a7ab3836d2a 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h @@ -157,12 +157,14 @@ class RegCache bool IsImm(Args... pregs) const { static_assert(sizeof...(pregs) > 0); - return (R(pregs).IsImm() && ...); + return (IsImm(preg_t(pregs)) && ...); } - u32 Imm32(preg_t preg) const { return R(preg).Imm32(); } - s32 SImm32(preg_t preg) const { return R(preg).SImm32(); } - bool IsBound(preg_t preg) const { return m_regs[preg].IsBound(); } + virtual bool IsImm(preg_t preg) const = 0; + virtual u32 Imm32(preg_t preg) const = 0; + virtual s32 SImm32(preg_t preg) const = 0; + + bool IsBound(preg_t preg) const { return m_regs[preg].IsInHostRegister(); } RCOpArg Use(preg_t preg, RCMode mode); RCOpArg UseNoImm(preg_t preg, RCMode mode); @@ -191,7 +193,8 @@ class RegCache friend class RCForkGuard; virtual Gen::OpArg GetDefaultLocation(preg_t preg) const = 0; - virtual void StoreRegister(preg_t preg, const Gen::OpArg& new_loc) = 0; + virtual void StoreRegister(preg_t preg, const Gen::OpArg& new_loc, + IgnoreDiscardedRegisters ignore_discarded_registers) = 0; virtual void LoadRegister(preg_t preg, Gen::X64Reg new_loc) = 0; virtual void DiscardImm(preg_t preg) = 0; @@ -201,16 +204,18 @@ class RegCache virtual BitSet32 CountRegsIn(preg_t preg, u32 lookahead) const = 0; void FlushX(Gen::X64Reg reg); - void DiscardRegContentsIfCached(preg_t preg); + void DiscardRegister(preg_t preg); void BindToRegister(preg_t preg, bool doLoad = true, bool makeDirty = true); - void StoreFromRegister(preg_t preg, FlushMode mode = FlushMode::Full); + void StoreFromRegister( + preg_t preg, FlushMode mode = FlushMode::Full, + IgnoreDiscardedRegisters ignore_discarded_registers = IgnoreDiscardedRegisters::No); Gen::X64Reg GetFreeXReg(); int NumFreeRegisters() const; float ScoreRegister(Gen::X64Reg xreg) const; - const Gen::OpArg& R(preg_t preg) const; + virtual Gen::OpArg R(preg_t preg) const = 0; Gen::X64Reg RX(preg_t preg) const; void Lock(preg_t preg); From 2995aa5be4e09aa880c0e2303084b81d1effa62e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 1 Sep 2024 15:34:17 +0200 Subject: [PATCH 054/123] JitArm64: Don't store immediate values in register cache Like the previous commit did for Jit64, JitArm64 can now handle the combination of a value simultaneously being in a host register and being a known immediate. Unlike with Jit64, I've put the codegen-affecting changes in this commit and the move away from the RegType enum in a follow-up commit. This is in part because the design of JitArm64 made it easy to implement the codegen-affecting changes without combining it with a big bang refactorization, and in part because we need to keep RegType around for keeping track of different float formats in Arm64FPRCache, complicating the refactorization a bit. --- Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 3 + .../PowerPC/JitArm64/JitArm64_RegCache.cpp | 90 +++++++++++-------- .../Core/PowerPC/JitArm64/JitArm64_RegCache.h | 22 ++--- 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index 975f5f95f7d1..37bfc4c24acd 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -1362,7 +1362,10 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) m_constant_propagation.Apply(constant_propagation_result); if (constant_propagation_result.gpr >= 0) + { + // Mark the GPR as dirty in the register cache gpr.SetImmediate(constant_propagation_result.gpr, constant_propagation_result.gpr_value); + } if (constant_propagation_result.instruction_fully_executed) { diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index d0c9ac71bde4..161a1e01e3b0 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -115,7 +115,7 @@ void Arm64RegCache::FlushMostStaleRegister() const u32 last_used = reg.GetLastUsed(); if (last_used > most_stale_amount && reg.GetType() != RegType::NotLoaded && - reg.GetType() != RegType::Discarded && reg.GetType() != RegType::Immediate) + reg.GetType() != RegType::Discarded) { most_stale_preg = i; most_stale_amount = last_used; @@ -145,6 +145,19 @@ void Arm64GPRCache::Start(PPCAnalyst::BlockRegStats& stats) { } +// Returns if a register is set as an immediate. Only valid for guest GPRs. +bool Arm64GPRCache::IsImm(size_t preg) const +{ + return m_jit->GetConstantPropagation().HasGPR(preg); +} + +// Gets the immediate that a register is set to. Only valid for guest GPRs. +u32 Arm64GPRCache::GetImm(size_t preg) const +{ + ASSERT(m_jit->GetConstantPropagation().HasGPR(preg)); + return m_jit->GetConstantPropagation().GetGPR(preg); +} + bool Arm64GPRCache::IsCallerSaved(ARM64Reg reg) const { return ARM64XEmitter::CALLER_SAVED_GPRS[DecodeReg(reg)]; @@ -186,6 +199,7 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; size_t bitsize = guest_reg.bitsize; + const bool is_gpr = index >= GUEST_GPR_OFFSET && index < GUEST_GPR_OFFSET + GUEST_GPR_COUNT; if (reg.GetType() == RegType::Register) { @@ -199,11 +213,12 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg reg.Flush(); } } - else if (reg.GetType() == RegType::Immediate) + else if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { if (reg.IsDirty()) { - if (!reg.GetImm()) + const u32 imm = GetImm(index - GUEST_GPR_OFFSET); + if (imm == 0) { m_emit->STR(IndexType::Unsigned, bitsize == 64 ? ARM64Reg::ZR : ARM64Reg::WZR, PPC_REG, u32(guest_reg.ppc_offset)); @@ -225,7 +240,7 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg const ARM64Reg encoded_tmp_reg = bitsize != 64 ? tmp_reg : EncodeRegTo64(tmp_reg); - m_emit->MOVI2R(encoded_tmp_reg, reg.GetImm()); + m_emit->MOVI2R(encoded_tmp_reg, imm); m_emit->STR(IndexType::Unsigned, encoded_tmp_reg, PPC_REG, u32(guest_reg.ppc_offset)); if (allocated_tmp_reg) @@ -244,10 +259,10 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r for (auto iter = regs.begin(); iter != regs.end(); ++iter) { const int i = *iter; - ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No || - m_guest_registers[GUEST_GPR_OFFSET + i].GetType() != RegType::Discarded, + m_guest_registers[GUEST_GPR_OFFSET + i].GetType() != RegType::Discarded || + IsImm(i), "Attempted to flush discarded register"); if (i + 1 < int(GUEST_GPR_COUNT) && regs[i + 1]) @@ -255,10 +270,10 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r // We've got two guest registers in a row to store OpArg& reg1 = m_guest_registers[GUEST_GPR_OFFSET + i]; OpArg& reg2 = m_guest_registers[GUEST_GPR_OFFSET + i + 1]; - const bool reg1_imm = reg1.GetType() == RegType::Immediate; - const bool reg2_imm = reg2.GetType() == RegType::Immediate; - const bool reg1_zero = reg1_imm && reg1.GetImm() == 0; - const bool reg2_zero = reg2_imm && reg2.GetImm() == 0; + const bool reg1_imm = IsImm(i); + const bool reg2_imm = IsImm(i + 1); + const bool reg1_zero = reg1_imm && GetImm(i) == 0; + const bool reg2_zero = reg2_imm && GetImm(i + 1) == 0; const bool flush_all = mode == FlushMode::All; if (reg1.IsDirty() && reg2.IsDirty() && (reg1.GetType() == RegType::Register || (reg1_imm && (reg1_zero || flush_all))) && @@ -334,6 +349,7 @@ ARM64Reg Arm64GPRCache::BindForRead(size_t index) GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; size_t bitsize = guest_reg.bitsize; + const bool is_gpr = index >= GUEST_GPR_OFFSET && index < GUEST_GPR_OFFSET + GUEST_GPR_COUNT; IncrementAllUsed(); reg.ResetLastUsed(); @@ -342,17 +358,15 @@ ARM64Reg Arm64GPRCache::BindForRead(size_t index) { case RegType::Register: // already in a reg return reg.GetReg(); - case RegType::Immediate: // Is an immediate + case RegType::Discarded: // Is an immediate or discarded { + ASSERT_MSG(DYNA_REC, is_gpr && IsImm(index - GUEST_GPR_OFFSET), + "Attempted to read discarded register"); ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); - m_emit->MOVI2R(host_reg, reg.GetImm()); + m_emit->MOVI2R(host_reg, GetImm(index - GUEST_GPR_OFFSET)); reg.Load(host_reg); return host_reg; } - break; - case RegType::Discarded: - ASSERT_MSG(DYNA_REC, false, "Attempted to read discarded register"); - break; case RegType::NotLoaded: // Register isn't loaded at /all/ { // This is a bit annoying. We try to keep these preloaded as much as possible @@ -379,7 +393,7 @@ void Arm64GPRCache::SetImmediateInternal(size_t index, u32 imm, bool dirty) OpArg& reg = guest_reg.reg; if (reg.GetType() == RegType::Register) UnlockRegister(EncodeRegTo32(reg.GetReg())); - reg.LoadToImm(imm); + reg.Discard(); reg.SetDirty(dirty); m_jit->GetConstantPropagation().SetGPR(index - GUEST_GPR_OFFSET, imm); } @@ -394,28 +408,32 @@ void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) reg.ResetLastUsed(); const RegType reg_type = reg.GetType(); - if (reg_type == RegType::NotLoaded || reg_type == RegType::Discarded) + if (reg_type != RegType::Register) { - const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); - reg.Load(host_reg); - reg.SetDirty(will_write); - if (will_read) + if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { - ASSERT_MSG(DYNA_REC, reg_type != RegType::Discarded, "Attempted to load a discarded value"); - m_emit->LDR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); + const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); + if (will_read || !will_write) + { + // TODO: Emitting this instruction when (!will_read && !will_write) would be unnecessary if + // we had some way to indicate to Flush that the immediate value should be written to + // ppcState even though there is a host register allocated + m_emit->MOVI2R(host_reg, GetImm(index - GUEST_GPR_OFFSET)); + } + reg.Load(host_reg); } - } - else if (reg_type == RegType::Immediate) - { - const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); - if (will_read || !will_write) + else { - // TODO: Emitting this instruction when (!will_read && !will_write) would be unnecessary if we - // had some way to indicate to Flush that the immediate value should be written to ppcState - // even though there is a host register allocated - m_emit->MOVI2R(host_reg, reg.GetImm()); + const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); + reg.Load(host_reg); + reg.SetDirty(will_write); + if (will_read) + { + ASSERT_MSG(DYNA_REC, reg_type != RegType::Discarded, "Attempted to load a discarded value"); + m_emit->LDR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); + } + return; } - reg.Load(host_reg); } if (will_write) @@ -521,7 +539,7 @@ void Arm64FPRCache::Flush(FlushMode mode, ARM64Reg tmp_reg, ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No, "Attempted to flush discarded register"); } - else if (reg_type != RegType::NotLoaded && reg_type != RegType::Immediate) + else if (reg_type != RegType::NotLoaded) { FlushRegister(i, mode, tmp_reg); } @@ -785,7 +803,7 @@ void Arm64FPRCache::FlushByHost(ARM64Reg host_reg, ARM64Reg tmp_reg) const RegType reg_type = reg.GetType(); if (reg_type != RegType::NotLoaded && reg_type != RegType::Discarded && - reg_type != RegType::Immediate && reg.GetReg() == host_reg) + reg.GetReg() == host_reg) { FlushRegister(i, FlushMode::All, tmp_reg); return; diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h index c973e3bc285a..82c0d941427f 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h @@ -61,9 +61,8 @@ static_assert(PPCSTATE_OFF(xer_so_ov) < 4096, "STRB can't store xer_so_ov!"); enum class RegType { NotLoaded, - Discarded, // Reg is not loaded because we know it won't be read before the next write + Discarded, // Reg is in ConstantPropagation, or isn't loaded at all Register, // Reg type is register - Immediate, // Reg is really a IMM LowerPair, // Only the lower pair of a paired register Duplicated, // The lower reg is the same as the upper one (physical upper doesn't actually have // the duplicated value) @@ -94,24 +93,17 @@ class OpArg RegType GetType() const { return m_type; } Arm64Gen::ARM64Reg GetReg() const { return m_reg; } - u32 GetImm() const { return m_value; } void Load(Arm64Gen::ARM64Reg reg, RegType type = RegType::Register) { m_type = type; m_reg = reg; } - void LoadToImm(u32 imm) - { - m_type = RegType::Immediate; - m_value = imm; - - m_reg = Arm64Gen::ARM64Reg::INVALID_REG; - } void Discard() { // Invalidate any previous information m_type = RegType::Discarded; m_reg = Arm64Gen::ARM64Reg::INVALID_REG; + m_dirty = true; // Arbitrarily large value that won't roll over on a lot of increments m_last_used = 0xFFFF; @@ -121,6 +113,7 @@ class OpArg // Invalidate any previous information m_type = RegType::NotLoaded; m_reg = Arm64Gen::ARM64Reg::INVALID_REG; + m_dirty = false; // Arbitrarily large value that won't roll over on a lot of increments m_last_used = 0xFFFF; @@ -137,9 +130,6 @@ class OpArg RegType m_type = RegType::NotLoaded; // store type Arm64Gen::ARM64Reg m_reg = Arm64Gen::ARM64Reg::INVALID_REG; // host register we are in - // For REG_IMM - u32 m_value = 0; // IMM value - u32 m_last_used = 0; bool m_dirty = false; @@ -339,11 +329,11 @@ class Arm64GPRCache : public Arm64RegCache SetImmediateInternal(GUEST_GPR_OFFSET + preg, imm, dirty); } - // Returns if a register is set as an immediate. Only valid for guest GPRs. - bool IsImm(size_t preg) const { return GetGuestGPROpArg(preg).GetType() == RegType::Immediate; } + // Returns whether a register is set as an immediate. Only valid for guest GPRs. + bool IsImm(size_t preg) const; // Gets the immediate that a register is set to. Only valid for guest GPRs. - u32 GetImm(size_t preg) const { return GetGuestGPROpArg(preg).GetImm(); } + u32 GetImm(size_t preg) const; bool IsImm(size_t preg, u32 imm) const { return IsImm(preg) && GetImm(preg) == imm; } From b9d9f36ce554e3af6f4f906047a3eaeebaa8030a Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 23 Oct 2024 20:55:51 +0200 Subject: [PATCH 055/123] JitArm64: Replace dirty flag and partially replace RegType enum Like Jit64, JitArm64 now keeps track of the location of a guest register using three booleans: Whether it is in ppcState, whether it is in a host register, and whether it is a known immediate. The RegType enum remains only for the purpose of keeping track of what format FPRs are stored in in host registers. --- .../PowerPC/JitArm64/JitArm64_RegCache.cpp | 172 ++++++++---------- .../Core/PowerPC/JitArm64/JitArm64_RegCache.h | 47 ++--- 2 files changed, 103 insertions(+), 116 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index 161a1e01e3b0..8ba7a6a3be4e 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -114,8 +114,7 @@ void Arm64RegCache::FlushMostStaleRegister() const auto& reg = m_guest_registers[i]; const u32 last_used = reg.GetLastUsed(); - if (last_used > most_stale_amount && reg.GetType() != RegType::NotLoaded && - reg.GetType() != RegType::Discarded) + if (last_used > most_stale_amount && reg.IsInHostRegister()) { most_stale_preg = i; most_stale_amount = last_used; @@ -201,10 +200,10 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg size_t bitsize = guest_reg.bitsize; const bool is_gpr = index >= GUEST_GPR_OFFSET && index < GUEST_GPR_OFFSET + GUEST_GPR_COUNT; - if (reg.GetType() == RegType::Register) + if (reg.IsInHostRegister()) { ARM64Reg host_reg = reg.GetReg(); - if (reg.IsDirty()) + if (!reg.IsInPPCState()) m_emit->STR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); if (mode == FlushMode::All) @@ -215,7 +214,7 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg } else if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { - if (reg.IsDirty()) + if (!reg.IsInPPCState()) { const u32 imm = GetImm(index - GUEST_GPR_OFFSET); if (imm == 0) @@ -259,10 +258,10 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r for (auto iter = regs.begin(); iter != regs.end(); ++iter) { const int i = *iter; + OpArg& reg = m_guest_registers[GUEST_GPR_OFFSET + i]; ASSERT_MSG(DYNA_REC, - ignore_discarded_registers != IgnoreDiscardedRegisters::No || - m_guest_registers[GUEST_GPR_OFFSET + i].GetType() != RegType::Discarded || - IsImm(i), + ignore_discarded_registers != IgnoreDiscardedRegisters::No || reg.IsInPPCState() || + reg.IsInHostRegister() || IsImm(i), "Attempted to flush discarded register"); if (i + 1 < int(GUEST_GPR_COUNT) && regs[i + 1]) @@ -275,9 +274,9 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r const bool reg1_zero = reg1_imm && GetImm(i) == 0; const bool reg2_zero = reg2_imm && GetImm(i + 1) == 0; const bool flush_all = mode == FlushMode::All; - if (reg1.IsDirty() && reg2.IsDirty() && - (reg1.GetType() == RegType::Register || (reg1_imm && (reg1_zero || flush_all))) && - (reg2.GetType() == RegType::Register || (reg2_imm && (reg2_zero || flush_all)))) + if (!reg1.IsInPPCState() && !reg2.IsInPPCState() && + (reg1.IsInHostRegister() || (reg1_imm && (reg1_zero || flush_all))) && + (reg2.IsInHostRegister() || (reg2_imm && (reg2_zero || flush_all)))) { const size_t ppc_offset = GetGuestByIndex(i).ppc_offset; if (ppc_offset <= 252) @@ -287,9 +286,9 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r m_emit->STP(IndexType::Signed, RX1, RX2, PPC_REG, u32(ppc_offset)); if (flush_all) { - if (reg1.GetType() == RegType::Register) + if (reg1.IsInHostRegister()) UnlockRegister(reg1.GetReg()); - if (reg2.GetType() == RegType::Register) + if (reg2.IsInHostRegister()) UnlockRegister(reg2.GetReg()); reg1.Flush(); reg2.Flush(); @@ -309,9 +308,10 @@ void Arm64GPRCache::FlushCRRegisters(BitSet8 regs, FlushMode mode, ARM64Reg tmp_ { for (int i : regs) { + OpArg& reg = m_guest_registers[GUEST_CR_OFFSET + i]; ASSERT_MSG(DYNA_REC, - ignore_discarded_registers != IgnoreDiscardedRegisters::No || - m_guest_registers[GUEST_CR_OFFSET + i].GetType() != RegType::Discarded, + ignore_discarded_registers != IgnoreDiscardedRegisters::No || reg.IsInPPCState() || + reg.IsInHostRegister(), "Attempted to flush discarded register"); FlushRegister(GUEST_CR_OFFSET + i, mode, tmp_reg); @@ -354,44 +354,33 @@ ARM64Reg Arm64GPRCache::BindForRead(size_t index) IncrementAllUsed(); reg.ResetLastUsed(); - switch (reg.GetType()) + if (reg.IsInHostRegister()) { - case RegType::Register: // already in a reg return reg.GetReg(); - case RegType::Discarded: // Is an immediate or discarded + } + else if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { - ASSERT_MSG(DYNA_REC, is_gpr && IsImm(index - GUEST_GPR_OFFSET), - "Attempted to read discarded register"); ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); m_emit->MOVI2R(host_reg, GetImm(index - GUEST_GPR_OFFSET)); reg.Load(host_reg); return host_reg; } - case RegType::NotLoaded: // Register isn't loaded at /all/ + else // Register isn't loaded at /all/ { - // This is a bit annoying. We try to keep these preloaded as much as possible - // This can also happen on cases where PPCAnalyst isn't feeing us proper register usage - // statistics + ASSERT_MSG(DYNA_REC, reg.IsInPPCState(), "Attempted to read discarded register"); ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); reg.Load(host_reg); reg.SetDirty(false); m_emit->LDR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); return host_reg; } - break; - default: - ERROR_LOG_FMT(DYNA_REC, "Invalid OpArg Type!"); - break; - } - // We've got an issue if we end up here - return ARM64Reg::INVALID_REG; } void Arm64GPRCache::SetImmediateInternal(size_t index, u32 imm, bool dirty) { GuestRegInfo guest_reg = GetGuestByIndex(index); OpArg& reg = guest_reg.reg; - if (reg.GetType() == RegType::Register) + if (reg.IsInHostRegister()) UnlockRegister(EncodeRegTo32(reg.GetReg())); reg.Discard(); reg.SetDirty(dirty); @@ -407,8 +396,7 @@ void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) reg.ResetLastUsed(); - const RegType reg_type = reg.GetType(); - if (reg_type != RegType::Register) + if (!reg.IsInHostRegister()) { if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { @@ -424,14 +412,12 @@ void Arm64GPRCache::BindForWrite(size_t index, bool will_read, bool will_write) } else { + ASSERT_MSG(DYNA_REC, !will_read || reg.IsInPPCState(), "Attempted to load a discarded value"); const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg()); reg.Load(host_reg); reg.SetDirty(will_write); if (will_read) - { - ASSERT_MSG(DYNA_REC, reg_type != RegType::Discarded, "Attempted to load a discarded value"); m_emit->LDR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); - } return; } } @@ -502,7 +488,7 @@ BitSet32 Arm64GPRCache::GetDirtyGPRs() const for (size_t i = 0; i < GUEST_GPR_COUNT; ++i) { const OpArg& arg = m_guest_registers[GUEST_GPR_OFFSET + i]; - registers[i] = arg.GetType() != RegType::NotLoaded && arg.IsDirty(); + registers[i] = !arg.IsInPPCState(); } return registers; } @@ -512,7 +498,7 @@ void Arm64GPRCache::FlushByHost(ARM64Reg host_reg, ARM64Reg tmp_reg) for (size_t i = 0; i < m_guest_registers.size(); ++i) { const OpArg& reg = m_guest_registers[i]; - if (reg.GetType() == RegType::Register && DecodeReg(reg.GetReg()) == DecodeReg(host_reg)) + if (reg.IsInHostRegister() && DecodeReg(reg.GetReg()) == DecodeReg(host_reg)) { FlushRegister(i, FlushMode::All, tmp_reg); return; @@ -532,16 +518,16 @@ void Arm64FPRCache::Flush(FlushMode mode, ARM64Reg tmp_reg, { for (size_t i = 0; i < m_guest_registers.size(); ++i) { - const RegType reg_type = m_guest_registers[i].GetType(); - - if (reg_type == RegType::Discarded) + if (m_guest_registers[i].IsInHostRegister()) { - ASSERT_MSG(DYNA_REC, ignore_discarded_registers != IgnoreDiscardedRegisters::No, - "Attempted to flush discarded register"); + FlushRegister(i, mode, tmp_reg); } - else if (reg_type != RegType::NotLoaded) + else { - FlushRegister(i, mode, tmp_reg); + ASSERT_MSG(DYNA_REC, + ignore_discarded_registers != IgnoreDiscardedRegisters::No || + m_guest_registers[i].IsInPPCState(), + "Attempted to flush discarded register"); } } } @@ -551,9 +537,32 @@ ARM64Reg Arm64FPRCache::R(size_t preg, RegType type) OpArg& reg = m_guest_registers[preg]; IncrementAllUsed(); reg.ResetLastUsed(); + + if (!reg.IsInHostRegister()) + { + ASSERT_MSG(DYNA_REC, reg.IsInPPCState(), "Attempted to read discarded register"); + + ARM64Reg host_reg = GetReg(); + u32 load_size; + if (type == RegType::Register) + { + load_size = 128; + reg.Load(host_reg, RegType::Register); + } + else + { + load_size = 64; + reg.Load(host_reg, RegType::LowerPair); + } + reg.SetDirty(false); + m_float_emit->LDR(load_size, IndexType::Unsigned, host_reg, PPC_REG, + static_cast(PPCSTATE_OFF_PS0(preg))); + return host_reg; + } + ARM64Reg host_reg = reg.GetReg(); - switch (reg.GetType()) + switch (reg.GetFPRType()) { case RegType::Single: { @@ -636,28 +645,6 @@ ARM64Reg Arm64FPRCache::R(size_t preg, RegType type) } return host_reg; } - case RegType::Discarded: - ASSERT_MSG(DYNA_REC, false, "Attempted to read discarded register"); - break; - case RegType::NotLoaded: // Register isn't loaded at /all/ - { - host_reg = GetReg(); - u32 load_size; - if (type == RegType::Register) - { - load_size = 128; - reg.Load(host_reg, RegType::Register); - } - else - { - load_size = 64; - reg.Load(host_reg, RegType::LowerPair); - } - reg.SetDirty(false); - m_float_emit->LDR(load_size, IndexType::Unsigned, host_reg, PPC_REG, - static_cast(PPCSTATE_OFF_PS0(preg))); - return host_reg; - } default: DEBUG_ASSERT_MSG(DYNA_REC, false, "Invalid OpArg Type!"); break; @@ -673,16 +660,17 @@ ARM64Reg Arm64FPRCache::RW(size_t preg, RegType type, bool set_dirty) IncrementAllUsed(); reg.ResetLastUsed(); - // Only the lower value will be overwritten, so we must be extra careful to store PSR1 if dirty. - if (reg.IsDirty() && (type == RegType::LowerPair || type == RegType::LowerPairSingle)) + // If PS1 is dirty, but the caller wants a RegType with only PS0, we must write PS1 to m_ppc_state + // now so the contents of PS1 aren't lost. + if (!reg.IsInPPCState() && (type == RegType::LowerPair || type == RegType::LowerPairSingle)) { - // We must *not* change host_reg as this register might still be in use. So it's fine to - // store this register, but it's *not* fine to convert it to double. So for double conversion, - // a temporary register needs to be used. + // We must *not* modify host_reg, as the current guest instruction might want to read its old + // value before overwriting it. So it's fine to store this register, but it's *not* fine to + // convert it to double in place. For double conversion, a temporary register needs to be used. ARM64Reg host_reg = reg.GetReg(); ARM64Reg flush_reg = host_reg; - switch (reg.GetType()) + switch (reg.GetFPRType()) { case RegType::Single: // For a store-safe register, conversion is just one instruction regardless of whether @@ -724,8 +712,8 @@ ARM64Reg Arm64FPRCache::RW(size_t preg, RegType type, bool set_dirty) // Store PSR1 (which is equal to PSR0) in memory. m_float_emit->STR(64, IndexType::Unsigned, flush_reg, PPC_REG, static_cast(PPCSTATE_OFF_PS1(preg))); - reg.Load(host_reg, reg.GetType() == RegType::DuplicatedSingle ? RegType::LowerPairSingle : - RegType::LowerPair); + reg.Load(host_reg, reg.GetFPRType() == RegType::DuplicatedSingle ? RegType::LowerPairSingle : + RegType::LowerPair); break; default: // All other types doesn't store anything in PSR1. @@ -736,7 +724,7 @@ ARM64Reg Arm64FPRCache::RW(size_t preg, RegType type, bool set_dirty) Unlock(flush_reg); } - if (reg.GetType() == RegType::NotLoaded || reg.GetType() == RegType::Discarded) + if (!reg.IsInHostRegister()) { // If not loaded at all, just alloc a new one. reg.Load(GetReg(), type); @@ -800,10 +788,8 @@ void Arm64FPRCache::FlushByHost(ARM64Reg host_reg, ARM64Reg tmp_reg) for (size_t i = 0; i < m_guest_registers.size(); ++i) { const OpArg& reg = m_guest_registers[i]; - const RegType reg_type = reg.GetType(); - if (reg_type != RegType::NotLoaded && reg_type != RegType::Discarded && - reg.GetReg() == host_reg) + if (reg.IsInHostRegister() && reg.GetReg() == host_reg) { FlushRegister(i, FlushMode::All, tmp_reg); return; @@ -820,8 +806,8 @@ bool Arm64FPRCache::IsTopHalfUsed(ARM64Reg reg) const { for (const OpArg& r : m_guest_registers) { - if (r.GetReg() != ARM64Reg::INVALID_REG && DecodeReg(r.GetReg()) == DecodeReg(reg)) - return r.GetType() == RegType::Register; + if (r.IsInHostRegister() && DecodeReg(r.GetReg()) == DecodeReg(reg)) + return r.GetFPRType() == RegType::Register; } return false; @@ -831,8 +817,8 @@ void Arm64FPRCache::FlushRegister(size_t preg, FlushMode mode, ARM64Reg tmp_reg) { OpArg& reg = m_guest_registers[preg]; const ARM64Reg host_reg = reg.GetReg(); - const bool dirty = reg.IsDirty(); - RegType type = reg.GetType(); + const bool dirty = !reg.IsInPPCState(); + RegType type = reg.GetFPRType(); bool allocated_tmp_reg = false; if (tmp_reg != ARM64Reg::INVALID_REG) @@ -939,7 +925,7 @@ BitSet32 Arm64FPRCache::GetCallerSavedUsed() const bool Arm64FPRCache::IsSingle(size_t preg, bool lower_only) const { - const RegType type = m_guest_registers[preg].GetType(); + const RegType type = m_guest_registers[preg].GetFPRType(); return type == RegType::Single || type == RegType::DuplicatedSingle || (lower_only && type == RegType::LowerPairSingle); } @@ -947,18 +933,18 @@ bool Arm64FPRCache::IsSingle(size_t preg, bool lower_only) const void Arm64FPRCache::FixSinglePrecision(size_t preg) { OpArg& reg = m_guest_registers[preg]; + if (!reg.IsInHostRegister()) + return; + ARM64Reg host_reg = reg.GetReg(); - switch (reg.GetType()) + if (reg.GetFPRType() == RegType::Duplicated) // only PS0 needs to be converted { - case RegType::Duplicated: // only PS0 needs to be converted m_float_emit->FCVT(32, 64, EncodeRegToDouble(host_reg), EncodeRegToDouble(host_reg)); reg.Load(host_reg, RegType::DuplicatedSingle); - break; - case RegType::Register: // PS0 and PS1 need to be converted + } + else if (reg.GetFPRType() == RegType::Register) // PS0 and PS1 need to be converted + { m_float_emit->FCVTN(32, EncodeRegToDouble(host_reg), EncodeRegToDouble(host_reg)); reg.Load(host_reg, RegType::Single); - break; - default: - break; } } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h index 82c0d941427f..d547ee9d4eed 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h @@ -60,15 +60,12 @@ static_assert(PPCSTATE_OFF(xer_so_ov) < 4096, "STRB can't store xer_so_ov!"); enum class RegType { - NotLoaded, - Discarded, // Reg is in ConstantPropagation, or isn't loaded at all - Register, // Reg type is register - LowerPair, // Only the lower pair of a paired register - Duplicated, // The lower reg is the same as the upper one (physical upper doesn't actually have - // the duplicated value) - Single, // Both registers are loaded as single - LowerPairSingle, // Only the lower pair of a paired register, as single - DuplicatedSingle, // The lower one contains both registers, as single + Register, // PS0 and PS1, each 64-bit + LowerPair, // PS0 only, 64-bit + Duplicated, // PS0 and PS1 are identical, host register only stores one lane (64-bit) + Single, // PS0 and PS1, each 32-bit + LowerPairSingle, // PS0 only, 32-bit + DuplicatedSingle, // PS0 and PS1 are identical, host register only stores one lane (32-bit) }; enum class FlushMode : bool @@ -91,19 +88,21 @@ class OpArg public: OpArg() = default; - RegType GetType() const { return m_type; } + RegType GetFPRType() const { return m_fpr_type; } Arm64Gen::ARM64Reg GetReg() const { return m_reg; } - void Load(Arm64Gen::ARM64Reg reg, RegType type = RegType::Register) + void Load(Arm64Gen::ARM64Reg reg, RegType format = RegType::Register) { - m_type = type; m_reg = reg; + m_fpr_type = format; + m_in_host_register = true; } void Discard() { // Invalidate any previous information - m_type = RegType::Discarded; m_reg = Arm64Gen::ARM64Reg::INVALID_REG; - m_dirty = true; + m_fpr_type = RegType::Register; + m_in_ppc_state = false; + m_in_host_register = false; // Arbitrarily large value that won't roll over on a lot of increments m_last_used = 0xFFFF; @@ -111,9 +110,10 @@ class OpArg void Flush() { // Invalidate any previous information - m_type = RegType::NotLoaded; m_reg = Arm64Gen::ARM64Reg::INVALID_REG; - m_dirty = false; + m_fpr_type = RegType::Register; + m_in_ppc_state = true; + m_in_host_register = false; // Arbitrarily large value that won't roll over on a lot of increments m_last_used = 0xFFFF; @@ -122,17 +122,18 @@ class OpArg u32 GetLastUsed() const { return m_last_used; } void ResetLastUsed() { m_last_used = 0; } void IncrementLastUsed() { ++m_last_used; } - void SetDirty(bool dirty) { m_dirty = dirty; } - bool IsDirty() const { return m_dirty; } + void SetDirty(bool dirty) { m_in_ppc_state = !dirty; } + bool IsInPPCState() const { return m_in_ppc_state; } + bool IsInHostRegister() const { return m_in_host_register; } private: - // For REG_REG - RegType m_type = RegType::NotLoaded; // store type Arm64Gen::ARM64Reg m_reg = Arm64Gen::ARM64Reg::INVALID_REG; // host register we are in + RegType m_fpr_type = RegType::Register; // for FPRs only u32 m_last_used = 0; - bool m_dirty = false; + bool m_in_ppc_state = true; + bool m_in_host_register = false; }; class HostReg @@ -446,9 +447,9 @@ class Arm64FPRCache : public Arm64RegCache // Returns a guest register inside of a host register // Will dump an immediate to the host register as well - Arm64Gen::ARM64Reg R(size_t preg, RegType type); + Arm64Gen::ARM64Reg R(size_t preg, RegType format); - Arm64Gen::ARM64Reg RW(size_t preg, RegType type, bool set_dirty = true); + Arm64Gen::ARM64Reg RW(size_t preg, RegType format, bool set_dirty = true); BitSet32 GetCallerSavedUsed() const override; From 41ab5c0ead382844964e107ceb0c8c89dae5d678 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 16 Nov 2025 14:51:56 +0100 Subject: [PATCH 056/123] Android: Ship Sys/Resources/ We were previously excluding this folder from Android builds because it didn't contain any files that were used on Android. However, we now have an OSD font file that we do want to use on Android, and there's also a few PNG files that will be needed by the RetroAchievements integration. In terms of file size, this is what gets added: OSD font: 48.1 KiB RetroAchievements graphics: 3.5 KiB Unused graphics: 116.8 KiB We're still excluding Sys/Themes/, which is 1.1 MiB and entirely unused. --- Source/Android/jni/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index be200affa9b6..c6d542bf08c5 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -66,7 +66,6 @@ file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/) file(COPY ${CMAKE_SOURCE_DIR}/Data/Sys DESTINATION ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/) # Delete folders that aren't used by the Android version of Dolphin -file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/Resources/) file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/Themes/) set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} main) From 3fcc0427c901cb44d553c090c39358e939661019 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Wed, 12 Nov 2025 12:45:18 -0500 Subject: [PATCH 057/123] Android: Convert NativeLibrary to Kotlin --- .../dolphinemu/dolphinemu/NativeLibrary.java | 601 ----------------- .../dolphinemu/dolphinemu/NativeLibrary.kt | 603 ++++++++++++++++++ .../dolphinemu/dialogs/AlertMessage.kt | 1 + .../dolphinemu/fragments/EmulationFragment.kt | 24 +- 4 files changed, 617 insertions(+), 612 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java deleted file mode 100644 index 7d4714d75806..000000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu; - -import android.os.Handler; -import android.os.Looper; -import android.util.DisplayMetrics; -import android.view.Surface; -import android.widget.Toast; - -import androidx.annotation.Keep; -import androidx.core.util.Pair; -import androidx.fragment.app.FragmentManager; - -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.dialogs.AlertMessage; -import org.dolphinemu.dolphinemu.utils.CompressCallback; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.lang.ref.WeakReference; -import java.util.concurrent.Semaphore; - -/** - * Class which contains methods that interact - * with the native side of the Dolphin code. - */ -public final class NativeLibrary -{ - private static final Semaphore sAlertMessageSemaphore = new Semaphore(0); - private static boolean sIsShowingAlertMessage = false; - - private static WeakReference sEmulationActivity = new WeakReference<>(null); - - /** - * Returns the current instance of EmulationActivity. - * There should only ever be one EmulationActivity instantiated. - */ - public static EmulationActivity getEmulationActivity() - { - return sEmulationActivity.get(); - } - - /** - * Button type, for legacy use only - */ - public static final class ButtonType - { - public static final int BUTTON_A = 0; - public static final int BUTTON_B = 1; - public static final int BUTTON_START = 2; - public static final int BUTTON_X = 3; - public static final int BUTTON_Y = 4; - public static final int BUTTON_Z = 5; - public static final int BUTTON_UP = 6; - public static final int BUTTON_DOWN = 7; - public static final int BUTTON_LEFT = 8; - public static final int BUTTON_RIGHT = 9; - public static final int STICK_MAIN = 10; - public static final int STICK_MAIN_UP = 11; - public static final int STICK_MAIN_DOWN = 12; - public static final int STICK_MAIN_LEFT = 13; - public static final int STICK_MAIN_RIGHT = 14; - public static final int STICK_C = 15; - public static final int STICK_C_UP = 16; - public static final int STICK_C_DOWN = 17; - public static final int STICK_C_LEFT = 18; - public static final int STICK_C_RIGHT = 19; - public static final int TRIGGER_L = 20; - public static final int TRIGGER_R = 21; - public static final int WIIMOTE_BUTTON_A = 100; - public static final int WIIMOTE_BUTTON_B = 101; - public static final int WIIMOTE_BUTTON_MINUS = 102; - public static final int WIIMOTE_BUTTON_PLUS = 103; - public static final int WIIMOTE_BUTTON_HOME = 104; - public static final int WIIMOTE_BUTTON_1 = 105; - public static final int WIIMOTE_BUTTON_2 = 106; - public static final int WIIMOTE_UP = 107; - public static final int WIIMOTE_DOWN = 108; - public static final int WIIMOTE_LEFT = 109; - public static final int WIIMOTE_RIGHT = 110; - public static final int WIIMOTE_IR = 111; - public static final int WIIMOTE_IR_UP = 112; - public static final int WIIMOTE_IR_DOWN = 113; - public static final int WIIMOTE_IR_LEFT = 114; - public static final int WIIMOTE_IR_RIGHT = 115; - public static final int WIIMOTE_IR_FORWARD = 116; - public static final int WIIMOTE_IR_BACKWARD = 117; - public static final int WIIMOTE_IR_HIDE = 118; - public static final int WIIMOTE_SWING = 119; - public static final int WIIMOTE_SWING_UP = 120; - public static final int WIIMOTE_SWING_DOWN = 121; - public static final int WIIMOTE_SWING_LEFT = 122; - public static final int WIIMOTE_SWING_RIGHT = 123; - public static final int WIIMOTE_SWING_FORWARD = 124; - public static final int WIIMOTE_SWING_BACKWARD = 125; - public static final int WIIMOTE_TILT = 126; - public static final int WIIMOTE_TILT_FORWARD = 127; - public static final int WIIMOTE_TILT_BACKWARD = 128; - public static final int WIIMOTE_TILT_LEFT = 129; - public static final int WIIMOTE_TILT_RIGHT = 130; - public static final int WIIMOTE_TILT_MODIFIER = 131; - public static final int WIIMOTE_SHAKE_X = 132; - public static final int WIIMOTE_SHAKE_Y = 133; - public static final int WIIMOTE_SHAKE_Z = 134; - public static final int NUNCHUK_BUTTON_C = 200; - public static final int NUNCHUK_BUTTON_Z = 201; - public static final int NUNCHUK_STICK = 202; - public static final int NUNCHUK_STICK_UP = 203; - public static final int NUNCHUK_STICK_DOWN = 204; - public static final int NUNCHUK_STICK_LEFT = 205; - public static final int NUNCHUK_STICK_RIGHT = 206; - public static final int NUNCHUK_SWING = 207; - public static final int NUNCHUK_SWING_UP = 208; - public static final int NUNCHUK_SWING_DOWN = 209; - public static final int NUNCHUK_SWING_LEFT = 210; - public static final int NUNCHUK_SWING_RIGHT = 211; - public static final int NUNCHUK_SWING_FORWARD = 212; - public static final int NUNCHUK_SWING_BACKWARD = 213; - public static final int NUNCHUK_TILT = 214; - public static final int NUNCHUK_TILT_FORWARD = 215; - public static final int NUNCHUK_TILT_BACKWARD = 216; - public static final int NUNCHUK_TILT_LEFT = 217; - public static final int NUNCHUK_TILT_RIGHT = 218; - public static final int NUNCHUK_TILT_MODIFIER = 219; - public static final int NUNCHUK_SHAKE_X = 220; - public static final int NUNCHUK_SHAKE_Y = 221; - public static final int NUNCHUK_SHAKE_Z = 222; - public static final int CLASSIC_BUTTON_A = 300; - public static final int CLASSIC_BUTTON_B = 301; - public static final int CLASSIC_BUTTON_X = 302; - public static final int CLASSIC_BUTTON_Y = 303; - public static final int CLASSIC_BUTTON_MINUS = 304; - public static final int CLASSIC_BUTTON_PLUS = 305; - public static final int CLASSIC_BUTTON_HOME = 306; - public static final int CLASSIC_BUTTON_ZL = 307; - public static final int CLASSIC_BUTTON_ZR = 308; - public static final int CLASSIC_DPAD_UP = 309; - public static final int CLASSIC_DPAD_DOWN = 310; - public static final int CLASSIC_DPAD_LEFT = 311; - public static final int CLASSIC_DPAD_RIGHT = 312; - public static final int CLASSIC_STICK_LEFT = 313; - public static final int CLASSIC_STICK_LEFT_UP = 314; - public static final int CLASSIC_STICK_LEFT_DOWN = 315; - public static final int CLASSIC_STICK_LEFT_LEFT = 316; - public static final int CLASSIC_STICK_LEFT_RIGHT = 317; - public static final int CLASSIC_STICK_RIGHT = 318; - public static final int CLASSIC_STICK_RIGHT_UP = 319; - public static final int CLASSIC_STICK_RIGHT_DOWN = 320; - public static final int CLASSIC_STICK_RIGHT_LEFT = 321; - public static final int CLASSIC_STICK_RIGHT_RIGHT = 322; - public static final int CLASSIC_TRIGGER_L = 323; - public static final int CLASSIC_TRIGGER_R = 324; - public static final int GUITAR_BUTTON_MINUS = 400; - public static final int GUITAR_BUTTON_PLUS = 401; - public static final int GUITAR_FRET_GREEN = 402; - public static final int GUITAR_FRET_RED = 403; - public static final int GUITAR_FRET_YELLOW = 404; - public static final int GUITAR_FRET_BLUE = 405; - public static final int GUITAR_FRET_ORANGE = 406; - public static final int GUITAR_STRUM_UP = 407; - public static final int GUITAR_STRUM_DOWN = 408; - public static final int GUITAR_STICK = 409; - public static final int GUITAR_STICK_UP = 410; - public static final int GUITAR_STICK_DOWN = 411; - public static final int GUITAR_STICK_LEFT = 412; - public static final int GUITAR_STICK_RIGHT = 413; - public static final int GUITAR_WHAMMY_BAR = 414; - public static final int DRUMS_BUTTON_MINUS = 500; - public static final int DRUMS_BUTTON_PLUS = 501; - public static final int DRUMS_PAD_RED = 502; - public static final int DRUMS_PAD_YELLOW = 503; - public static final int DRUMS_PAD_BLUE = 504; - public static final int DRUMS_PAD_GREEN = 505; - public static final int DRUMS_PAD_ORANGE = 506; - public static final int DRUMS_PAD_BASS = 507; - public static final int DRUMS_STICK = 508; - public static final int DRUMS_STICK_UP = 509; - public static final int DRUMS_STICK_DOWN = 510; - public static final int DRUMS_STICK_LEFT = 511; - public static final int DRUMS_STICK_RIGHT = 512; - public static final int TURNTABLE_BUTTON_GREEN_LEFT = 600; - public static final int TURNTABLE_BUTTON_RED_LEFT = 601; - public static final int TURNTABLE_BUTTON_BLUE_LEFT = 602; - public static final int TURNTABLE_BUTTON_GREEN_RIGHT = 603; - public static final int TURNTABLE_BUTTON_RED_RIGHT = 604; - public static final int TURNTABLE_BUTTON_BLUE_RIGHT = 605; - public static final int TURNTABLE_BUTTON_MINUS = 606; - public static final int TURNTABLE_BUTTON_PLUS = 607; - public static final int TURNTABLE_BUTTON_HOME = 608; - public static final int TURNTABLE_BUTTON_EUPHORIA = 609; - public static final int TURNTABLE_TABLE_LEFT = 610; - public static final int TURNTABLE_TABLE_LEFT_LEFT = 611; - public static final int TURNTABLE_TABLE_LEFT_RIGHT = 612; - public static final int TURNTABLE_TABLE_RIGHT = 613; - public static final int TURNTABLE_TABLE_RIGHT_LEFT = 614; - public static final int TURNTABLE_TABLE_RIGHT_RIGHT = 615; - public static final int TURNTABLE_STICK = 616; - public static final int TURNTABLE_STICK_UP = 617; - public static final int TURNTABLE_STICK_DOWN = 618; - public static final int TURNTABLE_STICK_LEFT = 619; - public static final int TURNTABLE_STICK_RIGHT = 620; - public static final int TURNTABLE_EFFECT_DIAL = 621; - public static final int TURNTABLE_CROSSFADE = 622; - public static final int TURNTABLE_CROSSFADE_LEFT = 623; - public static final int TURNTABLE_CROSSFADE_RIGHT = 624; - public static final int WIIMOTE_ACCEL_LEFT = 625; - public static final int WIIMOTE_ACCEL_RIGHT = 626; - public static final int WIIMOTE_ACCEL_FORWARD = 627; - public static final int WIIMOTE_ACCEL_BACKWARD = 628; - public static final int WIIMOTE_ACCEL_UP = 629; - public static final int WIIMOTE_ACCEL_DOWN = 630; - public static final int WIIMOTE_GYRO_PITCH_UP = 631; - public static final int WIIMOTE_GYRO_PITCH_DOWN = 632; - public static final int WIIMOTE_GYRO_ROLL_LEFT = 633; - public static final int WIIMOTE_GYRO_ROLL_RIGHT = 634; - public static final int WIIMOTE_GYRO_YAW_LEFT = 635; - public static final int WIIMOTE_GYRO_YAW_RIGHT = 636; - } - - /** - * Button states - */ - public static final class ButtonState - { - public static final int RELEASED = 0; - public static final int PRESSED = 1; - } - - private NativeLibrary() - { - // Disallows instantiation. - } - - /** - * Gets the Dolphin version string. - * - * @return the Dolphin version string. - */ - public static native String GetVersionString(); - - public static native String GetGitRevision(); - - /** - * Saves a screen capture of the game - */ - public static native void SaveScreenShot(); - - /** - * Saves a game state to the slot number. - * - * @param slot The slot location to save state to. - * @param wait If false, returns as early as possible. - * If true, returns once the savestate has been written to disk. - */ - public static native void SaveState(int slot, boolean wait); - - /** - * Saves a game state to the specified path. - * - * @param path The path to save state to. - * @param wait If false, returns as early as possible. - * If true, returns once the savestate has been written to disk. - */ - public static native void SaveStateAs(String path, boolean wait); - - /** - * Loads a game state from the slot number. - * - * @param slot The slot location to load state from. - */ - public static native void LoadState(int slot); - - /** - * Loads a game state from the specified path. - * - * @param path The path to load state from. - */ - public static native void LoadStateAs(String path); - - /** - * Returns when the savestate in the given slot was created, or 0 if the slot is empty. - */ - public static native long GetUnixTimeOfStateSlot(int slot); - - /** - * Sets the current working user directory - * If not set, it auto-detects a location - */ - public static native void SetUserDirectory(String directory); - - /** - * Returns the current working user directory - */ - public static native String GetUserDirectory(); - - public static native void SetCacheDirectory(String directory); - - public static native String GetCacheDirectory(); - - public static native int DefaultCPUCore(); - - public static native String GetDefaultGraphicsBackendConfigName(); - - public static native int GetMaxLogLevel(); - - public static native void ReloadConfig(); - - public static native void ResetDolphinSettings(); - - public static native void UpdateGCAdapterScanThread(); - - /** - * Initializes the native parts of the app. - * - * Should be called at app start before running any other native code - * (other than the native methods in DirectoryInitialization). - */ - public static native void Initialize(); - - /** - * Tells analytics that Dolphin has been started. - * - * Since users typically don't explicitly close Android apps, it's appropriate to - * call this not only when the app starts but also when the user returns to the app - * after not using it for a significant amount of time. - */ - public static native void ReportStartToAnalytics(); - - public static native void GenerateNewStatisticsId(); - - /** - * Begins emulation. - */ - public static native void Run(String[] path, boolean riivolution); - - /** - * Begins emulation from the specified savestate. - */ - public static native void Run(String[] path, boolean riivolution, String savestatePath, - boolean deleteSavestate); - - /** - * Begins emulation of the System Menu. - */ - public static native void RunSystemMenu(); - - public static native void ChangeDisc(String path); - - // Surface Handling - public static native void SurfaceChanged(Surface surf); - - public static native void SurfaceDestroyed(); - - public static native boolean HasSurface(); - - /** - * Unpauses emulation from a paused state. - */ - public static native void UnPauseEmulation(); - - /** - * Pauses emulation. - */ - public static native void PauseEmulation(boolean overrideAchievementRestrictions); - - /** - * Stops emulation. - */ - public static native void StopEmulation(); - - /** - * Ensures that IsRunning will return true from now on until emulation exits. - * (If this is not called, IsRunning will start returning true at some point - * after calling Run.) - */ - public static native void SetIsBooting(); - - /** - * Returns true if emulation is running (or is paused). - */ - public static native boolean IsRunning(); - - /** - * Returns true if emulation is running and not paused. - */ - public static native boolean IsRunningAndUnpaused(); - - /** - * Returns true if emulation is fully shut down. - */ - public static native boolean IsUninitialized(); - - /** - * Re-initialize software JitBlock profiling data - */ - public static native void WipeJitBlockProfilingData(); - - /** - * Writes out the JitBlock Cache log dump - */ - public static native void WriteJitBlockLogDump(); - - /** - * Native EGL functions not exposed by Java bindings - **/ - public static native void eglBindAPI(int api); - - /** - * Provides a way to refresh the connections on Wiimotes - */ - public static native void RefreshWiimotes(); - - public static native Pair[] GetLogTypeNames(); - - public static native void ReloadLoggerConfig(); - - public static native boolean ConvertDiscImage(String inPath, String outPath, int platform, - int format, int blockSize, int compression, int compressionLevel, boolean scrub, - CompressCallback callback); - - public static native String FormatSize(long bytes, int decimals); - - public static native void SetObscuredPixelsLeft(int width); - - public static native void SetObscuredPixelsTop(int height); - - public static native boolean IsGameMetadataValid(); - - public static boolean IsEmulatingWii() - { - CheckGameMetadataValid(); - return IsEmulatingWiiUnchecked(); - } - - public static String GetCurrentGameID() - { - CheckGameMetadataValid(); - return GetCurrentGameIDUnchecked(); - } - - public static String GetCurrentTitleDescription() - { - CheckGameMetadataValid(); - return GetCurrentTitleDescriptionUnchecked(); - } - - private static void CheckGameMetadataValid() - { - if (!IsGameMetadataValid()) - { - throw new IllegalStateException("No game is running"); - } - } - - private static native boolean IsEmulatingWiiUnchecked(); - - private static native String GetCurrentGameIDUnchecked(); - - private static native String GetCurrentTitleDescriptionUnchecked(); - - @Keep - public static void displayToastMsg(final String text, final boolean long_length) - { - final int length = long_length ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT; - new Handler(Looper.getMainLooper()) - .post(() -> Toast.makeText(DolphinApplication.getAppContext(), text, length).show()); - } - - @Keep - public static boolean displayAlertMsg(final String caption, final String text, - final boolean yesNo, final boolean isWarning, final boolean nonBlocking) - { - Log.error("[NativeLibrary] Alert: " + text); - final EmulationActivity emulationActivity = sEmulationActivity.get(); - boolean result = false; - - // We can't use AlertMessages unless we have a non-null activity reference - // and are allowed to block. As a fallback, we can use toasts. - if (emulationActivity == null || nonBlocking) - { - displayToastMsg(text, true); - } - else - { - sIsShowingAlertMessage = true; - - emulationActivity.runOnUiThread(() -> - { - FragmentManager fragmentManager = emulationActivity.getSupportFragmentManager(); - if (fragmentManager.isStateSaved()) - { - // The activity is being destroyed, so we can't use it to display an AlertMessage. - // Fall back to a toast. - Toast.makeText(emulationActivity, text, Toast.LENGTH_LONG).show(); - NotifyAlertMessageLock(); - } - else - { - AlertMessage.Companion.newInstance(caption, text, yesNo, isWarning) - .show(fragmentManager, "AlertMessage"); - } - }); - - // Wait for the lock to notify that it is complete. - try - { - sAlertMessageSemaphore.acquire(); - } - catch (InterruptedException ignored) - { - } - - if (yesNo) - { - result = AlertMessage.Companion.getAlertResult(); - } - } - - sIsShowingAlertMessage = false; - return result; - } - - public static boolean IsShowingAlertMessage() - { - return sIsShowingAlertMessage; - } - - public static void NotifyAlertMessageLock() - { - sAlertMessageSemaphore.release(); - } - - public static void setEmulationActivity(EmulationActivity emulationActivity) - { - Log.verbose("[NativeLibrary] Registering EmulationActivity."); - sEmulationActivity = new WeakReference<>(emulationActivity); - } - - public static void clearEmulationActivity() - { - Log.verbose("[NativeLibrary] Unregistering EmulationActivity."); - - sEmulationActivity.clear(); - } - - @Keep - public static void finishEmulationActivity() - { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) - { - Log.warning("[NativeLibrary] EmulationActivity is null."); - } - else - { - Log.verbose("[NativeLibrary] Finishing EmulationActivity."); - emulationActivity.runOnUiThread(emulationActivity::finish); - } - } - - @Keep - public static void updateTouchPointer() - { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) - { - Log.warning("[NativeLibrary] EmulationActivity is null."); - } - else - { - emulationActivity.runOnUiThread(emulationActivity::initInputPointer); - } - } - - @Keep - public static void onTitleChanged() - { - final EmulationActivity emulationActivity = sEmulationActivity.get(); - if (emulationActivity == null) - { - Log.warning("[NativeLibrary] EmulationActivity is null."); - } - else - { - emulationActivity.runOnUiThread(emulationActivity::onTitleChanged); - } - } - - @Keep - public static float getRenderSurfaceScale() - { - DisplayMetrics metrics = new DisplayMetrics(); - sEmulationActivity.get().getWindowManager().getDefaultDisplay().getMetrics(metrics); - return metrics.scaledDensity; - } - - public static native float GetGameAspectRatio(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt new file mode 100644 index 000000000000..0b6490b2908c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -0,0 +1,603 @@ +/* + * Copyright 2013 Dolphin Emulator Project + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +package org.dolphinemu.dolphinemu + +import android.content.res.Resources +import android.view.Surface +import android.widget.Toast +import androidx.annotation.Keep +import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.dialogs.AlertMessage +import org.dolphinemu.dolphinemu.utils.CompressCallback +import org.dolphinemu.dolphinemu.utils.Log +import java.lang.ref.WeakReference +import java.util.concurrent.Semaphore + +/** + * Class which contains methods that interact + * with the native side of the Dolphin code. + */ +object NativeLibrary { + /** + * Button type, for legacy use only. + */ + object ButtonType { + const val BUTTON_A = 0 + const val BUTTON_B = 1 + const val BUTTON_START = 2 + const val BUTTON_X = 3 + const val BUTTON_Y = 4 + const val BUTTON_Z = 5 + const val BUTTON_UP = 6 + const val BUTTON_DOWN = 7 + const val BUTTON_LEFT = 8 + const val BUTTON_RIGHT = 9 + const val STICK_MAIN = 10 + const val STICK_MAIN_UP = 11 + const val STICK_MAIN_DOWN = 12 + const val STICK_MAIN_LEFT = 13 + const val STICK_MAIN_RIGHT = 14 + const val STICK_C = 15 + const val STICK_C_UP = 16 + const val STICK_C_DOWN = 17 + const val STICK_C_LEFT = 18 + const val STICK_C_RIGHT = 19 + const val TRIGGER_L = 20 + const val TRIGGER_R = 21 + const val WIIMOTE_BUTTON_A = 100 + const val WIIMOTE_BUTTON_B = 101 + const val WIIMOTE_BUTTON_MINUS = 102 + const val WIIMOTE_BUTTON_PLUS = 103 + const val WIIMOTE_BUTTON_HOME = 104 + const val WIIMOTE_BUTTON_1 = 105 + const val WIIMOTE_BUTTON_2 = 106 + const val WIIMOTE_UP = 107 + const val WIIMOTE_DOWN = 108 + const val WIIMOTE_LEFT = 109 + const val WIIMOTE_RIGHT = 110 + const val WIIMOTE_IR = 111 + const val WIIMOTE_IR_UP = 112 + const val WIIMOTE_IR_DOWN = 113 + const val WIIMOTE_IR_LEFT = 114 + const val WIIMOTE_IR_RIGHT = 115 + const val WIIMOTE_IR_FORWARD = 116 + const val WIIMOTE_IR_BACKWARD = 117 + const val WIIMOTE_IR_HIDE = 118 + const val WIIMOTE_SWING = 119 + const val WIIMOTE_SWING_UP = 120 + const val WIIMOTE_SWING_DOWN = 121 + const val WIIMOTE_SWING_LEFT = 122 + const val WIIMOTE_SWING_RIGHT = 123 + const val WIIMOTE_SWING_FORWARD = 124 + const val WIIMOTE_SWING_BACKWARD = 125 + const val WIIMOTE_TILT = 126 + const val WIIMOTE_TILT_FORWARD = 127 + const val WIIMOTE_TILT_BACKWARD = 128 + const val WIIMOTE_TILT_LEFT = 129 + const val WIIMOTE_TILT_RIGHT = 130 + const val WIIMOTE_TILT_MODIFIER = 131 + const val WIIMOTE_SHAKE_X = 132 + const val WIIMOTE_SHAKE_Y = 133 + const val WIIMOTE_SHAKE_Z = 134 + const val NUNCHUK_BUTTON_C = 200 + const val NUNCHUK_BUTTON_Z = 201 + const val NUNCHUK_STICK = 202 + const val NUNCHUK_STICK_UP = 203 + const val NUNCHUK_STICK_DOWN = 204 + const val NUNCHUK_STICK_LEFT = 205 + const val NUNCHUK_STICK_RIGHT = 206 + const val NUNCHUK_SWING = 207 + const val NUNCHUK_SWING_UP = 208 + const val NUNCHUK_SWING_DOWN = 209 + const val NUNCHUK_SWING_LEFT = 210 + const val NUNCHUK_SWING_RIGHT = 211 + const val NUNCHUK_SWING_FORWARD = 212 + const val NUNCHUK_SWING_BACKWARD = 213 + const val NUNCHUK_TILT = 214 + const val NUNCHUK_TILT_FORWARD = 215 + const val NUNCHUK_TILT_BACKWARD = 216 + const val NUNCHUK_TILT_LEFT = 217 + const val NUNCHUK_TILT_RIGHT = 218 + const val NUNCHUK_TILT_MODIFIER = 219 + const val NUNCHUK_SHAKE_X = 220 + const val NUNCHUK_SHAKE_Y = 221 + const val NUNCHUK_SHAKE_Z = 222 + const val CLASSIC_BUTTON_A = 300 + const val CLASSIC_BUTTON_B = 301 + const val CLASSIC_BUTTON_X = 302 + const val CLASSIC_BUTTON_Y = 303 + const val CLASSIC_BUTTON_MINUS = 304 + const val CLASSIC_BUTTON_PLUS = 305 + const val CLASSIC_BUTTON_HOME = 306 + const val CLASSIC_BUTTON_ZL = 307 + const val CLASSIC_BUTTON_ZR = 308 + const val CLASSIC_DPAD_UP = 309 + const val CLASSIC_DPAD_DOWN = 310 + const val CLASSIC_DPAD_LEFT = 311 + const val CLASSIC_DPAD_RIGHT = 312 + const val CLASSIC_STICK_LEFT = 313 + const val CLASSIC_STICK_LEFT_UP = 314 + const val CLASSIC_STICK_LEFT_DOWN = 315 + const val CLASSIC_STICK_LEFT_LEFT = 316 + const val CLASSIC_STICK_LEFT_RIGHT = 317 + const val CLASSIC_STICK_RIGHT = 318 + const val CLASSIC_STICK_RIGHT_UP = 319 + const val CLASSIC_STICK_RIGHT_DOWN = 320 + const val CLASSIC_STICK_RIGHT_LEFT = 321 + const val CLASSIC_STICK_RIGHT_RIGHT = 322 + const val CLASSIC_TRIGGER_L = 323 + const val CLASSIC_TRIGGER_R = 324 + const val GUITAR_BUTTON_MINUS = 400 + const val GUITAR_BUTTON_PLUS = 401 + const val GUITAR_FRET_GREEN = 402 + const val GUITAR_FRET_RED = 403 + const val GUITAR_FRET_YELLOW = 404 + const val GUITAR_FRET_BLUE = 405 + const val GUITAR_FRET_ORANGE = 406 + const val GUITAR_STRUM_UP = 407 + const val GUITAR_STRUM_DOWN = 408 + const val GUITAR_STICK = 409 + const val GUITAR_STICK_UP = 410 + const val GUITAR_STICK_DOWN = 411 + const val GUITAR_STICK_LEFT = 412 + const val GUITAR_STICK_RIGHT = 413 + const val GUITAR_WHAMMY_BAR = 414 + const val DRUMS_BUTTON_MINUS = 500 + const val DRUMS_BUTTON_PLUS = 501 + const val DRUMS_PAD_RED = 502 + const val DRUMS_PAD_YELLOW = 503 + const val DRUMS_PAD_BLUE = 504 + const val DRUMS_PAD_GREEN = 505 + const val DRUMS_PAD_ORANGE = 506 + const val DRUMS_PAD_BASS = 507 + const val DRUMS_STICK = 508 + const val DRUMS_STICK_UP = 509 + const val DRUMS_STICK_DOWN = 510 + const val DRUMS_STICK_LEFT = 511 + const val DRUMS_STICK_RIGHT = 512 + const val TURNTABLE_BUTTON_GREEN_LEFT = 600 + const val TURNTABLE_BUTTON_RED_LEFT = 601 + const val TURNTABLE_BUTTON_BLUE_LEFT = 602 + const val TURNTABLE_BUTTON_GREEN_RIGHT = 603 + const val TURNTABLE_BUTTON_RED_RIGHT = 604 + const val TURNTABLE_BUTTON_BLUE_RIGHT = 605 + const val TURNTABLE_BUTTON_MINUS = 606 + const val TURNTABLE_BUTTON_PLUS = 607 + const val TURNTABLE_BUTTON_HOME = 608 + const val TURNTABLE_BUTTON_EUPHORIA = 609 + const val TURNTABLE_TABLE_LEFT = 610 + const val TURNTABLE_TABLE_LEFT_LEFT = 611 + const val TURNTABLE_TABLE_LEFT_RIGHT = 612 + const val TURNTABLE_TABLE_RIGHT = 613 + const val TURNTABLE_TABLE_RIGHT_LEFT = 614 + const val TURNTABLE_TABLE_RIGHT_RIGHT = 615 + const val TURNTABLE_STICK = 616 + const val TURNTABLE_STICK_UP = 617 + const val TURNTABLE_STICK_DOWN = 618 + const val TURNTABLE_STICK_LEFT = 619 + const val TURNTABLE_STICK_RIGHT = 620 + const val TURNTABLE_EFFECT_DIAL = 621 + const val TURNTABLE_CROSSFADE = 622 + const val TURNTABLE_CROSSFADE_LEFT = 623 + const val TURNTABLE_CROSSFADE_RIGHT = 624 + const val WIIMOTE_ACCEL_LEFT = 625 + const val WIIMOTE_ACCEL_RIGHT = 626 + const val WIIMOTE_ACCEL_FORWARD = 627 + const val WIIMOTE_ACCEL_BACKWARD = 628 + const val WIIMOTE_ACCEL_UP = 629 + const val WIIMOTE_ACCEL_DOWN = 630 + const val WIIMOTE_GYRO_PITCH_UP = 631 + const val WIIMOTE_GYRO_PITCH_DOWN = 632 + const val WIIMOTE_GYRO_ROLL_LEFT = 633 + const val WIIMOTE_GYRO_ROLL_RIGHT = 634 + const val WIIMOTE_GYRO_YAW_LEFT = 635 + const val WIIMOTE_GYRO_YAW_RIGHT = 636 + } + + object ButtonState { + const val RELEASED = 0 + const val PRESSED = 1 + } + + private val alertMessageSemaphore = Semaphore(0) + + @Volatile + private var isShowingAlertMessage = false + private var emulationActivityRef = WeakReference(null) + + /** + * Returns the current instance of EmulationActivity. + * There should only ever be one EmulationActivity instantiated. + */ + @JvmStatic + fun getEmulationActivity(): EmulationActivity? = emulationActivityRef.get() + + @JvmStatic + external fun GetVersionString(): String + + @JvmStatic + external fun GetGitRevision(): String + + /** + * Saves a screen capture of the game. + */ + @JvmStatic + external fun SaveScreenShot() + + /** + * Saves a game state to the slot number. + * + * @param slot The slot location to save state to. + * @param wait If false, returns as early as possible. If true, returns once the savestate has been written to disk. + */ + @JvmStatic + external fun SaveState(slot: Int, wait: Boolean) + + /** + * Saves a game state to the specified path. + * + * @param path The path to save state to. + * @param wait If false, returns as early as possible. If true, returns once the savestate has been written to disk. + */ + @JvmStatic + external fun SaveStateAs(path: String, wait: Boolean) + + /** + * Loads a game state from the slot number. + * + * @param slot The slot location to load state from. + */ + @JvmStatic + external fun LoadState(slot: Int) + + /** + * Loads a game state from the specified path. + * + * @param path The path to load state from. + */ + @JvmStatic + external fun LoadStateAs(path: String) + + /** + * Returns when the savestate in the given slot was created, or 0 if the slot is empty. + */ + @JvmStatic + external fun GetUnixTimeOfStateSlot(slot: Int): Long + + /** + * Sets the current working user directory. If not set, it auto-detects a location. + */ + @JvmStatic + external fun SetUserDirectory(directory: String) + + /** + * Returns the current working user directory. + */ + @JvmStatic + external fun GetUserDirectory(): String + + @JvmStatic + external fun SetCacheDirectory(directory: String) + + @JvmStatic + external fun GetCacheDirectory(): String + + @JvmStatic + external fun DefaultCPUCore(): Int + + @JvmStatic + external fun GetDefaultGraphicsBackendConfigName(): String + + @JvmStatic + external fun GetMaxLogLevel(): Int + + @JvmStatic + external fun ReloadConfig() + + @JvmStatic + external fun ResetDolphinSettings() + + @JvmStatic + external fun UpdateGCAdapterScanThread() + + /** + * Initializes the native parts of the app. + * + * Should be called at app start before running any other native code + * (other than the native methods in DirectoryInitialization). + */ + @JvmStatic + external fun Initialize() + + /** + * Tells analytics that Dolphin has been started. + * + * Since users typically don't explicitly close Android apps, it's appropriate to + * call this not only when the app starts but also when the user returns to the app + * after not using it for a significant amount of time. + */ + @JvmStatic + external fun ReportStartToAnalytics() + + @JvmStatic + external fun GenerateNewStatisticsId() + + /** + * Begins emulation. + */ + @JvmStatic + external fun Run(path: Array, riivolution: Boolean) + + /** + * Begins emulation from the specified savestate. + */ + @JvmStatic + external fun Run( + path: Array, riivolution: Boolean, savestatePath: String, deleteSavestate: Boolean + ) + + /** + * Begins emulation of the System Menu. + */ + @JvmStatic + external fun RunSystemMenu() + + @JvmStatic + external fun ChangeDisc(path: String) + + @JvmStatic + external fun SurfaceChanged(surf: Surface) + + @JvmStatic + external fun SurfaceDestroyed() + + @JvmStatic + external fun HasSurface(): Boolean + + @JvmStatic + external fun UnPauseEmulation() + + @JvmStatic + external fun PauseEmulation(overrideAchievementRestrictions: Boolean) + + @JvmStatic + external fun StopEmulation() + + /** + * Ensures that IsRunning will return true from now on until emulation exits. + * (If this is not called, IsRunning will start returning true at some point + * after calling Run.) + */ + @JvmStatic + external fun SetIsBooting() + + /** + * Returns true if emulation is running or paused. + */ + @JvmStatic + external fun IsRunning(): Boolean + + /** + * Returns true if emulation is running and not paused. + */ + @JvmStatic + external fun IsRunningAndUnpaused(): Boolean + + /** + * Returns true if emulation is fully shut down. + */ + @JvmStatic + external fun IsUninitialized(): Boolean + + /** + * Re-initializes software JitBlock profiling data. + */ + @JvmStatic + external fun WipeJitBlockProfilingData() + + /** + * Writes out the JitBlock Cache log dump. + */ + @JvmStatic + external fun WriteJitBlockLogDump() + + /** + * Native EGL functions not exposed by Java bindings. + */ + @JvmStatic + external fun eglBindAPI(api: Int) + + /** + * Provides a way to refresh Wiimote connections. + */ + @JvmStatic + external fun RefreshWiimotes() + + @JvmStatic + external fun GetLogTypeNames(): Array> + + @JvmStatic + external fun ReloadLoggerConfig() + + @JvmStatic + external fun ConvertDiscImage( + inPath: String, + outPath: String, + platform: Int, + format: Int, + blockSize: Int, + compression: Int, + compressionLevel: Int, + scrub: Boolean, + callback: CompressCallback + ): Boolean + + @JvmStatic + external fun FormatSize(bytes: Long, decimals: Int): String + + @JvmStatic + external fun SetObscuredPixelsLeft(width: Int) + + @JvmStatic + external fun SetObscuredPixelsTop(height: Int) + + @JvmStatic + external fun IsGameMetadataValid(): Boolean + + @JvmStatic + external fun GetGameAspectRatio(): Float + + @JvmStatic + fun IsEmulatingWii(): Boolean { + checkGameMetadataValid() + return IsEmulatingWiiUnchecked() + } + + @JvmStatic + fun GetCurrentGameID(): String { + checkGameMetadataValid() + return GetCurrentGameIDUnchecked() + } + + @JvmStatic + fun GetCurrentTitleDescription(): String { + checkGameMetadataValid() + return GetCurrentTitleDescriptionUnchecked() + } + + @JvmStatic + @Keep + fun displayToastMsg(text: String, long_length: Boolean) { + val context = DolphinApplication.getAppContext() + val length = if (long_length) Toast.LENGTH_LONG else Toast.LENGTH_SHORT + ContextCompat.getMainExecutor(context).execute { + Toast.makeText(context, text, length).show() + } + } + + @JvmStatic + @Keep + fun displayAlertMsg( + caption: String, text: String, yesNo: Boolean, isWarning: Boolean, nonBlocking: Boolean + ): Boolean { + Log.error("[NativeLibrary] Alert: $text") + val emulationActivity = emulationActivityRef.get() + var result = false + + // We can't use AlertMessages unless we have a non-null activity reference + // and are allowed to block. As a fallback, we can use toasts. + if (emulationActivity == null || nonBlocking) { + displayToastMsg(text, true) + } else { + isShowingAlertMessage = true + + emulationActivity.runOnUiThread { + val fragmentManager = emulationActivity.supportFragmentManager + if (fragmentManager.isStateSaved) { + // The activity is being destroyed, so we can't use it to display an AlertMessage. + // Fall back to a toast. + Toast.makeText(emulationActivity, text, Toast.LENGTH_LONG).show() + NotifyAlertMessageLock() + } else { + AlertMessage.newInstance(caption, text, yesNo, isWarning) + .show(fragmentManager, "AlertMessage") + } + } + + // Wait for the lock to notify that it is complete. + try { + alertMessageSemaphore.acquire() + } catch (ignored: InterruptedException) { + Thread.currentThread().interrupt() + } + + if (yesNo) { + result = AlertMessage.getAlertResult() + } + } + + isShowingAlertMessage = false + return result + } + + @JvmStatic + fun IsShowingAlertMessage(): Boolean = isShowingAlertMessage + + @JvmStatic + fun NotifyAlertMessageLock() { + alertMessageSemaphore.release() + } + + @JvmStatic + fun setEmulationActivity(emulationActivity: EmulationActivity) { + Log.verbose("[NativeLibrary] Registering EmulationActivity.") + emulationActivityRef = WeakReference(emulationActivity) + } + + @JvmStatic + fun clearEmulationActivity() { + Log.verbose("[NativeLibrary] Unregistering EmulationActivity.") + emulationActivityRef.clear() + } + + @JvmStatic + @Keep + fun finishEmulationActivity() { + val emulationActivity = emulationActivityRef.get() + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null.") + } else { + Log.verbose("[NativeLibrary] Finishing EmulationActivity.") + emulationActivity.runOnUiThread(emulationActivity::finish) + } + } + + @JvmStatic + @Keep + fun updateTouchPointer() { + val emulationActivity = emulationActivityRef.get() + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null.") + } else { + emulationActivity.runOnUiThread(emulationActivity::initInputPointer) + } + } + + @JvmStatic + @Keep + fun onTitleChanged() { + val emulationActivity = emulationActivityRef.get() + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null.") + } else { + emulationActivity.runOnUiThread(emulationActivity::onTitleChanged) + } + } + + @JvmStatic + @Keep + fun getRenderSurfaceScale(): Float { + val emulationActivity = emulationActivityRef.get() + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null.") + return Resources.getSystem().displayMetrics.scaledDensity + } + + return emulationActivity.resources.displayMetrics.scaledDensity + } + + private fun checkGameMetadataValid() { + check(IsGameMetadataValid()) { "No game is running" } + } + + private external fun IsEmulatingWiiUnchecked(): Boolean + + private external fun GetCurrentGameIDUnchecked(): String + + private external fun GetCurrentTitleDescriptionUnchecked(): String +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/AlertMessage.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/AlertMessage.kt index 3c87b324f634..f79fddcdab15 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/AlertMessage.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/AlertMessage.kt @@ -16,6 +16,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig class AlertMessage : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val emulationActivity: EmulationActivity = NativeLibrary.getEmulationActivity() + ?: throw IllegalStateException("EmulationActivity missing while showing alert") val args = requireArguments() val title = args.getString(ARG_TITLE).orEmpty() val message = args.getString(ARG_MESSAGE).orEmpty() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index 04db24c03adc..9cf87c3401aa 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -57,9 +57,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentEmulationBinding.inflate(inflater, container, false) return binding.root @@ -98,8 +96,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun onResume() { super.onResume() - if (NativeLibrary.IsGameMetadataValid()) + if (NativeLibrary.IsGameMetadataValid()) { inputOverlay?.refreshControls() + } AfterDirectoryInitializationRunner().runWithLifecycle(this) { run(emulationActivity!!.isActivityRecreated) @@ -126,8 +125,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { fun toggleInputOverlayVisibility(settings: Settings?) { BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.setBoolean( - settings!!, - !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean + settings!!, !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean ) inputOverlay?.refreshControls() @@ -208,14 +206,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val emulationThread = Thread({ if (loadPreviousTemporaryState) { Log.debug("[EmulationFragment] Starting emulation thread from previous state.") - NativeLibrary.Run(gamePaths, riivolution, temporaryStateFilePath, true) + val paths = requireNotNull(gamePaths) { + "Cannot start emulation without any game paths" + } + NativeLibrary.Run(paths, riivolution, temporaryStateFilePath, true) } if (launchSystemMenu) { Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") NativeLibrary.RunSystemMenu() } else { Log.debug("[EmulationFragment] Starting emulation thread.") - NativeLibrary.Run(gamePaths, riivolution) + val paths = requireNotNull(gamePaths) { + "Cannot start emulation without any game paths" + } + NativeLibrary.Run(paths, riivolution) } EmulationActivity.stopIgnoringLaunchRequests() }, "NativeEmulation") @@ -239,9 +243,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private const val KEY_SYSTEM_MENU = "systemMenu" fun newInstance( - gamePaths: Array?, - riivolution: Boolean, - systemMenu: Boolean + gamePaths: Array?, riivolution: Boolean, systemMenu: Boolean ): EmulationFragment { val args = Bundle() args.apply { From b662cd93cec27540c15c3ab58bc9831863b9291b Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Thu, 24 Apr 2025 07:51:01 -0400 Subject: [PATCH 058/123] Add Achievements submenu to Android settings --- .../settings/model/AchievementModel.kt | 11 +++ .../features/settings/model/BooleanSetting.kt | 43 +++++++++- .../features/settings/model/Settings.kt | 2 + .../features/settings/ui/MenuTag.kt | 1 + .../features/settings/ui/SettingsFragment.kt | 1 + .../settings/ui/SettingsFragmentPresenter.kt | 82 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 9 ++ Source/Android/jni/AchievementAdapter.cpp | 24 ++++++ Source/Android/jni/CMakeLists.txt | 1 + Source/Android/jni/Config/NativeConfig.cpp | 4 + 10 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt create mode 100644 Source/Android/jni/AchievementAdapter.cpp diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt new file mode 100644 index 000000000000..345daf54a16c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.settings.model + +object AchievementModel { + @JvmStatic + external fun init() + + @JvmStatic + external fun shutdown() +} 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 0b6945ff130f..1f8ef6e94bc0 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 @@ -883,6 +883,42 @@ enum class BooleanSetting( Settings.SECTION_LOGGER_OPTIONS, "WriteToFile", false + ), + ACHIEVEMENTS_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "Enabled", + false + ), + ACHIEVEMENTS_HARDCORE_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "HardcoreEnabled", + false + ), + ACHIEVEMENTS_UNOFFICIAL_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "UnofficialEnabled", + false + ), + ACHIEVEMENTS_ENCORE_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "EncoreEnabled", + false + ), + ACHIEVEMENTS_SPECTATOR_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "SpectatorEnabled", + false + ), + ACHIEVEMENTS_PROGRESS_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "ProgressEnabled", + true ); override val isOverridden: Boolean @@ -943,7 +979,12 @@ enum class BooleanSetting( MAIN_TIME_TRACKING, MAIN_EMULATE_SKYLANDER_PORTAL, MAIN_EMULATE_INFINITY_BASE, - MAIN_EMULATE_WII_SPEAK + MAIN_EMULATE_WII_SPEAK, + ACHIEVEMENTS_ENABLED, + ACHIEVEMENTS_HARDCORE_ENABLED, + ACHIEVEMENTS_UNOFFICIAL_ENABLED, + ACHIEVEMENTS_ENCORE_ENABLED, + ACHIEVEMENTS_SPECTATOR_ENABLED ) private val NOT_RUNTIME_EDITABLE: Set = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) 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 32ad11b3e1fa..9f91a4244c9a 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 @@ -104,6 +104,7 @@ class Settings : Closeable { const val FILE_GFX = "GFX" const val FILE_LOGGER = "Logger" const val FILE_WIIMOTE = "WiimoteNew" + const val FILE_ACHIEVEMENTS = "RetroAchievements" const val FILE_GAME_SETTINGS_ONLY = "GameSettingsOnly" const val SECTION_INI_ANDROID = "Android" const val SECTION_INI_ANDROID_OVERLAY_BUTTONS = "AndroidOverlayButtons" @@ -122,5 +123,6 @@ class Settings : Closeable { const val SECTION_EMULATED_USB_DEVICES = "EmulatedUSBDevices" const val SECTION_STEREOSCOPY = "Stereoscopy" const val SECTION_ANALYTICS = "Analytics" + const val SECTION_ACHIEVEMENTS = "Achievements" } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt index 0bfcb96d6ce2..74556b07d97f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt @@ -14,6 +14,7 @@ enum class MenuTag { CONFIG_GAME_CUBE("config_gamecube"), CONFIG_SERIALPORT1("config_serialport1"), CONFIG_WII("config_wii"), + CONFIG_ACHIEVEMENTS("config_achievements"), CONFIG_ADVANCED("config_advanced"), CONFIG_LOG("config_log"), DEBUG("debug"), diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index 991d3c3c2b91..513769a50da4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -268,6 +268,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView { titles[MenuTag.CONFIG_GAME_CUBE] = R.string.gamecube_submenu titles[MenuTag.CONFIG_SERIALPORT1] = R.string.serialport1_submenu titles[MenuTag.CONFIG_WII] = R.string.wii_submenu + titles[MenuTag.CONFIG_ACHIEVEMENTS] = R.string.achievements_submenu titles[MenuTag.CONFIG_ADVANCED] = R.string.advanced_submenu titles[MenuTag.DEBUG] = R.string.debug_submenu titles[MenuTag.GRAPHICS] = R.string.graphics_settings 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 25acccb2fc8d..a5ee2f28df07 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 @@ -111,6 +111,7 @@ class SettingsFragmentPresenter( MenuTag.CONFIG_PATHS -> addPathsSettings(sl) MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl) MenuTag.CONFIG_WII -> addWiiSettings(sl) + MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl); MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl) MenuTag.GRAPHICS -> addGraphicsSettings(sl) MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type) @@ -200,6 +201,7 @@ class SettingsFragmentPresenter( sl.add(SubmenuSetting(context, R.string.paths_submenu, MenuTag.CONFIG_PATHS)) sl.add(SubmenuSetting(context, R.string.gamecube_submenu, MenuTag.CONFIG_GAME_CUBE)) sl.add(SubmenuSetting(context, R.string.wii_submenu, MenuTag.CONFIG_WII)) + sl.add(SubmenuSetting(context, R.string.achievements_submenu, MenuTag.CONFIG_ACHIEVEMENTS)) sl.add(SubmenuSetting(context, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED)) sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG)) sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG)) @@ -914,6 +916,86 @@ class SettingsFragmentPresenter( ) } + private fun addAchievementSettings(sl: ArrayList) { + val achievementsEnabledSetting: AbstractBooleanSetting = object : AbstractBooleanSetting { + override val boolean: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.boolean + + override fun setBoolean(settings: Settings, newValue: Boolean) { + BooleanSetting.ACHIEVEMENTS_ENABLED.setBoolean(settings, newValue) + if (newValue) + AchievementModel.init() + else + AchievementModel.shutdown() + loadSettingsList() + } + + override val isOverridden: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isOverridden + + override val isRuntimeEditable: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isRuntimeEditable + + override fun delete(settings: Settings): Boolean { + val result = BooleanSetting.ACHIEVEMENTS_ENABLED.delete(settings) + AchievementModel.shutdown() + loadSettingsList() + return result + } + } + + sl.add( + SwitchSetting( + context, + achievementsEnabledSetting, + R.string.achievements_enabled, + 0 + ) + ) + if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) { + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_HARDCORE_ENABLED, + R.string.achievements_hardcore_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_UNOFFICIAL_ENABLED, + R.string.achievements_unofficial_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_ENCORE_ENABLED, + R.string.achievements_encore_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_SPECTATOR_ENABLED, + R.string.achievements_spectator_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_PROGRESS_ENABLED, + R.string.achievements_progress_enabled, + 0 + ) + ) + } + } + private fun addAdvancedSettings(sl: ArrayList) { val SYNC_GPU_NEVER = 0 val SYNC_GPU_ON_IDLE_SKIP = 1 diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 6342e35cb02e..67ac68f75ef0 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -75,6 +75,7 @@ Enable Cheats Speed Limit (0% = Unlimited) WARNING: Changing this from the default (100%) WILL break games and cause glitches. Please do not report bugs that occur with a non-default clock. + RetroAchievements GameCube IPL Settings Skip Main Menu @@ -953,4 +954,12 @@ It can efficiently compress both junk data and encrypted Wii data. Mute Wii Speak Missing Microphone Permission Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective. + + + Enable Achievements + Enable Hardcore Mode + Enable Unofficial Achievements + Enable Encore Mode + Enable Spectator Mode + Enable Progress Notifications diff --git a/Source/Android/jni/AchievementAdapter.cpp b/Source/Android/jni/AchievementAdapter.cpp new file mode 100644 index 000000000000..119ecbbd6fb9 --- /dev/null +++ b/Source/Android/jni/AchievementAdapter.cpp @@ -0,0 +1,24 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "Core/AchievementManager.h" +#include "jni/AndroidCommon/AndroidCommon.h" + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_init(JNIEnv* env, jclass) +{ + AchievementManager::GetInstance().Init(nullptr); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_shutdown(JNIEnv* env, + jclass) +{ + AchievementManager::GetInstance().Shutdown(); +} + +} // extern "C" diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index be200affa9b6..b825fa314b6b 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(main SHARED + AchievementAdapter.cpp ActivityTracker.cpp Cheats/ARCheat.cpp Cheats/Cheats.h diff --git a/Source/Android/jni/Config/NativeConfig.cpp b/Source/Android/jni/Config/NativeConfig.cpp index f367c5e16278..8478174dae20 100644 --- a/Source/Android/jni/Config/NativeConfig.cpp +++ b/Source/Android/jni/Config/NativeConfig.cpp @@ -48,6 +48,10 @@ static Config::Location GetLocation(JNIEnv* env, jstring file, jstring section, { system = Config::System::GameSettingsOnly; } + else if (decoded_file == "RetroAchievements") + { + system = Config::System::Achievements; + } else { ASSERT(false); From 98678e9a8b713ff8bb8aa5d1c58f44e8b57a16fc Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 13 Jun 2025 20:35:24 -0400 Subject: [PATCH 059/123] AchievementManager - Add Android to user agent If the build is an Android build, identify it as such in the AchievementManager user agent so that android builds can be tracked separately for debug purposes. --- Source/Core/Core/AchievementManager.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 5423bb29e03c..a319499d09e3 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -49,8 +49,13 @@ #include #endif // RC_CLIENT_SUPPORTS_RAINTEGRATION +#ifdef ANDROID +static const Common::HttpRequest::Headers USER_AGENT_HEADER = { + {"User-Agent", Common::GetUserAgentStr() + " (Android)"}}; +#else // ANDROID static const Common::HttpRequest::Headers USER_AGENT_HEADER = { {"User-Agent", Common::GetUserAgentStr()}}; +#endif // ANDROID AchievementManager& AchievementManager::GetInstance() { From f4f26a26d22e0c4be99a699277e5dfe2f1bbe652 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sun, 27 Apr 2025 09:15:01 -0400 Subject: [PATCH 060/123] Add login to achievement settings --- .../settings/model/AchievementModel.kt | 6 ++ .../features/settings/model/StringSetting.kt | 12 +++ .../features/settings/ui/LoginDialog.kt | 51 ++++++++++++ .../settings/ui/SettingsFragmentPresenter.kt | 28 +++++++ .../app/src/main/res/layout/dialog_login.xml | 78 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 4 + Source/Android/jni/AchievementAdapter.cpp | 13 ++++ 7 files changed, 192 insertions(+) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt create mode 100644 Source/Android/app/src/main/res/layout/dialog_login.xml diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt index 345daf54a16c..6626ba0df35f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt @@ -6,6 +6,12 @@ object AchievementModel { @JvmStatic external fun init() + @JvmStatic + external fun login(password: String) + + @JvmStatic + external fun logout() + @JvmStatic external fun shutdown() } 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 b150c845e186..29640aa92f4b 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 @@ -80,6 +80,18 @@ enum class StringSetting( Settings.SECTION_GFX_SETTINGS, "DriverLibName", "" + ), + ACHIEVEMENTS_USERNAME( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "Username", + "" + ), + ACHIEVEMENTS_API_TOKEN( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "ApiToken", + "" ); override val isOverridden: Boolean diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt new file mode 100644 index 000000000000..f49c9ff92242 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.settings.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import org.dolphinemu.dolphinemu.databinding.DialogLoginBinding +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.login +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting + +class LoginDialog : DialogFragment() { + private var _binding: DialogLoginBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogLoginBinding.inflate(inflater, container, false) + binding.usernameInput.setText( + StringSetting.ACHIEVEMENTS_USERNAME.string + ) + return binding.getRoot() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.buttonCancel.setOnClickListener { onCancelClicked() } + binding.buttonLogin.setOnClickListener { onLoginClicked() } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun onCancelClicked() { + dismiss() + } + + private fun onLoginClicked() { + StringSetting.ACHIEVEMENTS_USERNAME.setString(NativeConfig.LAYER_BASE_OR_CURRENT, + binding.usernameInput.text.toString()) + login(binding.passwordInput.text.toString()) + dismiss() + } +} 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 a5ee2f28df07..0fd8a97782b8 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 @@ -31,6 +31,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter import org.dolphinemu.dolphinemu.features.settings.model.* import org.dolphinemu.dolphinemu.features.settings.model.view.* +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout import org.dolphinemu.dolphinemu.model.GpuDriverMetadata import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.utils.* @@ -953,6 +954,33 @@ class SettingsFragmentPresenter( ) ) if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) { + if (StringSetting.ACHIEVEMENTS_API_TOKEN.string == "") { + sl.add( + RunRunnable( + context, + R.string.achievements_login, + 0, + 0, + 0, + false + ) { + fragmentView.showDialogFragment(LoginDialog()) + loadSettingsList() + }) + } else { + sl.add( + RunRunnable( + context, + R.string.achievements_logout, + 0, + 0, + 0, + false + ) { + logout() + loadSettingsList() + }) + } sl.add( SwitchSetting( context, diff --git a/Source/Android/app/src/main/res/layout/dialog_login.xml b/Source/Android/app/src/main/res/layout/dialog_login.xml new file mode 100644 index 000000000000..a8b2b3228c5a --- /dev/null +++ b/Source/Android/app/src/main/res/layout/dialog_login.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + +