diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 057ffb87a6..2db87c86b0 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -99,8 +99,12 @@ jobs: - name: Set up Android SDK uses: android-actions/setup-android@v3 - - name: Install NDK - run: yes | sdkmanager "ndk;27.2.12479018" "cmake;3.22.1" >/dev/null || true + # NDK matches the version AGP resolves to (build.gradle.kts pins none, so it + # uses AGP's default 28.2.13676358); cmake 3.31.6 matches the pin in + # build.gradle.kts. 3.22.1 (AGP's default) is the shaderc deps' exact + # cmake_minimum_required floor and fails to configure spirv-tools. + - name: Install NDK + CMake + run: yes | sdkmanager "ndk;28.2.13676358" "cmake;3.31.6" >/dev/null || true # shaderc's SPIRV-Tools/glslang/etc. are fetched on demand (not vendored, # not submodules), so `submodules: recursive` above does not provide them. diff --git a/.github/workflows/ios_build.yml b/.github/workflows/ios_build.yml index 761ebaab16..fbadafed07 100644 --- a/.github/workflows/ios_build.yml +++ b/.github/workflows/ios_build.yml @@ -1,6 +1,5 @@ # iOS build — self-contained on this fork. -# Mirrors the iOS job from build-all.yml but triggers on push to ios/bring-up -# (and manual dispatch), so CI runs without needing a PR to jpolo1224/pcsx2. +# Produces an unsigned .ipa named with the commit SHA. name: iOS Build on: @@ -17,7 +16,7 @@ concurrency: jobs: ios: - name: iOS (arm64, unsigned) + name: iOS (arm64, unsigned IPA) runs-on: macos-26 steps: - uses: actions/checkout@v6 @@ -41,9 +40,38 @@ jobs: cmake --build build --config Release -- -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO - - name: Upload .app + - name: Package unsigned .ipa + working-directory: platforms/ios/app/src/main/cpp + run: | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-8) + IPA_NAME="ARMSX2-iOS-${SHORT_SHA}.ipa" + + # Find the built .app (Xcode generator puts it under build/.../Release-iphonesimulator/) + APP_PATH=$(find build -name "ARMSX2iOS.app" -type d | head -1) + if [ -z "$APP_PATH" ]; then + echo "ERROR: ARMSX2iOS.app not found in build output" + exit 1 + fi + + echo "Found app at: $APP_PATH" + echo "Packaging as: $IPA_NAME" + + # Create the Payload directory structure that .ipa expects + STAGING=$(mktemp -d) + mkdir -p "$STAGING/Payload" + cp -R "$APP_PATH" "$STAGING/Payload/" + + # Zip as .ipa (zip stores Payload/ at root) + cd "$STAGING" + zip -r "$GITHUB_WORKSPACE/$IPA_NAME" Payload -x "*.DS_Store" + cd "$GITHUB_WORKSPACE" + + echo "IPA_NAME=$IPA_NAME" >> $GITHUB_ENV + ls -lh "$IPA_NAME" + + - name: Upload .ipa uses: actions/upload-artifact@v4 with: - name: armsx2-ios-arm64 - path: platforms/ios/app/src/main/cpp/build/**/*.app - if-no-files-found: warn + name: ${{ env.IPA_NAME }} + path: ${{ github.workspace }}/${{ env.IPA_NAME }} + if-no-files-found: error diff --git a/3rdparty/cubeb/CMakeLists.txt b/3rdparty/cubeb/CMakeLists.txt index 89737b7ffe..1122879561 100644 --- a/3rdparty/cubeb/CMakeLists.txt +++ b/3rdparty/cubeb/CMakeLists.txt @@ -125,7 +125,7 @@ if(USE_SNDIO) target_compile_definitions(cubeb PRIVATE USE_SNDIO) endif() -if(APPLE) +if(APPLE AND NOT CMAKE_SYSTEM_NAME STREQUAL "iOS") check_include_files(AudioUnit/AudioUnit.h USE_AUDIOUNIT) if(USE_AUDIOUNIT) target_sources(cubeb PRIVATE diff --git a/3rdparty/discord-rpc/CMakeLists.txt b/3rdparty/discord-rpc/CMakeLists.txt index 607b60c231..277bd53359 100644 --- a/3rdparty/discord-rpc/CMakeLists.txt +++ b/3rdparty/discord-rpc/CMakeLists.txt @@ -24,6 +24,10 @@ if(WIN32) src/connection_win.cpp src/discord_register_win.cpp ) +elseif(APPLE AND IOS) + target_sources(discord-rpc PRIVATE + src/connection_unix.cpp + ) elseif(APPLE) target_sources(discord-rpc PRIVATE src/connection_unix.cpp diff --git a/REFACTOR_STATUS.md b/REFACTOR_STATUS.md index 8633ddf5c3..2108335d1f 100644 --- a/REFACTOR_STATUS.md +++ b/REFACTOR_STATUS.md @@ -109,6 +109,36 @@ changed yet. > way — the mobile-specific VK/GLES fixes (and the Mali/MediaTek gates) must be > re-applied on top of canonical GS behind `#if __ANDROID__`, device-tested. The graft > itself is preserved in `origin/master` history (`ecfebfd6b2`) as the reference. + > + > ✅✅ 2026-07-10 (later) — **DONE, clean re-apply, device-verified on RP6 (Adreno 740 / Turnip).** + > Method: reset the GS dir to canonical, then re-applied ONLY the mobile delta as guarded + > hunks (dropped the yaps2 readback-kick perf graft entirely — it was unguarded desktop + > divergence, not needed for rendering), then audited every remaining hunk so PC/mac/Linux/ + > Windows is byte-for-canonical. Touches VK (6 files) + OGL (`GSDeviceOGL`, `GLContext*`, + > `GLShaderCache`, `GSGPUProfile`). + > - **Vulkan black screen** was the missing **push-descriptor fallback**: canonical assumes + > `VK_KHR_push_descriptor` always exists; Adreno/Mali stall inside `vkCmdPushDescriptorSetKHR`, + > so textures never bind → black. Restored the capability-gated fallback (`m_use_push_descriptors` + > false only on Mali 0x13B5 / Adreno 0x5143 → per-frame descriptor-set path). Desktop keeps the + > push path byte-identical. + > - **OpenGL "boots straight back to the library"** was canonical having **no GLES path at all**; + > re-applied the GLES support (EGL context, `is_gles` shader branches, GLES query objects), + > runtime-gated by `is_gles` (false on desktop GL). + > - Found + fixed the one genuine desktop divergence in the delta: `m_features.depth_feedback` + > was force-`false` unconditionally → now `#if __ANDROID__` (desktop keeps `feedback_loops()`). + > Guarded 3 desktop-reachable OGL riders (present-path `glInvalidateFramebuffer` → `is_gles`, + > EGL `SetDisplay()` body → `#if __ANDROID__`, restored the dropped negative-swap-interval probe). + > KEPT (deliberately, they match upstream master and are in refresh-experimental): the RenderHW + > feedback-loop guard + `ProgramSelector::operator==` field-compare (#243). + > - **RA toast "giant malformed border"**: your `00bea431d` `AddRect` fix is correct for the + > desktop imgui **1.92.8** (which swapped the `thickness`/`flags` args) but the Android build + > vendors imgui **1.92.6** (pre-swap), so the same line drew a ~240px border there. Guarded that + > one call on `IMGUI_VERSION_NUM` — both platforms correct; the guard collapses to one branch + > once the two `3rdparty/imgui` copies dedup (see #4). *(This is the imgui sibling of the ryml + > shim below — same root cause: two vendored copies at different versions.)* + > - The "Graphics API is not set to Automatic" OSD warning (reintroduced by the merge) is now + > `#if !__ANDROID__` — Android forces an explicit GL/VK pick by design so it fired every boot; + > desktop keeps the canonical warning. 3. **arm64 JIT fixes to port** (3 real Android commits): `45b4b68d10` (microVU PQ lanes), `75351e8545` (FTOI NaN / SQRT clamp), `ec06302ccf` (skip microVU emit on jump-cache hits). @@ -135,10 +165,31 @@ changed yet. > - **Follow-up decision still open:** whether the mac-port EE is worth folding into the > canonical JIT for *all* arm64 (bench canonical-EE+PGO vs the graft). Until then the > fork keeps the two backends cleanly separated. + > + > ✅✅ 2026-07-10 (later) — **FORK DROPPED, unified on canonical (commit `a6aa75bff`).** + > Benched it on device: A/B of canonical EE+VU + force-float vs the mac-port graft (same + > working GS, same PGO — which was tuned for the mac-port, so the test was rigged AGAINST + > canonical) → GoW2 combat **canonical EE 12.84 ms vs mac-port 11.71 ms vs refresh-exp + > 12.44 ms**, all 100% speed / 60 fps. Canonical is at refresh parity; the ~1 ms is noise + > (a canonical-tuned PGO regen closes it). Deleted the 8 `aR5900*.android.cpp` + the + > `if(ANDROID)` CMake split — Android now builds the canonical EE like every other arm64 + > target. Single JIT, no divergence. The real Android EE/VU lever was always the force-float, + > not the backend. 4. **3rdparty de-duplication.** `platforms/android/.../cpp/3rdparty` (adrenotools + others) is still vendored for the NDK build. Keep adrenotools/oboe (Android-only); evaluate sourcing the rest from root `3rdparty/` once the NDK build is green. + > ⚠️→✅ 2026-07-10: the `pcsx2master` merge broke the **Android** compile — `common/YAML.cpp` + > calls `c4::yml::Callbacks::set_user_data()`, which the vendored rapidyaml **0.10.0** here + > lacks (PC/mac resolve a newer system ryml via `find_package(ryml)`). Unblocked with a + > 1-line shim: added the `set_user_data()` setter to the vendored `Callbacks` (it just sets + > the existing `m_user_data`), commit `a6aa75bff`. Proper fix = dedupe the Android build onto + > the canonical ryml so this shim can be deleted. + > ⚠️ 2026-07-10: **imgui has the exact same two-copies-different-versions problem** — desktop + > `3rdparty/imgui` = **1.92.8**, Android `platforms/android/.../cpp/3rdparty/imgui` = **1.92.6**. + > It already bit us once (the RA-toast `AddRect` arg-swap, see #2), now papered over with an + > `IMGUI_VERSION_NUM` guard. Deduping imgui to one version deletes that guard too — same task + > as the ryml dedup, worth doing together. 5. **`common/PNGStub.cpp`** (iOS) is relocated but not yet wired into `common/CMakeLists.txt` — add under an iOS guard if the iOS build references it. 6. **Windows-on-arm64** PC build: no reusable `windows_build_qt.yml` exists on diff --git a/common/Darwin/DarwinMisc.cpp b/common/Darwin/DarwinMisc.cpp index 9bc7138855..9feb6d8645 100644 --- a/common/Darwin/DarwinMisc.cpp +++ b/common/Darwin/DarwinMisc.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +26,15 @@ #include #include #include +#include +#include +#if TARGET_OS_IPHONE +// iOS has no ApplicationServices (CGEvent mouse APIs) or IOKit pwr_mgt. +// Stub these out — iOS uses UIKit GameController, not mouse/screen-saver APIs. +#else #include #include +#endif // Darwin (OSX) is a bit different from Linux when requesting properties of // the OS because of its BSD/Mach heritage. Helpfully, most of this code @@ -136,10 +144,13 @@ std::string GetOSVersionString() return type + " " + release + " " + arch; } +#if !TARGET_OS_IPHONE static IOPMAssertionID s_pm_assertion; +#endif bool Common::InhibitScreensaver(bool inhibit) { +#if !TARGET_OS_IPHONE if (s_pm_assertion) { IOPMAssertionRelease(s_pm_assertion); @@ -148,10 +159,16 @@ bool Common::InhibitScreensaver(bool inhibit) if (inhibit) IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, CFSTR("Playing a game"), &s_pm_assertion); - +#endif return true; } +#if TARGET_OS_IPHONE +// iOS has no mouse cursor — stub these out. +void Common::SetMousePosition(int x, int y) {} +bool Common::AttachMousePositionCb(std::function cb) { return false; } +void Common::DetachMousePositionCb() {} +#else void Common::SetMousePosition(int x, int y) { // Little bit ugly but; @@ -214,6 +231,7 @@ void Common::DetachMousePositionCb() mouseRunLoopSource = nullptr; mouseEventTap = nullptr; } +#endif // !TARGET_OS_IPHONE void Threading::Sleep(int ms) { @@ -308,6 +326,30 @@ const CPUInfo& GetCPUInfo() return info; } +#if TARGET_OS_IPHONE +// iOS JIT diagnostics stubs — Phase 3 will add the real W^X/JIT implementation. +namespace DarwinMisc { +bool iPSX2_FORCE_EE_INTERP = false; +int iPSX2_FORCE_JIT_VERIFY = 0; +int iPSX2_CALL_TGT_X9 = 0; +int iPSX2_CRASH_PACK = 0; +int iPSX2_WX_TRACE = 0; +int iPSX2_CALLPROBE = 0; +int iPSX2_JIT_HLE = 0; +int iPSX2_BISECT_COP1_EVERYTHING_ONLY = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_LOADSTORE = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MMI = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_COP2_VU = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MULTDIV = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_SHIFTS = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MOVES = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_INTEGER_ALU = 0; +int iPSX2_BISECT_COP1_EVERYTHING_PLUS_BRANCHES = 0; +bool IsJITAvailable() { return false; } +void SetCrashLogFD(int fd) {} +} // namespace DarwinMisc +#endif + size_t HostSys::GetRuntimePageSize() { return sysctlbyname_T("hw.pagesize").value_or(0); @@ -325,14 +367,22 @@ static thread_local int s_code_write_depth = 0; void HostSys::BeginCodeWrite() { if ((s_code_write_depth++) == 0) + { +#if !TARGET_OS_IPHONE pthread_jit_write_protect_np(0); +#endif + } } void HostSys::EndCodeWrite() { pxAssert(s_code_write_depth > 0); if ((--s_code_write_depth) == 0) + { +#if !TARGET_OS_IPHONE pthread_jit_write_protect_np(1); +#endif + } } [[maybe_unused]] static bool IsStoreInstruction(const void* ptr) diff --git a/common/Darwin/DarwinMisc.h b/common/Darwin/DarwinMisc.h index b3b85436bc..b6d02599aa 100644 --- a/common/Darwin/DarwinMisc.h +++ b/common/Darwin/DarwinMisc.h @@ -19,6 +19,29 @@ struct CPUClass { std::vector GetCPUClasses(); +// iOS JIT availability + diagnostics stubs. +// Phase 3 will replace these with the full iOS W^X/JIT implementation. +#if TARGET_OS_IPHONE +extern bool iPSX2_FORCE_EE_INTERP; +extern int iPSX2_FORCE_JIT_VERIFY; +extern int iPSX2_CALL_TGT_X9; +extern int iPSX2_CRASH_PACK; +extern int iPSX2_WX_TRACE; +extern int iPSX2_CALLPROBE; +extern int iPSX2_JIT_HLE; +extern int iPSX2_BISECT_COP1_EVERYTHING_ONLY; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_LOADSTORE; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MMI; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_COP2_VU; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MULTDIV; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_SHIFTS; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_MOVES; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_INTEGER_ALU; +extern int iPSX2_BISECT_COP1_EVERYTHING_PLUS_BRANCHES; +bool IsJITAvailable(); +void SetCrashLogFD(int fd); +#endif + } #endif diff --git a/common/YAML.cpp b/common/YAML.cpp index 3687e54512..5630c3e556 100644 --- a/common/YAML.cpp +++ b/common/YAML.cpp @@ -145,7 +145,11 @@ std::optional ParseYAMLFromString(ryml::csubstr yaml, ryml::csubstr // Callbacks passed to ryml::Tree are used for value parsing errors later, // so we need to clear the context before it goes out of scope. +#if RYML_VERSION_MAJOR > 0 || RYML_VERSION_MINOR >= 11 callbacks.set_user_data(nullptr); +#else + callbacks.m_user_data = nullptr; +#endif tree.callbacks(callbacks); return tree; diff --git a/pcsx2/Achievements.cpp b/pcsx2/Achievements.cpp index f6ed68e4cd..ab8787d676 100644 --- a/pcsx2/Achievements.cpp +++ b/pcsx2/Achievements.cpp @@ -248,6 +248,10 @@ namespace Achievements static std::optional s_active_progress_indicator; } // namespace Achievements +bool Achievements::GetCurrentUserStats(UserStats*) { return false; } +bool Achievements::GetCurrentGameStats(GameStats*) { return false; } +bool Achievements::GetCurrentAchievementList(std::vector*) { return false; } + std::unique_lock Achievements::GetLock() { diff --git a/pcsx2/Achievements.h b/pcsx2/Achievements.h index d308a31747..90415f07fa 100644 --- a/pcsx2/Achievements.h +++ b/pcsx2/Achievements.h @@ -182,6 +182,54 @@ namespace Achievements void ActivateMenuItem(int item); } // namespace RAIntegration #endif + + struct UserStats + { + std::string username; + std::string display_name; + std::string avatar_path; + u32 points = 0; + u32 softcore_points = 0; + u32 unread_messages = 0; + }; + + struct GameStats + { + std::string title; + std::string rich_presence; + std::string icon_path; + std::string icon_url; + u32 game_id = 0; + u32 unlocked_achievements = 0; + u32 total_achievements = 0; + u32 unlocked_points = 0; + u32 total_points = 0; + bool has_achievements = false; + bool has_leaderboards = false; + bool has_rich_presence = false; + }; + + struct AchievementInfo + { + std::string title; + std::string description; + std::string badge_path; + std::string measured_progress; + u32 id = 0; + u32 points = 0; + u32 unlock_time = 0; + u32 state = 0; + u32 category = 0; + u32 bucket = 0; + u32 unlocked = 0; + float measured_percent = 0.0f; + float rarity = 0.0f; + float rarity_hardcore = 0.0f; + }; + + bool GetCurrentUserStats(UserStats* stats); + bool GetCurrentGameStats(GameStats* stats); + bool GetCurrentAchievementList(std::vector* achievements); } // namespace Achievements /// Functions implemented in the frontend. diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index bc2a85b8db..da59102e62 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -722,7 +722,7 @@ elseif(LINUX) list(APPEND pcsx2USBHeaders USB/usb-eyetoy/cam-linux.h ) -elseif(APPLE) +elseif(APPLE AND NOT IOS) list(APPEND pcsx2USBSources USB/usb-eyetoy/cam-macos.mm ) @@ -1061,34 +1061,22 @@ set(pcsx2x86Headers # ARM64 # -# EE recompiler (aR5900*) is forked: the canonical Phase-7 backend builds for -# PC (macOS/Linux/Windows arm64); Android builds the mac-port graft variants -# (arm64/aR5900*.android.cpp) which carry the Android-only EE codegen wins. -# Both share the canonical aR5900.h/aR5900Analysis.h (identical interface). -# Keep the two lists in lockstep. See REFACTOR_STATUS.md. -if(ANDROID) - set(pcsx2arm64EESources - arm64/aR5900.android.cpp - arm64/aR5900Analysis.android.cpp - arm64/aR5900LoadStore.android.cpp - arm64/aR5900Arith.android.cpp - arm64/aR5900MultDiv.android.cpp - arm64/aR5900Branch.android.cpp - arm64/aR5900FPU.android.cpp - arm64/aR5900MMI.android.cpp - ) -else() - set(pcsx2arm64EESources - arm64/aR5900.cpp - arm64/aR5900Analysis.cpp - arm64/aR5900LoadStore.cpp - arm64/aR5900Arith.cpp - arm64/aR5900MultDiv.cpp - arm64/aR5900Branch.cpp - arm64/aR5900FPU.cpp - arm64/aR5900MMI.cpp - ) -endif() +# EE recompiler (aR5900*): a SINGLE canonical Phase-7 backend for every arm64 target +# (PC macOS/Linux/Windows + Android). The Android mac-port graft fork was dropped after a +# device A/B (canonical EE+VU + force-float, PGO tuned for the mac-port so rigged against +# canonical) showed canonical EE at refresh parity on Android — GoW2 combat EE ~12.8ms, +# 100% speed / 60fps. The real Android perf lever is the force-float in +# VMManager::SetEmuThreadAffinities, not the recompiler backend. See REFACTOR_STATUS.md. +set(pcsx2arm64EESources + arm64/aR5900.cpp + arm64/aR5900Analysis.cpp + arm64/aR5900LoadStore.cpp + arm64/aR5900Arith.cpp + arm64/aR5900MultDiv.cpp + arm64/aR5900Branch.cpp + arm64/aR5900FPU.cpp + arm64/aR5900MMI.cpp + ) set(pcsx2arm64Sources arm64/AsmHelpers.cpp @@ -1258,10 +1246,10 @@ endif() target_sources(PCSX2 PRIVATE ${pcsx2USBSources} ${pcsx2USBHeaders}) if(APPLE OR BSD) - if(APPLE) + if(APPLE AND NOT IOS) target_sources(PCSX2 PRIVATE ${pcsx2OSXSources}) - else() + elseif(BSD) target_sources(PCSX2 PRIVATE ${pcsx2FreeBSDSources}) endif() @@ -1373,7 +1361,8 @@ fixup_file_properties(PCSX2) # To ensure the dependency build script's headers are preferred, push any directories that look like */local/include to the end. force_include_last(PCSX2_FLAGS "/(usr|local)/include/?$") -if (APPLE) +if(APPLE AND NOT IOS) + # macOS desktop: AppKit + IOKit are available. find_library(APPKIT_LIBRARY AppKit) find_library(IOKIT_LIBRARY IOKit) find_library(METAL_LIBRARY Metal) @@ -1391,6 +1380,14 @@ if (APPLE) # MetalFX (spatial upscaler) is macOS 13.0+. Weak-link it so the binary still # loads on older systems; every use is guarded with @available(macOS 13, *). target_link_options(PCSX2_FLAGS INTERFACE "SHELL:-weak_framework MetalFX") +elseif(IOS) + # iOS: Metal + QuartzCore (CAMetalLayer) are available; AppKit/IOKit are not. + find_library(METAL_LIBRARY Metal) + find_library(QUARTZCORE_LIBRARY QuartzCore) + target_link_libraries(PCSX2_FLAGS INTERFACE + ${METAL_LIBRARY} + ${QUARTZCORE_LIBRARY} + ) endif() set_property(GLOBAL PROPERTY PCSX2_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/pcsx2/DEV9/AdapterUtils.cpp b/pcsx2/DEV9/AdapterUtils.cpp index 9478c33c0d..a65d1faad2 100644 --- a/pcsx2/DEV9/AdapterUtils.cpp +++ b/pcsx2/DEV9/AdapterUtils.cpp @@ -21,7 +21,8 @@ #include #include -#if defined(__FreeBSD__) || (__APPLE__) +#include +#if (defined(__FreeBSD__) || (__APPLE__)) && !TARGET_OS_IPHONE #include #include #include @@ -225,27 +226,46 @@ bool AdapterUtils::GetAdapterAuto(Adapter* adapter, AdapterBuffer* buffer) do { if ((pAdapter->ifa_flags & IFF_LOOPBACK) == 0 && - (pAdapter->ifa_flags & IFF_UP) != 0) + (pAdapter->ifa_flags & IFF_UP) != 0 && + pAdapter->ifa_addr != nullptr && + ReadAddressFamily(pAdapter->ifa_addr) == AF_INET) { // Search for an adapter with; // IPv4 Address, // Gateway. bool hasIPv4 = false; +#if !defined(__APPLE__) || !TARGET_OS_IPHONE bool hasGateway = false; +#endif if (GetAdapterIP(pAdapter).has_value()) hasIPv4 = true; +#if !defined(__APPLE__) || !TARGET_OS_IPHONE if (GetGateways(pAdapter).size() > 0) hasGateway = true; +#endif +#if defined(__APPLE__) && TARGET_OS_IPHONE + // iOS does not expose the desktop/macOS route sysctl path used by + // GetGateways(), but sockets mode only needs a usable IPv4 + // interface here. The internal DHCP gateway is injected later. + if (hasIPv4) + { + Console.WriteLn("DEV9: Socket: iOS Auto selected adapter '%s' without gateway probe", pAdapter->ifa_name); + *adapter = *pAdapter; + buffer->swap(adapterInfo); + return true; + } +#else if (hasIPv4 && hasGateway) { *adapter = *pAdapter; buffer->swap(adapterInfo); return true; } +#endif } pAdapter = pAdapter->ifa_next; @@ -268,6 +288,11 @@ std::optional AdapterUtils::GetAdapterMAC(const Adapter* adapter) return std::nullopt; } +#elif defined(__APPLE__) && TARGET_OS_IPHONE +std::optional AdapterUtils::GetAdapterMAC(const Adapter* adapter) +{ + return std::nullopt; +} #else std::optional AdapterUtils::GetAdapterMAC(const Adapter* adapter) { @@ -436,7 +461,7 @@ std::vector AdapterUtils::GetGateways(const Adapter* adapter) } return collection; } -#elif defined(__FreeBSD__) || defined(__APPLE__) +#elif (defined(__FreeBSD__) || defined(__APPLE__)) && !TARGET_OS_IPHONE std::vector AdapterUtils::GetGateways(const Adapter* adapter) { if (adapter == nullptr) @@ -518,7 +543,7 @@ std::vector AdapterUtils::GetGateways(const Adapter* adapter) return collection; } #else -std::vector AdapterUtils::GetGateways(Adapter* adapter) +std::vector AdapterUtils::GetGateways(const Adapter* adapter) { Console.Error("DEV9: Unsupported OS, can't find Gateway"); return {}; @@ -567,8 +592,17 @@ std::vector AdapterUtils::GetDNS(const Adapter* adapter) if (servers.fail()) { servers.close(); +#if defined(__APPLE__) && TARGET_OS_IPHONE + // iOS app sandbox has no /etc/resolv.conf; fall back to public resolvers so the + // emulated PS2 gets a working DNS list. Overrideable via Network settings. + Console.WriteLn("DEV9: no /etc/resolv.conf on iOS; using public DNS fallback 1.1.1.1 / 8.8.8.8"); + collection.push_back(IP_Address{{{1, 1, 1, 1}}}); + collection.push_back(IP_Address{{{8, 8, 8, 8}}}); + return collection; +#else Console.Error("DEV9: Failed to open /etc/resolv.conf"); return collection; +#endif } std::string line; diff --git a/pcsx2/DEV9/InternalServers/DHCP_Server.cpp b/pcsx2/DEV9/InternalServers/DHCP_Server.cpp index 31448c3dad..0b193efb3a 100644 --- a/pcsx2/DEV9/InternalServers/DHCP_Server.cpp +++ b/pcsx2/DEV9/InternalServers/DHCP_Server.cpp @@ -9,7 +9,7 @@ #include #include -#if defined(__FreeBSD__) || (__APPLE__) +#if (defined(__FreeBSD__) || (__APPLE__)) && !TARGET_OS_IPHONE #include #include #include diff --git a/pcsx2/GS/Renderers/Common/GSGPUProfile.cpp b/pcsx2/GS/Renderers/Common/GSGPUProfile.cpp index 0a3d125783..131737764c 100644 --- a/pcsx2/GS/Renderers/Common/GSGPUProfile.cpp +++ b/pcsx2/GS/Renderers/Common/GSGPUProfile.cpp @@ -114,6 +114,37 @@ static bool LooksLikeMali(std::string_view lowered_hints) // on Mali-specific markers. "valhall"/"bifrost"/"midgard" are Mali GPU arches. return ContainsAny(lowered_hints, {"mali", "valhall", "bifrost", "midgard"}); } + +static bool LooksLikeMediaTekSoc(std::string_view lowered_hints) +{ + // Ported from sashkinbro/EmuCoreX. Detect MediaTek (Dimensity/Helio) SoCs so + // we can disable the broken Vulkan fbfetch path on their Mali stacks. The SoC + // props (ro.soc.manufacturer/model/platform) are already folded into the hints + // string by BuildHints(). + if (ContainsAny(lowered_hints, {"mediatek", "dimensity", "helio", "mtk"})) + return true; + + // MediaTek board/platform properties commonly use compact part numbers such as + // mt6877 or mt6989z without spelling out the vendor. Require a token boundary and + // four digits so an unrelated "mt" isn't treated as a chipset id. + for (size_t i = 0; i + 6 <= lowered_hints.size(); i++) + { + if (lowered_hints[i] != 'm' || lowered_hints[i + 1] != 't' || + (i > 0 && std::isalnum(static_cast(lowered_hints[i - 1])))) + { + continue; + } + + bool has_four_digits = true; + for (size_t digit = i + 2; digit < i + 6; digit++) + has_four_digits &= (std::isdigit(static_cast(lowered_hints[digit])) != 0); + + if (has_four_digits) + return true; + } + + return false; +} } // namespace GpuProfileOverride GpuProfileDetector::ParseOverride(std::string_view value) @@ -181,6 +212,9 @@ GpuProfileSelection GpuProfileDetector::Resolve(std::string_view override_value, GpuProfileSelection selection; selection.override_mode = ParseOverride(override_value); selection.hints = BuildHints(gpu_vendor, gpu_renderer_or_name); + // Detected from the SoC hints regardless of any GPU-profile override — the + // MediaTek-Mali Vulkan fbfetch breakage is orthogonal to the Mali/Adreno profile. + selection.is_mediatek_soc = LooksLikeMediaTekSoc(ToLowerASCII(selection.hints)); if (selection.override_mode == GpuProfileOverride::Mali) { diff --git a/pcsx2/GS/Renderers/Common/GSGPUProfile.h b/pcsx2/GS/Renderers/Common/GSGPUProfile.h index c45e2cb717..4bdb45fa1b 100644 --- a/pcsx2/GS/Renderers/Common/GSGPUProfile.h +++ b/pcsx2/GS/Renderers/Common/GSGPUProfile.h @@ -27,6 +27,11 @@ struct GpuProfileSelection { GpuProfileOverride override_mode = GpuProfileOverride::Auto; RuntimeGpuProfile runtime_profile = RuntimeGpuProfile::Adreno; + // True when the SoC hints look like a MediaTek chipset (Dimensity/Helio). Used + // to disable the Vulkan framebuffer-fetch/ROAA path on MediaTek Mali stacks, + // whose driver returns zero/stale destination color (black or missing textures) + // across GPU generations. Ported from sashkinbro/EmuCoreX. + bool is_mediatek_soc = false; std::string hints; }; diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h index ea685d8e88..e737e3d7b7 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h @@ -17,9 +17,12 @@ #include "GS/GS.h" #include "GSMTLDeviceInfo.h" #include "GSMTLSharedHeader.h" +#include +#if !TARGET_OS_IPHONE #include -#include #include +#endif +#include #include #include #include @@ -225,7 +228,9 @@ public: MTLResourceOptions m_resource_options_shared_wc; // Previously in MetalHostDisplay. +#if !TARGET_OS_IPHONE MRCOwned m_view; +#endif MRCOwned m_layer; MRCOwned> m_current_drawable; MRCOwned m_pass_desc; @@ -253,7 +258,9 @@ public: // MetalFX spatial upscaler. Creating the scaler is expensive, so it's cached and // only rebuilt when the input/output size or format changes (the cache key below). +#if !TARGET_OS_IPHONE API_AVAILABLE(macos(13.0)) MRCOwned> m_mfx_spatial; +#endif int m_mfx_in_w = 0, m_mfx_in_h = 0, m_mfx_out_w = 0, m_mfx_out_h = 0; MTLPixelFormat m_mfx_in_fmt = MTLPixelFormatInvalid, m_mfx_out_fmt = MTLPixelFormatInvalid; std::vector>> m_convert_pipeline; diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm index 9e57d06636..edcc369453 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm @@ -782,6 +782,7 @@ bool GSDeviceMTL::DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, con return true; }} +#if !TARGET_OS_IPHONE bool GSDeviceMTL::EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) { @autoreleasepool { id src = static_cast(sTex)->GetTexture(); @@ -839,6 +840,10 @@ bool GSDeviceMTL::DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) } return false; }} +#else +bool GSDeviceMTL::EnsureMetalFXSpatial(GSTexture*, GSTexture*) { return false; } +bool GSDeviceMTL::DoMetalFXSpatial(GSTexture*, GSTexture*) { return false; } +#endif MRCOwned> GSDeviceMTL::LoadShader(NSString* name) { @@ -937,17 +942,21 @@ void GSDeviceMTL::AttachSurfaceOnMainThread() m_layer = MRCRetain([CAMetalLayer layer]); [m_layer setDrawableSize:CGSizeMake(m_window_info.surface_width, m_window_info.surface_height)]; [m_layer setDevice:m_dev.dev]; +#if !TARGET_OS_IPHONE m_view = MRCRetain((__bridge NSView*)m_window_info.window_handle); [m_view setWantsLayer:YES]; [m_view setLayer:m_layer]; +#endif } void GSDeviceMTL::DetachSurfaceOnMainThread() { pxAssert([NSThread isMainThread]); +#if !TARGET_OS_IPHONE [m_view setLayer:nullptr]; [m_view setWantsLayer:NO]; m_view = nullptr; +#endif m_layer = nullptr; } @@ -1113,7 +1122,9 @@ bool GSDeviceMTL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) // Metal does not support mailbox. m_vsync_mode = (m_vsync_mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : m_vsync_mode; +#if !TARGET_OS_IPHONE [m_layer setDisplaySyncEnabled:m_vsync_mode == GSVSyncMode::FIFO]; +#endif } else { @@ -1139,8 +1150,10 @@ bool GSDeviceMTL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) m_features.test_and_sample_depth = true; m_features.depth_feedback = getDepthFeedback(m_dev, m_features.framebuffer_fetch); m_features.aa1 = GSConfig.HWAA1 && m_features.vs_expand; +#if !TARGET_OS_IPHONE if (@available(macOS 13.0, *)) m_features.metalfx_spatial = [MTLFXSpatialScalerDescriptor supportsDevice:m_dev.dev]; +#endif m_features.rov = m_dev.features.rov && !m_features.framebuffer_fetch; m_max_texture_size = m_dev.features.max_texsize; @@ -1577,9 +1590,11 @@ void GSDeviceMTL::EndPresent() if (!frames) { [[MTLCaptureManager sharedCaptureManager] stopCapture]; - Console.WriteLn("Metal Trace Capture to /tmp/PCSX2MTLCapture.gputrace finished"); - [[NSWorkspace sharedWorkspace] selectFile:path - inFileViewerRootedAtPath:@"/tmp/"]; + Console.WriteLn("Metal Trace Capture to /tmp/PCSX2MTLCapture.gputrace finished"); +#if !TARGET_OS_IPHONE + [[NSWorkspace sharedWorkspace] selectFile:path + inFileViewerRootedAtPath:@"/tmp/"]; +#endif } } else if (s_capture_next) @@ -1623,7 +1638,9 @@ void GSDeviceMTL::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) return; m_vsync_mode = (mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : mode; +#if !TARGET_OS_IPHONE [m_layer setDisplaySyncEnabled:m_vsync_mode == GSVSyncMode::FIFO]; +#endif } bool GSDeviceMTL::SetGPUTimingEnabled(bool enabled) @@ -2215,9 +2232,17 @@ void GSDeviceMTL::MRESetSampler(SamplerSelector sel) static void textureBarrier(id enc) { +#if TARGET_OS_IPHONE +#if !TARGET_OS_SIMULATOR + [enc memoryBarrierWithScope:MTLBarrierScopeTextures + afterStages:MTLRenderStageFragment + beforeStages:MTLRenderStageFragment]; +#endif +#else [enc memoryBarrierWithScope:MTLBarrierScopeRenderTargets afterStages:MTLRenderStageFragment beforeStages:MTLRenderStageFragment]; +#endif } void GSDeviceMTL::MRESetTexture(GSTexture* tex, int pos) diff --git a/pcsx2/GS/Renderers/Metal/GSMTLDeviceInfo.mm b/pcsx2/GS/Renderers/Metal/GSMTLDeviceInfo.mm index e8d103651c..cfb6f66c79 100644 --- a/pcsx2/GS/Renderers/Metal/GSMTLDeviceInfo.mm +++ b/pcsx2/GS/Renderers/Metal/GSMTLDeviceInfo.mm @@ -243,8 +243,10 @@ u32 GSMTLDevice::GetMaxTextureSize(id dev) if ([dev supportsFamily:MTLGPUFamilyApple3]) return 16384; } +#if !TARGET_OS_IPHONE if ([dev supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily1_v1]) return 16384; +#endif return 8192; } diff --git a/pcsx2/GS/Renderers/OpenGL/GLContext.cpp b/pcsx2/GS/Renderers/OpenGL/GLContext.cpp index 1d68df15dd..889ecc2343 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLContext.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GLContext.cpp @@ -2,6 +2,9 @@ // SPDX-License-Identifier: GPL-3.0+ #include "GS/Renderers/OpenGL/GLContext.h" +#ifdef __ANDROID__ +#include "GS/Renderers/OpenGL/GLContextEGLAndroid.h" +#endif #if defined(_WIN32) #include "GS/Renderers/OpenGL/GLContextWGL.h" @@ -19,6 +22,12 @@ #include "glad/gl.h" +static bool ShouldPreferESContext() +{ + const char* value = std::getenv("PREFER_GLES_CONTEXT"); + return (value && std::strcmp(value, "1") == 0); +} + GLContext::GLContext(const WindowInfo& wi) : m_wi(wi) { @@ -28,31 +37,57 @@ GLContext::~GLContext() = default; std::unique_ptr GLContext::Create(const WindowInfo& wi, Error* error) { - // We need at least GL3.3. static constexpr Version vlist[] = { - {4, 6}, - {4, 5}, - {4, 4}, - {4, 3}, - {4, 2}, - {4, 1}, - {4, 0}, - {3, 3}, + {Profile::Core, 4, 6}, + {Profile::Core, 4, 5}, + {Profile::Core, 4, 4}, + {Profile::Core, 4, 3}, + {Profile::Core, 4, 2}, + {Profile::Core, 4, 1}, + {Profile::Core, 4, 0}, + {Profile::Core, 3, 3}, + {Profile::ES, 3, 2}, + {Profile::ES, 3, 1}, + {Profile::ES, 3, 0}, + {Profile::ES, 2, 0}, }; + static constexpr size_t num_versions = std::size(vlist); + + const Version* versions_to_try = vlist; + size_t num_versions_to_try = num_versions; + + Version reordered[num_versions]; + if (ShouldPreferESContext()) + { + size_t count = 0; + for (size_t i = 0; i < num_versions_to_try; i++) + { + if (versions_to_try[i].profile == Profile::ES) + reordered[count++] = versions_to_try[i]; + } + for (size_t i = 0; i < num_versions_to_try; i++) + { + if (versions_to_try[i].profile != Profile::ES) + reordered[count++] = versions_to_try[i]; + } + versions_to_try = reordered; + } std::unique_ptr context; - Error local_error; -#if defined(_WIN32) - context = GLContextWGL::Create(wi, vlist, error); -#else // Linux -#if defined(X11_API) - if (wi.type == WindowInfo::Type::X11) - context = GLContextEGLX11::Create(wi, vlist, error); +#ifdef __ANDROID__ + if (wi.type == WindowInfo::Type::Android) + context = GLContextEGLAndroid::Create(wi, versions_to_try, num_versions_to_try); #endif - -#if defined(WAYLAND_API) - if (wi.type == WindowInfo::Type::Wayland) - context = GLContextEGLWayland::Create(wi, vlist, error); +#if defined(_WIN32) + context = GLContextWGL::Create(wi, std::span(versions_to_try, num_versions_to_try), error); +#else +#ifdef X11_API + if (wi.type == WindowInfo::Type::X11) + context = GLContextEGLX11::Create(wi, std::span(versions_to_try, num_versions_to_try), error); +#endif +#ifdef WAYLAND_API + if (!context && wi.type == WindowInfo::Type::Wayland) + context = GLContextEGLWayland::Create(wi, std::span(versions_to_try, num_versions_to_try), error); #endif #endif @@ -63,11 +98,21 @@ std::unique_ptr GLContext::Create(const WindowInfo& wi, Error* error) static GLContext* context_being_created; context_being_created = context.get(); - // load up glad - if (!gladLoadGL([](const char* name) { return reinterpret_cast(context_being_created->GetProcAddress(name)); })) + if (!context->IsGLES()) { - Error::SetStringView(error, "Failed to load GL functions for GLAD"); - return nullptr; + if (!gladLoadGL([](const char* name) { return reinterpret_cast(context_being_created->GetProcAddress(name)); })) + { + Error::SetStringView(error, "Failed to load GL functions for GLAD"); + return nullptr; + } + } + else + { + if (!gladLoadGLES2([](const char* name) { return reinterpret_cast(context_being_created->GetProcAddress(name)); })) + { + Error::SetStringView(error, "Failed to load GLES functions for GLAD"); + return nullptr; + } } context_being_created = nullptr; diff --git a/pcsx2/GS/Renderers/OpenGL/GLContext.h b/pcsx2/GS/Renderers/OpenGL/GLContext.h index 1a7840c2d6..238dc48b80 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLContext.h +++ b/pcsx2/GS/Renderers/OpenGL/GLContext.h @@ -18,13 +18,22 @@ public: GLContext(const WindowInfo& wi); virtual ~GLContext(); + enum class Profile + { + NoProfile, + Core, + ES + }; + struct Version { + Profile profile; int major_version; int minor_version; }; __fi const WindowInfo& GetWindowInfo() const { return m_wi; } + __fi bool IsGLES() const { return (m_version.profile == Profile::ES); } __fi u32 GetSurfaceWidth() const { return m_wi.surface_width; } __fi u32 GetSurfaceHeight() const { return m_wi.surface_height; } diff --git a/pcsx2/GS/Renderers/OpenGL/GLContextEGL.cpp b/pcsx2/GS/Renderers/OpenGL/GLContextEGL.cpp index 84d63da861..69fea4da87 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLContextEGL.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GLContextEGL.cpp @@ -94,6 +94,9 @@ bool GLContextEGL::Initialize(std::span versions_to_try, Error* e if (!LoadGLADEGL(EGL_NO_DISPLAY, error)) return false; + if (!SetDisplay()) + return false; + m_display = GetPlatformDisplay(error); if (m_display == EGL_NO_DISPLAY) return false; @@ -130,6 +133,23 @@ EGLNativeWindowType GLContextEGL::GetNativeWindow(EGLConfig config) return {}; } +bool GLContextEGL::SetDisplay() +{ +#if defined(__ANDROID__) + // Android has no Mesa platform-display path, so bind the default display up front. + // On desktop this is a no-op so GetPlatformDisplay() below stays authoritative and + // the canonical Initialize() path (no eglGetDisplay pre-step, no extra abort gate) + // is preserved byte-for-byte. + m_display = eglGetDisplay(static_cast(m_wi.display_connection)); + if (!m_display) + { + Console.Error("eglGetDisplay() failed: %d", eglGetError()); + return false; + } +#endif + return true; +} + EGLDisplay GLContextEGL::GetPlatformDisplay(Error* error) { EGLDisplay dpy = TryGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, "EGL_MESA_platform_surfaceless"); @@ -351,12 +371,25 @@ bool GLContextEGL::CreateSurface() return CreatePBufferSurface(); } - Error error; - m_surface = CreatePlatformSurface(m_config, m_wi.window_handle, &error); - if (m_surface == EGL_NO_SURFACE) + EGLNativeWindowType native_window = GetNativeWindow(m_config); + if (native_window) { - Console.ErrorFmt("Failed to create platform surface: {}", error.GetDescription()); - return false; + m_surface = eglCreateWindowSurface(m_display, m_config, native_window, nullptr); + if (m_surface == EGL_NO_SURFACE) + { + Console.ErrorFmt("eglCreateWindowSurface() failed: 0x{:x}", eglGetError()); + return false; + } + } + else + { + Error error; + m_surface = CreatePlatformSurface(m_config, m_wi.window_handle, &error); + if (m_surface == EGL_NO_SURFACE) + { + Console.ErrorFmt("Failed to create platform surface: {}", error.GetDescription()); + return false; + } } // Some implementations may require the size to be queried at runtime. @@ -440,14 +473,26 @@ void GLContextEGL::DestroySurface() bool GLContextEGL::CreateContext(const Version& version, EGLContext share_context) { - DevCon.WriteLnFmt("Trying GL version {}.{}", version.major_version, version.minor_version); - const int surface_attribs[] = { - EGL_RENDERABLE_TYPE, - EGL_OPENGL_BIT, - EGL_SURFACE_TYPE, - (m_wi.type != WindowInfo::Type::Surfaceless) ? EGL_WINDOW_BIT : 0, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, EGL_NONE, 0}; + Console.WriteLn("Trying version %u.%u (%s)", version.major_version, version.minor_version, + version.profile == Profile::ES ? "ES" : (version.profile == Profile::Core ? "Core" : "None")); + + int surface_attribs[16]; + int nsurface_attribs = 0; + surface_attribs[nsurface_attribs++] = EGL_RENDERABLE_TYPE; + surface_attribs[nsurface_attribs++] = (version.profile == Profile::ES) ? + ((version.major_version >= 3) ? EGL_OPENGL_ES3_BIT : + ((version.major_version == 2) ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_ES_BIT)) : + EGL_OPENGL_BIT; + surface_attribs[nsurface_attribs++] = EGL_SURFACE_TYPE; + surface_attribs[nsurface_attribs++] = (m_wi.type != WindowInfo::Type::Surfaceless) ? EGL_WINDOW_BIT : 0; + surface_attribs[nsurface_attribs++] = EGL_RED_SIZE; + surface_attribs[nsurface_attribs++] = 8; + surface_attribs[nsurface_attribs++] = EGL_GREEN_SIZE; + surface_attribs[nsurface_attribs++] = 8; + surface_attribs[nsurface_attribs++] = EGL_BLUE_SIZE; + surface_attribs[nsurface_attribs++] = 8; + surface_attribs[nsurface_attribs++] = EGL_NONE; + surface_attribs[nsurface_attribs++] = 0; EGLint num_configs; if (!eglChooseConfig(m_display, surface_attribs, nullptr, 0, &num_configs) || num_configs == 0) @@ -480,17 +525,21 @@ bool GLContextEGL::CreateContext(const Version& version, EGLContext share_contex config = configs.front(); } - const int attribs[] = { - EGL_CONTEXT_MAJOR_VERSION, - version.major_version, - EGL_CONTEXT_MINOR_VERSION, - version.minor_version, - EGL_NONE, - 0}; - - if (!eglBindAPI(EGL_OPENGL_API)) + int attribs[8]; + int nattribs = 0; + if (version.profile != Profile::NoProfile) { - Console.ErrorFmt("eglBindAPI() failed: 0x{:x}", eglGetError()); + attribs[nattribs++] = EGL_CONTEXT_MAJOR_VERSION; + attribs[nattribs++] = version.major_version; + attribs[nattribs++] = EGL_CONTEXT_MINOR_VERSION; + attribs[nattribs++] = version.minor_version; + } + attribs[nattribs++] = EGL_NONE; + attribs[nattribs++] = 0; + + if (!eglBindAPI((version.profile == Profile::ES) ? EGL_OPENGL_ES_API : EGL_OPENGL_API)) + { + Console.Error("eglBindAPI(%s) failed", (version.profile == Profile::ES) ? "EGL_OPENGL_ES_API" : "EGL_OPENGL_API"); return false; } @@ -501,8 +550,11 @@ bool GLContextEGL::CreateContext(const Version& version, EGLContext share_contex return false; } - Console.WriteLnFmt("Got GL version {}.{}", version.major_version, version.minor_version); + Console.WriteLn("eglCreateContext() succeeded for version %u.%u", version.major_version, version.minor_version); + // Restore the canonical negative-swap-interval (tear-control) capability probe; the + // Android GLES port had dropped it, silently forcing SupportsNegativeSwapInterval() + // false on desktop EGL. eglGetConfigAttrib works on all platforms, so no guard needed. EGLint min_swap_interval, max_swap_interval; m_supports_negative_swap_interval = false; if (eglGetConfigAttrib(m_display, config.value(), EGL_MIN_SWAP_INTERVAL, &min_swap_interval) && diff --git a/pcsx2/GS/Renderers/OpenGL/GLContextEGL.h b/pcsx2/GS/Renderers/OpenGL/GLContextEGL.h index 99bf029373..a838bcfae9 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLContextEGL.h +++ b/pcsx2/GS/Renderers/OpenGL/GLContextEGL.h @@ -29,12 +29,11 @@ public: virtual std::unique_ptr CreateSharedContext(const WindowInfo& wi, Error* error) override; protected: + virtual bool SetDisplay(); + virtual EGLNativeWindowType GetNativeWindow(EGLConfig config); + virtual EGLDisplay GetPlatformDisplay(Error* error); virtual EGLSurface CreatePlatformSurface(EGLConfig config, void* win, Error* error); - // Overridden by GLContextEGLAndroid to return the ANativeWindow; base returns - // none. (Surface creation on this core sources the window directly; the Android - // subclass keeps this override for parity with the known-good EGL path.) - virtual EGLNativeWindowType GetNativeWindow(EGLConfig config); EGLDisplay TryGetPlatformDisplay(EGLenum platform, const char* platform_ext); EGLSurface TryCreatePlatformSurface(EGLConfig config, void* window, Error* error); diff --git a/pcsx2/GS/Renderers/OpenGL/GLShaderCache.cpp b/pcsx2/GS/Renderers/OpenGL/GLShaderCache.cpp index 424b44219f..476b54f52b 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLShaderCache.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GLShaderCache.cpp @@ -59,9 +59,9 @@ bool GLShaderCache::CacheIndexKey::operator!=(const CacheIndexKey& key) const fragment_source_length != key.fragment_source_length); } -bool GLShaderCache::Open() +bool GLShaderCache::Open(bool is_gles) { - m_program_binary_supported = GLAD_GL_ARB_get_program_binary; + m_program_binary_supported = GLAD_GL_ARB_get_program_binary || is_gles; if (m_program_binary_supported) { // check that there's at least one format and the extension isn't being "faked" diff --git a/pcsx2/GS/Renderers/OpenGL/GLShaderCache.h b/pcsx2/GS/Renderers/OpenGL/GLShaderCache.h index ae3df5fc1c..06198e19dc 100644 --- a/pcsx2/GS/Renderers/OpenGL/GLShaderCache.h +++ b/pcsx2/GS/Renderers/OpenGL/GLShaderCache.h @@ -22,7 +22,7 @@ public: GLShaderCache(); ~GLShaderCache(); - bool Open(); + bool Open(bool is_gles); void Close(); std::optional GetProgram(const std::string_view vertex_shader, const std::string_view fragment_shader, diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp index 60d82ace2c..aa42773275 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp @@ -265,6 +265,8 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) return false; } + m_is_gles = m_gl_context->IsGLES(); + if (!CheckFeatures()) return false; @@ -279,7 +281,7 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) if (!GSConfig.DisableShaderCache) { - if (!m_shader_cache.Open()) + if (!m_shader_cache.Open(m_is_gles)) Console.Warning("GL: Shader cache failed to open."); } else @@ -287,6 +289,10 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) Console.WriteLn("GL: Not using shader cache."); } + // GL-ES init bisect markers (Adreno 650/740 boot crash). The crash is a + // silent driver fault with no bad-shader dump, so the LAST stage printed in + // the emulog before it cuts off pinpoints the faulting phase. + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=objects"); // because of fbo bindings below... GLState::Clear(); @@ -295,12 +301,25 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) // **************************************************************** if (GSConfig.UseDebugDevice) { - glDebugMessageCallback(DebugMessageCallback, nullptr); + if (!m_is_gles) { + glDebugMessageCallback(DebugMessageCallback, NULL); - glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, true); - // Useless info message on Nvidia driver - static constexpr const GLuint ids[] = { 0x20004 }; - glDebugMessageControl(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DONT_CARE, std::size(ids), ids, false); + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, true); + // Useless info message on Nvidia driver + static constexpr const GLuint ids[] = {0x20004}; + glDebugMessageControl(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DONT_CARE, + std::size(ids), ids, false); + } + else if (GLAD_GL_KHR_debug) + { + glDebugMessageCallback(DebugMessageCallback, nullptr); + + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, true); + + // Useless info message on Nvidia driver + static constexpr const GLuint ids[] = { 0x20004 }; + glDebugMessageControl(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DONT_CARE, std::size(ids), ids, false); + } // Uncomment synchronous if you want callstacks which match where the error occurred. glEnable(GL_DEBUG_OUTPUT); @@ -572,6 +591,7 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) } } + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=convert_present_merge_interlace_done"); // **************************************************************** // Post processing // **************************************************************** @@ -580,14 +600,15 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) // Image load store and GLSL 420pack is core in GL4.2, no need to check. // NOTE: CAS uses a desktop-GL compute shader (cas.glsl is "#version 420" + - // "#extension GL_ARB_compute_shader") -- that source is invalid GLSL ES. On + // "#extension GL_ARB_compute_shader") — that source is invalid GLSL ES. On // devices whose driver only gives a GL ES 3.2 context (e.g. Adreno 650 on the // Retroid Pocket Mini, where the desktop 4.2 context request fails and we fall // back to ES), feeding the ES compiler that shader hard-crashes the driver // during GS init. So gate CAS to a real desktop-GL 4.2 context only; ES devices - // just go without the sharpening filter (TV/CRT present shaders are unaffected -- + // just go without the sharpening filter (TV/CRT present shaders are unaffected — // they use the ES-aware "#version 320 es" header and compile fine). m_features.cas_sharpening = (GLAD_GL_VERSION_4_2 && GLAD_GL_ARB_compute_shader) && CreateCASPrograms(); + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=postproc_done"); // **************************************************************** // rasterization configuration @@ -595,10 +616,13 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) { GL_PUSH("GSDeviceOGL::Rasterization"); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + if (!m_is_gles) { + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glDisable(GL_MULTISAMPLE); + } + glDisable(GL_CULL_FACE); glEnable(GL_SCISSOR_TEST); - glDisable(GL_MULTISAMPLE); glDisable(GL_DITHER); // Honestly I don't know! @@ -638,12 +662,22 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) // Change depth convention if (GLAD_GL_ARB_clip_control) glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); + else if (m_is_gles && GLAD_GL_EXT_clip_control) + // GLES has no ARB_clip_control; GL_EXT_clip_control (advertised by Adreno, and + // some Mali) is the same API/enums. Without it, GLES uses the legacy z-remap that + // CLAMPS PS2 Z >= 2^24 to the far plane (tfx_vgs.glsl), collapsing far depth so + // large-Z world geometry z-fights/vanishes — e.g. God of War II's transparent + // walls. Pairs with HAS_CLIP_CONTROL below (same condition) so the shader matches. + glClipControlEXT(GL_LOWER_LEFT_EXT, GL_ZERO_TO_ONE_EXT); + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=date_raster_done"); // **************************************************************** // HW renderer shader // **************************************************************** + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=texturefx_begin"); if (!CreateTextureFX()) return false; + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=texturefx_done"); // **************************************************************** // Pbo Pool allocation @@ -666,6 +700,8 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) if (!CreateImGuiProgram()) return false; + // GLES has no pipeline-statistics queries; this extension is desktop-GL only, + // so on Android this stays false and the OSD line shows 0 / degrades gracefully. m_gpu_pipeline_statistics_supported = (GLAD_GL_ARB_pipeline_statistics_query != 0); // Basic to ensure structures are correctly packed @@ -675,6 +711,7 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle) static_assert(sizeof(OMDepthStencilSelector) == 1, "Wrong OMDepthStencilSelector size"); static_assert(sizeof(OMColorMaskSelector) == 1, "Wrong OMColorMaskSelector size"); + Console.WriteLn("@@ANDROID_GL_INIT@@ stage=create_done"); return true; } @@ -733,55 +770,65 @@ bool GSDeviceOGL::CheckFeatures() memset(&m_bugs, 0, sizeof(m_bugs)); - const char* vendor = (const char*)glGetString(GL_VENDOR); - if (std::strstr(vendor, "Advanced Micro Devices") || std::strstr(vendor, "ATI Technologies Inc.") || - std::strstr(vendor, "ATI")) + bool vendor_id_mali = false; + bool vendor_id_adreno = false; + + const char* vendor_raw = (const char*)glGetString(GL_VENDOR); + const char* renderer_raw = (const char*)glGetString(GL_RENDERER); + const char* vendor_str = vendor_raw ? vendor_raw : ""; + const char* renderer_str = renderer_raw ? renderer_raw : ""; + + if (std::strstr(vendor_str, "Advanced Micro Devices") || std::strstr(vendor_str, "ATI Technologies Inc.") || + std::strstr(vendor_str, "ATI")) { Console.WriteLn(Color_StrongRed, "GL: AMD GPU detected."); //vendor_id_amd = true; } - else if (std::strstr(vendor, "NVIDIA Corporation")) + else if (std::strstr(vendor_str, "NVIDIA Corporation")) { Console.WriteLn(Color_StrongGreen, "GL: NVIDIA GPU detected."); //vendor_id_nvidia = true; m_bugs.broken_blend_coherency = true; } - else if (std::strstr(vendor, "Intel")) + else if (std::strstr(vendor_str, "Intel")) { Console.WriteLn(Color_StrongBlue, "GL: Intel GPU detected."); //vendor_id_intel = true; } - // ARMSX2: resolve the runtime GPU profile (Mali/Adreno/PowerVR) so the tfx GL - // shaders can select their tile-based-GPU arms via GPU_PROFILE_* (emitted in - // GenGlslHeader below). The monorepo GS device base is a desktop PCSX2 base and - // has no Mali/Adreno vendor branch above, so we detect from the raw vendor/renderer - // strings here. Guard null pointers (glGetString may return null). + else if (std::strstr(vendor_str, "ARM") || std::strstr(renderer_str, "Mali")) { - const char* renderer = (const char*)glGetString(GL_RENDERER); - const char* vendor_safe = vendor ? vendor : ""; - const char* renderer_safe = renderer ? renderer : ""; -#if defined(__ANDROID__) - const GpuProfileSelection profile_selection = - GpuProfileDetector::Resolve(GSConfig.AndroidGpuProfileOverride, vendor_safe, renderer_safe); - SetRuntimeGPUProfile(profile_selection.runtime_profile); - Console.WriteLn("GL: GPU profile override='%s' resolved='%s'.", - GpuProfileDetector::OverrideToConfigString(profile_selection.override_mode), - GpuProfileDetector::RuntimeProfileToString(profile_selection.runtime_profile)); - DevCon.WriteLn("GL: GPU profile hints: %s", profile_selection.hints.c_str()); -#else - // Desktop/non-Android GL: no tile-based-GPU profile arms are used, keep the - // device's profile initialized to a benign default. - SetRuntimeGPUProfile(RuntimeGpuProfile::Adreno); - (void)vendor_safe; - (void)renderer_safe; -#endif + Console.WriteLn(Color_Yellow, "GL: ARM Mali GPU detected."); + vendor_id_mali = true; } + else if (std::strstr(vendor_str, "Qualcomm") || std::strstr(renderer_str, "Adreno")) + { + Console.WriteLn(Color_Cyan, "GL: Qualcomm Adreno GPU detected."); + vendor_id_adreno = true; + } + +#if defined(__ANDROID__) + const GpuProfileSelection profile_selection = + GpuProfileDetector::Resolve(GSConfig.AndroidGpuProfileOverride, vendor_str, renderer_str); + SetRuntimeGPUProfile(profile_selection.runtime_profile); + Console.WriteLn("GL: GPU profile override='%s' resolved='%s'.", + GpuProfileDetector::OverrideToConfigString(profile_selection.override_mode), + GpuProfileDetector::RuntimeProfileToString(profile_selection.runtime_profile)); + DevCon.WriteLn("GL: GPU profile hints: %s", profile_selection.hints.c_str()); + bool use_mali_profile = IsMaliGPUProfile(); + bool use_adreno_profile = IsAdrenoGPUProfile(); + bool use_powervr_profile = IsPowerVRGPUProfile(); +#else + SetRuntimeGPUProfile(vendor_id_mali ? RuntimeGpuProfile::Mali : RuntimeGpuProfile::Adreno); + bool use_mali_profile = vendor_id_mali; + bool use_adreno_profile = vendor_id_adreno; + bool use_powervr_profile = false; +#endif GLint major_gl = 0; GLint minor_gl = 0; glGetIntegerv(GL_MAJOR_VERSION, &major_gl); glGetIntegerv(GL_MINOR_VERSION, &minor_gl); - if (!GLAD_GL_VERSION_3_3) + if (!m_is_gles && !GLAD_GL_VERSION_3_3) { Host::ReportErrorAsync( "GS", fmt::format(TRANSLATE_FS("GSDeviceOGL", "OpenGL renderer is not supported. Only OpenGL {}.{}\n was found"), major_gl, minor_gl)); @@ -808,24 +855,28 @@ bool GSDeviceOGL::CheckFeatures() } DevCon.WriteLn(std::move(extensions)); - if (!GLAD_GL_ARB_shading_language_420pack) - { - Host::ReportFormattedErrorAsync( - "GS", "GL_ARB_shading_language_420pack is not supported, this is required for the OpenGL renderer."); - return false; + if (!m_is_gles) { + if (!GLAD_GL_ARB_shading_language_420pack) + { + Host::ReportFormattedErrorAsync( + "GS", "GL_ARB_shading_language_420pack is not supported, this is required for the OpenGL renderer."); + return false; + } + + if (!GLAD_GL_VERSION_4_3 && !GLAD_GL_ARB_copy_image && !GLAD_GL_EXT_copy_image && !GLAD_GL_NV_copy_image) + { + Host::AddOSDMessage( + "GL_ARB_copy_image is not supported, copies will be slower.", Host::OSD_ERROR_DURATION); + } + + if (!GLAD_GL_VERSION_4_5 && !GLAD_GL_ARB_clip_control && + !(m_is_gles && GLAD_GL_EXT_clip_control)) + { + Host::AddOSDMessage( + "GL_ARB_clip_control is not supported, depth will be less accurate.", Host::OSD_ERROR_DURATION); + } } - if (!GLAD_GL_VERSION_4_3 && !GLAD_GL_ARB_copy_image && !GLAD_GL_EXT_copy_image && !GLAD_GL_NV_copy_image) - { - Host::AddOSDMessage( - "GL_ARB_copy_image is not supported, copies will be slower.", Host::OSD_ERROR_DURATION); - } - - if (!GLAD_GL_VERSION_4_5 && !GLAD_GL_ARB_clip_control) - { - Host::AddOSDMessage( - "GL_ARB_clip_control is not supported, depth will be less accurate.", Host::OSD_ERROR_DURATION); - } if (!GLAD_GL_ARB_viewport_array) { @@ -844,8 +895,8 @@ bool GSDeviceOGL::CheckFeatures() { glTextureBarrier = ReplaceGL::TextureBarrier; m_features.multidraw_fb_copy = true; - Host::AddOSDMessage( - "GL_ARB_texture_barrier is not supported, blending will be slower.", Host::OSD_ERROR_DURATION); +/* Host::AddOSDMessage( + "GL_ARB_texture_barrier is not supported, blending will be slower.", Host::OSD_ERROR_DURATION);*/ } } @@ -857,7 +908,19 @@ bool GSDeviceOGL::CheckFeatures() // Don't use PBOs when we don't have ARB_buffer_storage, orphaning buffers probably ends up worse than just // using the normal texture update routines and letting the driver take care of it. - m_bugs.buggy_pbo = !GLAD_GL_VERSION_4_4 && !GLAD_GL_ARB_buffer_storage && !GLAD_GL_EXT_buffer_storage; + if (!m_is_gles) { + m_bugs.buggy_pbo = !GLAD_GL_VERSION_4_4 && !GLAD_GL_ARB_buffer_storage && !GLAD_GL_EXT_buffer_storage; + } else { + // Mirrors the desktop check: PBOs are useful only when EXT_buffer_storage + // (the GLES port of buffer_storage) is available so we can pin a + // persistent-mapped staging region. Without it the orphaning fallback + // is slower than letting the driver handle the upload directly. The + // previous form here had the test inverted (set buggy=TRUE when the + // extension WAS supported), disabling PBOs on every modern Adreno/Mali + // device. Aligning with the desktop branch's polarity. + m_bugs.buggy_pbo = !GLAD_GL_EXT_buffer_storage; + } + if (m_bugs.buggy_pbo) Console.Warning("GL: Not using PBOs for texture uploads because buffer_storage is unavailable."); @@ -871,7 +934,7 @@ bool GSDeviceOGL::CheckFeatures() m_features.broken_point_sampler = false; m_features.primitive_id = true; - m_features.framebuffer_fetch = GLAD_GL_EXT_shader_framebuffer_fetch; + m_features.framebuffer_fetch = (GLAD_GL_ARM_shader_framebuffer_fetch || GLAD_GL_EXT_shader_framebuffer_fetch); if (m_features.framebuffer_fetch && GSConfig.DisableFramebufferFetch) { Host::AddOSDMessage( @@ -914,26 +977,111 @@ bool GSDeviceOGL::CheckFeatures() m_features.depth_feedback |= GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto; } - // ARMSX2 (Adreno GLES only; inert on every other GPU/profile). Adreno's driver - // rejects a fragment shader declaring TWO framebuffer-fetch `inout` outputs (o_col0 - // colour + o_col1 depth), which the depth-as-colour SW-Z path emits for accurate- - // alpha-test draws -> link failure -> garbage (Everybody's Golf 4 / Minna no Golf 4). - // Route depth feedback through the depth path (a single fetch output) so it links, and - // read prior depth via the coherent ARM depth-stencil fetch (gl_LastFragDepthARM) when - // available -- the mode-1 depth sampler read is incoherent on GLES (no barrier on a - // sampled depth attachment) and makes occluded triangles poke through as white shards. - // Only overrides Auto; an explicit DepthFeedbackMode choice is honoured. The GPU - // profile is already resolved above (SetRuntimeGPUProfile), so IsAdrenoGPUProfile() - // is valid here. - if (m_features.framebuffer_fetch && IsAdrenoGPUProfile() && - GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto) + // Mobile tile-based GPU profiles. Both Mali and Adreno prefer fresh + // textures over reused ones (avoids tile-flush stalls on partial + // writes), so the texture-pool hint is shared. Mali additionally + // pins framebuffer_fetch to the ARM extension and (on Auto) reuses + // it as the texture-barrier substitute — matched by the + // `#if GPU_PROFILE_MALI` branch in tfx_fs.glsl which picks + // gl_LastFragColorARM. Adreno + Generic fall through to the EXT/PLS + // inout path in the shader's `#else` arm; GPU_PROFILE_ADRENO is + // emitted but not currently consumed by any shader. + if (use_mali_profile || use_adreno_profile || use_powervr_profile) + m_features.prefer_new_textures = true; + + if (use_mali_profile) { - m_features.depth_feedback = true; - m_arm_depth_fetch = GLAD_GL_ARM_shader_framebuffer_fetch_depth_stencil; - Console.WriteLn(m_arm_depth_fetch - ? "GL: Adreno - depth feedback via coherent ARM depth-stencil fetch (gl_LastFragDepthARM)." - : "GL: Adreno - routing depth feedback through the depth sampler " - "(avoids the dual framebuffer-fetch output link failure)."); + // Mali path prefers ARM_shader_framebuffer_fetch (gl_LastFragColorARM) because + // the EXT inout path has been broken across every tested Mali driver. If a + // device was force-overridden to Mali but lacks ARM fbfetch (rare but + // possible), demote to PowerVR profile which uses the same EXT/PLS path the + // catch-all default uses. + if (GLAD_GL_ARM_shader_framebuffer_fetch) + { + Console.WriteLn(Color_Yellow, "GL: Applying Mali-specific optimizations for tile-based rendering."); + m_features.framebuffer_fetch = true; + if (GSConfig.OverrideTextureBarriers == -1) + { + m_features.texture_barrier = m_features.framebuffer_fetch; + Console.WriteLn("GL: Mali optimization - using ARM framebuffer fetch over texture barriers."); + } + } + else + { + Console.Warning("GL: Mali profile selected but ARM framebuffer fetch is unavailable; demoting to PowerVR/EXT profile."); + SetRuntimeGPUProfile(RuntimeGpuProfile::PowerVR); + use_mali_profile = false; + use_powervr_profile = true; + } + } + + if (use_powervr_profile) + { + // PowerVR (Imagination) is tile-based like Mali but ships EXT/PLS fbfetch + // (PLS originated on PowerVR). framebuffer_fetch + texture_barrier values + // from line ~882/908 already reflect the EXT path, so no override needed — + // just confirm the path is wired up. + Console.WriteLn(Color_Yellow, "GL: PowerVR profile active (EXT/PLS framebuffer fetch)."); + } + else if (use_adreno_profile) + { + Console.WriteLn(Color_Cyan, "GL: Adreno profile active (EXT/PLS framebuffer fetch)."); + + // Adreno's GLES driver rejects a fragment shader that declares TWO + // framebuffer-fetch `inout` outputs. The depth-as-colour feedback path + // (DEPTH_FEEDBACK_SUPPORT 2) emits exactly that whenever the colour output + // already needs fetch AND a SW-Z depth draw is in flight -- o_col0 (colour + // fetch) at location 0 and o_col1 (depth fetch) at location 1 both become + // `inout`. That combination is produced by the accurate-alpha-test RGB-only + // + depth-write path, so any game carrying accurateAlphaTest (e.g. Everybody's + // Golf 4 / Minna no Golf 4, SCKA-20057 / SCPS-15059) fails to link those draws + // -> "Output o_col1 location or component exceeds max allowed" -> garbage + // (black-boxed faces, a floating RT rectangle, blue bars). Vulkan is unaffected + // (real depth attachment, no second fetch output). Route depth feedback through + // the real depth sampler (DEPTH_FEEDBACK_SUPPORT 1) so only o_col0 is a fetch + // output and the program links. test_and_sample_depth is already true above, + // and texture_barrier==true here keeps the DS-clone path (bind at ~3402) inert. + // Only override Auto -- an explicit DepthFeedbackMode choice is honoured. + if (m_features.framebuffer_fetch && GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto) + { + m_features.depth_feedback = true; + // The mode-1 depth SAMPLER read is incoherent on GLES (no texture_barrier + // for a sampled depth attachment) -> stale reads make occluded/interior + // triangles poke through as white shards. When the coherent ARM depth- + // stencil fetch extension is present, read prior depth via gl_LastFragDepthARM + // instead (tile-local, one output, no sampler, no feedback-loop bind). + m_arm_depth_fetch = GLAD_GL_ARM_shader_framebuffer_fetch_depth_stencil; + Console.WriteLn(m_arm_depth_fetch + ? "GL: Adreno - depth feedback via coherent ARM depth-stencil fetch (gl_LastFragDepthARM)." + : "GL: Adreno - routing depth feedback through the depth sampler " + "(avoids the dual framebuffer-fetch output link failure)."); + } + } + + { + const bool has_arm_fetch = GLAD_GL_ARM_shader_framebuffer_fetch; + const bool has_ext_fetch = GLAD_GL_EXT_shader_framebuffer_fetch; + const bool has_pls_fetch = GLAD_GL_EXT_shader_pixel_local_storage; + Console.WriteLn("GL: Framebuffer fetch extension caps: arm=%d ext=%d pls=%d.", + has_arm_fetch ? 1 : 0, has_ext_fetch ? 1 : 0, has_pls_fetch ? 1 : 0); + + const char* active_profile_name = use_mali_profile ? "Mali" : + (use_powervr_profile ? "PowerVR" : + (use_adreno_profile ? "Adreno" : "Generic")); + const char* active_fetch_backend = "None"; + if (m_features.framebuffer_fetch) + { + if (use_mali_profile) + active_fetch_backend = "ARM"; + else if (has_ext_fetch || has_pls_fetch) + active_fetch_backend = "EXT/PLS"; + else if (has_arm_fetch) + active_fetch_backend = "ARM"; + } + Console.WriteLn("GL: Active framebuffer fetch backend (%s profile): %s.", active_profile_name, active_fetch_backend); + + if (use_mali_profile && !has_arm_fetch) + Console.Warning("GL: Mali profile selected but ARM framebuffer fetch is unavailable; using non-fetch fallback."); } if (GLAD_GL_ARB_shader_storage_buffer_object) @@ -1156,6 +1304,18 @@ GSDevice::PresentResult GSDeviceOGL::BeginPresent(bool frame_skip) OMSetFBO(0); OMSetColorMaskState(); + // On TBDR, hint that the default framebuffer's prior content is throwaway + // before the tile is loaded for the present quad. The color attachment is + // fully overwritten by the clear+blit below, and depth/stencil are never + // used at all on the system framebuffer. Default-FBO uses GL_COLOR / DEPTH + // / STENCIL (not GL_*_ATTACHMENT). Pure TBDR tile-bandwidth win and inert on + // desktop immediate renderers, so gated to GLES to keep the desktop path canonical. + if (m_is_gles) + { + const GLenum attachments[] = {GL_COLOR, GL_DEPTH, GL_STENCIL}; + glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments); + } + glDisable(GL_SCISSOR_TEST); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); @@ -1175,6 +1335,16 @@ void GSDeviceOGL::EndPresent() if (m_gpu_timing_enabled) PopTimestampQuery(); + // Discard the default framebuffer's depth/stencil before the swap. We + // never wrote anything meaningful to them, so on TBDR drivers writing + // the tile back to system memory at SwapBuffers is wasted bandwidth. + // Color is preserved (it's what gets presented). GLES/TBDR-only (inert on desktop). + if (m_is_gles) + { + const GLenum attachments[] = {GL_DEPTH, GL_STENCIL}; + glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(attachments), attachments); + } + m_gl_context->SwapBuffers(); if (m_gpu_timing_enabled) @@ -1210,6 +1380,27 @@ void GSDeviceOGL::PopTimestampQuery() { while (m_waiting_timestamp_queries > 0) { +#if defined(__ANDROID__) + // GLES doesn't expose glGetQueryObjectiv / glGetQueryObjectui64v; both + // availability and result use the u32 form. Caps at ~4.29s of + // nanoseconds — fine for per-frame timing. Provided by the + // EXT_disjoint_timer_query extension (GL_TIME_ELAPSED_EXT === 0x88BF + // === GL_TIME_ELAPSED here). + // + // Prior version of this branch was broken: it called glBeginQuery on + // the read slot then immediately tried to read its result (always 0, + // query never ended) and incremented m_waiting_timestamp_queries + // instead of decrementing — accumulator stayed stuck at 0 in HW + // renderer OSD ("GPU: 0%" symptom). + GLuint available = 0; + glGetQueryObjectuiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT_AVAILABLE, &available); + if (!available) + break; + + GLuint result = 0; + glGetQueryObjectuiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT, &result); + m_accumulated_gpu_time += static_cast(static_cast(result) / 1000000.0); +#else GLint available = 0; glGetQueryObjectiv(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT_AVAILABLE, &available); @@ -1219,6 +1410,7 @@ void GSDeviceOGL::PopTimestampQuery() u64 result = 0; glGetQueryObjectui64v(m_timestamp_queries[m_read_timestamp_query], GL_QUERY_RESULT, &result); m_accumulated_gpu_time += static_cast(static_cast(result) / 1000000.0); +#endif m_read_timestamp_query = (m_read_timestamp_query + 1) % NUM_TIMESTAMP_QUERIES; m_waiting_timestamp_queries--; } @@ -1226,7 +1418,6 @@ void GSDeviceOGL::PopTimestampQuery() if (m_timestamp_query_started) { glEndQuery(GL_TIME_ELAPSED); - m_write_timestamp_query = (m_write_timestamp_query + 1) % NUM_TIMESTAMP_QUERIES; m_timestamp_query_started = false; m_waiting_timestamp_queries++; @@ -1263,8 +1454,15 @@ float GSDeviceOGL::GetAndResetAccumulatedGPUTime() return value; } +// NOTE: These GL pipeline-statistics queries are desktop-GL only. GLES (Android) +// has neither GL_ARB_pipeline_statistics_query nor the glGetQueryObjectiv / +// glGetQueryObjectui64v result readers (see PopTimestampQuery for the same +// GLES gap), so the whole path is compiled out under __ANDROID__. On Android +// m_gpu_pipeline_statistics_supported stays false and these are never invoked; +// the OSD line just shows 0 / degrades to n/a. Real stats come from Vulkan. void GSDeviceOGL::PopPipelineStatisticsQuery() { +#if !defined(__ANDROID__) while (m_waiting_pipeline_statistics_queries > 0) { GLint available[2] = {}; @@ -1288,33 +1486,39 @@ void GSDeviceOGL::PopPipelineStatisticsQuery() glEndQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB); glEndQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB); - m_write_pipeline_statistics_query = (m_write_pipeline_statistics_query + 1) % NUM_TIMESTAMP_QUERIES; + m_write_pipeline_statistics_query = (m_write_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES; m_pipeline_statistics_query_started = false; m_waiting_pipeline_statistics_queries++; } +#endif } void GSDeviceOGL::KickPipelineStatisticsQuery() { +#if !defined(__ANDROID__) if (m_pipeline_statistics_query_started || m_waiting_pipeline_statistics_queries == NUM_PIPELINE_STATISTICS_QUERIES) return; glBeginQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][0]); glBeginQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][1]); m_pipeline_statistics_query_started = true; +#endif } void GSDeviceOGL::CreatePipelineStatisticsQueries() { +#if !defined(__ANDROID__) for (int i = 0; i < NUM_PIPELINE_STATISTICS_QUERIES; i++) { glGenQueries(2, m_pipeline_statistics_queries[i].data()); } KickPipelineStatisticsQuery(); +#endif } void GSDeviceOGL::DestroyPipelineStatisticsQueries() { +#if !defined(__ANDROID__) if (m_pipeline_statistics_queries[0][0] == 0) return; @@ -1333,6 +1537,7 @@ void GSDeviceOGL::DestroyPipelineStatisticsQueries() m_write_pipeline_statistics_query = 0; m_waiting_pipeline_statistics_queries = 0; m_pipeline_statistics_query_started = false; +#endif } GPUPipelineStatistics GSDeviceOGL::GetAndResetAccumulatedGPUPipelineStatistics() @@ -1444,7 +1649,11 @@ void GSDeviceOGL::CommitClear(GSTexture* t, bool use_write_fbo) if (T->GetState() == GSTexture::State::Invalidated) { - if (GLAD_GL_VERSION_4_3) + // glInvalidateFramebuffer is core in GL 4.3 and GLES 3.0. The original + // gate skipped GLES, so on Adreno/Mali every "this content is dead" + // hint from the texture cache fell through to a no-op — the tile got + // written back to system memory anyway. On TBDR that's pure waste. + if (GLAD_GL_VERSION_4_3 || m_is_gles) { if (T->IsDepthStencil()) { @@ -1613,41 +1822,79 @@ std::string GSDeviceOGL::GetShaderSource(const std::string_view entry, GLenum ty std::string GSDeviceOGL::GenGlslHeader(const std::string_view entry, GLenum type, const std::string_view macro) { std::string header; + if (m_is_gles) + { + if (GLAD_GL_ES_VERSION_3_2) + header = "#version 320 es\n"; + else if (GLAD_GL_ES_VERSION_3_1) + header = "#version 310 es\n"; - if (m_features.vs_expand && GLAD_GL_VERSION_4_3) - { + if (GLAD_GL_EXT_blend_func_extended) + header += "#extension GL_EXT_blend_func_extended : require\n"; + if (GLAD_GL_ARB_blend_func_extended) + header += "#extension GL_ARB_blend_func_extended : require\n"; + + if (m_features.framebuffer_fetch) + { + if (GLAD_GL_ARM_shader_framebuffer_fetch) + header += "#extension GL_ARM_shader_framebuffer_fetch : require\n"; + else if (GLAD_GL_EXT_shader_framebuffer_fetch) + header += "#extension GL_EXT_shader_framebuffer_fetch : require\n"; + } + + // Coherent prior-depth read for SW-Z feedback (gl_LastFragDepthARM). + if (m_arm_depth_fetch) + header += "#extension GL_ARM_shader_framebuffer_fetch_depth_stencil : require\n"; + + header += "precision highp float;\n"; + header += "precision highp int;\n"; + header += "precision highp sampler2D;\n"; + if (GLAD_GL_ES_VERSION_3_1) + header += "precision highp sampler2DMS;\n"; + if (GLAD_GL_ES_VERSION_3_2) + header += "precision highp usamplerBuffer;\n"; + + if (!GLAD_GL_EXT_blend_func_extended && !GLAD_GL_ARB_blend_func_extended) + { + if (!GLAD_GL_ARM_shader_framebuffer_fetch) + fprintf(stderr, "Dual source blending is not supported\n"); + + header += "#define DISABLE_DUAL_SOURCE\n"; + } + } + else { // Intel's GL driver doesn't like the readonly qualifier with 3.3 GLSL. - header = "#version 430 core\n"; - } - else - { - header = "#version 330 core\n"; - header += "#extension GL_ARB_shading_language_420pack : require\n"; - if (GLAD_GL_ARB_gpu_shader5) - header += "#extension GL_ARB_gpu_shader5 : require\n"; - if (m_features.vs_expand) - header += "#extension GL_ARB_shader_storage_buffer_object: require\n"; - } + if (m_features.vs_expand && GLAD_GL_VERSION_4_3) + { + header = "#version 430 core\n"; + } + else + { + header = "#version 330 core\n"; + header += "#extension GL_ARB_shading_language_420pack : require\n"; + if (GLAD_GL_ARB_gpu_shader5) + header += "#extension GL_ARB_gpu_shader5 : require\n"; + if (m_features.vs_expand) + header += "#extension GL_ARB_shader_storage_buffer_object: require\n"; + } - if (m_features.framebuffer_fetch && GLAD_GL_EXT_shader_framebuffer_fetch) - header += "#extension GL_EXT_shader_framebuffer_fetch : require\n"; + if (m_features.framebuffer_fetch && GLAD_GL_EXT_shader_framebuffer_fetch) + header += "#extension GL_EXT_shader_framebuffer_fetch : require\n"; - // ARMSX2 (Adreno): coherent prior-depth read for SW-Z feedback (gl_LastFragDepthARM). - if (m_arm_depth_fetch) - header += "#extension GL_ARM_shader_framebuffer_fetch_depth_stencil : require\n"; + } if (m_features.framebuffer_fetch) header += "#define HAS_FRAMEBUFFER_FETCH 1\n"; else header += "#define HAS_FRAMEBUFFER_FETCH 0\n"; - // ARMSX2: emit the runtime GPU-profile selectors so the tfx GL shaders can pick - // their tile-based-GPU (Mali/Adreno/PowerVR) arms. GenGlslHeader() is prepended to - // every GL shader (GetShaderSource / GetTfxVertexShader / GetTfxFragmentShader), - // so this is the header string the tfx_fs.glsl `#if GPU_PROFILE_MALI` guards read. + + header += fmt::format("#define HAS_EXT_SHADER_FRAMEBUFFER_FETCH {}\n", GLAD_GL_EXT_shader_framebuffer_fetch ? 1 : 0); + header += fmt::format("#define HAS_ARM_SHADER_FRAMEBUFFER_FETCH {}\n", GLAD_GL_ARM_shader_framebuffer_fetch ? 1 : 0); + header += fmt::format("#define HAS_ARM_DEPTH_FETCH {}\n", m_arm_depth_fetch ? 1 : 0); + header += fmt::format("#define HAS_EXT_SHADER_PIXEL_LOCAL_STORAGE {}\n", GLAD_GL_EXT_shader_pixel_local_storage ? 1 : 0); header += fmt::format("#define GPU_PROFILE_MALI {}\n", IsMaliGPUProfile() ? 1 : 0); header += fmt::format("#define GPU_PROFILE_ADRENO {}\n", IsAdrenoGPUProfile() ? 1 : 0); header += fmt::format("#define GPU_PROFILE_POWERVR {}\n", IsPowerVRGPUProfile() ? 1 : 0); - header += fmt::format("#define HAS_ARM_DEPTH_FETCH {}\n", m_arm_depth_fetch ? 1 : 0); if (GLAD_GL_ARB_conservative_depth) { @@ -1672,7 +1919,8 @@ std::string GSDeviceOGL::GenGlslHeader(const std::string_view entry, GLenum type header += "#define DEPTH_FEEDBACK_SUPPORT 2\n"; // Depth as RT } - if (GLAD_GL_ARB_clip_control) + // Must match the glClipControl(EXT) enable above: desktop ARB, or GLES with EXT. + if (GLAD_GL_ARB_clip_control || (m_is_gles && GLAD_GL_EXT_clip_control)) header += "#define HAS_CLIP_CONTROL 1\n"; else header += "#define HAS_CLIP_CONTROL 0\n"; @@ -2667,8 +2915,18 @@ void GSDeviceOGL::RenderBlankFrame() { glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glDisable(GL_SCISSOR_TEST); + if (m_is_gles) // GLES/TBDR-only tile-bandwidth hint; inert on desktop, gated to keep it canonical + { + const GLenum pre[] = {GL_COLOR, GL_DEPTH, GL_STENCIL}; + glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(pre), pre); + } glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); + if (GLAD_GL_VERSION_4_3 || m_is_gles) + { + const GLenum post[] = {GL_DEPTH, GL_STENCIL}; + glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, std::size(post), post); + } m_gl_context->SwapBuffers(); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLState::fbo); glEnable(GL_SCISSOR_TEST); @@ -3049,8 +3307,8 @@ void GSDeviceOGL::RenderHW(GSHWDrawConfig& config) if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier)) PSSetShaderResource(TEXTURE_RT, colclip_rt ? colclip_rt : config.rt); if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier) && config.ps.IsFeedbackLoopDepth()) - // ARMSX2 (Adreno): with ARM depth-stencil fetch the shader reads gl_LastFragDepthARM, - // not a sampler, so don't bind the live depth attachment as a texture (avoids a + // With ARM depth-stencil fetch the shader reads gl_LastFragDepthARM, not a + // sampler, so don't bind the live depth attachment as a texture (avoids a // feedback-loop bind the driver may flag). PSSetShaderResource(TEXTURE_DEPTH, (m_features.depth_feedback && !m_arm_depth_fetch) ? config.ds : m_ds_as_rt); @@ -3151,15 +3409,17 @@ void GSDeviceOGL::RenderHW(GSHWDrawConfig& config) // Avoid changing framebuffer just to switch from rt+depth to rt and vice versa. bool fb_optimization_needs_barrier = false; - if (!(draw_rt || draw_ds_as_rt) && draw_ds && GLState::rt && GLState::rt->GetSize() == draw_ds->GetSize()) + if (!draw_rt && GLState::rt && GLState::ds == draw_ds && config.tex != GLState::rt && + draw_ds && GLState::rt->GetSize() == draw_ds->GetSize() && !draw_ds_as_rt) { draw_rt = GLState::rt; - fb_optimization_needs_barrier = !GLState::rt_written && GLState::ds == draw_ds; + fb_optimization_needs_barrier = !GLState::rt_written; } - else if (!(draw_ds || draw_ds_as_rt) && draw_rt && GLState::ds && GLState::ds->GetSize() == draw_rt->GetSize()) + else if (!draw_ds && GLState::ds && GLState::rt == draw_rt && config.tex != GLState::ds && + draw_rt && GLState::ds->GetSize() == draw_rt->GetSize() && !draw_ds_as_rt) { draw_ds = GLState::ds; - fb_optimization_needs_barrier = !GLState::ds_written && GLState::rt == draw_rt; + fb_optimization_needs_barrier = !GLState::ds_written; } // Be careful of the rt already being bound and the blend using the RT without a barrier. @@ -3178,7 +3438,6 @@ void GSDeviceOGL::RenderHW(GSHWDrawConfig& config) { // Requires a copy of the RT. draw_rt_clone = CreateTexture(rtsize.x, rtsize.y, 1, draw_rt->GetFormat(), true); - if (!draw_rt_clone) Console.Warning("GL: Failed to allocate temp texture for RT copy."); } @@ -3190,7 +3449,6 @@ void GSDeviceOGL::RenderHW(GSHWDrawConfig& config) { // Requires a copy of the DS. draw_ds_clone = CreateTexture(rtsize.x, rtsize.y, 1, draw_ds->GetFormat(), true); - if (!draw_ds_clone) Console.Warning("GL: Failed to allocate temp texture for DS copy."); } diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h index 5f6dc2af0a..0504753829 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h @@ -130,8 +130,18 @@ public: VSSelector vs; u8 pad[3]; - __fi bool operator==(const ProgramSelector& p) const { return BitEqual(*this, p); } - __fi bool operator!=(const ProgramSelector& p) const { return !BitEqual(*this, p); } + // Compare ONLY the meaningful key fields, matching ProgramSelectorHash above + // (which hashes {vs.key, ps.key_hi, ps.key_lo}). BitEqual() memcmp'd all 32 bytes + // including this struct's uninitialized trailing/alignment padding, so two + // logically-identical selectors landed in the same hash bucket yet failed ==, + // making m_programs.find() miss every time. Result: the same shaders recompiled + // every frame (issue #243 — Jackie Chan Adventures: 142 unique shaders, 41k+ + // recompiles, GS-thread pegged). Field compare is hash-consistent and padding-proof. + __fi bool operator==(const ProgramSelector& p) const + { + return vs.key == p.vs.key && ps.key_hi == p.ps.key_hi && ps.key_lo == p.ps.key_lo; + } + __fi bool operator!=(const ProgramSelector& p) const { return !(*this == p); } }; static_assert(sizeof(ProgramSelector) == 32, "Program selector is 32 bytes"); @@ -151,6 +161,8 @@ private: std::unique_ptr m_gl_context; + bool m_is_gles = false; + struct { bool buggy_pbo : 1; ///< Avoid PBOs and just use glTextureSubImage2D with immediate data @@ -158,9 +170,9 @@ private: } m_bugs; bool m_disable_download_pbo = false; - // ARMSX2 (Adreno GLES): read prior depth for SW-Z feedback via the coherent ARM - // depth-stencil fetch (gl_LastFragDepthARM) instead of the incoherent depth sampler, - // when GL_ARM_shader_framebuffer_fetch_depth_stencil is present. False everywhere else. + // Adreno: read prior depth for SW-Z feedback via the coherent ARM depth-stencil + // fetch (gl_LastFragDepthARM) instead of the incoherent depth sampler, when the + // GL_ARM_shader_framebuffer_fetch_depth_stencil extension is present. bool m_arm_depth_fetch = false; GLuint m_fbo = 0; // frame buffer container @@ -284,13 +296,13 @@ private: void PopTimestampQuery(); void KickTimestampQuery(); - GSTexture* CreateSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format) override; - void CreatePipelineStatisticsQueries(); void DestroyPipelineStatisticsQueries(); void PopPipelineStatisticsQuery(); void KickPipelineStatisticsQuery(); + GSTexture* CreateSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format) override; + void DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, GSVector4* dRect, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c, const Filter filter) override; void DoInterlace(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, ShaderInterlace shader, Filter filter, const InterlaceConstantBuffer& cb) override; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index 1637ef76f0..c4d7e40247 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -79,9 +79,13 @@ static constexpr VkClearValue s_present_clear_color = {{{0.0f, 0.0f, 0.0f, 1.0f} static std::mutex s_instance_mutex; // Device extensions that are required for PCSX2. -static constexpr const char* s_required_device_extensions[] = { - VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, -}; +// No hard-required device extensions beyond the swapchain (handled separately). +// VK_KHR_push_descriptor used to be required, but it's now OPTIONAL: some Mali +// drivers (e.g. Mali-G52) don't expose it at all, and we have a full +// non-push-descriptor binding fallback — so requiring it needlessly rejected +// otherwise-capable GPUs ("No physical devices found"). Kept as a (currently +// empty) list so the existing required-extension scan loops stay valid. +static constexpr std::array s_required_device_extensions = {}; GSDeviceVK::GSDeviceVK() { @@ -411,21 +415,37 @@ bool GSDeviceVK::SelectDeviceExtensions(ExtensionList* extension_list, bool enab return false; } + // Optional now (was required). Enabled when present; CreateDevice decides + // whether to actually use it (never on Mali — driver bug) or fall back. + m_optional_extensions.vk_khr_push_descriptor = SupportsExtension(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, false); m_optional_extensions.vk_ext_provoking_vertex = SupportsExtension(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, false); m_optional_extensions.vk_ext_memory_budget = SupportsExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, false); m_optional_extensions.vk_ext_calibrated_timestamps = SupportsExtension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME, false); // ROAA is DUAL-NAMED: ARM shipped VK_ARM_rasterization_order_attachment_access (Mali // driver r36p0), and the promoted VK_EXT_ alias only landed at r40p0. The structs/enums - // are identical (alias), so accept EITHER - otherwise Mali on r36-r39 blobs (a big chunk + // are identical (alias), so accept EITHER — otherwise Mali on r36-r39 blobs (a big chunk // of mid-tier, incl. Tensor G2/G3 on old blobs) exposes only the ARM name and gets // silently demoted to the per-primitive-barrier slideshow. SupportsExtension enables // whichever name it finds (EXT preferred via short-circuit). Matches upstream 5da4b7e. m_optional_extensions.vk_ext_rasterization_order_attachment_access = SupportsExtension(VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, false) || SupportsExtension(VK_ARM_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, false); + // VK_EXT_attachment_feedback_loop_layout: disable on Mali (vendorID 0x13B5). The Mali + // blob's implementation returns zero/stale destination color in the feedback-loop layout, + // producing black/missing textures and, on some driver revisions, device-lost crashes + // (found on MediaTek-Mali stacks by the EmuCoreX dev). We fall back to texture_barrier + // there. Adreno/other vendors are unaffected. m_device_properties isn't populated yet at + // this point, so read the vendor straight off the physical device. + bool is_mali_vendor = false; + if (m_physical_device != VK_NULL_HANDLE) + { + VkPhysicalDeviceProperties phys_props = {}; + vkGetPhysicalDeviceProperties(m_physical_device, &phys_props); + is_mali_vendor = (phys_props.vendorID == 0x13B5u); + } m_optional_extensions.vk_ext_attachment_feedback_loop_layout = - SupportsExtension(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME, false); + SupportsExtension(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME, false) && !is_mali_vendor; m_optional_extensions.vk_ext_line_rasterization = SupportsExtension(VK_EXT_LINE_RASTERIZATION_EXTENSION_NAME, false); m_optional_extensions.vk_khr_driver_properties = SupportsExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME, false); @@ -745,8 +765,15 @@ bool GSDeviceVK::ProcessDeviceExtensions() VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT rasterization_order_access_feature = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT}; // VK_EXT_swapchain_maintenance1 types/enums are aliases of VK_KHR_swapchain_maintenance1 types/enums. + // Preset to VK_FALSE, NOT VK_TRUE: Adreno's proprietary driver advertises + // the extension string but doesn't recognize the feature struct ("Unknown + // struct with type 0x3b9efc38" in logcat) and never writes it. With a + // VK_TRUE preset the ignored struct reads back as supported, the device is + // created without the feature actually enabled, and the swapchain then uses + // present fences illegally. A driver that does know the struct overwrites + // this preset either way. VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchain_maintenance1_feature = { - VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, nullptr, VK_TRUE}; + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, nullptr, VK_FALSE}; VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT attachment_feedback_loop_feature = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT}; VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT fragment_shader_interlock_ext_feature = { @@ -786,18 +813,39 @@ bool GSDeviceVK::ProcessDeviceExtensions() VkPhysicalDevicePushDescriptorPropertiesKHR push_descriptor_properties = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR}; - Vulkan::AddPointerToChain(&properties2, &push_descriptor_properties); + if (m_optional_extensions.vk_khr_push_descriptor) + Vulkan::AddPointerToChain(&properties2, &push_descriptor_properties); // query vkGetPhysicalDeviceProperties2(m_physical_device, &properties2); - // confirm we actually support it - if (push_descriptor_properties.maxPushDescriptors < NUM_TFX_TEXTURES) + // Decide whether to bind textures via VK_KHR_push_descriptor. It's optional + // now — when it's absent (some Mali, e.g. Mali-G52), unusable, or known-buggy + // we fall back to per-frame allocated descriptor sets so Vulkan still runs. + m_use_push_descriptors = m_optional_extensions.vk_khr_push_descriptor; + if (m_use_push_descriptors && push_descriptor_properties.maxPushDescriptors < NUM_TFX_TEXTURES) { - Console.Error("VK: maxPushDescriptors (%u) is below required (%u)", push_descriptor_properties.maxPushDescriptors, - NUM_TFX_TEXTURES); - return false; + Console.Warning("VK: maxPushDescriptors (%u) below required (%u) - using descriptor-set fallback.", + push_descriptor_properties.maxPushDescriptors, NUM_TFX_TEXTURES); + m_use_push_descriptors = false; } + // Mali (ARM, vendorID 0x13B5) advertises VK_KHR_push_descriptor but its driver + // null-derefs inside vkCmdPushDescriptorSetKHR on the first textured draw, so + // never use it there even when present. + if (m_use_push_descriptors && properties2.properties.vendorID == 0x13B5u) + m_use_push_descriptors = false; + // Adreno (Qualcomm, 0x5143): push descriptors stall on the per-draw TFX texture-rebind hot + // path (both Eden and Dolphin avoid them on Adreno); the descriptor-set fallback is faster. + if (m_use_push_descriptors && properties2.properties.vendorID == 0x5143u) + m_use_push_descriptors = false; + if (!m_use_push_descriptors) + Console.Warning("VK: Using non-push-descriptor texture binding fallback."); + + // Adreno mis-selects the provoking vertex with VK_EXT_provoking_vertex (Eden strips it on + // Qualcomm); drop it so GSRendererHW's software provoking-vertex-first path runs instead. + // A/B on Adreno: if this regresses perf without fixing a visible flat-shading glitch, revert. + if (m_optional_extensions.vk_ext_provoking_vertex && properties2.properties.vendorID == 0x5143u) + m_optional_extensions.vk_ext_provoking_vertex = false; if (m_optional_extensions.vk_ext_line_rasterization && !line_rasterization_feature.bresenhamLines) { @@ -972,6 +1020,31 @@ bool GSDeviceVK::CreateCommandBuffers() return false; } Vulkan::SetObjectName(m_device, resources.fence, "Frame Fence %u", frame_index); + + // Non-push-descriptor path (Mali): per-frame pool for texture descriptor sets, reset wholesale + // in ActivateCommandBuffer when the frame is recycled. Sized generously for a heavy frame; if a + // frame ever exceeds this, AllocateFrameDescriptorSet logs and the bind is skipped (visual only, + // no crash) - this is the tuning knob if a Mali tester reports missing textures. + if (!m_use_push_descriptors) + { + static constexpr u32 MAX_FRAME_TEXTURE_SETS = 8192; + const VkDescriptorPoolSize frame_pool_sizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_FRAME_TEXTURE_SETS * 2}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, MAX_FRAME_TEXTURE_SETS * 3}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, MAX_FRAME_TEXTURE_SETS * 2}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, MAX_FRAME_TEXTURE_SETS * 2}, + }; + const VkDescriptorPoolCreateInfo frame_pool_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + nullptr, 0, MAX_FRAME_TEXTURE_SETS, static_cast(std::size(frame_pool_sizes)), frame_pool_sizes}; + res = vkCreateDescriptorPool(m_device, &frame_pool_info, nullptr, &resources.descriptor_pool); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkCreateDescriptorPool (frame) failed: "); + return false; + } + Vulkan::SetObjectName(m_device, resources.descriptor_pool, "Frame Texture Descriptor Pool %u", frame_index); + } + ++frame_index; } @@ -1016,7 +1089,7 @@ bool GSDeviceVK::CreateGlobalDescriptorPool() { const VkQueryPoolCreateInfo query_create_info = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, nullptr, 0, VK_QUERY_TYPE_PIPELINE_STATISTICS, NUM_COMMAND_BUFFERS, - VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT }; + VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT}; res = vkCreateQueryPool(m_device, &query_create_info, nullptr, &m_pipeline_statistics_query_pool); if (res != VK_SUCCESS) { @@ -1046,6 +1119,17 @@ VkRenderPass GSDeviceVK::GetRenderPass(VkFormat color_format, VkFormat depth_for key.color_feedback_loop = color_feedback_loop; key.depth_sampling = depth_sampling; + // Mali driver bug (ported from PPSSPP): a packed depth/stencil attachment whose + // depth vs stencil load-ops MISMATCH corrupts on ARM Mali. PCSX2's GS uses one + // combined D24S8/D32S8 attachment whose aspects are normally loaded/cleared + // together, so this is a no-op in practice — normalize defensively (prefer the + // depth aspect's op) so a stray mismatch can't trip the bug. Mali-only. + if (IsDeviceMali() && key.stencil_load_op != key.depth_load_op) + { + key.stencil_load_op = key.depth_load_op; + key.stencil_store_op = key.depth_store_op; + } + auto it = m_render_pass_cache.find(key.key); if (it != m_render_pass_cache.end()) return it->second; @@ -1114,6 +1198,25 @@ void GSDeviceVK::FreePersistentDescriptorSet(VkDescriptorSet set) vkFreeDescriptorSets(m_device, m_global_descriptor_pool, 1, &set); } +VkDescriptorSet GSDeviceVK::AllocateFrameDescriptorSet(VkDescriptorSetLayout set_layout) +{ + // Non-push-descriptor path only. The pool is reset wholesale each frame, so no per-set free. + const VkDescriptorPool pool = m_frame_resources[m_current_frame].descriptor_pool; + const VkDescriptorSetAllocateInfo allocate_info = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1, &set_layout}; + + VkDescriptorSet descriptor_set; + VkResult res = vkAllocateDescriptorSets(m_device, &allocate_info, &descriptor_set); + if (res != VK_SUCCESS) + { + // Pool exhausted for this frame - skip the bind rather than crash (see pool sizing note). + LOG_VULKAN_ERROR(res, "vkAllocateDescriptorSets (frame) failed: "); + return VK_NULL_HANDLE; + } + + return descriptor_set; +} + void GSDeviceVK::WaitForFenceCounter(u64 fence_counter) { if (m_completed_fence_counter >= fence_counter) @@ -1438,6 +1541,15 @@ void GSDeviceVK::ActivateCommandBuffer(u32 index) if (res != VK_SUCCESS) LOG_VULKAN_ERROR(res, "vkResetCommandPool failed: "); + // Non-push-descriptor path (Mali): the GPU is done with this frame, so recycle its texture + // descriptor sets wholesale. Cheaper than per-set frees and matches the command-pool lifecycle. + if (resources.descriptor_pool != VK_NULL_HANDLE) + { + res = vkResetDescriptorPool(m_device, resources.descriptor_pool, 0); + if (res != VK_SUCCESS) + LOG_VULKAN_ERROR(res, "vkResetDescriptorPool failed: "); + } + // Enable commands to be recorded to the two buffers again. VkCommandBufferBeginInfo begin_info = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, nullptr}; @@ -1458,17 +1570,17 @@ void GSDeviceVK::ActivateCommandBuffer(u32 index) // Collect the pipeline statistics from the last time this cmdbuffer was used. resources.pipeline_statistics_query = QueryState::None; GPUPipelineStatistics stats{}; - VkResult res = + VkResult ps_res = vkGetQueryPoolResults(m_device, m_pipeline_statistics_query_pool, index, 1, sizeof(stats), &stats, sizeof(u64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); - if (res == VK_SUCCESS) + if (ps_res == VK_SUCCESS) { m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.vs_invocations; m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.ps_invocations; } else { - LOG_VULKAN_ERROR(res, "vkGetQueryPoolResults failed: "); + LOG_VULKAN_ERROR(ps_res, "vkGetQueryPoolResults failed: "); } } @@ -2787,38 +2899,78 @@ bool GSDeviceVK::CheckFeatures() //const bool isAMD = (vendorID == 0x1002 || vendorID == 0x1022); //const bool isNVIDIA = (vendorID == 0x10DE); - // framebuffer_fetch: the tiler-native ordered Cd read (ROAA / subpassLoad in tile memory). - // It lets DetermineBarriers() drop every per-primitive barrier and makes ROV auto-disable -- - // the fast, correct path for blend-heavy games on a TBDR. - // - // MALI (0x13B5): ENABLED by default when ROAA is present -- without it the per-PRIMITIVE - // texture-barrier path tanks blend-heavy games (GT4 = 10-20fps slideshow). No-op on any Mali - // lacking the extension. - // - // ADRENO / other non-Mali: OPT-IN only (GSConfig.EnableAdrenoFramebufferFetch, default off). - // The proprietary Adreno driver returned STALE ROAA reads above Basic blending (alpha cutouts / - // invisible floors, A/B 2026-06-10); gated behind a toggle to ship dark and be A/B-verified per - // device+driver. Gated on ROAA presence, so a no-op on any device lacking the extension. - // Keep the runtime GPU profile consistent on Vulkan too. The OpenGL device sets this from the - // GpuProfileDetector so the shared HW blend path (GSRendererHW.cpp alpha_mali_custom_set) can route - // Mali's broken HW dual-source blends through the in-shader SW-blend path; on Vulkan we reach Cd via - // texture-barriers instead of GL_ARM_shader_framebuffer_fetch. The GL-shader GPU_PROFILE_* defines are - // unused by the VK path, so this only affects that one cross-platform blend workaround. - if (IsDeviceMali()) + // Disabled on the upstream-sync codebase: the reworked SW-blend path reads + // the RT through rasterization-order input attachments, and on Adreno + // (proprietary driver 0842.x) that produces stale reads — alpha cutouts + // around sprites and invisible floor patches at blending accuracy above + // Basic. The pre-sync code drove the same extension differently and was + // fine; until that interplay is root-caused, take the barrier path, which + // renders correctly. (A/B-verified 2026-06-10 on Adreno 840.) + // Mali (ARM, vendorID 0x13B5): the hardware dual-source blend unit (INV_SRC1_COLOR, + // used for the blend-mix destination blends PCSX2 emits below Maximum accuracy) + // mis-renders on these drivers → white-band / blowout corruption on translucency and + // bloom unless the user forces Maximum blending. These GPUs also expose none of the + // coherent in-shader RT-read extensions (rasterization_order / feedback_loop_layout / + // fragment_shader_interlock), so framebuffer fetch is no help either — an earlier + // attempt to enable it only dropped the per-draw barriers and still corrupted (the + // HW dual-source path was still taken). Fix: select the Mali runtime GPU profile so + // the shared blend path's alpha_mali_custom_set (GSRendererHW.cpp) forces these alpha + // blends through the in-shader SW-blend path — reading Cd via the texture-barrier full + // barriers and never touching the broken HW unit. This is the exact workaround the + // OpenGL renderer already uses for Mali; on Vulkan we reach Cd via barriers instead of + // GL_ARM_shader_framebuffer_fetch. Its only cross-platform effect is enabling that one + // blend workaround (the GL-shader GPU_PROFILE_MALI defines are not used by the VK path). + if (m_device_properties.vendorID == 0x13B5u) SetRuntimeGPUProfile(RuntimeGpuProfile::Mali); - else if (IsDevicePowerVR()) - SetRuntimeGPUProfile(RuntimeGpuProfile::PowerVR); - else if (IsDeviceAdreno()) - SetRuntimeGPUProfile(RuntimeGpuProfile::Adreno); - const bool is_mali_vk = IsDeviceMali(); - // Turnip/Mesa is the open Adreno driver and does NOT exhibit the proprietary blob's stale-ROAA - // reads (the reason Adreno fbfetch shipped opt-in), so default it ON there. +#if defined(__ANDROID__) + // MediaTek (Dimensity/Helio) Mali Vulkan stacks return zero/stale destination color + // through ROAA (black / missing textures) across GPU generations, so detect the SoC + // here and disable fbfetch below. Ported from sashkinbro/EmuCoreX. Detection reads the + // ro.soc.* props already folded into the profile hints (no new JNI needed). + m_is_mediatek_soc = GpuProfileDetector::Resolve( + GSConfig.AndroidGpuProfileOverride, std::string_view(), m_device_properties.deviceName) + .is_mediatek_soc; +#endif + + // framebuffer_fetch: the tiler-native ordered Cd read (ROAA / subpassLoad in tile + // memory). It lets DetermineBarriers() (GSRendererHW.cpp) drop every per-primitive + // barrier and makes ROV (m_features.rov below) auto-disable — the fast, correct path + // for blend-heavy games on a TBDR. + // + // MALI (0x13B5): ENABLED by default when ROAA is present. The Mali profile forces SW + // blend, so fbfetch reads Cd in-shader and never touches Mali's broken HW dual-source + // unit; without fbfetch the per-PRIMITIVE texture-barrier path tanks blend-heavy games + // (GT4 = 10-20fps slideshow). No-op on any Mali lacking the extension. + // + // ADRENO / other non-Mali: OPT-IN only (EnableAdrenoFramebufferFetch, default off). + // ROV is the wrong primitive on a tiler (fragment_shader_interlock serializes same-pixel + // fragments + bypasses tile memory), so on Adreno fbfetch is the way to make accurate + // blending fast. Historically kept off because the Adreno-840 PROPRIETARY driver returned + // STALE ROAA reads above Basic blending (alpha cutouts / invisible floors, A/B 2026-06-10); + // that was never confirmed on other Adreno gens or on Turnip/Mesa, so this is gated behind + // a toggle to ship dark and be A/B-verified per device+driver. Gated on ROAA presence, so + // it is a no-op on any device that does not expose the extension. + const bool is_mali_vk = (m_device_properties.vendorID == 0x13B5u); + // Turnip/Mesa is the open Adreno driver and does NOT exhibit the proprietary + // blob's stale-ROAA reads (the reason Adreno fbfetch shipped opt-in), so default + // it ON there — the fast blend path on a tiler that drops the per-primitive + // barriers spiking GS on transparency-heavy scenes. Proprietary Adreno stays + // opt-in via EnableAdrenoFramebufferFetch; DisableFramebufferFetch still overrides. const bool is_turnip = (m_device_driver_properties.driverID == VK_DRIVER_ID_MESA_TURNIP); - // Samsung Xclipse (Exynos AMD-RDNA2) has no working ROAA-based framebuffer fetch -- force it off - // there. Inert if the 0x144D vendorID guess is wrong. + // Samsung Xclipse (Exynos AMD-RDNA2) has no working ROAA-based framebuffer fetch — force it off + // there so we never route the fast-blend path into a broken unit. Inert if the 0x144D vendorID + // guess is wrong (a real Xclipse tester must confirm IsDeviceXclipse() fires). const bool is_xclipse_vk = IsDeviceXclipse(); - const bool vendor_allows_fbfetch = + // MediaTek Mali + Mali-G57 expose ROAA but return zero/stale destination color from it + // (black or intermittently missing textures); force those onto the texture-barrier path + // instead of fbfetch. Ported from sashkinbro/EmuCoreX (MediaTek across GPU generations, + // plus the older Mali-G57 case). deviceName is null-terminated by Vulkan. + const bool is_mali_g57 = is_mali_vk && + (std::string_view(m_device_properties.deviceName).find("Mali-G57") != std::string_view::npos); + const bool is_mediatek_mali_vk = is_mali_vk && m_is_mediatek_soc; + const bool unreliable_mali_fbfetch = is_mediatek_mali_vk || is_mali_g57; + const bool vendor_allows_fbfetch = !unreliable_mali_fbfetch && (is_mali_vk || is_turnip || GSConfig.EnableAdrenoFramebufferFetch) && !is_xclipse_vk; m_features.framebuffer_fetch = vendor_allows_fbfetch && m_optional_extensions.vk_ext_rasterization_order_attachment_access && !GSConfig.DisableFramebufferFetch; @@ -2852,18 +3004,32 @@ bool GSDeviceVK::CheckFeatures() // Use D32F depth instead of D32S8 when we have framebuffer fetch. m_features.stencil_buffer &= !m_features.framebuffer_fetch; + // @@MALI_TELEMETRY@@ One-line device/driver banner so Mali (and Adreno) field reports are - // actionable: which GPU/driver, and - critically - which accurate-blend path was resolved: + // actionable: which GPU/driver, and — critically — which accurate-blend path was resolved: // in-tile framebuffer_fetch (cheap) vs the per-primitive barrier fallback (the tile-flush - // slideshow). ROAA=yes but fbfetch=NO means the barrier path is active. - Console.WriteLn("VK: GPU '%s' vendor=0x%04X driver='%s' (%s) | ROAA=%s fbfetch=%s texbarrier=%s", + // slideshow). ROAA=yes but fbfetch=NO on Mali means the barrier path is active. See the + // Mali driver-support deep dive. + Console.WriteLn("VK: GPU '%s' vendor=0x%04X driver='%s' (%s) | ROAA=%s fbfetch=%s texbarrier=%s pushdesc=%s", m_device_properties.deviceName, m_device_properties.vendorID, m_device_driver_properties.driverName, m_device_driver_properties.driverInfo, m_optional_extensions.vk_ext_rasterization_order_attachment_access ? "yes" : "NO", m_features.framebuffer_fetch ? "yes(in-tile)" : "NO(barrier-fallback)", - m_features.texture_barrier ? "on" : "off"); + m_features.texture_barrier ? "on" : "off", + m_use_push_descriptors ? "on" : "off"); + + // Adreno colorWriteMask-with-depthtest bug (PPSSPP #10421 / thin3d_vulkan.cpp): on + // Adreno 5xx and pre-0x801EA000 drivers the pipeline colorWriteMask is ignored while a + // depth test is active, so masked RGBA channels get written. PS2 FBMASK relies on the + // write mask; we emulate the one case Vulkan blend can express (RGB fully masked, alpha + // independent) in CreateTFXPipeline. No user toggle; excludes Adreno 6xx/7xx/8xx. + m_broken_colormask_with_depth = IsDeviceAdreno() && + (m_device_properties.deviceID < 0x06000000u || m_device_properties.driverVersion < 0x801EA000u); + if (m_broken_colormask_with_depth) + Console.WriteLn("VK: Adreno colorWriteMask-with-depthtest workaround active (deviceID=0x%08X driver=0x%08X)", + m_device_properties.deviceID, m_device_properties.driverVersion); // whether we can do point/line expand depends on the range of the device const float f_upscale = static_cast(GSConfig.UpscaleMultiplier); @@ -2872,7 +3038,16 @@ bool GSDeviceVK::CheckFeatures() m_features.line_expand = (m_device_features.wideLines && limits.lineWidthRange[0] <= f_upscale && limits.lineWidthRange[1] >= f_upscale); + // Same class of issue as framebuffer_fetch above: the upstream-sync SW-Z + // depth feedback (depth bound as input attachment + shader depth test/write) + // is untested on the Android mobile GPUs and the pre-sync core never used it. + // Force it off on Android so the renderer takes the well-tested avoid/copy + // fallbacks (same as D3D11); desktop keeps canonical feedback-loop behavior. +#if defined(__ANDROID__) + m_features.depth_feedback = false; +#else m_features.depth_feedback = m_features.feedback_loops(); +#endif m_features.aa1 = GSConfig.HWAA1 && m_features.vs_expand && m_features.feedback_loops(); DevCon.WriteLn("Optional features:%s%s%s%s%s", m_features.primitive_id ? " primitive_id" : "", @@ -3917,6 +4092,10 @@ static void AddShaderHeader(std::stringstream& ss) ss << "#version 460 core\n"; ss << "#extension GL_EXT_samplerless_texture_functions : require\n"; + // Mali driver-bug shader gate (currently the EQUAL_WZ_CORRUPTS_DEPTH z-nudge in + // tfx.glsl). 1 only on Mali; the guarded code compiles out on every other GPU. + ss << "#define GPU_PROFILE_MALI " << (dev->IsDeviceMali() ? 1 : 0) << "\n"; + if (!features.texture_barrier) ss << "#define DISABLE_TEXTURE_BARRIER 1\n"; if (features.texture_barrier && dev->UseFeedbackLoopLayout()) @@ -4059,7 +4238,8 @@ bool GSDeviceVK::CreatePipelineLayouts() // Convert Pipeline Layout ////////////////////////////////////////////////////////////////////////// - dslb.SetPushFlag(); + if (m_use_push_descriptors) + dslb.SetPushFlag(); dslb.AddBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NUM_UTILITY_SAMPLERS, VK_SHADER_STAGE_FRAGMENT_BIT); if ((m_utility_ds_layout = dslb.Create(dev)) == VK_NULL_HANDLE) return false; @@ -4088,7 +4268,8 @@ bool GSDeviceVK::CreatePipelineLayouts() return false; Vulkan::SetObjectName(dev, m_tfx_ubo_ds_layout, "TFX UBO descriptor layout"); - dslb.SetPushFlag(); + if (m_use_push_descriptors) + dslb.SetPushFlag(); dslb.AddBinding(TFX_TEXTURE_TEXTURE, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT); dslb.AddBinding(TFX_TEXTURE_PALETTE, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT); dslb.AddBinding(TFX_TEXTURE_RT, @@ -4617,7 +4798,8 @@ bool GSDeviceVK::CompileCASPipelines() Vulkan::DescriptorSetLayoutBuilder dslb; Vulkan::PipelineLayoutBuilder plb; - dslb.SetPushFlag(); + if (m_use_push_descriptors) + dslb.SetPushFlag(); dslb.AddBinding(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); dslb.AddBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); if ((m_cas_ds_layout = dslb.Create(dev)) == VK_NULL_HANDLE) @@ -4825,7 +5007,20 @@ bool GSDeviceVK::DoCAS( Vulkan::DescriptorSetUpdateBuilder dsub; dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, 0, sTexVK->GetView(), sTexVK->GetVkLayout()); dsub.AddStorageImageDescriptorWrite(VK_NULL_HANDLE, 1, dTexVK->GetView(), dTexVK->GetVkLayout()); - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, false); + if (m_use_push_descriptors) + { + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, false); + } + else + { + const VkDescriptorSet ds = AllocateFrameDescriptorSet(m_cas_ds_layout); + if (ds != VK_NULL_HANDLE) + { + dsub.SetDestinationSet(ds); + dsub.Update(m_device, false); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, 1, &ds, 0, nullptr); + } + } // the actual meat and potatoes! only four commands. static const int threadGroupWorkRegionDim = 16; @@ -4962,6 +5157,8 @@ void GSDeviceVK::DestroyResources() } if (resources.command_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, resources.command_pool, nullptr); + if (resources.descriptor_pool != VK_NULL_HANDLE) + vkDestroyDescriptorPool(m_device, resources.descriptor_pool, nullptr); } if (m_timestamp_query_pool != VK_NULL_HANDLE) @@ -5192,6 +5389,23 @@ VkPipeline GSDeviceVK::CreateTFXPipeline(const PipelineSelector& p) vk_blend_ops[pbs.op], vk_blend_factors[pbs.src_factor_alpha], vk_blend_factors[pbs.dst_factor_alpha], VK_BLEND_OP_ADD, p.cms.wrgba); } + else if (m_broken_colormask_with_depth && (p.cms.wrgba & 0x7u) == 0 && + (p.dss.ztst != ZTST_ALWAYS || p.dss.zwe)) + { + // Adreno colorWriteMask-with-depthtest bug (PPSSPP #10421): with a depth test + // active the pipeline write mask is ignored, so a masked-RGB draw (FBMASK RGB=off) + // would wrongly write colour. Only alpha can differ here (RGB is fully masked), + // which Vulkan blend CAN express: open the write mask so the broken HW mask can't + // misfire, keep old RGB via (src=ZERO,dst=ONE), gate alpha on wa. Arbitrary + // per-channel RGB masks are not emulable this way (would corrupt), so they fall + // through to the normal path below. + const bool write_alpha = (p.cms.wrgba & 0x8u) != 0; + gpb.SetBlendAttachment(0, true, + VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, + write_alpha ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ZERO, + write_alpha ? VK_BLEND_FACTOR_ZERO : VK_BLEND_FACTOR_ONE, + VK_BLEND_OP_ADD, 0xFu); + } else { gpb.SetBlendAttachment(0, false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, @@ -5222,6 +5436,22 @@ VkPipeline GSDeviceVK::GetTFXPipeline(const PipelineSelector& p) VkPipeline pipeline = CreateTFXPipeline(p); m_tfx_pipelines.emplace(p, pipeline); + + // Persist the pipeline cache every N new compiles so an Android OOM-kill + // or crash mid-session doesn't throw away pipelines that compiled after + // the last onPause flush. Android gets a larger threshold because the log + // showed repeated synchronous disk writes during normal gameplay in heavy + // scenes; onPause still performs a final flush. +#ifdef __ANDROID__ + static constexpr u32 PIPELINE_CACHE_FLUSH_THRESHOLD = 256; +#else + static constexpr u32 PIPELINE_CACHE_FLUSH_THRESHOLD = 32; +#endif + if (g_vulkan_shader_cache && ++m_tfx_pipeline_compile_counter >= PIPELINE_CACHE_FLUSH_THRESHOLD) + { + m_tfx_pipeline_compile_counter = 0; + g_vulkan_shader_cache->FlushPipelineCache(); + } return pipeline; } @@ -5824,6 +6054,13 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (flags & DIRTY_FLAG_TFX_TEXTURES) { + // Non-push path allocates a fresh (empty) descriptor set every time, so every binding the + // shader may read must be written - not just the dirty ones (push descriptors persist the rest + // in command-buffer state; allocated sets do not). Force all texture sub-flags on. All + // m_tfx_textures[] slots are always valid (null slots hold m_null_texture), so this is safe. + if (!m_use_push_descriptors) + flags |= DIRTY_FLAG_TFX_TEXTURES; + if (flags & DIRTY_FLAG_TFX_TEXTURE_TEX) { dsub.AddCombinedImageSamplerDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_TEXTURE, @@ -5877,7 +6114,25 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetVkLayout(), true); } - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, TFX_DESCRIPTOR_SET_TEXTURES); + if (m_use_push_descriptors) + { + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, TFX_DESCRIPTOR_SET_TEXTURES); + } + else + { + const VkDescriptorSet ds = AllocateFrameDescriptorSet(m_tfx_texture_ds_layout); + if (ds != VK_NULL_HANDLE) + { + dsub.SetDestinationSet(ds); + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, + TFX_DESCRIPTOR_SET_TEXTURES, 1, &ds, 0, nullptr); + } + else + { + dsub.Clear(); + } + } } ApplyBaseState(flags, cmdbuf); @@ -5900,7 +6155,20 @@ bool GSDeviceVK::ApplyUtilityState(bool already_execed) Vulkan::DescriptorSetUpdateBuilder dsub; dsub.AddCombinedImageSamplerDescriptorWrite( VK_NULL_HANDLE, 0, m_utility_texture->GetView(), m_utility_sampler, m_utility_texture->GetVkLayout()); - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, false); + if (m_use_push_descriptors) + { + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, false); + } + else + { + const VkDescriptorSet ds = AllocateFrameDescriptorSet(m_utility_ds_layout); + if (ds != VK_NULL_HANDLE) + { + dsub.SetDestinationSet(ds); + dsub.Update(m_device, false); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, 1, &ds, 0, nullptr); + } + } } @@ -6536,7 +6804,7 @@ void GSDeviceVK::UpdateHWPipelineSelector(GSHWDrawConfig& config, PipelineSelect pipe.ds = config.ds != nullptr && !config.ps.HasDepthROV(); pipe.line_width = config.line_expand; pipe.feedback_loop_flags = FeedbackLoopFlag_None; - if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier)) + if (m_features.texture_barrier) { if (config.IsFeedbackLoopRT(config.ps)) pipe.feedback_loop_flags |= FeedbackLoopFlag_ReadAndWriteRT; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h index 8572f1d2f1..9460de290a 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h @@ -47,6 +47,7 @@ public: bool vk_khr_shader_non_semantic_info : 1; bool vk_ext_attachment_feedback_loop_layout : 1; bool vk_ext_fragment_shader_interlock : 1; + bool vk_khr_push_descriptor : 1; }; // Global state accessors @@ -81,17 +82,22 @@ public: /// Returns true if running on an AMD GPU. __fi bool IsDeviceAMD() const { return (m_device_properties.vendorID == 0x1002); } + /// Returns true if running on an ARM Mali GPU (vendorID 0x13B5). __fi bool IsDeviceMali() const { return (m_device_properties.vendorID == 0x13B5u); } /// Returns true if running on a Qualcomm Adreno GPU (vendorID 0x5143). __fi bool IsDeviceAdreno() const { return (m_device_properties.vendorID == 0x5143u); } + // Adreno-5xx / pre-0x801EA000 driver bug: colorWriteMask is ignored while a depth + // test is active (PPSSPP #10421). Cached in CheckFeatures, consumed in CreateTFXPipeline. + bool m_broken_colormask_with_depth = false; + /// Returns true if running on an Imagination PowerVR GPU (vendorID 0x1010). __fi bool IsDevicePowerVR() const { return (m_device_properties.vendorID == 0x1010u); } /// Returns true if running on a Samsung Xclipse (Exynos AMD-RDNA2) GPU. - /// NOTE: 0x144D (Samsung) is unverified across driver revisions -- a real Xclipse tester + /// NOTE: 0x144D (Samsung) is unverified across driver revisions — a real Xclipse tester /// must confirm this fires; if it reports a different vendorID the gate is simply inert. __fi bool IsDeviceXclipse() const { return (m_device_properties.vendorID == 0x144Du); } @@ -120,6 +126,14 @@ public: /// Frees a descriptor set allocated from the global pool. void FreePersistentDescriptorSet(VkDescriptorSet set); + /// True when the device uses VK_KHR_push_descriptor for texture binding (everything except Mali, + /// whose driver crashes inside vkCmdPushDescriptorSetKHR). When false, textures are bound via + /// per-frame allocated descriptor sets (vkUpdateDescriptorSets + vkCmdBindDescriptorSets). + __fi bool UsePushDescriptors() const { return m_use_push_descriptors; } + + /// Allocates a descriptor set from the current frame's reset-per-frame pool (non-push path only). + VkDescriptorSet AllocateFrameDescriptorSet(VkDescriptorSetLayout set_layout); + // Gets the fence that will be signaled when the currently executing command buffer is // queued and executed. Do not wait for this fence before the buffer is executed. __fi VkFence GetCurrentCommandBufferFence() const { return m_frame_resources[m_current_frame].fence; } @@ -235,6 +249,9 @@ private: // [0] - Init (upload) command buffer, [1] - draw command buffer VkCommandPool command_pool = VK_NULL_HANDLE; std::array command_buffers{VK_NULL_HANDLE, VK_NULL_HANDLE}; + // Per-frame texture descriptor pool, reset wholesale each time the frame is reused. + // Only created/used on the non-push-descriptor path (Mali workaround). + VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; VkFence fence = VK_NULL_HANDLE; u64 fence_counter = 0; s32 spin_id = -1; @@ -266,6 +283,15 @@ private: VkDescriptorPool m_global_descriptor_pool = VK_NULL_HANDLE; + // Set false for Mali (vendorID 0x13B5) in CreateDevice: its driver crashes inside + // vkCmdPushDescriptorSetKHR, so texture binding falls back to per-frame descriptor sets. + bool m_use_push_descriptors = true; + + // True when the SoC hints look like MediaTek (Dimensity/Helio). Set in CheckFeatures + // from GpuProfileDetector; used to disable the broken Vulkan fbfetch path on their + // Mali stacks (zero/stale Cd → black/missing textures). Ported from EmuCoreX. + bool m_is_mediatek_soc = false; + VkQueue m_graphics_queue = VK_NULL_HANDLE; VkQueue m_present_queue = VK_NULL_HANDLE; u32 m_graphics_queue_family_index = 0; @@ -296,13 +322,13 @@ private: float m_accumulated_gpu_time = 0.0f; bool m_gpu_timing_enabled = false; bool m_gpu_timing_supported = false; - bool m_wants_new_timestamp_calibration = false; - VkTimeDomainEXT m_calibrated_timestamp_type = VK_TIME_DOMAIN_DEVICE_EXT; VkQueryPool m_pipeline_statistics_query_pool = VK_NULL_HANDLE; GPUPipelineStatistics m_accumulated_gpu_pipeline_statistics{}; bool m_gpu_pipeline_statistics_enabled = false; bool m_gpu_pipeline_statistics_supported = false; + bool m_wants_new_timestamp_calibration = false; + VkTimeDomainEXT m_calibrated_timestamp_type = VK_TIME_DOMAIN_DEVICE_EXT; std::array m_frame_resources; u64 m_next_fence_counter = 1; @@ -474,6 +500,7 @@ private: std::unordered_map m_tfx_fragment_shaders; std::unordered_map m_tfx_pipelines; + u32 m_tfx_pipeline_compile_counter = 0; VkRenderPass m_utility_color_render_pass_load = VK_NULL_HANDLE; VkRenderPass m_utility_color_render_pass_clear = VK_NULL_HANDLE; diff --git a/pcsx2/GS/Renderers/Vulkan/VKBuilders.cpp b/pcsx2/GS/Renderers/Vulkan/VKBuilders.cpp index bbec5beeed..03300a0bcf 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKBuilders.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKBuilders.cpp @@ -282,12 +282,24 @@ VkPipeline Vulkan::GraphicsPipelineBuilder::Create( { const GSShaderCompileIndicator::CompileTimer compile_timer; - VkPipeline pipeline; + VkPipeline pipeline = VK_NULL_HANDLE; VkResult res = vkCreateGraphicsPipelines(device, pipeline_cache, 1, &m_ci, nullptr, &pipeline); if (res != VK_SUCCESS) { - LOG_VULKAN_ERROR(res, "vkCreateGraphicsPipelines() failed: "); - return VK_NULL_HANDLE; + // Some Adreno drivers illegally return VK_INCOMPLETE from + // vkCreateGraphicsPipelines even though the pipeline was actually created + // (ported from PPSSPP; seen in Burnout). Dropping the "failed" pipeline + // skips the draw and leaves missing graphics — so if the handle came back + // valid, keep it and just warn. Any other result is a genuine failure. + if (res == VK_INCOMPLETE && pipeline != VK_NULL_HANDLE) + { + Console.Warning("Vulkan: vkCreateGraphicsPipelines() returned VK_INCOMPLETE (Adreno driver quirk); using the created pipeline."); + } + else + { + LOG_VULKAN_ERROR(res, "vkCreateGraphicsPipelines() failed: "); + return VK_NULL_HANDLE; + } } if (clear) @@ -732,6 +744,12 @@ void Vulkan::DescriptorSetUpdateBuilder::PushUpdate( Clear(); } +void Vulkan::DescriptorSetUpdateBuilder::SetDestinationSet(VkDescriptorSet set) +{ + for (u32 i = 0; i < m_num_writes; i++) + m_writes[i].dstSet = set; +} + void Vulkan::DescriptorSetUpdateBuilder::AddImageDescriptorWrite(VkDescriptorSet set, u32 binding, VkImageView view, VkImageLayout layout /*= VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL*/, bool storage_image /*= false*/) { diff --git a/pcsx2/GS/Renderers/Vulkan/VKBuilders.h b/pcsx2/GS/Renderers/Vulkan/VKBuilders.h index 39806ffd9f..c1ed2b47c9 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKBuilders.h +++ b/pcsx2/GS/Renderers/Vulkan/VKBuilders.h @@ -240,6 +240,11 @@ namespace Vulkan void PushUpdate(VkCommandBuffer cmdbuf, VkPipelineBindPoint bind_point, VkPipelineLayout layout, u32 set, bool clear = true); + // Patches the dstSet of every pending write to the given set. Used by the non-push-descriptor + // binding path (Mali workaround) where writes are accumulated with a VK_NULL_HANDLE set and the + // real per-frame allocated set is only known just before Update(). + void SetDestinationSet(VkDescriptorSet set); + void AddImageDescriptorWrite(VkDescriptorSet set, u32 binding, VkImageView view, VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, bool storage_image = false); void AddSamplerDescriptorWrite(VkDescriptorSet set, u32 binding, VkSampler sampler); diff --git a/pcsx2/GS/Renderers/Vulkan/VKShaderCache.cpp b/pcsx2/GS/Renderers/Vulkan/VKShaderCache.cpp index 5da483a781..561d5d0dab 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKShaderCache.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKShaderCache.cpp @@ -100,6 +100,61 @@ static void FillPipelineCacheHeader(VK_PIPELINE_CACHE_HEADER* header) std::memcpy(header->uuid, GSDeviceVK::GetInstance()->GetDeviceProperties().pipelineCacheUUID, VK_UUID_SIZE); } +#if defined(__ANDROID__) + +// Android: shaderc is statically linked, call functions directly. +namespace dyn_shaderc +{ + static bool Open(); + static void Close(); + + static shaderc_compiler_t s_compiler = nullptr; + + // Direct function pointers to the statically-linked shaderc. + static constexpr auto shaderc_compiler_initialize = ::shaderc_compiler_initialize; + static constexpr auto shaderc_compiler_release = ::shaderc_compiler_release; + static constexpr auto shaderc_compile_options_initialize = ::shaderc_compile_options_initialize; + static constexpr auto shaderc_compile_options_release = ::shaderc_compile_options_release; + static constexpr auto shaderc_compile_options_set_source_language = ::shaderc_compile_options_set_source_language; + static constexpr auto shaderc_compile_options_set_generate_debug_info = ::shaderc_compile_options_set_generate_debug_info; + static constexpr auto shaderc_compile_options_set_optimization_level = ::shaderc_compile_options_set_optimization_level; + static constexpr auto shaderc_compile_options_set_target_env = ::shaderc_compile_options_set_target_env; + static constexpr auto shaderc_compile_into_spv = ::shaderc_compile_into_spv; + static constexpr auto shaderc_result_release = ::shaderc_result_release; + static constexpr auto shaderc_result_get_length = ::shaderc_result_get_length; + static constexpr auto shaderc_result_get_num_warnings = ::shaderc_result_get_num_warnings; + static constexpr auto shaderc_result_get_bytes = ::shaderc_result_get_bytes; + static constexpr auto shaderc_result_get_error_message = ::shaderc_result_get_error_message; + static constexpr auto shaderc_result_get_compilation_status = ::shaderc_result_get_compilation_status; +} // namespace dyn_shaderc + +bool dyn_shaderc::Open() +{ + if (s_compiler) + return true; + + s_compiler = shaderc_compiler_initialize(); + if (!s_compiler) + { + ERROR_LOG("shaderc_compiler_initialize() failed"); + return false; + } + + std::atexit(&dyn_shaderc::Close); + return true; +} + +void dyn_shaderc::Close() +{ + if (s_compiler) + { + shaderc_compiler_release(s_compiler); + s_compiler = nullptr; + } +} + +#else // !__ANDROID__ + #define SHADERC_FUNCTIONS(X) \ X(shaderc_compiler_initialize) \ X(shaderc_compiler_release) \ @@ -141,15 +196,13 @@ bool dyn_shaderc::Open() #ifdef _WIN32 const std::string libname = DynamicLibrary::GetVersionedFilename("shaderc_shared"); -#elif defined(__ANDROID__) - // Android jniLibs are unversioned: the APK ships libshaderc_shared.so (no ".so.1"), - // extracted to the app's nativeLibraryDir. Requesting the versioned name fails dlopen. - const std::string libname = DynamicLibrary::GetVersionedFilename("shaderc_shared"); #else // Use versioned, bundle post-processing adds it.. const std::string libname = DynamicLibrary::GetVersionedFilename("shaderc_shared", 1); + // Debian packages the library as libshaderc.so.1 + const std::string libname_fallback = DynamicLibrary::GetVersionedFilename("shaderc", 1); #endif - if (!s_library.Open(libname.c_str(), &error)) + if (!s_library.Open(libname.c_str(), &error) && !s_library.Open(libname_fallback.c_str(), &error)) { ERROR_LOG("Failed to load shaderc: {}", error.GetDescription()); return false; @@ -196,6 +249,8 @@ void dyn_shaderc::Close() #undef SHADERC_FUNCTIONS #undef SHADERC_INIT_FUNCTIONS +#endif // !__ANDROID__ + static void DumpBadShader(std::string_view code, std::string_view errors) { const std::string filename = Path::Combine(EmuFolders::Logs, fmt::format("pcsx2_bad_shader_{}.txt", ++s_next_bad_shader_id)); @@ -561,9 +616,17 @@ bool VKShaderCache::FlushPipelineCache() if (!FileSystem::StatFile(m_pipeline_cache_filename.c_str(), &sd) || sd.Size != static_cast(data_size)) { Console.WriteLn("Writing %zu bytes to '%s'", data_size, m_pipeline_cache_filename.c_str()); - if (!FileSystem::WriteBinaryFile(m_pipeline_cache_filename.c_str(), data.data(), data.size())) + // @@ARMSX2_VKCACHE_ATOMIC@@ Stage to a temp file then rename over the real one. A swipe-kill + // or crash mid-write otherwise leaves a half-written pipeline cache that some drivers (Adreno) + // still accept past the header check and then render garbage from — the "corrupt VK cache" + // bug that manifested as dark/purple textures until the cache was manually cleared. rename() + // is atomic on POSIX, so the previous valid cache survives a failed/interrupted write. + const std::string tmp_filename = m_pipeline_cache_filename + ".tmp"; + if (!FileSystem::WriteBinaryFile(tmp_filename.c_str(), data.data(), data.size()) || + !FileSystem::RenamePath(tmp_filename.c_str(), m_pipeline_cache_filename.c_str())) { Console.Error("Failed to write pipeline cache to '%s'", m_pipeline_cache_filename.c_str()); + FileSystem::DeleteFilePath(tmp_filename.c_str()); return false; } } diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp index 94f48db21f..77cc8107ad 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp @@ -304,7 +304,7 @@ bool VKSwapChain::SelectPresentMode(VkSurfaceKHR surface, GSVSyncMode* vsync_mod // Prefer FIFO_RELAXED (adaptive vsync): behaves exactly like FIFO while the app // keeps pace, but a frame that misses its refresh interval is presented late // (with tearing) instead of stalling a whole interval. This avoids the hard - // 60->30 fps cliff on borderline titles - a big felt win on weak devices. Plain + // 60->30 fps cliff on borderline titles — a big felt win on weak devices. Plain // FIFO is always available as the fallback when the driver lacks relaxed support. if (CheckForMode(VK_PRESENT_MODE_FIFO_RELAXED_KHR)) { @@ -376,6 +376,18 @@ bool VKSwapChain::CreateSwapChain() size.height = std::clamp(size.height, surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height); + // PowerVR (Imagination) driver bug: older drivers heavily corrupt the output + // unless the swap-chain width is a multiple of 32. Ported from PPSSPP + // (VulkanContext::InitSwapchain, issues #11743/#15773) — round the width down + // to a /32 boundary on affected PowerVR drivers. ARMSX2 had zero PowerVR + // handling, so this can only help. Gated by vendorID (0x1010) + driver version. + if (GSDeviceVK::GetInstance()->IsDevicePowerVR() && + GSDeviceVK::GetInstance()->GetDeviceProperties().driverVersion < 0x00582558u && + (size.width & ~31u) >= surface_capabilities.minImageExtent.width) + { + size.width &= ~31u; + } + // Prefer identity transform if possible VkSurfaceTransformFlagBitsKHR transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; if (!(surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)) diff --git a/pcsx2/ImGui/ImGuiFullscreen.cpp b/pcsx2/ImGui/ImGuiFullscreen.cpp index 3bc8de63a7..4c6f18debc 100644 --- a/pcsx2/ImGui/ImGuiFullscreen.cpp +++ b/pcsx2/ImGui/ImGuiFullscreen.cpp @@ -3248,11 +3248,20 @@ void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing) ImVec2(box_max.x + shadow_size, box_max.y + shadow_size), IM_COL32(20, 20, 20, (180 * opacity) / 255u), rounding, ImDrawFlags_RoundCornersAll); dl->AddRectFilled(box_min, box_max, background_color, rounding, ImDrawFlags_RoundCornersAll); - // ImGui v1.92 AddRect signature: (p_min, p_max, col, rounding, float thickness, - // ImDrawFlags flags). The old arg order passed ImDrawFlags_RoundCornersAll (~240) as the - // thickness (→ a giant malformed border) and LayoutScale(1.0f) as the flags (→ only - // TopLeft corner rounded) — that's the "broken" RetroAchievements toast. + // ImGui 1.92.8 SWAPPED the AddRect thickness/flags argument order, and the desktop + // (1.92.8) and Android (1.92.6) builds vendor DIFFERENT ImGui copies — so this call + // must be written per-version or the toast border breaks on one platform. Passing + // ImDrawFlags_RoundCornersAll (~240) as the thickness draws a giant malformed border + // (the "broken" RetroAchievements toast); passing LayoutScale(1.0f) as the flags only + // rounds the top-left corner. Guarded on IMGUI_VERSION_NUM so both platforms are correct; + // collapses to the >= branch once the two 3rdparty/imgui copies are deduped. +#if IMGUI_VERSION_NUM >= 19280 + // 1.92.8+ signature: AddRect(p_min, p_max, col, rounding, float thickness, ImDrawFlags flags) dl->AddRect(box_min, box_max, border_color, rounding, ImGuiFullscreen::LayoutScale(1.0f), ImDrawFlags_RoundCornersAll); +#else + // <= 1.92.6 signature: AddRect(p_min, p_max, col, rounding, ImDrawFlags flags, float thickness) + dl->AddRect(box_min, box_max, border_color, rounding, ImDrawFlags_RoundCornersAll, ImGuiFullscreen::LayoutScale(1.0f)); +#endif const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding); const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size); diff --git a/pcsx2/MacOSStubs.cpp b/pcsx2/MacOSStubs.cpp index f6cc961244..23eeae7ef1 100644 --- a/pcsx2/MacOSStubs.cpp +++ b/pcsx2/MacOSStubs.cpp @@ -125,5 +125,51 @@ bool PCAPAdapter::recv(NetPacket* p) { return false; } bool PCAPAdapter::send(NetPacket* p) { return false; } void PCAPAdapter::reloadSettings() {} +#else // TARGET_OS_IPHONE — iOS stubs for CocoaTools (CocoaTools.mm is macOS-only) + +// On iOS, CocoaTools.mm is excluded from the build. Provide stubs for the +// functions referenced by iOS-compiled core code (DynamicLibrary, WindowInfo, +// Pcsx2Config, etc.). iOS uses UIKit/Foundation, not Cocoa/AppKit. +#include "common/CocoaTools.h" +#include "common/WindowInfo.h" +#include +#include + +namespace CocoaTools +{ + bool CreateMetalLayer(WindowInfo* wi) { return false; } + void DestroyMetalLayer(WindowInfo* wi) {} + std::optional GetViewRefreshRate(const WindowInfo& wi) { return std::nullopt; } + void MarkHelpMenu(void* menu) {} + std::optional GetBundlePath() { return std::nullopt; } + std::optional GetNonTranslocatedBundlePath() { return std::nullopt; } + std::optional MoveToTrash(std::string_view file) { return std::nullopt; } + bool DelayedLaunch(std::string_view file) { return false; } + bool ShowInFinder(std::string_view file) { return false; } + std::optional GetResourcePath() { return std::nullopt; } + void* CreateWindow(std::string_view title, uint32_t width, uint32_t height) { return nullptr; } + void DestroyWindow(void* window) {} + void GetWindowInfoFromWindow(WindowInfo* wi, void* window) {} + void RunCocoaEventLoop(bool wait_forever) {} + void StopMainThreadEventLoop() {} +} + +// --- Discord Register stubs (discord_register_osx.m excluded on iOS) --- +extern "C" { +void Discord_Register(const char* applicationId, const char* command) {} +void Discord_RegisterSteamGame(const char* applicationId, const char* steamId) {} +} + +// --- Host capture + hotkey stubs (frontend callbacks not yet wired) --- +#include "Host.h" +#include "GS/GS.h" +#include "Input/InputManager.h" +void Host::OnCaptureStarted(const std::string& filename) {} +void Host::OnCaptureStopped() {} + +// g_host_hotkeys - normally defined in pcsx2-qt, empty on iOS +BEGIN_HOTKEY_LIST(g_host_hotkeys) +END_HOTKEY_LIST() + #endif // !TARGET_OS_IPHONE diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 65776d47f2..a98fff86b9 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -3374,6 +3374,10 @@ void VMManager::WarnAboutUnsafeSettings() append(ICON_FA_TV, TRANSLATE_SV("VMManager", "Integer scaling is enabled. This may shrink the image.")); } +#if !defined(__ANDROID__) + // On Android the setup wizard forces an explicit GL/VK renderer pick by design — + // there is no "Automatic" backend to resolve to — so this banner would fire on every + // boot regardless of correctness. Desktop keeps the canonical warning. static bool render_change_warn = false; if (EmuConfig.GS.Renderer != GSRendererType::Auto && EmuConfig.GS.Renderer != GSRendererType::SW && !render_change_warn) { @@ -3383,6 +3387,7 @@ void VMManager::WarnAboutUnsafeSettings() append(ICON_FA_CIRCLE_EXCLAMATION, TRANSLATE_SV("VMManager", "Graphics API is not set to Automatic. This may cause performance problems and graphical issues.")); } +#endif } if (EmuConfig.GS.DumpGSData) { diff --git a/pcsx2/arm64/aR5900.android.cpp b/pcsx2/arm64/aR5900.android.cpp deleted file mode 100644 index 662f89e526..0000000000 --- a/pcsx2/arm64/aR5900.android.cpp +++ /dev/null @@ -1,5546 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — skeleton (Phase 1). -// -// ARM64 counterpart to pcsx2/x86/ix86-32/iR5900.cpp. At this stage every entry -// point is a stub: the recompiler is *defined* and links, providing the recCpu -// provider so VMManager can be wired to call Reserve/Reset/Shutdown on ARM64, -// but no guest code is actually compiled yet (recExecute fails loudly if reached). -// Real codegen lands incrementally in later phases (vtlb fastmem -> EE int -> -// branches -> coprocessors). The interpreter remains ground truth and the active -// provider until this is functional. - -#include "arm64/aR5900.h" -#include "arm64/aR5900Analysis.h" - -#include "Config.h" -#include "Memory.h" -#include "R5900.h" -#include "R5900OpcodeTables.h" -#include "VMManager.h" -#include "VU.h" -#include "VUmicro.h" -#include "vtlb.h" -#include "AndroidEEOpHist.h" -#include "EEDiffVerify.h" // @@EEDIFF@@ recompiler-vs-interpreter differential verifier - -#include "common/Assertions.h" -#include "common/Console.h" -#include "common/FastJmp.h" -#include "common/Pcsx2Defs.h" - -#include -#include -#include -#include -#include -#include -#include - -extern void _vu0WaitMicro(); -extern void vu0Sync(); // VU0.cpp — catch VU0 up to the EE clock (no force-finish); vc111 - - -namespace a64 = vixl::aarch64; - -// -------------------------------------------------------------------------------------- -// EE code-cache layout (Phase 1.3) -// -------------------------------------------------------------------------------------- -// The EE recompiler region is pre-reserved by SysMemory (HostMemoryMap::EErec*, 64 MB). -// We do NOT allocate it ourselves; we just carve it: -// -// [ GetEERec() ............................ recPtrEnd ) emitted block code -// [ recPtrEnd ............................. GetEERecEnd() ) ArmConstantPool -// -// The constant pool holds far-jump trampolines and 64/128-bit literals that VIXL -// loads PC-relative (see ArmConstantPool in AsmHelpers). x86 has no such pool (it -// inlines immediates), so this tail carve-out is ARM64-specific. recPtr is the -// rolling emit cursor; block compilation (Phase 1.4) advances it and resets the -// whole cache when it runs past recPtrEnd. - -// Space reserved at the tail of the EE rec region for the constant pool. -static constexpr u32 EE_CONSTPOOL_SIZE = static_cast(_1mb); - -static u8* recPtr = nullptr; // rolling emit cursor (start of next block) -static u8* recPtrEnd = nullptr; // end of the code region / start of the constant pool - -static ArmConstantPool s_const_pool; - -// -------------------------------------------------------------------------------------- -// recLUT block-lookup table (Phase 4.4) -// -------------------------------------------------------------------------------------- -// Two-level guest-PC -> host-block lookup, ported from the x86 rec (iR5900.cpp + -// BaseblockEx.h). The top-level page table recLUT[pc>>16] holds, per 64 KB guest -// page, a base pointer pre-biased so that -// -// slot = (uptr*)(recLUT[pc >> 16] + pc * 2) -// fnptr = *slot -// -// indexes one host code pointer per 4-byte guest word (sizeof(uptr)==8, so pc*2 == -// (pc/4)*8). The emitted DispatcherReg performs exactly this arithmetic and branches -// to `fnptr`. Uncompiled words point at the JITCompile stub (compile-on-jump); -// unmapped pages point every word at UnmappedRecLUTPage. The per-word slots live in -// recLutReserve (one contiguous array covering RAM + the three BIOS ROM regions); -// recLUT_SetPage maps each guest page (including its address mirrors) onto that -// array, mirroring x86 so the same code can be reached through any of its mirror -// addresses via a single shared block. hwLUT is intentionally omitted: invalidation -// is whole-cache reset for bring-up (Phase 4.5), so no HWADDR folding is needed. -alignas(16) static uptr recLUT[0x10000]; - -// One host-pointer slot per guest word of RAM + ROM + ROM1 + ROM2, plus a single -// shared 64 KB page worth of slots that every unmapped guest page aliases onto. -static std::vector recLutReserve; -static std::vector recLutUnmapped; -static size_t recLutEntries = 0; -static uptr* recRAM = nullptr; -static uptr* recROM = nullptr; // BIOS (0x1fc0..0x2000 in 64 KB pages) -static uptr* recROM1 = nullptr; // DVD player -static uptr* recROM2 = nullptr; // Chinese ROM extension - -// C++-side equivalent of the emitted lookup: address of the block slot for `pc`. -static __fi uptr* recPtrToBlock(u32 pc) -{ - return reinterpret_cast(recLUT[pc >> 16] + pc * (sizeof(uptr) / 4)); -} - -static __fi u32 recHWAddr(u32 pc) -{ - // Match the x86 recompiler's HWADDR() comparisons for RAM/BIOS mirrors. Fast - // Boot EELOAD and ELF-entry checks are stored as physical addresses. - const u32 ram_offset = pc & (Ps2MemSize::ExposedRam - 1); - const u32 ram_base = pc - ram_offset; - switch (ram_base) - { - case 0x00000000u: - case 0x20000000u: - case 0x30000000u: - case 0x80000000u: - case 0xa0000000u: - case 0xb0000000u: - case 0xc0000000u: - case 0xd0000000u: - return ram_offset; - default: - return pc; - } -} - -// Hard cap on instructions per block, so straight-line code can't run the emit -// cursor away before we get a chance to reset. (x86 uses page/branch boundaries; -// this is a simpler bring-up bound.) -static constexpr u32 MAX_BLOCK_INSTS = 256; - -// Safety headroom kept free at the end of the code region. The cache-full check fires -// when recPtr crosses (recPtrEnd - this), guaranteeing the block currently being -// emitted always fits without VIXL ever trying to grow (realloc) the MAP_JIT buffer — -// which it cannot, and which aborts the process. A single block is at most -// MAX_BLOCK_INSTS guest ops plus a delay slot and the dispatch tail; even the largest -// host expansions stay well under 256 KB, so 1 MB is comfortably safe. -static constexpr u32 RECOMPILE_HEADROOM = static_cast(_1mb); - -// Byte offsets (from RESTATEPTR = &cpuRegs) of the 64-bit cycle counters the emitted -// block tail reads/writes for the inline event test. -static constexpr u32 EE_CYCLE_OFFSET = static_cast(offsetof(cpuRegisters, cycle)); -static constexpr u32 EE_NEXTEVENTCYCLE_OFFSET = static_cast(offsetof(cpuRegisters, nextEventCycle)); -static constexpr u32 EE_HI_SCALAR_OFFSET = 32u * 16u; -static constexpr u32 EE_LO_SCALAR_OFFSET = 33u * 16u; - -// Byte offset of cpuRegs.CP0.n.Status (the COP0 interrupt/mode status word the DI -// generator clears Status.EIE in — see recEmitCop0DI). -static constexpr u32 EE_COP0_STATUS_OFFSET = - static_cast(offsetof(cpuRegisters, CP0) + offsetof(CP0regs, n.Status)); - -// Dynamically-generated dispatcher stubs (emitted into the head of the code cache by -// recGenDispatchers on every reset; addresses are stable across a reset because the -// stubs regenerate byte-identically at the same location — see recRecompile). -static const void* DispatcherReg = nullptr; // lookup cpuRegs.pc in recLUT, jump -static const void* DispatcherEvent = nullptr; // run event test, then fall to DispatcherReg -static const void* JITCompile = nullptr; // compile block at cpuRegs.pc, then dispatch -static const void* EnterRecompiledCode = nullptr; // C entry: pin RESTATEPTR, then dispatch -static const void* UnmappedRecLUTPage = nullptr; // jumped to on an unmapped guest PC -static const void* DispatchBlockDiscard = nullptr; // manual block failed its checksum -> clear + recompile -static const void* DispatchPageReset = nullptr; // counted manual block -> retry write-protection - -// ============================================================================ -// EE block chaining — direct-B tail links @@MAC_EE_BLOCKLINK@@ -// ---------------------------------------------------------------------------- -// Ported from the stock arm64 EE recompiler (arm64/aR5900.cpp:145-1355). On a -// statically-known block exit, recEmitEventTestAndDispatch normally emits a -// LUT-indirect tail dispatch (recEmitDispatchToKnownPc: adrp+add+ldr+br). This -// replaces that — on the same path, AFTER the untouched "B.pl DispatcherEvent" -// cycle guard — with a single patchable direct B to the successor block's -// entry, removing an indirect branch + LUT memory load per block exit on the -// hot (no-event-due) path. -// -// Safety invariants: -// * Event timing is byte-identical: the direct B is reached ONLY when no event -// is due (it sits after the B.pl guard); a pending event still diverts to -// DispatcherEvent first. cpuRegs.pc == dispatch_pc is already guaranteed at -// this tail (recEmitWritePc / branch codegen), so the unlinked form -// (B -> DispatcherReg, which dispatches from cpuRegs.pc) is exactly the old -// behavior, and the linked form jumps straight to that same block. -// * No stale jumps: eeInvalidateLinks (from recClear, which every SMC path -// funnels through) rewrites any inbound B back to DispatcherReg BEFORE the -// target's host code is recycled; recResetRaw drops every record when the -// whole cache is thrown away. -// * A not-yet-compiled target leaves the site at DispatcherReg (correct -// fallback); eePatchWaitingPredecessors wires it when the target compiles. -// -// Flip s_eeBlockLinkEnabled to false to fall back to pure LUT dispatch. -static bool s_eeBlockLinkEnabled = true; - -struct EEBlockLinkExit -{ - u32 target_pc; // statically-known successor PC (== cpuRegs.pc at the tail) - u8* patch_site; // address of the unconditional B to rewrite - u8* fallthrough; // unlinked target (DispatcherReg) — used by unpatch - u8* current_target; // where patch_site currently points -}; - -struct EEBlockLinks -{ - u8* entry; // block's compiled entry — linked callers jump here - EEBlockLinkExit exits[2]; // mac emits at most one; keep the stock shape for safety - u32 num_exits; // 0 or 1 -}; - -// hwaddr(startpc) -> link record. recHWAddr folds RAM/BIOS mirrors so mirrored -// PCs collapse to one entry, matching the recLUT. -static std::unordered_map s_eeBlockLinks; -// target_hw -> predecessor hwaddrs waiting for that target to compile. -static std::unordered_map> s_eeWaitingForHw; - -// Exit staged by recEmitEventTestAndDispatch, consumed by the registration in -// recRecompile's tail. Reset at the top of each recRecompile. -static bool s_eeLinkStaged = false; -static u32 s_eeLinkTargetPc = 0; -static u8* s_eeLinkPatchSite = nullptr; - -static void eePatchLinkSite(EEBlockLinkExit& exit, u8* target) -{ - if (!exit.patch_site || exit.current_target == target) - return; - armEmitJmpPtr(exit.patch_site, target, true); - exit.current_target = target; -} - -static void eeUnpatchLinkSite(EEBlockLinkExit& exit) -{ - eePatchLinkSite(exit, exit.fallthrough); // back to DispatcherReg -} - -static u8* eeFindBlockEntry(u32 target_pc) -{ - auto it = s_eeBlockLinks.find(recHWAddr(target_pc)); - return (it == s_eeBlockLinks.end()) ? nullptr : it->second.entry; -} - -// Wire this block's exits to any targets already compiled. -static void eeTryForwardLink(EEBlockLinks& block) -{ - for (u32 e = 0; e < block.num_exits; e++) - { - if (u8* target_entry = eeFindBlockEntry(block.exits[e].target_pc)) - eePatchLinkSite(block.exits[e], target_entry); - } -} - -// Add this block to the reverse index for each unique exit target. -static void eeIndexBlockExits(u32 my_pc, const EEBlockLinks& bl) -{ - const u32 my_hw = recHWAddr(my_pc); - for (u32 e = 0; e < bl.num_exits; e++) - { - const u32 target_hw = recHWAddr(bl.exits[e].target_pc); - bool dup = false; - for (u32 j = 0; j < e; j++) - dup |= (recHWAddr(bl.exits[j].target_pc) == target_hw); - if (!dup) - s_eeWaitingForHw[target_hw].push_back(my_hw); - } -} - -// After a block compiles at my_pc/my_entry, patch any predecessor exits that -// were waiting for this target to jump straight here. -static void eePatchWaitingPredecessors(u32 my_pc, u8* my_entry) -{ - if (!my_entry) - return; - const u32 my_hw = recHWAddr(my_pc); - auto wit = s_eeWaitingForHw.find(my_hw); - if (wit == s_eeWaitingForHw.end()) - return; - for (u32 pred_hw : wit->second) - { - auto bit = s_eeBlockLinks.find(pred_hw); - if (bit == s_eeBlockLinks.end()) - continue; // stale — pred was invalidated - EEBlockLinks& pred = bit->second; - for (u32 e = 0; e < pred.num_exits; e++) - { - EEBlockLinkExit& exit = pred.exits[e]; - if (recHWAddr(exit.target_pc) == my_hw && exit.current_target != my_entry) - eePatchLinkSite(exit, my_entry); - } - } -} - -// Unpatch every inbound link whose target is in [start_hw, end_hw), then drop -// records for blocks whose own start is in that range. Called from recClear on -// EVERY SMC invalidation, so its cost is on the hot path during code-streaming -// loads (BF2 online MP.BIN, OPL/DEV9hdd — #283 / #272). -// -// The straight two-pass scan below is O(total compiled blocks) per call. During a -// disc-stream that overwrites EE code in thousands of small spans it becomes an -// O(N*M) host-cycle storm — the sole reason a chained build loads such titles ~4x -// slower in WALL-CLOCK than an unchained one (identical emulated work either way). -// The fast path walks only the CLEARED span instead: inbound links are found via -// the s_eeWaitingForHw reverse index (target_hw -> predecessor hwaddrs, already -// maintained by eeIndexBlockExits), and records are erased by direct key lookup. -// That is O(span/4 + inbound links) — bounded by the write, not by the cache size. -// When the span is wider than the whole block table (rare page/TLB resets) the flat -// scan is cheaper, so keep it as the fallback. recHWAddr is linear over the span -// (asserted by the caller), so word-stepped keys hit every block-start / target hw. -static void eeInvalidateLinksScan(u32 start_hw, u32 end_hw) -{ - for (auto& kv : s_eeBlockLinks) - { - EEBlockLinks& pred = kv.second; - for (u32 e = 0; e < pred.num_exits; e++) - { - const u32 t = recHWAddr(pred.exits[e].target_pc); - if (t >= start_hw && t < end_hw) - eeUnpatchLinkSite(pred.exits[e]); - } - } - for (auto it = s_eeBlockLinks.begin(); it != s_eeBlockLinks.end();) - { - if (it->first >= start_hw && it->first < end_hw) - it = s_eeBlockLinks.erase(it); - else - ++it; - } -} - -static void eeInvalidateLinks(u32 start_hw, u32 end_hw) -{ - const u32 span_words = (end_hw - start_hw) >> 2; - if (span_words > s_eeBlockLinks.size()) - { - eeInvalidateLinksScan(start_hw, end_hw); // cleared span wider than the table - return; - } - for (u32 hw = start_hw; hw < end_hw; hw += 4) - { - // Unpatch inbound direct-B links whose target == hw, via the reverse index. - auto wit = s_eeWaitingForHw.find(hw); - if (wit != s_eeWaitingForHw.end()) - { - for (u32 pred_hw : wit->second) - { - auto bit = s_eeBlockLinks.find(pred_hw); - if (bit == s_eeBlockLinks.end()) - continue; // stale index entry — predecessor already gone - EEBlockLinks& pred = bit->second; - for (u32 e = 0; e < pred.num_exits; e++) - { - if (recHWAddr(pred.exits[e].target_pc) == hw) - eeUnpatchLinkSite(pred.exits[e]); - } - } - } - // Drop the record whose own start == hw. - auto bit = s_eeBlockLinks.find(hw); - if (bit != s_eeBlockLinks.end()) - s_eeBlockLinks.erase(bit); - } -} - -// Drop all link state (full cache reset — every block is thrown away). -static void eeResetBlockLinks() -{ - s_eeBlockLinks.clear(); - s_eeWaitingForHw.clear(); - s_eeLinkStaged = false; -} - -// Emit a single patchable B for a statically-known block exit (initially -> -// DispatcherReg) and stage it for registration. Exactly one 4-byte B so the -// site is stably patchable. Reached only when no event is due, with -// cpuRegs.pc == pc — so the unlinked DispatcherReg dispatch hits the same block. -static void recEmitLinkableExitToKnownPc(u32 pc) -{ - u8* patch_site; - { - // Capture the site INSIDE the scope: its ctor flushes any pending pool first, - // so patch_site points exactly at the single B (never at a flushed pool). - a64::SingleEmissionCheckScope guard(armAsm); - patch_site = armGetCurrentCodePointer(); - const s64 disp = static_cast( - reinterpret_cast(DispatcherReg) - reinterpret_cast(patch_site)); - pxAssert((disp & 3) == 0 && vixl::IsInt26(disp >> 2)); - armAsm->b(static_cast(disp >> 2)); - } - s_eeLinkStaged = true; - s_eeLinkTargetPc = pc; - s_eeLinkPatchSite = patch_site; -} - -// Register a freshly-compiled block (entry + any staged exit) and resolve -// forward/backward links. Called from recRecompile after the block installs. -static void recRegisterBlockLinks(u32 startpc, u8* block_entry) -{ - EEBlockLinks bl{}; - bl.entry = block_entry; - bl.num_exits = 0; - if (s_eeLinkStaged) - { - EEBlockLinkExit& e = bl.exits[0]; - e.target_pc = s_eeLinkTargetPc; - e.patch_site = s_eeLinkPatchSite; - e.fallthrough = const_cast(static_cast(DispatcherReg)); - e.current_target = const_cast(static_cast(DispatcherReg)); - bl.num_exits = 1; - } - // Insert first, then index / forward-link / back-patch (mirrors stock order - // so a self-loop resolves against the just-inserted record). - EEBlockLinks& slot = (s_eeBlockLinks[recHWAddr(startpc)] = bl); - if (slot.num_exits) - eeIndexBlockExits(startpc, slot); - eeTryForwardLink(slot); - eePatchWaitingPredecessors(startpc, block_entry); -} - -// Self-modifying-code (SMC) manual protection, mirroring x86 iR5900.cpp. Both arrays are -// indexed by host RAM page (the protection granularity, __pageshift — 16 KB on Apple -// Silicon, 4 KB on x86), so they stay consistent with the vtlb's m_PageProtectInfo. See -// recEmitManualProtection for how these drive the three-tier Write/Manual/uncounted scheme -// that stops the recompile storm on pages that mix code and data (the FMV/IPU case). -alignas(16) static u16 manual_page[Ps2MemSize::TotalRam >> __pageshift]; -alignas(16) static u8 manual_counter[Ps2MemSize::TotalRam >> __pageshift]; - -// Execution / reset / exit plumbing, mirroring the x86 rec (iR5900.cpp). -static bool eeRecExecuting = false; -static bool eeRecNeedsReset = false; -static std::atomic_bool eeRecExitRequested{false}; -static volatile u8 eeRecExitSignal = 0; -static jmp_buf s_jmp_buf; -// Landing pad for Cpu->CancelInstruction() raised by an interpreter single-step -// (intExecuteOneInst) op — e.g. a MIPS trap (TGE/TNE/...) whose condition is met. -// Distinct from s_jmp_buf: s_jmp_buf EXITS recExecute, this one re-dispatches so EE -// execution continues. Mirrors the old arm64 backend's m_SetJmp_CancelInstruction -// and the interpreter's intCancelInstruction (Interpreter.cpp). -static jmp_buf s_cancel_jmp_buf; - -static void recResetRaw(); -static void recGenDispatchers(); -static void recRecompile(u32 startpc); -static void recEventTest(); -static void recCheckExitAfterInterp(); -static void recClear(u32 addr, u32 size); -static void dyna_block_discard(u32 start, u32 sz); -static void dyna_page_reset(u32 start, u32 sz); - -// Associate one 64 KB guest page `pagebase+pageidx` with the slot array `mapbase`, -// biased so recPtrToBlock(pc) lands at &mapbase[mappage<<14 + (pc&0xffff)/4]. Direct -// port of x86 recLUT_SetPage (BaseblockEx.h) minus the hwLUT side-table. -static void recLUT_SetPage(uptr* mapbase, uint pagebase, uint pageidx, uint mappage) -{ - const uint page = pagebase + pageidx; - pxAssert(page < 0x10000); - recLUT[page] = reinterpret_cast(&mapbase[(static_cast(mappage) - static_cast(page)) << 14]); -} - -// Allocate the per-word slot arrays and build the page table mapping every mapped -// guest page (and its mirrors) onto them. Mirrors x86 recReserveRAM. -static void recReserveLUT() -{ - recLutEntries = (Ps2MemSize::ExposedRam + Ps2MemSize::Rom + Ps2MemSize::Rom1 + Ps2MemSize::Rom2) / 4; - recLutReserve.assign(recLutEntries, 0); - recLutUnmapped.assign(_64kb / 4, 0); - - uptr* basepos = recLutReserve.data(); - recRAM = basepos; - basepos += (Ps2MemSize::ExposedRam / 4); - recROM = basepos; - basepos += (Ps2MemSize::Rom / 4); - recROM1 = basepos; - basepos += (Ps2MemSize::Rom1 / 4); - recROM2 = basepos; - basepos += (Ps2MemSize::Rom2 / 4); - - uptr* const unmapped = recLutUnmapped.data(); - for (int i = 0; i < 0x10000; i++) - recLUT_SetPage(unmapped, i, 0, 0); - - for (int i = 0x0000; i < static_cast(Ps2MemSize::ExposedRam / 0x10000); i++) - { - recLUT_SetPage(recRAM, 0x0000, i, i); - recLUT_SetPage(recRAM, 0x2000, i, i); - recLUT_SetPage(recRAM, 0x3000, i, i); - recLUT_SetPage(recRAM, 0x8000, i, i); - recLUT_SetPage(recRAM, 0xa000, i, i); - recLUT_SetPage(recRAM, 0xb000, i, i); - recLUT_SetPage(recRAM, 0xc000, i, i); - recLUT_SetPage(recRAM, 0xd000, i, i); - } - - for (int i = 0x1fc0; i < 0x2000; i++) - { - recLUT_SetPage(recROM, 0x0000, i, i - 0x1fc0); - recLUT_SetPage(recROM, 0x8000, i, i - 0x1fc0); - recLUT_SetPage(recROM, 0xa000, i, i - 0x1fc0); - } - - for (int i = 0x1e00; i < 0x1e40; i++) - { - recLUT_SetPage(recROM1, 0x0000, i, i - 0x1e00); - recLUT_SetPage(recROM1, 0x8000, i, i - 0x1e00); - recLUT_SetPage(recROM1, 0xa000, i, i - 0x1e00); - } - - for (int i = 0x1e40; i < 0x1e80; i++) - { - recLUT_SetPage(recROM2, 0x0000, i, i - 0x1e40); - recLUT_SetPage(recROM2, 0x8000, i, i - 0x1e40); - recLUT_SetPage(recROM2, 0xa000, i, i - 0x1e40); - } -} - -// Point every block slot at JITCompile (mapped words) / UnmappedRecLUTPage (unmapped -// pages) so the next jump to any guest PC compiles-on-demand or faults cleanly. -static void recClearLUT() -{ - for (uptr& slot : recLutReserve) - slot = reinterpret_cast(JITCompile); - for (uptr& slot : recLutUnmapped) - slot = reinterpret_cast(UnmappedRecLUTPage); -} - -static void recReserve() -{ - recPtr = SysMemory::GetEERec(); - recPtrEnd = SysMemory::GetEERecEnd() - EE_CONSTPOOL_SIZE; - - s_const_pool.Init(recPtrEnd, EE_CONSTPOOL_SIZE); - - recReserveLUT(); -} - -static void recShutdown() -{ - s_const_pool.Destroy(); - - recLutReserve.clear(); - recLutReserve.shrink_to_fit(); - recLutUnmapped.clear(); - recLutUnmapped.shrink_to_fit(); - recRAM = recROM = recROM1 = recROM2 = nullptr; - - recPtr = nullptr; - recPtrEnd = nullptr; -} - -// @@COP2MODE@@: COP2 interp-routing mode under FullVU0SyncHack (VU0<->EE handshake games, -// e.g. Ratchet: Deadlocked SCUS-97465 via GameDB). Default = mode 12, the on-device-validated -// maximum-native config: SPECIAL1 + ACC families (the hot VMULA/VMADDA matrix chains) + -// transfers + LQC2/SQC2 all NATIVE; only the rare SPECIAL2 residue (DIV/SQRT/RSQRT, VI -// load/stores, ABS/CLIP/MOVE/MR32, ITOF/FTOI, RNG) runs the inline interpreter. Any other -// combination of ≥2 of those groups native breaks the game's transforms (interaction bug, -// individual groups all test clean — see the vc117-vc124 bisect). /cop2mode.txt -// overrides for debugging (re-read at every rec reset; emit-time only, never the hot path). -static int s_cop2InterpMode = 12; -static bool s_cop2ModeLoaded = false; -static void recLoadCop2Mode() -{ - int m = 12; - const std::string path = EmuFolders::DataRoot + "/cop2mode.txt"; - if (FILE* f = fopen(path.c_str(), "r")) - { - if (fscanf(f, "%d", &m) != 1) - m = 12; - fclose(f); - Console.WriteLn("@@COP2MODE@@ override mode=%d (%s)", m, path.c_str()); - } - s_cop2InterpMode = m; - s_cop2ModeLoaded = true; -} - -static void recResetRaw() -{ - s_cop2ModeLoaded = false; // @@COP2MODE@@ re-read on every cache reset - // Rewind the emit cursor, drop all cached trampolines/literals, regenerate the - // dispatcher stubs at the head of the cache, then reset every block slot. Order - // matters: recGenDispatchers fills the JITCompile / UnmappedRecLUTPage pointers - // that recClearLUT writes into the slots. - recPtr = SysMemory::GetEERec(); - s_const_pool.Reset(); - recGenDispatchers(); - recClearLUT(); - eeRecExitSignal = 0; - - // Drop every block-chaining link — all host code is being discarded, so all - // patch sites vanish with it and every record must go. - eeResetBlockLinks(); - - // Same for the fastmem backpatch registry: it is keyed by HOST code address, and the - // rewound buffer reuses those addresses, so a stale LoadstoreBackpatchInfo would - // mis-backpatch a fresh access (wrong guest_pc/registers/size). @@MAC_FASTMEM_BACKPATCH@@ - vtlb_ClearLoadStoreInfo(); - - // Drop all SMC manual-protection state — every block is being thrown away, so the - // per-page counters/weights must start fresh (mirrors x86 lpReset in recResetRaw). - std::memset(manual_page, 0, sizeof(manual_page)); - std::memset(manual_counter, 0, sizeof(manual_counter)); - - eeRecNeedsReset = false; -} - -static void recResetEE() -{ - if (eeRecExecuting) - { - // Can't safely rewind the code cache out from under a running block; defer - // the reset and bail out to the dispatcher loop at the next safe point. - eeRecNeedsReset = true; - eeRecExitRequested.store(true, std::memory_order_release); - eeRecExitSignal = 1; - cpuRegs.nextEventCycle = 0; // force an event test promptly - return; - } - - recResetRaw(); -} - -static void recStep() -{ - // Debugger single-step. Recompilers fall back to the interpreter for this. -} - -// -------------------------------------------------------------------------------------- -// Single-instruction decode + dispatch (Phase 2.3) -// -------------------------------------------------------------------------------------- -// MIPS primary opcodes we can translate so far. Everything else falls back to a -// NOP placeholder for now (interpreter remains the active provider — see below). -// The unaligned variants (LWL/LWR/LDL/LDR, SWL/SWR/SDL/SDR) need byte-merge -// codegen and are deferred; scalar + quad aligned access is covered here. -enum : u32 -{ - OP_LQ = 0x1e, - OP_SQ = 0x1f, - OP_LB = 0x20, - OP_LH = 0x21, - OP_LW = 0x23, - OP_LBU = 0x24, - OP_LHU = 0x25, - OP_LWU = 0x27, - OP_SB = 0x28, - OP_SH = 0x29, - OP_SW = 0x2b, - OP_LD = 0x37, - OP_SD = 0x3f, - OP_LWC1 = 0x31, - OP_SWC1 = 0x39, - OP_LQC2 = 0x36, - OP_SQC2 = 0x3e, -}; - -// Defined below (block-compile helpers) — used by recTranslateOp's COP2 inline path. -static void recEmitInterpInline(u32 op); -static void recEmitWritePc(u32 pc); -static bool recTranslateOp(u32 op, u32 pc); - -// Macro-mode native COP2 transfer ops (defined after the M2 sync helpers) — used by -// recTranslateOp's COP2 dispatch. -static void recCFC2(); -static void recCTC2(); -static void recQMFC2(); -static void recQMTC2(); -static void recLQC2(); -static void recSQC2(); - -// Macro-mode native COP2 SPECIAL ALU emission (Phase 7.9 / M5). Defined in the aVU -// translation unit (aVU_Macro.inl) so they can reach the static microVU0 single-op -// emitters. recVUMacroIsMode0 classifies; recVUMacroEmitMode0 emits (true if a Mode-0 -// op was emitted). The EE rec owns the sync prologue + cycle accounting (the case 0x12 -// default below gates the FINISH + native emit on recVUMacroIsMode0, mVUFinishVU0). -bool recVUMacroIsMode0(u32 op); -bool recVUMacroEmitMode0(u32 op); -static void mVUFinishVU0(); -static bool recCop2ForceInterp(u32 op); // vc105: FullVU0SyncHack → COP2 on inline interp - -struct RecGprConstState -{ - bool known[32] = {}; - u64 value[32] = {}; - - RecGprConstState() - { - known[0] = true; - value[0] = 0; - } -}; - -static void recConstKillAll(RecGprConstState& state) -{ - state = RecGprConstState(); -} - -static void recConstSetUnknown(RecGprConstState& state, u32 reg) -{ - if (reg == 0) - return; - - state.known[reg] = false; - state.value[reg] = 0; -} - -static void recConstSetKnown(RecGprConstState& state, u32 reg, u64 value) -{ - if (reg == 0) - return; - - state.known[reg] = true; - state.value[reg] = value; -} - -static void recEmitStoreGprConst(u32 reg, u64 value) -{ - if (reg == 0) - return; - - armAsm->Mov(RSCRATCHADDR, value); - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(reg))); -} - -static void recConstEmitKnown(RecGprConstState& state, u32 reg, u64 value) -{ - recEmitStoreGprConst(reg, value); - recConstSetKnown(state, reg, value); -} - -static __fi u64 recSignExtend32(u32 value) -{ - return static_cast(static_cast(static_cast(value))); -} - -static bool recTryTranslateConstOp(u32 op, RecGprConstState& state) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 sa = (op >> 6) & 0x1f; - const u32 funct = op & 0x3f; - const s32 imm = static_cast(op); - const u32 imm_u = static_cast(op); - - auto src_known = [&](u32 reg) -> bool { - return state.known[reg]; - }; - auto src = [&](u32 reg) -> u64 { - return state.value[reg]; - }; - - switch (opcode) - { - case 0x08: // ADDI - case 0x09: // ADDIU - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, recSignExtend32(static_cast(src(rs)) + static_cast(imm))); - return true; - - case 0x18: // DADDI - case 0x19: // DADDIU - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, src(rs) + static_cast(static_cast(imm))); - return true; - - case 0x0A: // SLTI - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, (static_cast(src(rs)) < static_cast(imm)) ? 1 : 0); - return true; - - case 0x0B: // SLTIU - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, (src(rs) < static_cast(static_cast(imm))) ? 1 : 0); - return true; - - case 0x0C: // ANDI - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, src(rs) & imm_u); - return true; - - case 0x0D: // ORI - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, src(rs) | imm_u); - return true; - - case 0x0E: // XORI - if (!src_known(rs)) - return false; - recConstEmitKnown(state, rt, src(rs) ^ imm_u); - return true; - - case 0x0F: // LUI - recConstEmitKnown(state, rt, recSignExtend32(static_cast(imm_u) << 16)); - return true; - - case 0x00: - switch (funct) - { - case 0x00: // SLL - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rt)) << sa)); - return true; - case 0x02: // SRL - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rt)) >> sa)); - return true; - case 0x03: // SRA - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(static_cast(static_cast(src(rt))) >> sa))); - return true; - case 0x04: // SLLV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rt)) << (src(rs) & 0x1f))); - return true; - case 0x06: // SRLV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rt)) >> (src(rs) & 0x1f))); - return true; - case 0x07: // SRAV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(static_cast(static_cast(src(rt))) >> (src(rs) & 0x1f)))); - return true; - case 0x14: // DSLLV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, src(rt) << (src(rs) & 0x3f)); - return true; - case 0x16: // DSRLV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, src(rt) >> (src(rs) & 0x3f)); - return true; - case 0x17: // DSRAV - if (!src_known(rt) || !src_known(rs)) return false; - recConstEmitKnown(state, rd, static_cast(static_cast(src(rt)) >> (src(rs) & 0x3f))); - return true; - case 0x38: // DSLL - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rt) << sa); - return true; - case 0x3A: // DSRL - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rt) >> sa); - return true; - case 0x3B: // DSRA - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, static_cast(static_cast(src(rt)) >> sa)); - return true; - case 0x3C: // DSLL32 - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rt) << (sa + 32)); - return true; - case 0x3E: // DSRL32 - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rt) >> (sa + 32)); - return true; - case 0x3F: // DSRA32 - if (!src_known(rt)) return false; - recConstEmitKnown(state, rd, static_cast(static_cast(src(rt)) >> (sa + 32))); - return true; - - case 0x20: // ADD - case 0x21: // ADDU - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rs)) + static_cast(src(rt)))); - return true; - case 0x22: // SUB - case 0x23: // SUBU - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, recSignExtend32(static_cast(src(rs)) - static_cast(src(rt)))); - return true; - case 0x24: // AND - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rs) & src(rt)); - return true; - case 0x25: // OR - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rs) | src(rt)); - return true; - case 0x26: // XOR - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rs) ^ src(rt)); - return true; - case 0x27: // NOR - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, ~(src(rs) | src(rt))); - return true; - case 0x2A: // SLT - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, (static_cast(src(rs)) < static_cast(src(rt))) ? 1 : 0); - return true; - case 0x2B: // SLTU - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, (src(rs) < src(rt)) ? 1 : 0); - return true; - case 0x2C: // DADD - case 0x2D: // DADDU - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rs) + src(rt)); - return true; - case 0x2E: // DSUB - case 0x2F: // DSUBU - if (!src_known(rs) || !src_known(rt)) return false; - recConstEmitKnown(state, rd, src(rs) - src(rt)); - return true; - case 0x0A: // MOVZ - if (!src_known(rt)) return false; - if (src(rt) != 0) - return true; - if (!src_known(rs)) return false; - recConstEmitKnown(state, rd, src(rs)); - return true; - case 0x0B: // MOVN - if (!src_known(rt)) return false; - if (src(rt) == 0) - return true; - if (!src_known(rs)) return false; - recConstEmitKnown(state, rd, src(rs)); - return true; - default: - return false; - } - - default: - return false; - } -} - -static void recConstApplyNativeEffects(u32 op, RecGprConstState& state) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 funct = op & 0x3f; - - switch (opcode) - { - case 0x00: - switch (funct) - { - case 0x11: // MTHI - case 0x13: // MTLO - case 0x18: // MULT - case 0x19: // MULTU - case 0x1A: // DIV - case 0x1B: // DIVU - if (funct == 0x18 || funct == 0x19) - recConstSetUnknown(state, rd); - return; - default: - recConstSetUnknown(state, rd); - return; - } - - case 0x08: case 0x09: case 0x0A: case 0x0B: - case 0x0C: case 0x0D: case 0x0E: case 0x0F: - case 0x18: case 0x19: - recConstSetUnknown(state, rt); - return; - - case OP_LQ: case OP_LB: case OP_LH: case OP_LW: - case OP_LBU: case OP_LHU: case OP_LWU: case OP_LD: - case 0x22: case 0x26: case 0x1A: case 0x1B: // LWL/LWR/LDL/LDR merge into rt - recConstSetUnknown(state, rt); - return; - - case 0x11: // COP1: MFC1/CFC1 write rt, other native FPU ops do not touch GPRs. - if (rs == 0x00 || rs == 0x02) - recConstSetUnknown(state, rt); - return; - - case 0x10: // COP0 inline interpreter may touch CPU state. - case 0x12: // COP2 inline interpreter may move VU data through GPRs. - case OP_LQC2: - case OP_SQC2: - recConstKillAll(state); - return; - - case 0x1C: - recConstSetUnknown(state, rd); - return; - - default: - return; - } -} - -static bool recTranslateOpWithConst(u32 op, RecGprConstState& state) -{ - if (recTryTranslateConstOp(op, state)) - return true; - - if (!recTranslateOp(op, /*pc*/ 0)) // dead path (recTranslateOpWithConst has no callers) - { - recConstKillAll(state); - return false; - } - - recConstApplyNativeEffects(op, state); - return true; -} - -static bool recConstApplyCachedEffects(u32 op, RecGprConstState& state) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 sa = (op >> 6) & 0x1f; - const u32 funct = op & 0x3f; - const s32 imm = static_cast(op); - const u32 imm_u = static_cast(op); - - auto known = [&](u32 reg) -> bool { - return state.known[reg]; - }; - auto value = [&](u32 reg) -> u64 { - return state.value[reg]; - }; - auto set_known_or_unknown = [&](u32 reg, bool is_known, u64 val = 0) { - if (is_known) - recConstSetKnown(state, reg, val); - else - recConstSetUnknown(state, reg); - }; - - switch (opcode) - { - case 0x08: // ADDI - case 0x09: // ADDIU - set_known_or_unknown(rt, known(rs), recSignExtend32(static_cast(value(rs)) + static_cast(imm))); - return true; - - case 0x18: // DADDI - case 0x19: // DADDIU - set_known_or_unknown(rt, known(rs), value(rs) + static_cast(static_cast(imm))); - return true; - - case 0x0A: // SLTI - set_known_or_unknown(rt, known(rs), (static_cast(value(rs)) < static_cast(imm)) ? 1 : 0); - return true; - - case 0x0B: // SLTIU - set_known_or_unknown(rt, known(rs), (value(rs) < static_cast(static_cast(imm))) ? 1 : 0); - return true; - - case 0x0C: // ANDI - set_known_or_unknown(rt, known(rs), value(rs) & imm_u); - return true; - - case 0x0D: // ORI - set_known_or_unknown(rt, known(rs), value(rs) | imm_u); - return true; - - case 0x0E: // XORI - set_known_or_unknown(rt, known(rs), value(rs) ^ imm_u); - return true; - - case 0x0F: // LUI - recConstSetKnown(state, rt, recSignExtend32(static_cast(imm_u) << 16)); - return true; - - case 0x00: - switch (funct) - { - case 0x00: // SLL - set_known_or_unknown(rd, known(rt), recSignExtend32(static_cast(value(rt)) << sa)); - return true; - case 0x02: // SRL - set_known_or_unknown(rd, known(rt), recSignExtend32(static_cast(value(rt)) >> sa)); - return true; - case 0x03: // SRA - set_known_or_unknown(rd, known(rt), recSignExtend32(static_cast(static_cast(static_cast(value(rt))) >> sa))); - return true; - case 0x04: // SLLV - set_known_or_unknown(rd, known(rt) && known(rs), recSignExtend32(static_cast(value(rt)) << (value(rs) & 0x1f))); - return true; - case 0x06: // SRLV - set_known_or_unknown(rd, known(rt) && known(rs), recSignExtend32(static_cast(value(rt)) >> (value(rs) & 0x1f))); - return true; - case 0x07: // SRAV - set_known_or_unknown(rd, known(rt) && known(rs), recSignExtend32(static_cast(static_cast(static_cast(value(rt))) >> (value(rs) & 0x1f)))); - return true; - case 0x14: // DSLLV - set_known_or_unknown(rd, known(rt) && known(rs), value(rt) << (value(rs) & 0x3f)); - return true; - case 0x16: // DSRLV - set_known_or_unknown(rd, known(rt) && known(rs), value(rt) >> (value(rs) & 0x3f)); - return true; - case 0x17: // DSRAV - set_known_or_unknown(rd, known(rt) && known(rs), static_cast(static_cast(value(rt)) >> (value(rs) & 0x3f))); - return true; - case 0x38: // DSLL - set_known_or_unknown(rd, known(rt), value(rt) << sa); - return true; - case 0x3A: // DSRL - set_known_or_unknown(rd, known(rt), value(rt) >> sa); - return true; - case 0x3B: // DSRA - set_known_or_unknown(rd, known(rt), static_cast(static_cast(value(rt)) >> sa)); - return true; - case 0x3C: // DSLL32 - set_known_or_unknown(rd, known(rt), value(rt) << (sa + 32)); - return true; - case 0x3E: // DSRL32 - set_known_or_unknown(rd, known(rt), value(rt) >> (sa + 32)); - return true; - case 0x3F: // DSRA32 - set_known_or_unknown(rd, known(rt), static_cast(static_cast(value(rt)) >> (sa + 32))); - return true; - case 0x20: // ADD - case 0x21: // ADDU - set_known_or_unknown(rd, known(rs) && known(rt), recSignExtend32(static_cast(value(rs)) + static_cast(value(rt)))); - return true; - case 0x22: // SUB - case 0x23: // SUBU - set_known_or_unknown(rd, known(rs) && known(rt), recSignExtend32(static_cast(value(rs)) - static_cast(value(rt)))); - return true; - case 0x2C: // DADD - case 0x2D: // DADDU - set_known_or_unknown(rd, known(rs) && known(rt), value(rs) + value(rt)); - return true; - case 0x2E: // DSUB - case 0x2F: // DSUBU - set_known_or_unknown(rd, known(rs) && known(rt), value(rs) - value(rt)); - return true; - case 0x24: // AND - set_known_or_unknown(rd, known(rs) && known(rt), value(rs) & value(rt)); - return true; - case 0x25: // OR - set_known_or_unknown(rd, known(rs) && known(rt), value(rs) | value(rt)); - return true; - case 0x26: // XOR - set_known_or_unknown(rd, known(rs) && known(rt), value(rs) ^ value(rt)); - return true; - case 0x27: // NOR - set_known_or_unknown(rd, known(rs) && known(rt), ~(value(rs) | value(rt))); - return true; - case 0x2A: // SLT - set_known_or_unknown(rd, known(rs) && known(rt), (static_cast(value(rs)) < static_cast(value(rt))) ? 1 : 0); - return true; - case 0x2B: // SLTU - set_known_or_unknown(rd, known(rs) && known(rt), (value(rs) < value(rt)) ? 1 : 0); - return true; - case 0x0A: // MOVZ - if (rd == 0 || rs == rd) - return true; - if (!known(rt)) - recConstSetUnknown(state, rd); - else if (value(rt) == 0) - set_known_or_unknown(rd, known(rs), value(rs)); - return true; - case 0x0B: // MOVN - if (rd == 0 || rs == rd) - return true; - if (!known(rt)) - recConstSetUnknown(state, rd); - else if (value(rt) != 0) - set_known_or_unknown(rd, known(rs), value(rs)); - return true; - case 0x10: // MFHI - case 0x12: // MFLO - recConstSetUnknown(state, rd); - return true; - default: - return false; - } - - default: - return false; - } -} - -struct RecGprCacheEntry -{ - bool valid = false; - bool dirty = false; - u32 guest = 0; - u32 age = 0; -}; - -struct RecGprCacheState -{ - RecGprCacheEntry entries[7]; - u32 age = 1; -}; - -// AAPCS64 callee-saved registers dedicated to the guest-GPR cache. x19/x21 hold -// &cpuRegs / the vtlb vmap base; x28 is now pinned as RFASTMEMBASE (the host-MMU -// fastmem base — see recGenDispatchers, @@MAC_FASTMEM_BACKPATCH@@), so it was dropped -// from the cache, leaving 7 slots. NOTE: x23-x26 double as microVU flag regs mVU_F0-F3 -// (safe because the cache is killed before any COP2/VU0-macro emit); x27/x28 are outside -// the VU allocator's tracked range, which is why x28 is safe to pin. All of these survive -// the C helper calls a block makes (vtlb slow path, inline interpreter ops): the VU rec -// saves x19-x28 in its prologue, the IOP rec only touches x19 (saved), and the EE rec -// itself exits via longjmp which restores the full caller context. -static constexpr int REC_GPR_CACHE_REGS[7] = {20, 22, 23, 24, 25, 26, 27}; -static_assert(std::size(RecGprCacheState{}.entries) == std::size(REC_GPR_CACHE_REGS), - "guest-GPR cache entry count must match the register list"); - -static const a64::Register& recCacheReg(size_t index) -{ - return armXRegister(REC_GPR_CACHE_REGS[index]); -} - -static const a64::Register& recCacheWReg(size_t index) -{ - return armWRegister(REC_GPR_CACHE_REGS[index]); -} - -static void recCacheEmitFlushEntry(const RecGprCacheEntry& entry, size_t index) -{ - if (!entry.valid || !entry.dirty) - return; - - armAsm->Str(recCacheReg(index), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(entry.guest))); -} - -static int recCacheFind(const RecGprCacheState& cache, u32 guest) -{ - for (size_t i = 0; i < std::size(cache.entries); i++) - { - if (cache.entries[i].valid && cache.entries[i].guest == guest) - return static_cast(i); - } - - return -1; -} - -static void recCacheFlushEntry(RecGprCacheState& cache, size_t index) -{ - RecGprCacheEntry& entry = cache.entries[index]; - if (!entry.valid || !entry.dirty) - return; - - recCacheEmitFlushEntry(entry, index); - entry.dirty = false; -} - -// Drop a guest register from the cache without writing it back. Only correct when -// the instruction fully redefines the guest register in memory (e.g. LQ). -static void recCacheDiscardGuest(RecGprCacheState& cache, u32 guest) -{ - if (guest == 0) - return; - - const int found = recCacheFind(cache, guest); - if (found >= 0) - cache.entries[static_cast(found)] = RecGprCacheEntry(); -} - -// Write a single guest register back to cpuRegs if it is cached dirty. The entry -// stays valid (clean), so later ops can keep using the cached copy. -static void recCacheFlushGuest(RecGprCacheState& cache, u32 guest) -{ - if (guest == 0) - return; - - const int found = recCacheFind(cache, guest); - if (found >= 0) - recCacheFlushEntry(cache, static_cast(found)); -} - -static void recCacheFlushAll(RecGprCacheState& cache) -{ - for (size_t i = 0; i < std::size(cache.entries); i++) - recCacheFlushEntry(cache, i); -} - -static void recCacheEmitFlushAll(const RecGprCacheState& cache) -{ - for (size_t i = 0; i < std::size(cache.entries); i++) - recCacheEmitFlushEntry(cache.entries[i], i); -} - -static void recCacheKillAll(RecGprCacheState& cache) -{ - cache = RecGprCacheState(); -} - -static size_t recCacheAllocate(RecGprCacheState& cache, u32 guest, u32 pin_a = 0xff, u32 pin_b = 0xff) -{ - int found = recCacheFind(cache, guest); - if (found >= 0) - { - cache.entries[found].age = cache.age++; - return static_cast(found); - } - - size_t victim = std::size(cache.entries); - u32 oldest = UINT32_MAX; - for (size_t i = 0; i < std::size(cache.entries); i++) - { - const RecGprCacheEntry& entry = cache.entries[i]; - if (!entry.valid) - { - victim = i; - break; - } - if (entry.guest == pin_a || entry.guest == pin_b) - continue; - if (entry.age < oldest) - { - oldest = entry.age; - victim = i; - } - } - - if (victim == std::size(cache.entries)) - { - // All cache registers are pinned by this instruction. This should be rare, but - // flushing keeps the fallback path simple and correct. - recCacheFlushAll(cache); - recCacheKillAll(cache); - victim = 0; - } - else - { - recCacheFlushEntry(cache, victim); - } - - RecGprCacheEntry& entry = cache.entries[victim]; - entry.valid = true; - entry.dirty = false; - entry.guest = guest; - entry.age = cache.age++; - return victim; -} - -static const a64::Register& recCacheLoad(RecGprCacheState& cache, u32 guest) -{ - if (guest == 0) - return a64::xzr; - - int found = recCacheFind(cache, guest); - const bool already_cached = (found >= 0); - const size_t index = already_cached ? static_cast(found) : recCacheAllocate(cache, guest); - RecGprCacheEntry& entry = cache.entries[index]; - entry.age = cache.age++; - if (!already_cached) - armAsm->Ldr(recCacheReg(index), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(guest))); - - return recCacheReg(index); -} - -static const a64::Register& recCacheDest(RecGprCacheState& cache, u32 guest, u32 pin_a = 0xff, u32 pin_b = 0xff) -{ - if (guest == 0) - return a64::xzr; - - const size_t index = recCacheAllocate(cache, guest, pin_a, pin_b); - cache.entries[index].dirty = true; - return recCacheReg(index); -} - -static void recEmitCachedEffectiveAddr(RecGprCacheState& cache, const RecGprConstState& const_state, - u32 rs, s32 imm, const a64::Register& addr) -{ - if (rs == 0) - { - armAsm->Mov(addr.W(), imm); - return; - } - - // Const-propagated address: GPR[rs] is a tracked compile-time constant (LUI/ORI - // pairs, hardware register bases, ...), so the whole effective address collapses - // to one immediate move instead of a cache load + add. - if (const_state.known[rs]) - { - const u32 ea = static_cast(const_state.value[rs]) + static_cast(imm); - armAsm->Mov(addr.W(), ea); - return; - } - - const a64::Register& src = recCacheLoad(cache, rs); - if (!addr.W().Is(src.W())) - armAsm->Mov(addr.W(), src.W()); - if (imm != 0) - armAsm->Add(addr.W(), addr.W(), imm); -} - -static void recEmitVmapHostPointer(const a64::Register& host, const a64::Register& addr, a64::Label* slow_path) -{ - static_assert(sizeof(vtlb_private::VTLBVirtual) == sizeof(uptr), "VTLBVirtual is expected to be a raw pointer-sized entry"); - - armAsm->Lsr(a64::w11, addr.W(), vtlb_private::VTLB_PAGE_BITS); - armAsm->Ldr(host, a64::MemOperand(REVTLBPTR, a64::x11, a64::LSL, 3)); - armAsm->Add(host, host, addr.X()); - armAsm->Tbnz(host, sizeof(uptr) * 8 - 1, slow_path); -} - -static void recEmitCachedDirectLoad(u32 bits, bool sign, const a64::Register& dst, const a64::Register& host) -{ - switch (bits) - { - case 8: - sign ? armAsm->Ldrsb(dst.X(), a64::MemOperand(host)) : armAsm->Ldrb(dst.W(), a64::MemOperand(host)); - break; - case 16: - sign ? armAsm->Ldrsh(dst.X(), a64::MemOperand(host)) : armAsm->Ldrh(dst.W(), a64::MemOperand(host)); - break; - case 32: - sign ? armAsm->Ldrsw(dst.X(), a64::MemOperand(host)) : armAsm->Ldr(dst.W(), a64::MemOperand(host)); - break; - case 64: - armAsm->Ldr(dst.X(), a64::MemOperand(host)); - break; - jNO_DEFAULT - } -} - -static void recEmitCachedDirectStore(u32 bits, const a64::Register& src, const a64::Register& host) -{ - switch (bits) - { - case 8: - armAsm->Strb(src.W(), a64::MemOperand(host)); - break; - case 16: - armAsm->Strh(src.W(), a64::MemOperand(host)); - break; - case 32: - armAsm->Str(src.W(), a64::MemOperand(host)); - break; - case 64: - armAsm->Str(src.X(), a64::MemOperand(host)); - break; - jNO_DEFAULT - } -} - -// Host-MMU fastmem backpatch toggle (@@MAC_FASTMEM_BACKPATCH@@). When on, EE integer -// load/store emit a single Ldr/Str through RFASTMEMBASE (x28); a fault backpatches to the -// slow path via vtlb_DynBackpatchLoadStore (RecStubs.cpp). Flip off = inline-vmap fallback. -static bool s_eeFastmemBackpatch = true; - -static bool recUseBackpatchFastmem(u32 pc) -{ - // Skip PCs that already faulted once (settled MMIO): re-emit the vmap path so we don't - // re-backpatch the same instruction on every recompile. - return s_eeFastmemBackpatch && CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); -} - -// Record a single fastmem Ldr/Str for SIGSEGV backpatch. code_start must point at exactly -// one 4-byte access instruction (the whole premise of host-MMU backpatch). -static void recRecordFastmem(const u8* code_start, u32 pc, u8 addr_reg, u8 data_reg, - u32 bits, bool is_signed, bool is_load) -{ - const u32 code_size = static_cast(armGetCurrentCodePointer() - code_start); - pxAssert(code_size == 4); - vtlb_AddLoadStoreInfo(reinterpret_cast(code_start), code_size, pc, - /*gpr_bitmask*/ 0, /*fpr_bitmask*/ 0, addr_reg, data_reg, - static_cast(bits), is_signed, is_load, /*is_fpr*/ false); -} - -// Exposed for aR5900FPU.cpp (LWC1/SWC1 live in a separate translation unit): emit a single- -// instruction backpatch fastmem 32-bit access when eligible. The 32-bit vaddr must already be -// in RXARG1 (x0, zero-extended). `data` is the value register (load: destination; store: -// source). Returns true if fastmem was emitted, so the caller then skips the vmap path. -// @@MAC_FASTMEM_BACKPATCH@@ -bool armTryEmitFastmemScalar32(u32 pc, bool is_load, const a64::Register& data) -{ - if (!recUseBackpatchFastmem(pc)) - return false; - const u8* code_start = armGetCurrentCodePointer(); - if (is_load) - armAsm->Ldr(data.W(), a64::MemOperand(RFASTMEMBASE, RXARG1)); - else - armAsm->Str(data.W(), a64::MemOperand(RFASTMEMBASE, RXARG1)); - recRecordFastmem(code_start, pc, RXARG1.GetCode(), data.GetCode(), 32, /*sign*/ false, is_load); - return true; -} - -static bool recTryTranslateCachedLoad(u32 bits, bool sign, u32 rt, u32 rs, s32 imm, - RecGprCacheState& cache, const RecGprConstState& const_state, u32 pc) -{ - static const a64::Register RADDR = a64::x9; - static const a64::Register RHOST = a64::x10; - static const a64::Register RTEMP = a64::x11; - - recEmitCachedEffectiveAddr(cache, const_state, rs, imm, RADDR); - const RecGprCacheState pre_load_cache = cache; - - const a64::Register& dst = (rt == 0) ? RTEMP : recCacheDest(cache, rt, rs); - - if (recUseBackpatchFastmem(pc)) - { - // Single register-offset load through the pinned fastmem base. A handler/MMIO/unmapped - // page faults -> HandlePageFault -> vtlb_BackpatchLoadStore -> the thunk. No slow branch, - // no cache flush: the fast path is the common one. dst/RADDR high bits are already clean - // (RADDR = zero-extended 32-bit vaddr), so [x28 + vaddr] lands inside the 4 GB window. - const u8* code_start = armGetCurrentCodePointer(); - switch (bits) - { - case 8: sign ? armAsm->Ldrsb(dst.X(), a64::MemOperand(RFASTMEMBASE, RADDR)) - : armAsm->Ldrb(dst.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 16: sign ? armAsm->Ldrsh(dst.X(), a64::MemOperand(RFASTMEMBASE, RADDR)) - : armAsm->Ldrh(dst.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 32: sign ? armAsm->Ldrsw(dst.X(), a64::MemOperand(RFASTMEMBASE, RADDR)) - : armAsm->Ldr(dst.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 64: armAsm->Ldr(dst.X(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - } - recRecordFastmem(code_start, pc, RADDR.GetCode(), dst.GetCode(), bits, sign, /*is_load*/ true); - return true; - } - - a64::Label slow_path; - a64::Label done; - recEmitVmapHostPointer(RHOST, RADDR, &slow_path); - recEmitCachedDirectLoad(bits, sign, dst, RHOST); - armAsm->B(&done); - - armAsm->Bind(&slow_path); - recCacheEmitFlushAll(pre_load_cache); - armEmitVtlbRead(bits, sign, RXRET, RADDR); - if (rt != 0 && !dst.Is(RXRET)) - armAsm->Mov(dst, RXRET); - - armAsm->Bind(&done); - return true; -} - -static bool recTryTranslateCachedStore(u32 bits, u32 rt, u32 rs, s32 imm, - RecGprCacheState& cache, const RecGprConstState& const_state, u32 pc) -{ - static const a64::Register RADDR = a64::x9; - static const a64::Register RHOST = a64::x10; - - recEmitCachedEffectiveAddr(cache, const_state, rs, imm, RADDR); - const a64::Register& src = recCacheLoad(cache, rt); - const RecGprCacheState pre_store_cache = cache; - - if (recUseBackpatchFastmem(pc)) - { - // Single register-offset store through the pinned fastmem base. A store into a - // write-protected code page faults through HandlePageFault's ProtMode_Write branch - // (mmap_ClearCpuBlock + retry), NOT the backpatch path — SMC stays correct. - const u8* code_start = armGetCurrentCodePointer(); - switch (bits) - { - case 8: armAsm->Strb(src.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 16: armAsm->Strh(src.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 32: armAsm->Str(src.W(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - case 64: armAsm->Str(src.X(), a64::MemOperand(RFASTMEMBASE, RADDR)); break; - } - recRecordFastmem(code_start, pc, RADDR.GetCode(), src.GetCode(), bits, /*is_signed*/ false, /*is_load*/ false); - return true; - } - - a64::Label slow_path; - a64::Label done; - recEmitVmapHostPointer(RHOST, RADDR, &slow_path); - recEmitCachedDirectStore(bits, src, RHOST); - armAsm->B(&done); - - armAsm->Bind(&slow_path); - recCacheEmitFlushAll(pre_store_cache); - armEmitVtlbWrite(bits, RADDR, src); - - armAsm->Bind(&done); - return true; -} - -static bool recTryTranslateCachedLoadQuad(u32 rt, u32 rs, s32 imm, - RecGprCacheState& cache, const RecGprConstState& const_state, u32 pc) -{ - static const a64::Register RADDR = a64::x9; - static const a64::Register RHOST = a64::x10; - - // Effective address, forced 16-byte aligned (the EE silently aligns 128-bit - // accesses; matches the x86 recLQ `xAND(arg1regd, ~0x0F)` and armEmitLoadQuad). - recEmitCachedEffectiveAddr(cache, const_state, rs, imm, RADDR); - armAsm->And(RADDR.W(), RADDR.W(), ~0x0F); - - // Snapshot taken before the rt discard below on purpose: if the slow-path read - // hits a TLB miss the handler longjmps out of the block, so at the call site - // every guest register — including rt's old dirty low half — must already be - // flushed to cpuRegs. - const RecGprCacheState pre_load_cache = cache; - - // LQ overwrites the full 128-bit destination, but the scalar GPR cache only - // tracks the low 64 bits. Discard any cached low half so a stale dirty entry - // can't be flushed over the freshly loaded quad later. Done after the address - // computation so rt==rs still uses the pre-load value above. - recCacheDiscardGuest(cache, rt); - - if (recUseBackpatchFastmem(pc)) - { - // Single 128-bit register-offset load through the fastmem base; a fault backpatches to - // the thunk (size 128 -> vtlb_memRead128). Perform the read even when rt==0 (MMIO side - // effects). RADDR is the 16-byte-aligned zero-extended vaddr -> stays in the 4 GB window. - const u8* code_start = armGetCurrentCodePointer(); - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RFASTMEMBASE, RADDR)); - vtlb_AddLoadStoreInfo(reinterpret_cast(code_start), - static_cast(armGetCurrentCodePointer() - code_start), pc, - /*gpr*/ 0, /*fpr*/ 0, RADDR.GetCode(), RQSCRATCH.GetCode(), - /*size*/ 128, /*sign*/ false, /*is_load*/ true, /*is_fpr*/ false); - if (rt != 0) - armAsm->Str(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - return true; - } - - a64::Label slow_path; - a64::Label done; - recEmitVmapHostPointer(RHOST, RADDR, &slow_path); - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RHOST)); - if (rt != 0) - armAsm->Str(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->B(&done); - - armAsm->Bind(&slow_path); - recCacheEmitFlushAll(pre_load_cache); - // Perform the read even when rt==0 (the access can have I/O side effects). - armEmitVtlbReadQuad(RQSCRATCH, RADDR); - if (rt != 0) - armAsm->Str(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - - armAsm->Bind(&done); - return true; -} - -static bool recTryTranslateCachedStoreQuad(u32 rt, u32 rs, s32 imm, - RecGprCacheState& cache, const RecGprConstState& const_state, u32 pc) -{ - static const a64::Register RADDR = a64::x9; - static const a64::Register RHOST = a64::x10; - - recEmitCachedEffectiveAddr(cache, const_state, rs, imm, RADDR); - armAsm->And(RADDR.W(), RADDR.W(), ~0x0F); - - // SQ reads the whole 128-bit GPR from cpuRegs. If prior cached scalar ops - // dirtied the low half of rt, write it back first so the vector load sees a - // coherent register (rt==0 reads the always-zero GPR[0] slot, no special case). - recCacheFlushGuest(cache, rt); - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - const RecGprCacheState pre_store_cache = cache; - - if (recUseBackpatchFastmem(pc)) - { - // Single 128-bit store through the fastmem base. A store into a write-protected code - // page faults through HandlePageFault's ProtMode_Write branch (clear + retry), NOT the - // backpatch decoder — SMC stays correct (same as the scalar/vmap quad store). - const u8* code_start = armGetCurrentCodePointer(); - armAsm->Str(RQSCRATCH, a64::MemOperand(RFASTMEMBASE, RADDR)); - vtlb_AddLoadStoreInfo(reinterpret_cast(code_start), - static_cast(armGetCurrentCodePointer() - code_start), pc, - /*gpr*/ 0, /*fpr*/ 0, RADDR.GetCode(), RQSCRATCH.GetCode(), - /*size*/ 128, /*sign*/ false, /*is_load*/ false, /*is_fpr*/ false); - return true; - } - - a64::Label slow_path; - a64::Label done; - recEmitVmapHostPointer(RHOST, RADDR, &slow_path); - armAsm->Str(RQSCRATCH, a64::MemOperand(RHOST)); - armAsm->B(&done); - - armAsm->Bind(&slow_path); - recCacheEmitFlushAll(pre_store_cache); - armEmitVtlbWriteQuad(RADDR, RQSCRATCH); - - armAsm->Bind(&done); - return true; -} - -// Constant folding into the register cache: when every source operand of an ALU op -// is const-known, compute the result at compile time and emit a single immediate Mov -// into the destination's cache register (dirty — flushed on demand like any cached -// write). The folding formulas below are kept textually identical to the tracking -// formulas in recConstApplyCachedEffects so the emitted value and the const state can -// never diverge. Runs before recTryTranslateCachedOp in recTranslateOpOptimized; -// returns false to fall through when any needed source is unknown. -static bool recTryTranslateCachedConstOp(u32 op, RecGprConstState& const_state, RecGprCacheState& cache) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 sa = (op >> 6) & 0x1f; - const u32 funct = op & 0x3f; - const s32 imm = static_cast(op); - const u32 imm_u = static_cast(op); - - auto known = [&](u32 reg) -> bool { - return const_state.known[reg]; - }; - auto value = [&](u32 reg) -> u64 { - return const_state.value[reg]; - }; - auto emit_known = [&](u32 reg, u64 val) -> bool { - if (reg != 0) - { - const a64::Register& dst = recCacheDest(cache, reg); - armAsm->Mov(dst, val); - } - recConstSetKnown(const_state, reg, val); - return true; - }; - - switch (opcode) - { - case 0x08: // ADDI - case 0x09: // ADDIU - if (!known(rs)) - return false; - return emit_known(rt, recSignExtend32(static_cast(value(rs)) + static_cast(imm))); - - case 0x18: // DADDI - case 0x19: // DADDIU - if (!known(rs)) - return false; - return emit_known(rt, value(rs) + static_cast(static_cast(imm))); - - case 0x0A: // SLTI - if (!known(rs)) - return false; - return emit_known(rt, (static_cast(value(rs)) < static_cast(imm)) ? 1 : 0); - - case 0x0B: // SLTIU - if (!known(rs)) - return false; - return emit_known(rt, (value(rs) < static_cast(static_cast(imm))) ? 1 : 0); - - case 0x0C: // ANDI - if (!known(rs)) - return false; - return emit_known(rt, value(rs) & imm_u); - - case 0x0D: // ORI - if (!known(rs)) - return false; - return emit_known(rt, value(rs) | imm_u); - - case 0x0E: // XORI - if (!known(rs)) - return false; - return emit_known(rt, value(rs) ^ imm_u); - - case 0x0F: // LUI - return emit_known(rt, recSignExtend32(static_cast(imm_u) << 16)); - - case 0x00: - break; - - default: - return false; - } - - switch (funct) - { - case 0x00: // SLL - if (!known(rt)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rt)) << sa)); - case 0x02: // SRL - if (!known(rt)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rt)) >> sa)); - case 0x03: // SRA - if (!known(rt)) return false; - return emit_known(rd, recSignExtend32(static_cast(static_cast(static_cast(value(rt))) >> sa))); - case 0x04: // SLLV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rt)) << (value(rs) & 0x1f))); - case 0x06: // SRLV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rt)) >> (value(rs) & 0x1f))); - case 0x07: // SRAV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, recSignExtend32(static_cast(static_cast(static_cast(value(rt))) >> (value(rs) & 0x1f)))); - case 0x14: // DSLLV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, value(rt) << (value(rs) & 0x3f)); - case 0x16: // DSRLV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, value(rt) >> (value(rs) & 0x3f)); - case 0x17: // DSRAV - if (!known(rt) || !known(rs)) return false; - return emit_known(rd, static_cast(static_cast(value(rt)) >> (value(rs) & 0x3f))); - case 0x38: // DSLL - if (!known(rt)) return false; - return emit_known(rd, value(rt) << sa); - case 0x3A: // DSRL - if (!known(rt)) return false; - return emit_known(rd, value(rt) >> sa); - case 0x3B: // DSRA - if (!known(rt)) return false; - return emit_known(rd, static_cast(static_cast(value(rt)) >> sa)); - case 0x3C: // DSLL32 - if (!known(rt)) return false; - return emit_known(rd, value(rt) << (sa + 32)); - case 0x3E: // DSRL32 - if (!known(rt)) return false; - return emit_known(rd, value(rt) >> (sa + 32)); - case 0x3F: // DSRA32 - if (!known(rt)) return false; - return emit_known(rd, static_cast(static_cast(value(rt)) >> (sa + 32))); - - case 0x20: // ADD - case 0x21: // ADDU - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rs)) + static_cast(value(rt)))); - case 0x22: // SUB - case 0x23: // SUBU - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, recSignExtend32(static_cast(value(rs)) - static_cast(value(rt)))); - case 0x2C: // DADD - case 0x2D: // DADDU - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, value(rs) + value(rt)); - case 0x2E: // DSUB - case 0x2F: // DSUBU - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, value(rs) - value(rt)); - case 0x24: // AND - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, value(rs) & value(rt)); - case 0x25: // OR - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, value(rs) | value(rt)); - case 0x26: // XOR - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, value(rs) ^ value(rt)); - case 0x27: // NOR - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, ~(value(rs) | value(rt))); - case 0x2A: // SLT - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, (static_cast(value(rs)) < static_cast(value(rt))) ? 1 : 0); - case 0x2B: // SLTU - if (!known(rs) || !known(rt)) return false; - return emit_known(rd, (value(rs) < value(rt)) ? 1 : 0); - - case 0x0A: // MOVZ - if (!known(rt)) - return false; - if (value(rt) != 0) // condition false at compile time -> architectural no-op - return true; - if (!known(rs)) - return false; - return emit_known(rd, value(rs)); - case 0x0B: // MOVN - if (!known(rt)) - return false; - if (value(rt) == 0) // condition false at compile time -> architectural no-op - return true; - if (!known(rs)) - return false; - return emit_known(rd, value(rs)); - - default: - return false; - } -} - -// --- @@MAC_EE_CONSTFOLD@@ Mixed-operand constant folding (task #121) ------------------- -// When exactly one source of a reg-reg ALU op is a compile-time-known constant (the other -// runtime), fold that constant into an ARM immediate instead of loading it from guest -// memory into a cache slot. This reuses the SAME const value that recConstEmitKnown already -// stored to memory and that recTryTranslateCachedConstOp already trusts (it Movs folded -// constants straight into dest cache regs) — so it adds no new correctness trust, only -// better instruction selection. Both the fully-const case (handled earlier by -// recTryTranslateCachedConstOp) and this mixed case leave the const tracker to -// recConstApplyCachedEffects, which marks the runtime destination unknown exactly as the -// plain reg-reg path would. The fold is taken ONLY when the immediate encodes as a single -// add/sub or logical instruction (IsImmAddSub / IsImmLogical), so it is never worse than -// the memory load it replaces. Flip to false to fall back to the plain reg-reg emitters. -static bool s_eeGprMixedConstFold = true; - -// True iff `addend` (mod 2^width) can be added to a register with a single add/sub -// immediate — either directly (ADD) or via its two's complement (SUB). Pure test, emits -// nothing, so encodability can be decided before any cache load/dest allocation (avoids -// the load-after-dest aliasing hazard). -static __fi bool recAddImmEncodableW(u32 addend) -{ - return addend == 0 || a64::Assembler::IsImmAddSub(static_cast(addend)) || - a64::Assembler::IsImmAddSub(static_cast(static_cast(0u - addend))); -} -static __fi bool recAddImmEncodableX(u64 addend) -{ - return addend == 0 || a64::Assembler::IsImmAddSub(static_cast(addend)) || - a64::Assembler::IsImmAddSub(static_cast(0ull - addend)); -} - -// Emit dst.W = src.W + addend (mod 2^32) as a single add/sub immediate. Precondition: -// recAddImmEncodableW(addend) is true, so exactly one instruction is emitted. -static __fi void recEmitAddImmW(const a64::Register& dst, const a64::Register& src, u32 addend) -{ - if (addend == 0) - { - if (!dst.W().Is(src.W())) - armAsm->Mov(dst.W(), src.W()); - return; - } - if (a64::Assembler::IsImmAddSub(static_cast(addend))) - armAsm->Add(dst.W(), src.W(), addend); - else - armAsm->Sub(dst.W(), src.W(), static_cast(0u - addend)); -} -// Emit dst.X = src.X + addend (mod 2^64) as a single add/sub immediate. Precondition: -// recAddImmEncodableX(addend) is true. -static __fi void recEmitAddImmX(const a64::Register& dst, const a64::Register& src, u64 addend) -{ - if (addend == 0) - { - if (!dst.X().Is(src.X())) - armAsm->Mov(dst.X(), src.X()); - return; - } - if (a64::Assembler::IsImmAddSub(static_cast(addend))) - armAsm->Add(dst.X(), src.X(), addend); - else - armAsm->Sub(dst.X(), src.X(), 0ull - addend); -} - -static bool recTryTranslateCachedOp(u32 op, RecGprCacheState& cache, const RecGprConstState& const_state, u32 pc) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 sa = (op >> 6) & 0x1f; - const u32 funct = op & 0x3f; - const s32 imm = static_cast(op); - const u32 imm_u = static_cast(op); - - auto move_x = [](const a64::Register& dst, const a64::Register& src) { - if (!dst.Is(src)) - armAsm->Mov(dst, src); - }; - auto move_w = [](const a64::Register& dst, const a64::Register& src) { - if (!dst.Is(src)) - armAsm->Mov(dst, src); - }; - - switch (opcode) - { - case 0x08: // ADDI - case 0x09: // ADDIU - { - if (rt == 0) - return true; - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rt, rs); - move_w(dst.W(), src.W()); - if (imm != 0) - armAsm->Add(dst.W(), dst.W(), imm); - armAsm->Sxtw(dst, dst.W()); - return true; - } - - case 0x18: // DADDI - case 0x19: // DADDIU - { - if (rt == 0) - return true; - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rt, rs); - move_x(dst, src); - if (imm != 0) - armAsm->Add(dst, dst, imm); - return true; - } - - case 0x0A: // SLTI - case 0x0B: // SLTIU - { - if (rt == 0) - return true; - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rt, rs); - armAsm->Cmp(src, imm); - armAsm->Cset(dst, opcode == 0x0A ? a64::lt : a64::lo); - return true; - } - - case 0x0C: // ANDI - case 0x0D: // ORI - case 0x0E: // XORI - { - if (rt == 0) - return true; - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rt, rs); - // The vixl MacroAssembler encodes these as single logical-immediate - // instructions when the mask is encodable (0xff, 0xffff, ... — the common - // cases) and only falls back to materializing into a scratch register - // otherwise, so this is never worse than the manual Mov+op pair. - if (opcode == 0x0C) - { - if (imm_u == 0) - armAsm->Mov(dst, 0); - else - armAsm->And(dst, src, imm_u); - } - else if (opcode == 0x0D) - { - if (imm_u == 0) - move_x(dst, src); - else - armAsm->Orr(dst, src, imm_u); - } - else - { - if (imm_u == 0) - move_x(dst, src); - else - armAsm->Eor(dst, src, imm_u); - } - return true; - } - - case 0x0F: // LUI - { - if (rt == 0) - return true; - const s32 val = static_cast(static_cast(imm_u) << 16); - const a64::Register& dst = recCacheDest(cache, rt); - if (val == 0) - armAsm->Mov(dst, 0); - else - { - armAsm->Mov(dst.W(), val); - armAsm->Sxtw(dst, dst.W()); - } - return true; - } - - case OP_LB: return recTryTranslateCachedLoad(8, true, rt, rs, imm, cache, const_state, pc); - case OP_LBU: return recTryTranslateCachedLoad(8, false, rt, rs, imm, cache, const_state, pc); - case OP_LH: return recTryTranslateCachedLoad(16, true, rt, rs, imm, cache, const_state, pc); - case OP_LHU: return recTryTranslateCachedLoad(16, false, rt, rs, imm, cache, const_state, pc); - case OP_LW: return recTryTranslateCachedLoad(32, true, rt, rs, imm, cache, const_state, pc); - case OP_LWU: return recTryTranslateCachedLoad(32, false, rt, rs, imm, cache, const_state, pc); - case OP_LD: return recTryTranslateCachedLoad(64, false, rt, rs, imm, cache, const_state, pc); - case OP_LQ: return recTryTranslateCachedLoadQuad(rt, rs, imm, cache, const_state, pc); - - case OP_SB: return recTryTranslateCachedStore(8, rt, rs, imm, cache, const_state, pc); - case OP_SH: return recTryTranslateCachedStore(16, rt, rs, imm, cache, const_state, pc); - case OP_SW: return recTryTranslateCachedStore(32, rt, rs, imm, cache, const_state, pc); - case OP_SD: return recTryTranslateCachedStore(64, rt, rs, imm, cache, const_state, pc); - case OP_SQ: return recTryTranslateCachedStoreQuad(rt, rs, imm, cache, const_state, pc); - - case 0x00: - break; - - default: - return false; - } - - switch (funct) - { - case 0x0A: // MOVZ - case 0x0B: // MOVN - { - if (rd == 0 || rs == rd) - return true; - - const a64::Register& cond = recCacheLoad(cache, rt); - armAsm->Cmp(cond, 0); - const a64::Register& old_dst = recCacheLoad(cache, rd); - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); - armAsm->Csel(dst, src, old_dst, funct == 0x0A ? a64::eq : a64::ne); - return true; - } - - case 0x10: // MFHI - case 0x12: // MFLO - { - if (rd == 0) - return true; - - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Ldr(dst, a64::MemOperand(RESTATEPTR, funct == 0x10 ? EE_HI_SCALAR_OFFSET : EE_LO_SCALAR_OFFSET)); - return true; - } - - // --- Fast-tier cache coverage (@@MAC_FASTTIER@@): keep the guest-GPR cache LIVE across - // the hot integer-math slow-tier ops instead of recCacheFlushAll'ing all 7 regs. HI/LO - // live in memory (not cache regs), so the already-cached MFHI/MFLO read these stores. - // Codegen is bit-identical to emitMult/emitDivS/emitDivU (aR5900MultDiv.cpp), minus the - // reference's redundant memory reloads of rs/rt (we source them from cache regs). --- - case 0x11: // MTHI - case 0x13: // MTLO - { - const a64::Register& src = recCacheLoad(cache, rs); - armAsm->Str(src, a64::MemOperand(RESTATEPTR, funct == 0x11 ? EE_HI_SCALAR_OFFSET : EE_LO_SCALAR_OFFSET)); - return true; - } - - case 0x18: // MULT - case 0x19: // MULTU - { - // 32x32->64. LO = sxt32(prod low), HI = sxt32(prod high) (sign-extended even for - // MULTU); if rd != 0, GPR[rd] = LO (R5900 3-operand form). Keep the product in x17 - // (RSCRATCHADDR) — it survives the recCacheDest below (which may run cache macros). - const bool mult_sign = (funct == 0x18); - const a64::Register& lhs = recCacheLoad(cache, rs); - const a64::Register& rhs = recCacheLoad(cache, rt); - if (mult_sign) - armAsm->Smull(RSCRATCHADDR, lhs.W(), rhs.W()); - else - armAsm->Umull(RSCRATCHADDR, lhs.W(), rhs.W()); - armAsm->Sxtw(RXVIXLSCRATCH, RSCRATCHADDR.W()); // LO = sxt32(low) - armAsm->Str(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO_SCALAR_OFFSET)); - if (rd != 0) - { - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); // pin rs/rt (sources) - armAsm->Sxtw(dst, RSCRATCHADDR.W()); // recompute LO from x17 - } - armAsm->Asr(RSCRATCHADDR, RSCRATCHADDR, 32); // HI = sxt32(high) - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_HI_SCALAR_OFFSET)); - return true; - } - - case 0x1A: // DIV - case 0x1B: // DIVU - { - // LO = rs/rt, HI = rs%rt (both sxt32). ARM S/UDIV reproduce the EE INT_MIN/-1 and - // div-by-zero quotient for free; only the div-by-zero LO needs a fixup. Writes no - // GPR cache reg (HI/LO memory only), so no recCacheDest / cache-effect needed. - const bool div_sign = (funct == 0x1A); - a64::Label div_done; - const a64::Register& num = recCacheLoad(cache, rs); // dividend - const a64::Register& den = recCacheLoad(cache, rt); // divisor - if (div_sign) - armAsm->Sdiv(RSCRATCHADDR.W(), num.W(), den.W()); - else - armAsm->Udiv(RSCRATCHADDR.W(), num.W(), den.W()); - armAsm->Mul(RXVIXLSCRATCH.W(), RSCRATCHADDR.W(), den.W()); // x16 = quotient*divisor - armAsm->Sxtw(RSCRATCHADDR, RSCRATCHADDR.W()); // LO = sxt(quotient) - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_LO_SCALAR_OFFSET)); - armAsm->Sub(RXVIXLSCRATCH.W(), num.W(), RXVIXLSCRATCH.W()); // remainder = num - q*den - armAsm->Sxtw(RXVIXLSCRATCH, RXVIXLSCRATCH.W()); // HI = sxt(remainder) - armAsm->Str(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_HI_SCALAR_OFFSET)); - armAsm->Cmp(den.W(), 0); // div-by-zero LO fixup - armAsm->B(a64::ne, &div_done); - if (div_sign) - { - armAsm->Cmp(num.W(), 0); - armAsm->Mov(RXVIXLSCRATCH.W(), 1); - armAsm->Csneg(RXVIXLSCRATCH.W(), RXVIXLSCRATCH.W(), RXVIXLSCRATCH.W(), a64::lt); // (num<0)?1:-1 - armAsm->Sxtw(RXVIXLSCRATCH, RXVIXLSCRATCH.W()); - } - else - { - armAsm->Mov(RXVIXLSCRATCH, 0xFFFFFFFFFFFFFFFFull); // LO = -1 - } - armAsm->Str(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO_SCALAR_OFFSET)); - armAsm->Bind(&div_done); - return true; - } - - case 0x00: // SLL - case 0x02: // SRL - case 0x03: // SRA - { - if (rd == 0) - return true; - if (rt == 0) - { - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Mov(dst, 0); - return true; - } - const a64::Register& src = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rt); - if (funct == 0x00) - armAsm->Lsl(dst.W(), src.W(), sa); - else if (funct == 0x02) - armAsm->Lsr(dst.W(), src.W(), sa); - else - armAsm->Asr(dst.W(), src.W(), sa); - armAsm->Sxtw(dst, dst.W()); - return true; - } - - case 0x04: // SLLV - case 0x06: // SRLV - case 0x07: // SRAV - { - if (rd == 0) - return true; - if (rt == 0) - { - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Mov(dst, 0); - return true; - } - const a64::Register& src = recCacheLoad(cache, rt); - if (rs == 0) - { - const a64::Register& dst = recCacheDest(cache, rd, rt); - move_w(dst.W(), src.W()); - armAsm->Sxtw(dst, dst.W()); - return true; - } - const a64::Register& sh = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rd, rt, rs); - if (funct == 0x04) - armAsm->Lsl(dst.W(), src.W(), sh.W()); - else if (funct == 0x06) - armAsm->Lsr(dst.W(), src.W(), sh.W()); - else - armAsm->Asr(dst.W(), src.W(), sh.W()); - armAsm->Sxtw(dst, dst.W()); - return true; - } - - case 0x14: // DSLLV - case 0x16: // DSRLV - case 0x17: // DSRAV - { - if (rd == 0) - return true; - if (rt == 0) - { - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Mov(dst, 0); - return true; - } - const a64::Register& src = recCacheLoad(cache, rt); - if (rs == 0) - { - const a64::Register& dst = recCacheDest(cache, rd, rt); - move_x(dst, src); - return true; - } - const a64::Register& sh = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rd, rt, rs); - if (funct == 0x14) - armAsm->Lsl(dst, src, sh); - else if (funct == 0x16) - armAsm->Lsr(dst, src, sh); - else - armAsm->Asr(dst, src, sh); - return true; - } - - case 0x38: // DSLL - case 0x3A: // DSRL - case 0x3B: // DSRA - case 0x3C: // DSLL32 - case 0x3E: // DSRL32 - case 0x3F: // DSRA32 - { - if (rd == 0) - return true; - const u32 shift = sa + ((funct == 0x3C || funct == 0x3E || funct == 0x3F) ? 32 : 0); - if (rt == 0) - { - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Mov(dst, 0); - return true; - } - const a64::Register& src = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rt); - if (shift == 0) - move_x(dst, src); - else if (funct == 0x38 || funct == 0x3C) - armAsm->Lsl(dst, src, shift); - else if (funct == 0x3A || funct == 0x3E) - armAsm->Lsr(dst, src, shift); - else - armAsm->Asr(dst, src, shift); - return true; - } - - case 0x20: // ADD - case 0x21: // ADDU - case 0x22: // SUB - case 0x23: // SUBU - { - if (rd == 0) - return true; - const bool is_add = (funct == 0x20 || funct == 0x21); - // @@MAC_EE_CONSTFOLD@@ Exactly one const source -> fold into an add/sub imm. - if (s_eeGprMixedConstFold) - { - // rt const (both ADD and SUB: dst = rs +/- c). Encode as rs + (add ? c : -c). - if (const_state.known[rt] && !const_state.known[rs]) - { - const u32 addend = is_add ? static_cast(const_state.value[rt]) - : static_cast(0u - static_cast(const_state.value[rt])); - if (recAddImmEncodableW(addend)) - { - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rd, rs); - recEmitAddImmW(dst, src, addend); - armAsm->Sxtw(dst, dst.W()); - return true; - } - } - // rs const, ADD only (commutative): dst = rt + c. - else if (is_add && const_state.known[rs] && !const_state.known[rt]) - { - const u32 addend = static_cast(const_state.value[rs]); - if (recAddImmEncodableW(addend)) - { - const a64::Register& src = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rt); - recEmitAddImmW(dst, src, addend); - armAsm->Sxtw(dst, dst.W()); - return true; - } - } - } - const a64::Register& lhs = recCacheLoad(cache, rs); - const a64::Register& rhs = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); - if (is_add) - armAsm->Add(dst.W(), lhs.W(), rhs.W()); - else - armAsm->Sub(dst.W(), lhs.W(), rhs.W()); - armAsm->Sxtw(dst, dst.W()); - return true; - } - - case 0x2C: // DADD - case 0x2D: // DADDU - case 0x2E: // DSUB - case 0x2F: // DSUBU - { - if (rd == 0) - return true; - const bool is_add = (funct == 0x2C || funct == 0x2D); - // @@MAC_EE_CONSTFOLD@@ Exactly one const source -> fold into a 64-bit add/sub imm. - if (s_eeGprMixedConstFold) - { - if (const_state.known[rt] && !const_state.known[rs]) - { - const u64 addend = is_add ? const_state.value[rt] : (0ull - const_state.value[rt]); - if (recAddImmEncodableX(addend)) - { - const a64::Register& src = recCacheLoad(cache, rs); - const a64::Register& dst = recCacheDest(cache, rd, rs); - recEmitAddImmX(dst, src, addend); - return true; - } - } - else if (is_add && const_state.known[rs] && !const_state.known[rt]) - { - const u64 addend = const_state.value[rs]; - if (recAddImmEncodableX(addend)) - { - const a64::Register& src = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rt); - recEmitAddImmX(dst, src, addend); - return true; - } - } - } - const a64::Register& lhs = recCacheLoad(cache, rs); - const a64::Register& rhs = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); - if (is_add) - armAsm->Add(dst, lhs, rhs); - else - armAsm->Sub(dst, lhs, rhs); - return true; - } - - case 0x24: // AND - case 0x25: // OR - case 0x26: // XOR - case 0x27: // NOR - { - if (rd == 0) - return true; - // @@MAC_EE_CONSTFOLD@@ Exactly one const source -> fold into a 64-bit logical imm - // (all four ops are commutative in their two register sources). - if (s_eeGprMixedConstFold) - { - u32 vreg = 0xff; // the runtime (non-const) source register - u64 c = 0; - if (const_state.known[rt] && !const_state.known[rs]) { vreg = rs; c = const_state.value[rt]; } - else if (const_state.known[rs] && !const_state.known[rt]) { vreg = rt; c = const_state.value[rs]; } - if (vreg != 0xff) - { - if (c == 0) - { - // c==0 identities (not encodable as logical immediates): AND->0, - // OR/XOR->src, NOR->~src. - if (funct == 0x24) - { - const a64::Register& dst = recCacheDest(cache, rd); - armAsm->Mov(dst, 0); - return true; - } - const a64::Register& src = recCacheLoad(cache, vreg); - const a64::Register& dst = recCacheDest(cache, rd, vreg); - if (funct == 0x27) - armAsm->Mvn(dst, src); - else if (!dst.Is(src)) - armAsm->Mov(dst, src); - return true; - } - if (a64::Assembler::IsImmLogical(c, 64)) - { - const a64::Register& src = recCacheLoad(cache, vreg); - const a64::Register& dst = recCacheDest(cache, rd, vreg); - if (funct == 0x24) - armAsm->And(dst, src, c); - else if (funct == 0x25) - armAsm->Orr(dst, src, c); - else if (funct == 0x26) - armAsm->Eor(dst, src, c); - else - { - armAsm->Orr(dst, src, c); - armAsm->Mvn(dst, dst); - } - return true; - } - } - } - const a64::Register& lhs = recCacheLoad(cache, rs); - const a64::Register& rhs = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); - if (funct == 0x24) - armAsm->And(dst, lhs, rhs); - else if (funct == 0x25) - armAsm->Orr(dst, lhs, rhs); - else if (funct == 0x26) - armAsm->Eor(dst, lhs, rhs); - else - { - armAsm->Orr(dst, lhs, rhs); - armAsm->Mvn(dst, dst); - } - return true; - } - - case 0x2A: // SLT - case 0x2B: // SLTU - { - if (rd == 0) - return true; - const a64::Register& lhs = recCacheLoad(cache, rs); - const a64::Register& rhs = recCacheLoad(cache, rt); - const a64::Register& dst = recCacheDest(cache, rd, rs, rt); - armAsm->Cmp(lhs, rhs); - armAsm->Cset(dst, funct == 0x2A ? a64::lt : a64::lo); - return true; - } - - default: - return false; - } -} - -// Cache-side mirror of recConstApplyNativeEffects: after a native (non-cached) -// generator ran, discard the cached copy of every GPR it wrote to memory, so the -// cache never holds a stale value. Ops whose inline-interpreter handler can touch -// arbitrary CPU state (COP0/COP2/LQC2/SQC2) kill the whole cache, exactly like the -// const tracker. Keeping this switch in lockstep with recConstApplyNativeEffects is -// the correctness contract for the precise-invalidation path below. -static void recCacheApplyNativeEffects(u32 op, RecGprCacheState& cache) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 funct = op & 0x3f; - - switch (opcode) - { - case 0x00: - switch (funct) - { - case 0x11: // MTHI - case 0x13: // MTLO - case 0x18: // MULT - case 0x19: // MULTU - case 0x1A: // DIV - case 0x1B: // DIVU - if (funct == 0x18 || funct == 0x19) - recCacheDiscardGuest(cache, rd); - return; - default: - recCacheDiscardGuest(cache, rd); - return; - } - - case 0x08: case 0x09: case 0x0A: case 0x0B: - case 0x0C: case 0x0D: case 0x0E: case 0x0F: - case 0x18: case 0x19: - recCacheDiscardGuest(cache, rt); - return; - - case OP_LQ: case OP_LB: case OP_LH: case OP_LW: - case OP_LBU: case OP_LHU: case OP_LWU: case OP_LD: - case 0x22: case 0x26: case 0x1A: case 0x1B: // LWL/LWR/LDL/LDR merge into rt - recCacheDiscardGuest(cache, rt); - return; - - case 0x11: // COP1: MFC1/CFC1 write rt, other native FPU ops do not touch GPRs. - if (rs == 0x00 || rs == 0x02) - recCacheDiscardGuest(cache, rt); - return; - - case 0x10: // COP0 inline interpreter may touch CPU state. - case 0x12: // COP2 inline interpreter may move VU data through GPRs. - case OP_LQC2: - case OP_SQC2: - recCacheKillAll(cache); - return; - - case 0x1C: - recCacheDiscardGuest(cache, rd); - return; - - default: - return; - } -} - -static bool recTranslateOpOptimized(u32 op, RecGprConstState& const_state, RecGprCacheState& cache, u32 pc) -{ - // Fold ops with fully const-known sources first: emits one immediate Mov into the - // destination's cache register and updates the const state itself, so neither the - // generic cached emitter nor the apply-effects pass runs for them. - if (recTryTranslateCachedConstOp(op, const_state, cache)) - return true; - - if (recTryTranslateCachedOp(op, cache, const_state, pc)) - { - if (!recConstApplyCachedEffects(op, const_state)) - recConstApplyNativeEffects(op, const_state); - return true; - } - - // Native generators (and the interpreter fallback) read and write guest GPRs - // directly through cpuRegs in memory: write every dirty cached value back first - // so they observe current state. Entries stay valid (clean), so subsequent - // cached ops keep their registers — the previous flush-AND-kill here threw the - // whole cache away around every MULT/DIV/MMI/COP1 op in mixed blocks. - recCacheFlushAll(cache); - - if (recTryTranslateConstOp(op, const_state)) - { - // The const store wrote the destination GPR to memory behind the cache's back. - recCacheApplyNativeEffects(op, cache); - return true; - } - - if (!recTranslateOp(op, pc)) - { - // Caller falls back to the inline interpreter, which can write any GPR. - recCacheKillAll(cache); - recConstKillAll(const_state); - return false; - } - - recCacheApplyNativeEffects(op, cache); - recConstApplyNativeEffects(op, const_state); - return true; -} - -// Translate a single guest instruction (cpuRegs.code) into the open block. Returns -// true if a real generator handled it, false if it fell through to a placeholder. -// Decodes the MIPS fields explicitly and hands them to the Phase 2.3 load/store -// generators (which read/write guest GPRs through RESTATEPTR and route memory -// access via the slow-path vtlb helpers). -// MMI sub-group decoders (Phase 5.4). The MMI0/1/2/3 classes carry their real -// opcode in the `sa` field (bits 10:6); each indexes a 32-entry table (see -// R5900OpcodeTables.cpp tbl_MMI0..3) — the case labels below mirror those tables -// exactly. Any sub-op without a native generator returns false and falls back to -// the interpreter (e.g. QFSRV, whose shift amount is the runtime SA register). -static bool recTranslateMMI0(u32 sa, u32 rd, u32 rs, u32 rt) -{ - switch (sa) - { - case 0x00: armEmitPADDW(rd, rs, rt); return true; - case 0x01: armEmitPSUBW(rd, rs, rt); return true; - case 0x02: armEmitPCGTW(rd, rs, rt); return true; - case 0x03: armEmitPMAXW(rd, rs, rt); return true; - case 0x04: armEmitPADDH(rd, rs, rt); return true; - case 0x05: armEmitPSUBH(rd, rs, rt); return true; - case 0x06: armEmitPCGTH(rd, rs, rt); return true; - case 0x07: armEmitPMAXH(rd, rs, rt); return true; - case 0x08: armEmitPADDB(rd, rs, rt); return true; - case 0x09: armEmitPSUBB(rd, rs, rt); return true; - case 0x0A: armEmitPCGTB(rd, rs, rt); return true; - case 0x10: armEmitPADDSW(rd, rs, rt); return true; - case 0x11: armEmitPSUBSW(rd, rs, rt); return true; - case 0x12: armEmitPEXTLW(rd, rs, rt); return true; - case 0x13: armEmitPPACW(rd, rs, rt); return true; - case 0x14: armEmitPADDSH(rd, rs, rt); return true; - case 0x15: armEmitPSUBSH(rd, rs, rt); return true; - case 0x16: armEmitPEXTLH(rd, rs, rt); return true; - case 0x17: armEmitPPACH(rd, rs, rt); return true; - case 0x18: armEmitPADDSB(rd, rs, rt); return true; - case 0x19: armEmitPSUBSB(rd, rs, rt); return true; - case 0x1A: armEmitPEXTLB(rd, rs, rt); return true; - case 0x1B: armEmitPPACB(rd, rs, rt); return true; - case 0x1E: armEmitPEXT5(rd, rt); return true; - case 0x1F: armEmitPPAC5(rd, rt); return true; - default: return false; - } -} - -static bool recTranslateMMI1(u32 sa, u32 rd, u32 rs, u32 rt) -{ - switch (sa) - { - case 0x01: armEmitPABSW(rd, rt); return true; - case 0x02: armEmitPCEQW(rd, rs, rt); return true; - case 0x03: armEmitPMINW(rd, rs, rt); return true; - case 0x04: armEmitPADSBH(rd, rs, rt); return true; - case 0x05: armEmitPABSH(rd, rt); return true; - case 0x06: armEmitPCEQH(rd, rs, rt); return true; - case 0x07: armEmitPMINH(rd, rs, rt); return true; - case 0x0A: armEmitPCEQB(rd, rs, rt); return true; - case 0x10: armEmitPADDUW(rd, rs, rt); return true; - case 0x11: armEmitPSUBUW(rd, rs, rt); return true; - case 0x12: armEmitPEXTUW(rd, rs, rt); return true; - case 0x14: armEmitPADDUH(rd, rs, rt); return true; - case 0x15: armEmitPSUBUH(rd, rs, rt); return true; - case 0x16: armEmitPEXTUH(rd, rs, rt); return true; - case 0x18: armEmitPADDUB(rd, rs, rt); return true; - case 0x19: armEmitPSUBUB(rd, rs, rt); return true; - case 0x1A: armEmitPEXTUB(rd, rs, rt); return true; - // 0x1B QFSRV: shift amount comes from the runtime SA register (cpuRegs.sa), - // not the instruction — left to the interpreter. - default: return false; - } -} - -static bool recTranslateMMI2(u32 sa, u32 rd, u32 rs, u32 rt) -{ - // Indices mirror R5900OpcodeTables.cpp tbl_MMI2[(op>>6)&0x1F]. - switch (sa) - { - case 0x00: armEmitPMADDW(rd, rs, rt); return true; - case 0x02: armEmitPSLLVW(rd, rs, rt); return true; - case 0x03: armEmitPSRLVW(rd, rs, rt); return true; - case 0x04: armEmitPMSUBW(rd, rs, rt); return true; - case 0x08: armEmitPMFHI(rd); return true; - case 0x09: armEmitPMFLO(rd); return true; - case 0x0A: armEmitPINTH(rd, rs, rt); return true; - case 0x0C: armEmitPMULTW(rd, rs, rt); return true; - case 0x0E: armEmitPCPYLD(rd, rs, rt); return true; - case 0x10: armEmitPMADDH(rd, rs, rt); return true; - case 0x11: armEmitPHMADH(rd, rs, rt); return true; - case 0x12: armEmitPAND(rd, rs, rt); return true; - case 0x13: armEmitPXOR(rd, rs, rt); return true; - case 0x14: armEmitPMSUBH(rd, rs, rt); return true; - case 0x15: armEmitPHMSBH(rd, rs, rt); return true; - case 0x1A: armEmitPEXEH(rd, rt); return true; - case 0x1B: armEmitPREVH(rd, rt); return true; - case 0x1C: armEmitPMULTH(rd, rs, rt); return true; - case 0x1E: armEmitPEXEW(rd, rt); return true; - case 0x1F: armEmitPROT3W(rd, rt); return true; - default: return false; - } -} - -static bool recTranslateMMI3(u32 sa, u32 rd, u32 rs, u32 rt) -{ - // Indices mirror R5900OpcodeTables.cpp tbl_MMI3[(op>>6)&0x1F]. - switch (sa) - { - case 0x00: armEmitPMADDUW(rd, rs, rt); return true; - case 0x03: armEmitPSRAVW(rd, rs, rt); return true; - case 0x08: armEmitPMTHI(rs); return true; - case 0x09: armEmitPMTLO(rs); return true; - case 0x0A: armEmitPINTEH(rd, rs, rt); return true; - case 0x0C: armEmitPMULTUW(rd, rs, rt); return true; - case 0x0E: armEmitPCPYUD(rd, rs, rt); return true; - case 0x12: armEmitPOR(rd, rs, rt); return true; - case 0x13: armEmitPNOR(rd, rs, rt); return true; - case 0x1A: armEmitPEXCH(rd, rt); return true; - case 0x1B: armEmitPCPYH(rd, rt); return true; - case 0x1E: armEmitPEXCW(rd, rt); return true; - default: return false; - } -} - -static bool recTranslateOp(u32 op, u32 pc) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 funct = op & 0x3f; - const s32 imm = static_cast(op); - - const u32 sa = (op >> 6) & 0x1f; - - switch (opcode) - { - // SPECIAL — R-type register-register ops (Phase 3.2 + 3.3) - case 0x00: - switch (funct) - { - // Shifts (Phase 3.3) — immediate - case 0x00: armEmitSLL(rd, rt, sa); return true; - case 0x02: armEmitSRL(rd, rt, sa); return true; - case 0x03: armEmitSRA(rd, rt, sa); return true; - // Shifts (Phase 3.3) — variable - case 0x04: armEmitSLLV(rd, rt, rs); return true; - case 0x06: armEmitSRLV(rd, rt, rs); return true; - case 0x07: armEmitSRAV(rd, rt, rs); return true; - // Arithmetic (Phase 3.2) - case 0x20: armEmitADD(rd, rs, rt); return true; - case 0x21: armEmitADDU(rd, rs, rt); return true; - case 0x22: armEmitSUB(rd, rs, rt); return true; - case 0x23: armEmitSUBU(rd, rs, rt); return true; - case 0x24: armEmitAND(rd, rs, rt); return true; - case 0x25: armEmitOR(rd, rs, rt); return true; - case 0x26: armEmitXOR(rd, rs, rt); return true; - case 0x27: armEmitNOR(rd, rs, rt); return true; - case 0x2A: armEmitSLT(rd, rs, rt); return true; - case 0x2B: armEmitSLTU(rd, rs, rt); return true; - case 0x2C: armEmitDADD(rd, rs, rt); return true; - case 0x2D: armEmitDADDU(rd, rs, rt); return true; - case 0x2E: armEmitDSUB(rd, rs, rt); return true; - case 0x2F: armEmitDSUBU(rd, rs, rt); return true; - // Shifts (Phase 3.3) — variable 64-bit - case 0x14: armEmitDSLLV(rd, rt, rs); return true; - case 0x16: armEmitDSRLV(rd, rt, rs); return true; - case 0x17: armEmitDSRAV(rd, rt, rs); return true; - // Shifts (Phase 3.3) — immediate 64-bit + DS*32 - case 0x38: armEmitDSLL(rd, rt, sa); return true; - case 0x3A: armEmitDSRL(rd, rt, sa); return true; - case 0x3B: armEmitDSRA(rd, rt, sa); return true; - case 0x3C: armEmitDSLL32(rd, rt, sa); return true; - case 0x3E: armEmitDSRL32(rd, rt, sa); return true; - case 0x3F: armEmitDSRA32(rd, rt, sa); return true; - // Moves (Phase 3.4) - case 0x0A: armEmitMOVZ(rd, rs, rt); return true; - case 0x0B: armEmitMOVN(rd, rs, rt); return true; - case 0x10: armEmitMFHI(rd); return true; - case 0x11: armEmitMTHI(rs); return true; - case 0x12: armEmitMFLO(rd); return true; - case 0x13: armEmitMTLO(rs); return true; - // Multiply/Divide (Phase 3.5) - case 0x18: armEmitMULT(rd, rs, rt); return true; - case 0x19: armEmitMULTU(rd, rs, rt); return true; - case 0x1A: armEmitDIV(rs, rt); return true; - case 0x1B: armEmitDIVU(rs, rt); return true; - // SYNC (funct 0x0f): a pipeline/memory barrier whose interpreter body - // is EMPTY in this emulator (no EE pipeline/cache modelled — see - // R5900OpcodeImpl SYNC()). Emit nothing instead of block-terminating - // and single-stepping it. By far the dominant EE fallback op in real - // games (The Getaway: ~59% of all single-stepped ops were SYNC). - case 0x0f: return true; - default: return false; - } - - // MMI — second-pipeline multiply/divide (Phase 3.5) + multiply-accumulate - // and the pipeline-1 HI/LO moves. Remaining MMI ops fall through to false. - case 0x1C: - switch (funct) - { - case 0x00: armEmitMADD(rd, rs, rt); return true; // MADD - case 0x01: armEmitMADDU(rd, rs, rt); return true; // MADDU - case 0x10: armEmitMFHI1(rd); return true; // MFHI1 - case 0x11: armEmitMTHI1(rs); return true; // MTHI1 - case 0x12: armEmitMFLO1(rd); return true; // MFLO1 - case 0x13: armEmitMTLO1(rs); return true; // MTLO1 - case 0x18: armEmitMULT1(rd, rs, rt); return true; - case 0x19: armEmitMULTU1(rd, rs, rt); return true; - case 0x1A: armEmitDIV1(rs, rt); return true; - case 0x1B: armEmitDIVU1(rs, rt); return true; - case 0x20: armEmitMADD1(rd, rs, rt); return true; // MADD1 - case 0x21: armEmitMADDU1(rd, rs, rt); return true; // MADDU1 - // Direct tbl_MMI entries (indexed by funct = op & 0x3F). - case 0x04: armEmitPLZCW(rd, rs); return true; - // MMI0/1/2/3 SIMD sub-groups (Phase 5.4); sub-op in `sa`. - case 0x08: return recTranslateMMI0(sa, rd, rs, rt); - case 0x28: return recTranslateMMI1(sa, rd, rs, rt); - case 0x09: return recTranslateMMI2(sa, rd, rs, rt); - case 0x29: return recTranslateMMI3(sa, rd, rs, rt); - // PMFHL variant is in `sa`; PMTHL is only defined for sa==0. - case 0x30: return armEmitPMFHL(rd, sa); - case 0x31: armEmitPMTHL(rs, sa); return true; - // Parallel shifts by immediate (Phase 5.4 continuation). - case 0x34: armEmitPSLLH(rd, rt, sa); return true; - case 0x36: armEmitPSRLH(rd, rt, sa); return true; - case 0x37: armEmitPSRAH(rd, rt, sa); return true; - case 0x3C: armEmitPSLLW(rd, rt, sa); return true; - case 0x3E: armEmitPSRLW(rd, rt, sa); return true; - case 0x3F: armEmitPSRAW(rd, rt, sa); return true; - default: return false; - } - - // COP1 (FPU). The sub-opcode is the rs field, S-format ops sub-decode on - // funct. Remaining float arithmetic / compares / BC1 branches return false - // and fall to the interpreter until they get native EE FPU semantics. - // Operand mapping per R5900OpcodeTables: ft=rt, fs=rd, fd=sa. - case 0x11: - switch (rs) - { - case 0x00: armEmitMFC1(rt, rd); return true; // MFC1 - case 0x02: armEmitCFC1(rt, rd); return true; // CFC1 - case 0x04: armEmitMTC1(rd, rt); return true; // MTC1 (fs=rd) - case 0x06: armEmitCTC1(rd, rt); return true; // CTC1 (fs=rd) - case 0x10: // COP1_S (single-precision) - switch (funct) - { - // Float arithmetic (Phase 5.2b): ft=rt, fs=rd, fd=sa. - case 0x00: armEmitADD_S(sa, rd, rt); return true; // ADD_S - case 0x01: armEmitSUB_S(sa, rd, rt); return true; // SUB_S - case 0x02: armEmitMUL_S(sa, rd, rt); return true; // MUL_S - case 0x03: armEmitDIV_S(sa, rd, rt); return true; // DIV_S - case 0x04: armEmitSQRT_S(sa, rt); return true; // SQRT_S (ft=rt) - case 0x16: armEmitRSQRT_S(sa, rd, rt); return true; // RSQRT_S - case 0x18: armEmitADDA_S(rd, rt); return true; // ADDA_S (-> ACC) - case 0x19: armEmitSUBA_S(rd, rt); return true; // SUBA_S (-> ACC) - case 0x1A: armEmitMULA_S(rd, rt); return true; // MULA_S (-> ACC) - case 0x1C: armEmitMADD_S(sa, rd, rt); return true; // MADD_S - case 0x1D: armEmitMSUB_S(sa, rd, rt); return true; // MSUB_S - case 0x1E: armEmitMADDA_S(rd, rt); return true; // MADDA_S (-> ACC) - case 0x1F: armEmitMSUBA_S(rd, rt); return true; // MSUBA_S (-> ACC) - case 0x28: armEmitMAX_S(sa, rd, rt); return true; // MAX_S - case 0x29: armEmitMIN_S(sa, rd, rt); return true; // MIN_S - case 0x24: armEmitCVT_W(sa, rd); return true; // CVT_W (fd=sa, fs=rd) - case 0x30: armEmitC_F(rd, rt); return true; // C.F (set FCR31 C-bit; fs=rd, ft=rt) - case 0x32: armEmitC_EQ(rd, rt); return true; // C.EQ - case 0x34: armEmitC_LT(rd, rt); return true; // C.LT - case 0x36: armEmitC_LE(rd, rt); return true; // C.LE - // Bit-exact ops (Phase 5.2a). - case 0x05: armEmitABS_S(sa, rd); return true; // ABS_S (fd=sa, fs=rd) - case 0x06: armEmitMOV_S(sa, rd); return true; // MOV_S - case 0x07: armEmitNEG_S(sa, rd); return true; // NEG_S - default: return false; - } - case 0x14: // COP1_W: only CVT_S (funct 0x20); fd=sa, fs=rd. - if (funct == 0x20) { armEmitCVT_S(sa, rd); return true; } - return false; - default: return false; - } - - // REGIMM — rt-field selector. Only the trap-immediates are native here; the - // BLTZ/BGEZ/... branches are emitted by the branch path (block-terminating) and - // never reach recTranslateOp. Unhandled rt values fall through to false. - case 0x01: - return false; - - // Immediate arithmetic (Phase 3.1) - case 0x08: armEmitADDI(rt, rs, imm); return true; - case 0x09: armEmitADDIU(rt, rs, imm); return true; - case 0x0A: armEmitSLTI(rt, rs, imm); return true; - case 0x0B: armEmitSLTIU(rt, rs, imm); return true; - case 0x0C: armEmitANDI(rt, rs, static_cast(op)); return true; - case 0x0D: armEmitORI(rt, rs, static_cast(op)); return true; - case 0x0E: armEmitXORI(rt, rs, static_cast(op)); return true; - case 0x0F: armEmitLUI(rt, static_cast(op)); return true; - case 0x18: armEmitDADDI(rt, rs, imm); return true; - case 0x19: armEmitDADDIU(rt, rs, imm); return true; - - // Scalar loads. The (bits, sign) pair drives the extend inside the helper: - // LWU zero-extends a word, LD is a full 64-bit load (sign is irrelevant). - case OP_LB: armEmitLoadGpr(8, true, rt, rs, imm); return true; - case OP_LBU: armEmitLoadGpr(8, false, rt, rs, imm); return true; - case OP_LH: armEmitLoadGpr(16, true, rt, rs, imm); return true; - case OP_LHU: armEmitLoadGpr(16, false, rt, rs, imm); return true; - case OP_LW: armEmitLoadGpr(32, true, rt, rs, imm); return true; - case OP_LWU: armEmitLoadGpr(32, false, rt, rs, imm); return true; - case OP_LD: armEmitLoadGpr(64, false, rt, rs, imm); return true; - - // Scalar stores (the low `bits` bits of GPR[rt]). - case OP_SB: armEmitStoreGpr(8, rt, rs, imm); return true; - case OP_SH: armEmitStoreGpr(16, rt, rs, imm); return true; - case OP_SW: armEmitStoreGpr(32, rt, rs, imm); return true; - case OP_SD: armEmitStoreGpr(64, rt, rs, imm); return true; - - // Unaligned load/store byte-merge forms (interpreter-exact; heavily used in - // memcpy-style loops — previously interpreter single-steps). - case 0x22: armEmitLWL(rt, rs, imm); return true; - case 0x26: armEmitLWR(rt, rs, imm); return true; - case 0x2A: armEmitSWL(rt, rs, imm); return true; - case 0x2E: armEmitSWR(rt, rs, imm); return true; - case 0x1A: armEmitLDL(rt, rs, imm); return true; - case 0x1B: armEmitLDR(rt, rs, imm); return true; - case 0x2C: armEmitSDL(rt, rs, imm); return true; - case 0x2D: armEmitSDR(rt, rs, imm); return true; - - // CACHE (0x2f): EE data-cache hint/maintenance. It DOES do real work in the - // interpreter (Cache.cpp CACHE(): line invalidate/writeback, writes CP0.TagLo) - // so it can't be a no-op — but it reads rs, writes no GPR, never touches - // cpuRegs.pc, and raises no exception, so inline-interpret it in-block exactly - // like the COP0 ops below instead of block-terminating + single-stepping. recTranslateOp - // runs after recCacheFlushAll, so cpuRegs holds the current rs for the handler. - // 2nd-most-dominant EE fallback op (The Getaway: ~39% of single-stepped ops). - case 0x2f: recEmitInterpInline(op); return true; - - // 128-bit quadword load/store (16-byte aligned). - case OP_LQ: armEmitLoadQuad(rt, rs, imm); return true; - case OP_SQ: armEmitStoreQuad(rt, rs, imm); return true; - - // FPU load/store (Phase 5.2a) — 32-bit transfer between memory and FPR[rt]. - case OP_LWC1: armEmitLWC1(rt, rs, imm, pc); return true; - case OP_SWC1: armEmitSWC1(rt, rs, imm, pc); return true; - - // COP0 (Phase 5.1) — same inline-interpreter strategy as COP2: keep straight-line - // COP0 ops in the block instead of breaking it + single-stepping. COP0 is not a - // per-op perf item (see x86/iCOP0.cpp's note), so the win is purely avoiding block - // fragmentation. We must NOT inline anything that: - // - writes cpuRegs.pc: BC0 branches (rs==0x08), ERET (C0 funct 0x18); - // - needs a live cpuRegs.cycle: MFC0/MTC0 of Count (Rd==9) or the PERF counters - // (Rd==25). This rec only flushes cpuRegs.cycle at the block tail, so a - // mid-block read would be stale — COP0.cpp warns that two MFC0 Count in one - // block before the cycle update return increment 0 and games lock up; - // - gates interrupts with timing the x86 rec specifically branches after: EI/DI, - // WAIT. - // Those stay on the interpreter single-step path (return false). MTC0 Status/Config - // are fine to inline: the x86 rec doesn't force a branch after them either, so a - // resulting interrupt is recognised at the block-tail event test just the same; - // TLB writes call MapTLB→recClear, which is safe mid-block (targeted recLUT reset, - // the running block keeps its valid host code and recompiles cleared slots on the - // next dispatch). - case 0x10: - switch (rs) - { - case 0x00: // MFC0 - case 0x04: // MTC0 - if (rd == 9 || rd == 25) - return false; // Count / PERF need a live cpuRegs.cycle - recEmitInterpInline(op); - return true; - case 0x10: // C0 — inline the TLB ops only - switch (funct) - { - case 0x01: // TLBR - case 0x02: // TLBWI - case 0x06: // TLBWR - case 0x08: // TLBP - recEmitInterpInline(op); - return true; - default: - return false; // ERET (0x18) writes PC; EI/DI/WAIT gate interrupts - } - default: - return false; // BC0 branches (rs==0x08) + COP0_Unknown - } - - // COP2 — VU0 macro mode. CpuVU0 is microVU0 (a recompiler), so a COP2 op may need - // to finish/sync a deferred VU0 micro program before touching VU0 state. Macro mode - // (Phase 7.9) drives that precise, analysis-driven sync via the M2 helpers + M1 flags. - // Transfer ops ported natively as M3 lands them; the rest still inline the interpreter - // (which self-syncs via _vu0FinishMicro) until M5 ports the ALU. The host-side - // cpuRegs.code is set before the native handlers because their _Rt_/_Rd_ macros and - // COP2_Interlock read it at emit time. The BC2 branches (rs==0x08) write cpuRegs.pc - // and are emitted natively by recRecompile (recIsHandledBranch/recIsLikelyBranch + - // recEmitBranch/armEmitBranchLikelyTest, Phase M4), which ends the block at them — so - // they never reach here as a straight-line op (the case below is a defensive fallback). - case 0x12: - // vc105: FullVU0SyncHack routes every straight-line COP2 op to the inline - // interpreter (the interpreter fully syncs VU0 on each op, so the EE/VU0 clocks - // never drift and the reconciliation can't wedge the scheduler — Ratchet freeze). - // BC2 branches (rs==0x08) are block-terminating control flow, handled by - // recRecompile, so they never reach here; the predicate excludes them anyway. - if (recCop2ForceInterp(op)) - { - cpuRegs.code = op; - recEmitInterpInline(op); - return true; - } - switch (rs) - { - case 0x01: // QMFC2 (M3.3) — native, memory-backed - cpuRegs.code = op; - recQMFC2(); - return true; - case 0x02: // CFC2 (M3.1) — native, memory-backed - cpuRegs.code = op; - recCFC2(); - return true; - case 0x05: // QMTC2 (M3.3) — native, memory-backed - cpuRegs.code = op; - recQMTC2(); - return true; - case 0x06: // CTC2 (M3.2) — native, memory-backed - cpuRegs.code = op; - recCTC2(); - return true; - case 0x08: - return false; // BC2F/BC2T/BC2FL/BC2TL — handled natively as a block-terminating - // branch in recRecompile (M4); never reached here in practice. - default: - // SPECIAL1/SPECIAL2 macro ops. All the VU ALU/transfer families emit natively - // via the microVU0 single-op emitters (M5.1-M5.4). Faithful to x86 recCOP2_SPEC1: - // emit the FINISH prologue — mVUFinishVU0 on EEINST_COP2_{SYNC,FINISH}_VU0, a - // full finish (ALU ops never lazy-SYNC and never interlock) — then the native - // op. mVUFinishVU0 commits no cycles (so the macro ops are excluded from - // recOpNeedsCycleFlush and their cycles ride forward). - // - // The else branch is reached only by CALLMS/CALLMSR (M5.5), which stay on the - // interpreter by design — x86 emits them via INTERPRETATE_COP2_FUNC, not a - // native macro. The inline-interp path is faithful: the interpreter - // (vu0ExecMicro) self-finishes any running VU0 and launches the microprogram, - // reading VU state from the memory the macro emitters keep committed — at least - // as strong as x86's iFlushCall(FLUSH_FREE_XMM | FLUSH_FREE_VU0). The matching - // cycle commit (x86's scaleblockcycles_clear before recCall) is emitted in - // recRecompile via recCop2IsCallms/recEmitCommitBlockCycles. (An unknown/illegal - // COP2 SPECIAL op would also land here and harmlessly run the interpreter.) - cpuRegs.code = op; // _Fs_/_Ft_/_X_Y_Z_W read microVU0.code = cpuRegs.code - if (recVUMacroIsMode0(op)) - { - if (g_pCurInstInfo->info & (EEINST_COP2_SYNC_VU0 | EEINST_COP2_FINISH_VU0)) - mVUFinishVU0(); - recVUMacroEmitMode0(op); - } - else - { - recEmitInterpInline(op); // CALLMS/CALLMSR (interp by design, M5.5) - } - return true; - } - - // COP2 quadword load/store (VF[rt] ↔ memory). Native (M3.4): the analysis-driven - // SYNC/FINISH dispatch + the vtlb quad path, targeting VU0.VF[rt]. No COP2_Interlock - // (faithful to microVU_Macro.inl). cpuRegs.code set for the _Rt_/_Rs_/_Imm_ macros. - case OP_LQC2: - cpuRegs.code = op; - if (recCop2ForceInterp(op)) { recEmitInterpInline(op); return true; } // vc105 - recLQC2(); - return true; - case OP_SQC2: - cpuRegs.code = op; - if (recCop2ForceInterp(op)) { recEmitInterpInline(op); return true; } // vc105 - recSQC2(); - return true; - - default: return false; - } -} - -// -------------------------------------------------------------------------------------- -// Branch / jump compilation (Phase 4.3) -// -------------------------------------------------------------------------------------- -// Decode a control-flow opcode at branchpc and emit the matching Phase 4.1/4.2 -// generator (which writes cpuRegs.pc and any link register). Returns true if a -// generator handled it. The compile-time target/fallthrough/link constants follow -// the interpreter's macros with _PC_ == branchpc + 4 (the delay-slot address): -// J/JAL target = (instr_index << 2) | ((branchpc + 4) & 0xF0000000) -// branch target = (branchpc + 4) + (s16(imm) << 2) -// fallthrough / link = branchpc + 8 -// Likely branches, coprocessor branches, and traps return false (interpreter -// fallback handles them, including their delay-slot semantics). -static bool recEmitBranch(u32 op, u32 branchpc) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 funct = op & 0x3f; - - const u32 delaypc = branchpc + 4; - const u32 jtarget = ((op & 0x03ffffff) << 2) | (delaypc & 0xf0000000u); - const u32 btarget = delaypc + (static_cast(static_cast(static_cast(op))) << 2); - const u32 fallthrough = branchpc + 8; - const u32 linkpc = branchpc + 8; - - switch (opcode) - { - case 0x02: armEmitJ(jtarget); return true; - case 0x03: armEmitJAL(jtarget, linkpc); return true; - case 0x04: armEmitBEQ(rs, rt, btarget, fallthrough); return true; - case 0x05: armEmitBNE(rs, rt, btarget, fallthrough); return true; - case 0x06: armEmitBLEZ(rs, btarget, fallthrough); return true; - case 0x07: armEmitBGTZ(rs, btarget, fallthrough); return true; - - case 0x00: // SPECIAL: JR / JALR - if (funct == 0x08) { armEmitJR(rs); return true; } - if (funct == 0x09) { armEmitJALR(rd, rs, linkpc); return true; } - return false; - - case 0x01: // REGIMM: BLTZ / BGEZ / BLTZAL / BGEZAL (rt selector) - switch (rt) - { - case 0x00: armEmitBLTZ(rs, btarget, fallthrough); return true; - case 0x01: armEmitBGEZ(rs, btarget, fallthrough); return true; - case 0x10: armEmitBLTZAL(rs, btarget, fallthrough, linkpc); return true; - case 0x11: armEmitBGEZAL(rs, btarget, fallthrough, linkpc); return true; - default: return false; // likely (BLTZL/...) + traps - } - - case 0x11: // COP1: BC1 branches live under rs==0x08 (BC); rt selects tf/likely. - if (rs == 0x08) - { - if (rt == 0x00) { armEmitBC1F(btarget, fallthrough); return true; } // BC1F - if (rt == 0x01) { armEmitBC1T(btarget, fallthrough); return true; } // BC1T - } - return false; // BC1FL/BC1TL (likely) + non-branch COP1 ops - - case 0x12: // COP2: BC2 branches live under rs==0x08 (BC); rt selects tf/likely. - if (rs == 0x08) - { - if (rt == 0x00) { armEmitBC2F(btarget, fallthrough); return true; } // BC2F - if (rt == 0x01) { armEmitBC2T(btarget, fallthrough); return true; } // BC2T - } - return false; // BC2FL/BC2TL (likely) + COP2 transfer/macro ops (straight-line) - - case 0x10: // COP0: BC0 branches live under rs==0x08 (BC); rt selects tf/likely. - if (rs == 0x08) - { - if (rt == 0x00) { armEmitBC0F(btarget, fallthrough); return true; } // BC0F - if (rt == 0x01) { armEmitBC0T(btarget, fallthrough); return true; } // BC0T - } - return false; // BC0FL/BC0TL (likely) + COP0 transfer/TLB/DI ops (handled elsewhere) - - default: return false; - } -} - -static void recConstApplyBranchLink(u32 op, u32 branchpc, RecGprConstState& state) -{ - const u32 opcode = op >> 26; - const u32 rd = (op >> 11) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 funct = op & 0x3f; - const u32 linkpc = branchpc + 8; - - if (opcode == 0x03) // JAL - recConstSetKnown(state, 31, linkpc); - else if (opcode == 0x00 && funct == 0x09) // JALR - recConstSetKnown(state, rd, linkpc); - else if (opcode == 0x01 && (rt == 0x10 || rt == 0x11)) // BLTZAL / BGEZAL - recConstSetKnown(state, 31, linkpc); -} - -static bool recConstGetBranchSource(const RecGprConstState& state, u32 reg, bool link_before_read, u32 linkpc, u64* value) -{ - if (link_before_read && reg == 31) - { - *value = linkpc; - return true; - } - - if (!state.known[reg]) - return false; - - *value = state.value[reg]; - return true; -} - -// Return a compile-time known next PC for branches whose condition is unconditional or -// collapses through tracked constants. The branch generator still emits the normal PC -// write; this is only used by the block tail to skip the generic dispatcher lookup. -static bool recGetKnownBranchTarget(u32 op, u32 branchpc, const RecGprConstState& state, u32* target) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - - const u32 delaypc = branchpc + 4; - const u32 jtarget = ((op & 0x03ffffff) << 2) | (delaypc & 0xf0000000u); - const u32 btarget = delaypc + (static_cast(static_cast(static_cast(op))) << 2); - const u32 fallthrough = branchpc + 8; - const u32 linkpc = branchpc + 8; - u64 lhs = 0; - u64 rhs = 0; - - switch (opcode) - { - case 0x02: // J - case 0x03: // JAL - *target = jtarget; - return true; - - case 0x04: // BEQ - if (recConstGetBranchSource(state, rs, false, linkpc, &lhs) && - recConstGetBranchSource(state, rt, false, linkpc, &rhs)) - { - *target = (lhs == rhs) ? btarget : fallthrough; - return true; - } - return false; - - case 0x05: // BNE - if (recConstGetBranchSource(state, rs, false, linkpc, &lhs) && - recConstGetBranchSource(state, rt, false, linkpc, &rhs)) - { - *target = (lhs != rhs) ? btarget : fallthrough; - return true; - } - return false; - - case 0x06: // BLEZ - if (recConstGetBranchSource(state, rs, false, linkpc, &lhs)) - { - *target = (static_cast(lhs) <= 0) ? btarget : fallthrough; - return true; - } - return false; - - case 0x07: // BGTZ - if (recConstGetBranchSource(state, rs, false, linkpc, &lhs)) - { - *target = (static_cast(lhs) > 0) ? btarget : fallthrough; - return true; - } - return false; - - case 0x01: // REGIMM - switch (rt) - { - case 0x00: // BLTZ - case 0x10: // BLTZAL - if (!recConstGetBranchSource(state, rs, rt == 0x10, linkpc, &lhs)) - return false; - *target = (static_cast(lhs) < 0) ? btarget : fallthrough; - return true; - case 0x01: // BGEZ - case 0x11: // BGEZAL - if (!recConstGetBranchSource(state, rs, rt == 0x11, linkpc, &lhs)) - return false; - *target = (static_cast(lhs) >= 0) ? btarget : fallthrough; - return true; - default: - return false; - } - - default: - return false; - } -} - -// Is this opcode a control-flow op we have a generator for? (Used to detect the -// block-terminating branch; everything else is either straight-line codegen or an -// interpreter fallback.) -static bool recIsHandledBranch(u32 op) -{ - const u32 opcode = op >> 26; - const u32 funct = op & 0x3f; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - switch (opcode) - { - case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: - return true; - case 0x00: - return funct == 0x08 || funct == 0x09; - case 0x01: - return rt == 0x00 || rt == 0x01 || rt == 0x10 || rt == 0x11; - case 0x11: // COP1: only BC1F/BC1T (rs==BC, rt 0/1); all other COP1 ops are straight-line. - return rs == 0x08 && (rt == 0x00 || rt == 0x01); - case 0x12: // COP2: only BC2F/BC2T (rs==BC, rt 0/1); all other COP2 ops are straight-line/macro. - return rs == 0x08 && (rt == 0x00 || rt == 0x01); - case 0x10: // COP0: only BC0F/BC0T (rs==BC, rt 0/1); MFC0/MTC0/TLB/DI handled elsewhere. - return rs == 0x08 && (rt == 0x00 || rt == 0x01); - default: - return false; - } -} - -// Branch-likely forms (delay slot nullified when not taken). These get native -// codegen via armEmitBranchLikelyTest + a conditional skip over the delay-slot -// code in recRecompile; previously every one forced an interpreter single-step -// block (a C call + full dispatcher round-trip per execution). -static bool recIsLikelyBranch(u32 op) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - switch (opcode) - { - case 0x14: // BEQL - case 0x15: // BNEL - case 0x16: // BLEZL - case 0x17: // BGTZL - return true; - case 0x01: // REGIMM: BLTZL / BGEZL - return rt == 0x02 || rt == 0x03; - case 0x11: // COP1: BC1FL / BC1TL - return rs == 0x08 && (rt == 0x02 || rt == 0x03); - case 0x12: // COP2: BC2FL / BC2TL - return rs == 0x08 && (rt == 0x02 || rt == 0x03); - case 0x10: // COP0: BC0FL / BC0TL - return rs == 0x08 && (rt == 0x02 || rt == 0x03); - default: - return false; - } -} - -// COP0 DI (disable interrupts): COP0, CO (rs==0x10), funct 0x39. -static bool recIsCop0DI(u32 op) -{ - return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x39; -} - -// COP0 EI (enable interrupts, funct 0x38) / ERET (exception return, funct 0x18) — -// CO ops (rs==0x10). x86 compiles both via recBranchCall: end the block, run the -// interpreter handler, then event-test + dispatch (EI so a now-unmasked pending IRQ -// is serviced; ERET because it writes cpuRegs.pc). recRecompile handles them that way -// instead of single-stepping (the top EE interpreter fallback after the BC0/MADD work). -static bool recIsCop0EI(u32 op) -{ - return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x38; -} -static bool recIsCop0ERET(u32 op) -{ - return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x18; -} -static bool recIsCop0EIorERET(u32 op) -{ - return recIsCop0EI(op) || recIsCop0ERET(op); -} - -// MIPS trap ops: SPECIAL T{GE,GEU,LT,LTU,EQ,NE} (funct 0x30-0x34,0x36) and REGIMM -// T{GE,GEU,LT,LTU,EQ,NE}I (rt 0x08-0x0C,0x0E). Emitted natively (block-conditional) -// in recRecompile — see recEmitTrapCompareIfTrap. -static bool recIsTrap(u32 op) -{ - const u32 opcode = op >> 26; - if (opcode == 0x00) - { - const u32 funct = op & 0x3f; - return funct == 0x30 || funct == 0x31 || funct == 0x32 || - funct == 0x33 || funct == 0x34 || funct == 0x36; - } - if (opcode == 0x01) - { - const u32 rt = (op >> 16) & 0x1f; - return rt == 0x08 || rt == 0x09 || rt == 0x0A || - rt == 0x0B || rt == 0x0C || rt == 0x0E; - } - return false; -} - -// Can `op` be safely emitted inline as DI's one-instruction-delayed slot? The x86 -// recDI compiles whatever follows DI before applying the interrupt-disable; on this -// rec the delayed op is emitted straight-line via recEmitOp (native, else inline -// interpreter), so it must be an op that is correct to splice mid-block in program -// order. That excludes control-flow / PC-writing / interrupt-gating / exception- -// raising / cycle-sensitive ops, for which we instead end the block at DI and let it -// single-step (DI then applies immediately — an accepted corner; a benign straight- -// line op is what virtually always follows a DI). Branches are caught by the -// recIsHandledBranch / recIsLikelyBranch checks the caller already does. -static bool recCop0DelayOpUnsafe(u32 op) -{ - const u32 opcode = op >> 26; - const u32 funct = op & 0x3f; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - switch (opcode) - { - case 0x00: // SPECIAL: SYSCALL / BREAK / traps (JR/JALR already caught as branches) - return funct == 0x0C || funct == 0x0D || (funct >= 0x30 && funct <= 0x37); - case 0x01: // REGIMM traps: TGEI/TGEIU/TLTI/TLTIU/TEQI/TNEI (rt 0x08-0x0F) - return rt >= 0x08 && rt <= 0x0F; - case 0x10: // COP0: BC0 (rs 0x08); CO ERET/EI/DI/WAIT; cycle-sensitive Count/PERF - if (rs == 0x08) - return true; - if (rs == 0x10) // CO - return funct == 0x18 || funct == 0x38 || funct == 0x39 || funct == 0x20; - if ((rs == 0x00 || rs == 0x04) && (rd == 9 || rd == 25)) - return true; - return false; - case 0x12: // COP2 / VU0 macro — keep off the inline delay path - case 0x36: // LQC2 — VU0-syncing - case 0x3E: // SQC2 — VU0-syncing - return true; - default: - return false; - } -} - -// -------------------------------------------------------------------------------------- -// Wait-loop (idle-loop) detection -// -------------------------------------------------------------------------------------- -// A block that ends with a branch back to its own start and whose body carries NO -// register state between iterations (every written GPR derives only from memory -// loads / constants / regs not written in the loop) is a poll loop: its condition -// can only change through an external event (interrupt, DMA, MTVU). Spinning it -// one tiny block at a time until cpuRegs.nextEventCycle burns a full host core — -// the classic EE-at-99% heat case. For such blocks the dispatch tail bumps -// cpuRegs.cycle up to nextEventCycle when the branch was taken, so the next event -// fires after one iteration instead of millions. This mirrors the x86 rec's -// WaitLoop speedhack semantics; conditional loops are gated behind -// EmuConfig.Speedhacks.WaitLoop (default on), unconditional self-loops (which can -// ONLY exit via an event, making the skip exact) are always optimized. -// -// The dataflow check walks the body+delay ops in program order: an op may only -// read a register that is (a) never written in the loop, (b) $zero, or (c) already -// (re)defined earlier in this iteration from allowed sources. A loop-carried -// counter (`addiu t0,t0,-1`) reads its own previous-iteration value and is -// rejected, so calibration/delay loops keep their exact iteration counts. -static constexpr u32 REC_WAITLOOP_MAX_OPS = 8; - -// Decode the GPRs an allowed op reads/writes. Returns false if the op is not in -// the allowed (side-effect-free, natively compiled) set. -static bool recWaitLoopClassifyOp(u32 op, u32* reads, u32* writes) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const u32 rd = (op >> 11) & 0x1f; - const u32 funct = op & 0x3f; - - *reads = 0; - *writes = 0; - - if (op == 0) // NOP - return true; - - switch (opcode) - { - case 0x00: // SPECIAL: pure ALU/shift/select subset only - switch (funct) - { - case 0x00: case 0x02: case 0x03: // SLL/SRL/SRA - case 0x38: case 0x3A: case 0x3B: // DSLL/DSRL/DSRA - case 0x3C: case 0x3E: case 0x3F: // DSLL32/DSRL32/DSRA32 - *reads = (1u << rt); - *writes = (1u << rd); - return true; - case 0x04: case 0x06: case 0x07: // SLLV/SRLV/SRAV - case 0x14: case 0x16: case 0x17: // DSLLV/DSRLV/DSRAV - *reads = (1u << rt) | (1u << rs); - *writes = (1u << rd); - return true; - case 0x20: case 0x21: case 0x22: case 0x23: // ADD/ADDU/SUB/SUBU - case 0x24: case 0x25: case 0x26: case 0x27: // AND/OR/XOR/NOR - case 0x2A: case 0x2B: // SLT/SLTU - case 0x2C: case 0x2D: case 0x2E: case 0x2F: // DADD/DADDU/DSUB/DSUBU - *reads = (1u << rs) | (1u << rt); - *writes = (1u << rd); - return true; - case 0x0A: case 0x0B: // MOVZ/MOVN (rd is read AND written) - *reads = (1u << rs) | (1u << rt) | (1u << rd); - *writes = (1u << rd); - return true; - default: - return false; - } - - case 0x08: case 0x09: case 0x0A: case 0x0B: // ADDI/ADDIU/SLTI/SLTIU - case 0x0C: case 0x0D: case 0x0E: // ANDI/ORI/XORI - case 0x18: case 0x19: // DADDI/DADDIU - *reads = (1u << rs); - *writes = (1u << rt); - return true; - - case 0x0F: // LUI (pure constant) - *writes = (1u << rt); - return true; - - case OP_LB: case OP_LBU: case OP_LH: case OP_LHU: - case OP_LW: case OP_LWU: case OP_LD: // scalar loads: rt = mem[rs+imm] - *reads = (1u << rs); - *writes = (1u << rt); - return true; - - default: - return false; - } -} - -// Run the dataflow check over the loop body (+ branch sources + delay slot). -// `ops` are the straight-line body ops in order; `branch_reads` the GPRs the -// branch condition reads; `delay_op` the delay-slot instruction. Program order -// per iteration is: body ops, branch condition read, delay slot. -static bool recWaitLoopBodyIsPure(const u32* ops, u32 num_ops, u32 branch_reads, u32 delay_op) -{ - // +1 slot for the delay op. - u32 op_reads[REC_WAITLOOP_MAX_OPS + 1]; - u32 op_writes[REC_WAITLOOP_MAX_OPS + 1]; - - for (u32 i = 0; i < num_ops; i++) - { - if (!recWaitLoopClassifyOp(ops[i], &op_reads[i], &op_writes[i])) - return false; - } - if (!recWaitLoopClassifyOp(delay_op, &op_reads[num_ops], &op_writes[num_ops])) - return false; - - // All registers written anywhere in the loop (delay slot included — it runs - // before the next iteration's body). $zero writes are discarded by codegen. - u32 written = 0; - for (u32 i = 0; i <= num_ops; i++) - written |= op_writes[i] & ~1u; - - // Program-order scan: reading a written-in-loop register before it has been - // redefined this iteration means loop-carried state (e.g. a decrementing - // counter) -> reject. - u32 defined = 0; - for (u32 i = 0; i < num_ops; i++) - { - if (((op_reads[i] & ~1u) & written & ~defined) != 0) - return false; - defined |= op_writes[i] & ~1u; - } - // Branch condition reads happen after the body... - if (((branch_reads & ~1u) & written & ~defined) != 0) - return false; - // ...and the delay slot runs last. - if (((op_reads[num_ops] & ~1u) & written & ~defined) != 0) - return false; - - return true; -} - -// GPRs a handled branch op's condition reads. -static u32 recBranchConditionReads(u32 op) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - switch (opcode) - { - case 0x02: return 0; // J - case 0x04: case 0x05: return (1u << rs) | (1u << rt); // BEQ/BNE - case 0x06: case 0x07: return (1u << rs); // BLEZ/BGTZ - case 0x01: // REGIMM: BLTZ/BGEZ only — the AL forms write a link register. - return (rt == 0x00 || rt == 0x01) ? (1u << rs) : 0xffffffffu; - case 0x10: // COP0: BC0F/BC0T read CPCOND0 (DMAC STAT/PCR), not GPRs -> 0 GPR reads. - // Lets the DMA-wait spin qualify as a wait-loop so the existing fast-forward - // idle-skips it (the big win). CPCOND0 flips only at event-scheduled DMA - // completion, so the skip lands exactly at the next event. Gated by the - // WaitLoop speedhack (BC0 is left conditional in recBranchIsUnconditional). - return (rs == 0x08 && (rt == 0x00 || rt == 0x01)) ? 0u : 0xffffffffu; - default: return 0xffffffffu; // anything else: not a candidate - } -} - -// Is this branch unconditionally taken (compile-time)? Such a self-loop can only -// exit via an event, so skipping its cycles is exact, not a speedhack. -static bool recBranchIsUnconditional(u32 op) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - switch (opcode) - { - case 0x02: return true; // J - case 0x04: return rs == rt; // BEQ r,r - case 0x06: return rs == 0; // BLEZ $zero - case 0x01: return rt == 0x01 && rs == 0; // BGEZ $zero - default: return false; - } -} - -// Emit cpuRegs.code = op, then call the interpreter's handler for `op`. Used for a -// delay-slot instruction the straight-line generators can't handle. Does NOT touch -// cpuRegs.pc (the branch generator already committed the next PC, and a normal -// delay-slot op never writes PC). RESTATEPTR(x19) is callee-saved across the call. -static void recEmitInterpInline(u32 op) -{ - armAsm->Mov(RSCRATCHADDR.W(), op); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_CODE_OFFSET)); -#if ARMSX2_ANDROID_EE_OPHIST - // Route every in-block interp call through the counting thunk so the INLINE - // fallback profile (COP2/VU0 etc.) is captured. It reads cpuRegs.code (set above). - armEmitCall(reinterpret_cast(&recOphistInlineThunk)); -#else - armEmitCall(reinterpret_cast(::R5900::GetInstruction(op).interpret)); -#endif -} - -// @@EEDIFF@@ --------------------------------------------------------------------------- -// Differential verifier: wrap ONE straight-line op with a pre-op register snapshot and a -// post-op interpreter re-run + compare. Only emitted when g_ee_diff_verify was set at -// block-compile time (the toggle clears the EE block cache, so blocks recompile with the -// hooks). The caller must have flushed+killed the GPR/const cache first so cpuRegs in -// memory is authoritative at op entry (the recTranslateOp generators read/write cpuRegs -// directly through RESTATEPTR, so after the op runs cpuRegs == REC-post). RESTATEPTR(x19) -// is callee-saved across both C calls; the op itself sits between them unchanged. -// -// Returns true if a native generator handled the op (verify emitted), false if the op -// has no native generator — the caller then falls back to recEmitInterpInline WITHOUT a -// verify (an interp-fallback op can't diverge from itself; the bug is in *rec* codegen). -static bool recEmitDiffVerifyOp(u32 op, u32 pc) -{ - const u32 primary = op >> 26; - - // EXCLUDE coprocessor ops from the verify wrapper. recTranslateOp handles some COP0 - // (MFC0/MTC0/TLB) and COP2 (QMFC2/CFC2/QMTC2/CTC2/LQC2/SQC2/VU0 macro) ops by - // emitting an inline interpreter call and/or touching VU0/COP0 state — NOT plain - // GPRs. Re-running those on the interpreter would (a) compare interp-against-interp - // (a tautology) and (b) DANGEROUSLY re-execute VU0 finish/launch side effects (only - // memory writes are captured; VU state changes are not). The True Crime texture bug - // is pure EE data-gen (ALU/shift/MMI/load-store), so restrict the verifier to those. - // Returning false makes the caller single-step these on the interpreter, un-verified. - // 0x10 COP0, 0x11 COP1(FPU), 0x12 COP2, 0x31 LWC1, 0x36 LQC2, 0x39 SWC1, 0x3e SQC2. - // The COP1 family (COP1 + LWC1/SWC1) is excluded too: the snapshot/compare covers - // GPR/HI/LO only, not fpr[]/ACC/FCR31, so an FPU re-run would (a) never be checked and - // (b) leave the interpreter's fpr value in memory (restoreFrom only rewinds - // GPR/HI/LO/PC/sa). The texture bug is integer, so this is a safe, deliberate scope - // limit — extend the snapshot to the FPU file if an FP miscompile is ever suspected. - if (primary == 0x10 || primary == 0x11 || primary == 0x12 || primary == 0x31 || - primary == 0x36 || primary == 0x39 || primary == 0x3e) - return false; - - // Pre-op snapshot of cpuRegs -> g_diff_pre (no args). - armEmitCall(reinterpret_cast(&eeDiffSnapshotPre)); - - // The real recompiled op — the UN-cached, memory-committed generator path (same one - // recTranslateOpOptimized falls through to). Reads/writes guest state via RESTATEPTR. - if (!recTranslateOp(op, pc)) - return false; - - // Post-op verify: eeDiffVerify(pc, op). Args in x0/x1 (RXARG1/RXARG2). - armAsm->Mov(RXARG1.W(), pc); - armAsm->Mov(RXARG2.W(), op); - armEmitCall(reinterpret_cast(&eeDiffVerify)); - return true; -} -// @@EEDIFF@@ --------------------------------------------------------------------------- - -// COP0 DI — clear Status.EIE (disable interrupts) under the same condition as -// Interpreter::COP0::DI and the x86 recDI (iCOP0.cpp): only when the CPU is in a -// privileged context, i.e. (Status & (EXL|ERL|EDI)) != 0 || Status.KSU == 0. -// This emits just the "DI takes effect" status update; the one-instruction delay -// the x86 rec applies (recompileNextInstruction before this) is reproduced by the -// caller in recRecompile, which emits the following guest instruction first. -// -// Emitted with only encodable logical immediates so VIXL never needs a scratch -// register, and the status word is held in RSCRATCHADDR.W() (x17, removed from the -// VIXL scratch pool in armStartBlock) — so it cannot be clobbered by an implicit -// VIXL temp. Touches only cpuRegs.CP0.n.Status (no guest GPRs), so it is safe to -// splice into the middle of a block after the delayed instruction. -static void recEmitCop0DI() -{ - const a64::Register status = RSCRATCHADDR.W(); - armAsm->Ldr(status, a64::MemOperand(RESTATEPTR, EE_COP0_STATUS_OFFSET)); - - a64::Label do_clear, done; - armAsm->Tst(status, 0x6); // EXL | ERL set -> privileged, clear EIE - armAsm->B(&do_clear, a64::ne); - armAsm->Tst(status, 0x20000); // EDI set -> clear EIE - armAsm->B(&do_clear, a64::ne); - armAsm->Tst(status, 0x18); // KSU: non-zero == user/supervisor -> leave EIE - armAsm->B(&done, a64::ne); - armAsm->Bind(&do_clear); - armAsm->Bic(status, status, 0x10000); // EIE - armAsm->Str(status, a64::MemOperand(RESTATEPTR, EE_COP0_STATUS_OFFSET)); - armAsm->Bind(&done); -} - -// Compile one straight-line or delay-slot instruction: const-folded/native generator -// if we have one, otherwise an inline interpreter call. -// Block cycles accumulated up to and including the current COP2/LQC2/SQC2 op, stashed by the -// emit loop (recRecompile) for the op's handler to hand to the M2 sync helpers. The faithful -// analog of x86's s_nBlockCycles fed to scaleblockcycles_clear(): the helpers commit it to -// cpuRegs.cycle only on a real SYNC (mVUSyncVU0 / the COP2_Interlock SYNC branch), and the emit -// loop clears the accumulator only then. FINISH-only / no-sync ops leave the cycles in the -// accumulator so they ride forward and survive _vu0FinishMicro's cpuRegs.cycle = VU0.cycle -// collapse (a pre-commit, as the old unconditional pre-flush did, would be lost there). -static u32 s_cop2RawCycles = 0; - -static void recEmitOp(u32 op, RecGprConstState& const_state, RecGprCacheState& cache_state, u32 pc) -{ - // Used only for branch delay slots, which the main emit loop's COP2 cycle stash does not - // reach. A COP2/LQC2/SQC2 op here would otherwise read a stale s_cop2RawCycles; zero it so - // its sync helper commits nothing (the block ends right after the delay slot, so the block - // tail commits the accumulated cycles for accounting). The VU catch-up still reads the - // current cpuRegs.cycle. Harmless for non-COP2 ops (they ignore it). - s_cop2RawCycles = 0; - if (!recTranslateOpOptimized(op, const_state, cache_state, pc)) - recEmitInterpInline(op); -} - -// cpuRegs.pc = imm (block fallthrough / early-exit target). -static void recEmitWritePc(u32 pc) -{ - armAsm->Mov(RSCRATCHADDR.W(), pc); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_PC_OFFSET)); -} - -// Tail-dispatch to a compile-time-known next PC via the block's recLUT slot -// (adrp+add+ldr+br). This is now the FALLBACK path: when s_eeBlockLinkEnabled is -// set (default), recEmitEventTestAndDispatch instead emits a patchable direct B -// (recEmitLinkableExitToKnownPc) and the inbound-link backpatching this comment -// once warned was missing is implemented in eeInvalidateLinks (@@MAC_EE_BLOCKLINK@@). -// The LUT slot remains the single SMC-invalidation rewrite point, so this fallback -// can never enter a stale block; the slot load is a same-cacheline hit in steady state. -static void recEmitDispatchToKnownPc(u32 pc) -{ - armMoveAddressToReg(RXARG3, recPtrToBlock(pc)); - armAsm->Ldr(RXARG3, a64::MemOperand(RXARG3)); - armAsm->Br(RXARG3); -} - -// EE cycle scaling — mirrors iR5900.cpp scaleblockcycles_calculation() so block -// timing matches the x86 rec / interpreter for a given EECycleRate. -static u32 recScaleBlockCycles(u32 raw) -{ - const bool lowcycles = (raw <= 40); - const s8 cyclerate = EmuConfig.Speedhacks.EECycleRate; - u32 scale_cycles; - - if (cyclerate == 0 || lowcycles || cyclerate < -99 || cyclerate > 3) - scale_cycles = raw >> 3; - else if (cyclerate > 1) - scale_cycles = raw >> (2 + cyclerate); - else if (cyclerate == 1) - scale_cycles = static_cast((raw >> 3) / 1.3f); - else if (cyclerate == -1) - scale_cycles = (raw <= 80 || raw > 168 ? 5 : 7) * raw / 32; - else - scale_cycles = ((5 + (-2 * (cyclerate + 1))) * raw) >> 5; - - return (scale_cycles < 1) ? 1 : scale_cycles; -} - -// Commit the block's accumulated (scaled) cycles to cpuRegs.cycle, mirroring x86's -// scaleblockcycles_clear() add. Used by the CALLMS/CALLMSR path: x86's INTERPRETATE_COP2_FUNC -// does `cpuRegs.cycle += scaleblockcycles_clear()` immediately before calling the interpreter, -// so the VU0 microprogram it launches (vu0ExecMicro sets VU0.cycle = cpuRegs.cycle) starts at -// the correct EE time. This is the same commit emitted inside mVUSyncVU0, minus the VU0 -// catch-up — a LAUNCH (unlike a FINISH) does not collapse cpuRegs.cycle, so the cycles must be -// committed here rather than ridden forward. RXVIXLSCRATCH (x16) is dead between ops. -static void recEmitCommitBlockCycles(u32 raw) -{ - if (raw == 0) - return; - armAsm->Ldr(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); - armAsm->Add(RXVIXLSCRATCH, RXVIXLSCRATCH, recScaleBlockCycles(raw)); - armAsm->Str(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); -} - -// -------------------------------------------------------------------------------------- -// MIPS trap opcodes — native codegen (block-conditional). The interpreter trap funcs -// (R5900OpcodeImpl.cpp) compute "if (cond) trap()", and trap() does cpuRegs.pc -= 4 then -// cpuException(0x34) which redirects pc to the exception vector. The recompiler can't -// continue straight-line through a taken trap, so this mirrors x86's recBranchCall -// treatment (block-terminating) — but only on the rare TAKEN path: we emit a native -// 64-bit compare and branch OVER the raise block when the trap is NOT taken (the common -// case stays in-block, no dispatch). On the taken path we run the interpreter op (which -// raises), commit the block's cycles, and tail into DispatcherEvent to service events and -// re-dispatch from the new pc. The caller has already flushed+killed the GPR cache (so -// memory is authoritative for both the compare and the interpreter), exactly like a -// branch. RSCRATCHADDR(x17)=lhs, RXVIXLSCRATCH(x16)=rhs — dead scratch between ops; both -// are consumed by the Cmp before the raise block (which reuses x17) runs. -// -// The skip condition passed in is the INVERSE of the interpreter's trap-if test: -// TGE/TGEI rs>=rt -> skip lt TGEU/TGEIU rs>=rt(u) -> skip lo -// TLT/TLTI rs skip ge TLTU/TLTIU rs skip hs -// TEQ/TEQI rs==rt -> skip ne TNE/TNEI rs!=rt -> skip eq -static void recEmitTrapRegCompare(u32 rs, u32 rt, a64::Condition skip_cond, a64::Label* skip) -{ - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // GPR[rs].UD[0] - if (rt == 0) - armAsm->Cmp(RSCRATCHADDR, 0); - else - { - armAsm->Ldr(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // GPR[rt].UD[0] - armAsm->Cmp(RSCRATCHADDR, RXVIXLSCRATCH); - } - armAsm->B(skip, skip_cond); -} - -static void recEmitTrapImmCompare(u32 rs, s32 imm, a64::Condition skip_cond, a64::Label* skip) -{ - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // GPR[rs].UD[0] - // _Imm_ is the sign-extended 16-bit immediate. The signed forms compare against the - // s64 value; the unsigned forms (TGEIU/TLTIU) compare (u64)_Imm_, which is the same - // bit pattern — only the branch condition differs. - armAsm->Mov(RXVIXLSCRATCH, static_cast(static_cast(imm))); - armAsm->Cmp(RSCRATCHADDR, RXVIXLSCRATCH); - armAsm->B(skip, skip_cond); -} - -// Emits the trap-condition compare + "branch over the raise block when NOT taken" for a -// trap op; returns false (emitting nothing) for a non-trap op. Decode mirrors recIsTrap. -static bool recEmitTrapCompareIfTrap(u32 op, a64::Label* skip) -{ - const u32 opcode = op >> 26; - const u32 funct = op & 0x3f; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - const s32 imm = static_cast(op); - if (opcode == 0x00) // SPECIAL register-form traps - { - switch (funct) - { - case 0x30: recEmitTrapRegCompare(rs, rt, a64::lt, skip); return true; // TGE - case 0x31: recEmitTrapRegCompare(rs, rt, a64::lo, skip); return true; // TGEU - case 0x32: recEmitTrapRegCompare(rs, rt, a64::ge, skip); return true; // TLT - case 0x33: recEmitTrapRegCompare(rs, rt, a64::hs, skip); return true; // TLTU - case 0x34: recEmitTrapRegCompare(rs, rt, a64::ne, skip); return true; // TEQ - case 0x36: recEmitTrapRegCompare(rs, rt, a64::eq, skip); return true; // TNE - default: return false; - } - } - if (opcode == 0x01) // REGIMM immediate-form traps - { - switch (rt) - { - case 0x08: recEmitTrapImmCompare(rs, imm, a64::lt, skip); return true; // TGEI - case 0x09: recEmitTrapImmCompare(rs, imm, a64::lo, skip); return true; // TGEIU - case 0x0A: recEmitTrapImmCompare(rs, imm, a64::ge, skip); return true; // TLTI - case 0x0B: recEmitTrapImmCompare(rs, imm, a64::hs, skip); return true; // TLTIU - case 0x0C: recEmitTrapImmCompare(rs, imm, a64::ne, skip); return true; // TEQI - case 0x0E: recEmitTrapImmCompare(rs, imm, a64::eq, skip); return true; // TNEI - default: return false; - } - } - return false; -} - -// vc105: FullVU0SyncHack correctness path. Native macro-mode COP2 uses an analysis-driven, -// one-block-at-a-time VU0 sync that lets cpuRegs.cycle and VU0.cycle drift billions apart when -// the EE defers a launched program; the eventual `cpuRegs.cycle += VU0.cycle - startcycle` -// reconciliation in _vu0run then slams the EE clock backward and the event scheduler wedges -// (Ratchet: Deadlocked SCUS-97465 — image freezes, audio keeps running). The interpreter never -// drifts because it fully syncs (vu0Sync/_vu0FinishMicro) on EVERY COP2 op (VU0.cpp). So under -// FullVU0SyncHack we route every straight-line COP2 op (transfers + macro ALU + LQC2/SQC2, but -// NOT the BC2 branches, which are block-terminating control flow) to the inline interpreter — -// exactly the mechanism CALLMS/CALLMSR already use (recEmitInterpInline + a live-cycle commit). -// The EE recompiler still handles all non-COP2 code, so this is far faster than EE-interpreter. -static bool recCop2ForceInterp(u32 op) -{ - // FullVU0SyncHack correctness path. The native macro-mode COP2 recompiler has two timing - // defects on VU0-handshake games (Ratchet: Deadlocked SCUS-97465): (1) a backward EE-clock - // jump on deferred VU0 finish — fixed by the monotonic guard in _vu0run (VU0.cpp); and (2) a - // deeper EE event-scheduler wedge when the game idles that the guard does NOT fix (vc106 - // still froze at d=+3.9e9). Routing every straight-line COP2 op to the inline interpreter - // (proven: vc105 runs at 100% / native res) sidesteps BOTH — the interpreter keeps VU0 - // tightly synced every op, so the scheduler never wedges. The EE rec still runs all non-COP2 - // code, so the cost is only the few VU0 ops per frame. kForceInterp stays true; the _vu0run - // guard is kept anyway (a correct monotonic-clock invariant, harmless in the normal case). - // vc110: the freeze is a VU0<->EE HANDSHAKE DEADLOCK — the game's main thread blocks in the - // kernel idle loop waiting for a VU0-side signal the native macro-mode COP2 path drops (the - // interpreter produces it; vc105 never froze). That signal rides the EE<->VU0 TRANSFER ops, - // not the vertex math. kMode: 0 = all-native; 1 = transfers (QMFC2/CFC2/QMTC2/CTC2) + LQC2/ - // SQC2 → interp, keep hot native VU0 macro ALU on the rec (this test — perf-preserving); - // 2 = all COP2 → interp (vc105 fallback, safe/slower). - if (!EmuConfig.Gamefixes.FullVU0SyncHack) - return false; - // kMode: 0 = all native (fast, alignment/flicker glitch); 1 = transfers+LQC2/SQC2 → interp; - // 2 = all COP2 → interp (correct, slower); 3 = READS (QMFC2/CFC2) → interp (vc115: still broken); - // 4 = ALU/SPECIAL (rs>=0x10) → interp, transfers+LQC2/SQC2 native (vc116 — the never-tested - // complement of kMode 1). Evidence: EVERY correct config (EE-interp, vc105) runs the macro ALU - // on the interpreter; EVERY broken one (native, vc110, vc115) runs it native — transfers/reads - // have been interp-routed both ways without fixing the graphics. This isolates the ALU. The - // interp COP2_SPECIAL wrapper also computes FULL flags (no FLAGHACK elision) — a wrong - // MAC/status-flag read is exactly the boolean-flip signature (stuck facing / model blinking). - // vc117 bisect: kMode 5 = SPECIAL2 (funct>=0x3c: ADDA/MADDA/MULA/SUBA/MSUBA families, CLIP, - // DIV/SQRT/RSQRT, MOVE/MR32, ITOF/FTOI, LQI/SQI/LQD/SQD, MFIR/MTIR, RNG) → interp; SPECIAL1 - // (funct<0x3c: VADD/VSUB/VMUL/VMADD/VMSUB/VMAX/VMINI + bc/i/q, IADD/ISUB/IAND/IOR) stays - // NATIVE. kMode 6 = the complement (SPECIAL1 → interp, SPECIAL2 native). vc116 (kMode 4, - // all-ALU→interp) proved the bug is in one of these; this halves the search. - // vc124: runtime mode from /cop2mode.txt (see recLoadCop2Mode). Modes: - // 0=all native; 1=transfers+LQ/SQ→interp; 2=ALL COP2→interp (safe); 3=reads→interp; - // 4=all ALU→interp (vc116-good); 5=SPECIAL2→interp (vc117-good); 6=SPECIAL1→interp; - // 7-11 = suspect-group→interp bisects; 12-16 = ONLY-one-group-NATIVE probes - // (12=ACC,13=Q,14=mem/VI,15=moves/clip,16=converts — broken verdict convicts that group). - if (!s_cop2ModeLoaded) - recLoadCop2Mode(); - const int kMode = s_cop2InterpMode; - if (kMode == 0) - return false; - const u32 opc = op >> 26; - if (opc == OP_LQC2 || opc == OP_SQC2) - return (kMode == 1 || kMode == 2); // kMode 3/4/5/6 keep LQC2/SQC2 native - if (opc != 0x12) - return false; - const u32 rs = (op >> 21) & 0x1f; - if (rs == 0x08) - return false; // BC2 branches — control flow, native - if (kMode == 2) - return true; // all straight-line COP2 → interp - if (kMode == 3) - return (rs == 0x01 || rs == 0x02); // READS only (QMFC2/CFC2) → interp - if (kMode == 4) - return rs >= 0x10; // ALL ALU/SPECIAL → interp (vc116 — known-good graphics) - if (kMode == 5) - return rs >= 0x10 && (op & 0x3f) >= 0x3c; // SPECIAL2 → interp, SPECIAL1 native - if (kMode == 6) - return rs >= 0x10 && (op & 0x3f) < 0x3c; // SPECIAL1 → interp, SPECIAL2 native - if (kMode == 7) - { - // vc118: ONLY the SPECIAL2 ACC families → interp (ADDA/SUBA/MADDA/MSUBA/MULA + bc/i/q, - // OPMULA — everything that writes/reads the accumulator); the rest of SPECIAL2 (ITOF/ - // FTOI/ABS/CLIP/MOVE/MR32/LQI/SQI/DIV/SQRT/RSQRT/MTIR/MFIR/ILWR/ISWR/RNG) NATIVE, all - // of SPECIAL1 NATIVE. Suspect: macro-mode ACC doesn't survive the per-op regAlloc - // reset/flush round-trip (MULA's ACC lost before MADDA reads it). - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); // SPECIAL2 table index - return idx <= 0x0f // ADDAbc/SUBAbc/MADDAbc/MSUBAbc - || (idx >= 0x18 && idx <= 0x1c) // MULAbc + MULAq - || idx == 0x1e // MULAi - || (idx >= 0x20 && idx <= 0x2e && idx != 0x2b); // *Aq/*Ai/plain A-forms + OPMULA - } - if (kMode == 8) - { - // vc119: vc118 (ACC→interp, rest native) BROKE → ACC families are INNOCENT; the bug is - // in the rest of SPECIAL2. Prime suspect = the Q-register group (DIV/SQRT/RSQRT, mode - // 0x112: bespoke Q lane load/store + D/I flag fold). Route ONLY those (+WAITQ) → interp; - // EVERYTHING else native. - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); - return idx >= 0x38 && idx <= 0x3b; // DIV/SQRT/RSQRT/WAITQ → interp - } - if (kMode == 9) - { - // vc120: vc119 exonerated the Q group (interp'd, still broke). Guilty ∈ misc SPECIAL2. - // This splits it: VI-pointer/memory ops (LQI/SQI/LQD/SQD 0x34-0x37 — matrix-row walking — - // + MTIR/MFIR/ILWR/ISWR 0x3c-0x3f) → interp; converts/moves/CLIP/RNG + ACC + Q all NATIVE. - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); - return (idx >= 0x34 && idx <= 0x37) || (idx >= 0x3c && idx <= 0x3f); - } - if (kMode == 10) - { - // vc121: vc120 exonerated mem/VI ops. Remaining: ITOF/FTOI, ABS, CLIP, MOVE, MR32, RNG. - // This puts the moves/clip cluster (ABS 0x1d, CLIP 0x1f, MOVE 0x30, MR32 0x31) → interp; - // converts (ITOF/FTOI 0x10-0x17) + RNG (0x40-0x43) stay NATIVE with everything else. - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); - return idx == 0x1d || idx == 0x1f || idx == 0x30 || idx == 0x31; - } - if (kMode == 11) - { - // vc122: single-culprit ledger leaves {ITOF/FTOI, RNG}. Converts only → interp - // (idx 0x10-0x17); RNG + everything else NATIVE. If broken too, the single-culprit - // assumption is wrong (two guilty families in different bisect groups). - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); - return idx >= 0x10 && idx <= 0x17; // ITOF0/4/12/15 + FTOI0/4/12/15 → interp - } - // vc123+ (kMode 12..16): MULTI-CULPRIT hunt — flip ONE group native at a time from the - // known-good all-SPECIAL2-interp baseline (vc117). Each build independently convicts or - // clears its group: broken => that group is guilty; good => innocent. SPECIAL1 stays - // native throughout (exonerated by vc117). - if (kMode >= 12 && kMode <= 16) - { - if (rs < 0x10 || (op & 0x3f) < 0x3c) - return false; // not SPECIAL2 - const u32 idx = (op & 3) | ((op >> 4) & 0x7c); - const bool acc = idx <= 0x0f || (idx >= 0x18 && idx <= 0x1c) || idx == 0x1e - || (idx >= 0x20 && idx <= 0x2e && idx != 0x2b); - const bool qgrp = idx >= 0x38 && idx <= 0x3b; - const bool mem = (idx >= 0x34 && idx <= 0x37) || (idx >= 0x3c && idx <= 0x3f); - const bool moves = idx == 0x1d || idx == 0x1f || idx == 0x30 || idx == 0x31; - const bool conv = idx >= 0x10 && idx <= 0x17; - switch (kMode) - { - case 12: return !acc; // ONLY ACC native (vc123) - case 13: return !qgrp; // ONLY Q-group native - case 14: return !mem; // ONLY mem/VI native - case 15: return !moves; // ONLY ABS/CLIP/MOVE/MR32 native - case 16: return !conv; // ONLY converts native - } - } - return (rs == 0x01 || rs == 0x02 || rs == 0x05 || rs == 0x06); // transfers only (kMode 1) -} - -// True for ops that run the interpreter inline AND need a live, current cpuRegs.cycle — -// COP2 / VU0-macro ops (opcode 0x12, excluding the BC2 branches which already single-step). -// The VU sync inside the COP2 handler reads cpuRegs.cycle, so the block's accumulated cycles -// must be committed first; x86 does this via `cpuRegs.cycle += scaleblockcycles_clear()` before -// every COP2 op (microVU_Macro.inl). Without it the VU kicks at a stale EE time and geometry -// is submitted a beat early/late (e.g. Crash Twinsanity object pop-in / overlap). -// True for COP2 / VU0-macro ops (opcode 0x12, excluding the BC2 branches) and the COP2 quad -// load/stores (LQC2/SQC2). Their macro-mode handlers may emit a VU0 catch-up sync that reads -// cpuRegs.cycle, so the emit loop stashes the block's accumulated cycles (s_cop2RawCycles) for -// the handler to pass into the M2 sync helpers. The helpers commit those cycles to cpuRegs.cycle -// exactly where x86 does — inside mVUSyncVU0 / the COP2_Interlock SYNC branch — and ONLY when the -// op actually syncs VU0. (The cycles must NOT be committed before a FINISH: _vu0FinishMicro -// overwrites cpuRegs.cycle with VU0.cycle (VU0.cpp), so a pre-commit would be lost; x86 keeps -// the uncommitted cycles in s_nBlockCycles so they ride past the finish.) See recRecompile. -static bool recOpNeedsCycleFlush(u32 op) -{ - // Force-interp COP2 ops run the interpreter inline (like CALLMS) and need a live clock. - if (recCop2ForceInterp(op)) - return true; - if ((op >> 26) == 0x12) - { - if (((op >> 21) & 0x1f) == 0x08) - return false; // BC2 branch — no sync / cycle commit (M4) - // Native Mode-0 ALU ops (M5.1) only ever FINISH (mVUFinishVU0 commits nothing), - // so their cycles must accumulate and ride forward to the next real sync / block - // tail — not be stashed-and-cleared on EEINST_COP2_SYNC_VU0. Treat them like a - // normal op. Transfer ops + still-inline-interp ALU ops keep the stash+clear path. - return !recVUMacroIsMode0(op); - } - return (op >> 26) == OP_LQC2 || (op >> 26) == OP_SQC2; -} - -// True for MFC0/MTC0 of the Count (rd 9) or PERF (rd 25) registers — the COP0 ops the EE -// rec previously single-stepped (recTranslateOpOptimized returns false for them) because -// they read a live cpuRegs.cycle this rec only flushes at the block tail. Games busy-poll -// Count for timing, so the single-step path can dominate EE (Jackie Chan Adventures: ~80% -// of EE fallbacks). recRecompile handles these by committing the block's accumulated cycles -// before an INLINE interp call (so the read is live), instead of the expensive single-step. -// Per-op commit also fixes the historic "two MFC0 Count in one block read the same stale -// value -> games lock up" hazard: each read now advances cpuRegs.cycle. Excludes BC0 / ERET -// / EI / DI / WAIT, which still single-step (they write PC or gate interrupts). -static bool recCop0NeedsLiveCycle(u32 op) -{ - if ((op >> 26) != 0x10) - return false; // COP0 - const u32 rs = (op >> 21) & 0x1f; - if (rs != 0x00 && rs != 0x04) - return false; // MFC0 / MTC0 only (BC0 rs==0x08 + C0 rs==0x10 keep their paths) - const u32 rd = (op >> 11) & 0x1f; - return (rd == 9 || rd == 25); // Count / PERF -} - -// CALLMS (COP2 SPECIAL1 funct 0x38) / CALLMSR (0x39) — x86's only INTERPRETATE_COP2_FUNC ops -// (microVU_Macro.inl:295-296). M5.5 keeps them on the inline interpreter (faithful: the interp -// path self-finishes VU0 and launches the microprogram via vu0ExecMicro), but unlike the native -// FINISH macro ops they must commit the block cycles before the launch — see recRecompile. The -// rs>=0x10 guard restricts to CO/SPECIAL1 ops (excludes the transfer ops, whose low 6 bits are -// rd/sa, not a funct); funct 0x38/0x39 is always SPECIAL1 (SPECIAL2 is funct 0x3c-0x3f). -static bool recCop2IsCallms(u32 op) -{ - if ((op >> 26) != 0x12 || ((op >> 21) & 0x1f) < 0x10) - return false; - const u32 funct = op & 0x3f; - return funct == 0x38 || funct == 0x39; -} - -// -------------------------------------------------------------------------------------- -// Macro mode (Phase 7.9 / M2) — EE↔VU0 sync / interlock emit helpers -// -------------------------------------------------------------------------------------- -// Faithful VIXL ports of microVU_Macro.inl's mVUFinishVU0 / mVUSyncVU0 / COP2_Interlock. -// These emit the *precise, analysis-driven* VU0 catch-up that x86 macro mode does, to -// replace the current blanket inline-interp self-sync (Phase 5.3). They are not wired -// into the COP2 path yet — M3 consumes the M1 EEINST_COP2_* flags through them — so they -// are [[maybe_unused]] for now (no behavior change this phase). -// -// Translation notes vs x86: -// - No EE register allocator on ARM64, so the x86 iFlushCall(FLUSH_FOR_POSSIBLE_MICRO_EXEC) -// / _freeX86reg(eax) calls have no equivalent — we are memory-backed and use the -// caller-saved scratch GPRs directly (M3's transfer ops likewise spill to cpuRegs). -// - x86's `rax` (block-cycle accumulator -> VU0 catch-up delta) maps to RXVIXLSCRATCH (x16), -// which is dead before the ExecuteBlockJIT args are loaded into x0/x1. -// - x86 scaleblockcycles_clear() is reproduced with recScaleBlockCycles(raw): the caller -// passes the block's accumulated raw cycles (s_cop2RawCycles), the helper commits them to -// cpuRegs.cycle here (its `if (raw != 0)` branch), and the emit loop clears its accumulator -// iff this op syncs — see recOpNeedsCycleFlush / s_cop2RawCycles. -// - xLoadFarAddr(arg1reg, CpuVU0) bakes the (stable, post-init) CpuVU0 object pointer as an -// immediate; armMoveAddressToReg(RXARG1, CpuVU0) does the same. s_nBlockInterlocked is a -// compile-time bool baked into arg2 just like x86. - -// Per-block "this block contains an interlocked (cpuRegs.code & 1) COP2 op" flag — x86's -// s_nBlockInterlocked. Set by COP2_Interlock, baked into the ExecuteBlockJIT `interlocked` -// arg, reset per block in recRecompile. -static bool s_nBlockInterlocked = false; - -// mVUFinishVU0: if VU0 is running a micro program (VPU_STAT&1), finish it (run to E-bit). -static void mVUFinishVU0() -{ - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RWARG3, a64::MemOperand(RSCRATCHADDR)); - a64::Label skipvuidle; - armAsm->Tbz(RWARG3, 0, &skipvuidle); // VPU_STAT&1 == 0 -> nothing running - armEmitCall(reinterpret_cast(_vu0FinishMicro)); - armAsm->Bind(&skipvuidle); -} - -// mVUSyncVU0: commit the block's cycles, then if VU0 is running and has fallen >=4 cycles -// behind the EE, run one VU0 block to catch it up (lazy sync, not a full finish). -static void mVUSyncVU0(u32 raw) -{ - const a64::Register rax = RXVIXLSCRATCH; // x16 (dead before the call args are set up) - - // scaleblockcycles_clear(): cpuRegs.cycle += scaled raw; keep the new value in rax. - armAsm->Ldr(rax, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); - if (raw != 0) - { - armAsm->Add(rax, rax, recScaleBlockCycles(raw)); - armAsm->Str(rax, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); - } - - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RWARG3, a64::MemOperand(RSCRATCHADDR)); - a64::Label skipvuidle; - armAsm->Tbz(RWARG3, 0, &skipvuidle); - - // rax -= VU0.cycle (and, under the VU-sync gamefixes, -= VU0.nextBlockCycles) - armMoveAddressToReg(RSCRATCHADDR, &VU0.cycle); - armAsm->Ldr(RXARG3, a64::MemOperand(RSCRATCHADDR)); - armAsm->Sub(rax, rax, RXARG3); - if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.nextBlockCycles); - armAsm->Ldr(RXARG3, a64::MemOperand(RSCRATCHADDR)); - armAsm->Sub(rax, rax, RXARG3); - } - - a64::Label skip; - armAsm->Cmp(rax, 4); - armAsm->B(&skip, a64::lt); // < 4 cycles behind: don't bother running a block - armMoveAddressToReg(RXARG1, CpuVU0); - armAsm->Mov(RWARG2, s_nBlockInterlocked ? 1 : 0); - armEmitCall(reinterpret_cast(&BaseVUmicroCPU::ExecuteBlockJIT)); - armAsm->Bind(&skip); - armAsm->Bind(&skipvuidle); -} - -// COP2_Interlock: the cpuRegs.code & 1 interlocked path. For an interlocked op that the -// M1 MicroFinish pass flagged as needing sync (EEINST_COP2_SYNC_VU0), commit cycles and -// either run-to-catch-up + _vu0WaitMicro (M-bit sync) or _vu0FinishMicro. -static void COP2_Interlock(bool mBitSync, u32 raw) -{ - // vc112: the interpreter does vu0Sync() at the TOP of every COP2 transfer op (VU0.cpp - // QMFC2/CFC2/QMTC2/CTC2) — with a LIVE cpuRegs.cycle — catching VU0 up to the EE clock - // BEFORE any force-finish. The native path skipped this, so its force-finish ran VU0 from a - // stale, far-behind position and broke the VU0 handshake (Ratchet freeze). vc111 added the - // leading vu0Sync but with a STALE clock (block cycles not yet committed), so it only - // PARTIALLY caught VU0 up → VF reads returned stale data (model flicker / stuck transform). - // Fix: commit the block's cycles FIRST so vu0Sync sees the live clock and fully syncs — the - // interpreter's exact order. This is the sole cycle commit for transfer ops now (the handlers - // pass 0 to their downstream mVUSyncVU0 so there is no double-commit). - recEmitCommitBlockCycles(raw); - armEmitCall(reinterpret_cast(::vu0Sync)); // both reads+writes (freeze lives on writes) - - if (!(cpuRegs.code & 1)) - return; - - s_nBlockInterlocked = true; - - // We can safely skip the sync when nothing between CFC2/CTC2/COP2 ops can kick VU0. - if (!(g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0)) - return; - - const a64::Register rax = RXVIXLSCRATCH; // x16 - - armAsm->Ldr(rax, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); // already committed above (vc112) - - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RWARG3, a64::MemOperand(RSCRATCHADDR)); - a64::Label skipvuidle; - armAsm->Tbz(RWARG3, 0, &skipvuidle); - - if (mBitSync) - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.cycle); - armAsm->Ldr(RXARG3, a64::MemOperand(RSCRATCHADDR)); - armAsm->Sub(rax, rax, RXARG3); - - // Ratchet (and maybe others) flicker polygons under lazy COP2 sync unless the - // micro resumption isn't deferred an extra EE block — hence the extra subtract. - if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.nextBlockCycles); - armAsm->Ldr(RXARG3, a64::MemOperand(RSCRATCHADDR)); - armAsm->Sub(rax, rax, RXARG3); - } - - a64::Label skip; - armAsm->Cmp(rax, 4); - armAsm->B(&skip, a64::lt); - armMoveAddressToReg(RXARG1, CpuVU0); - armAsm->Mov(RWARG2, s_nBlockInterlocked ? 1 : 0); - armEmitCall(reinterpret_cast(&BaseVUmicroCPU::ExecuteBlockJIT)); - armAsm->Bind(&skip); - - armEmitCall(reinterpret_cast(::_vu0WaitMicro)); - } - else - { - armEmitCall(reinterpret_cast(_vu0FinishMicro)); - } - armAsm->Bind(&skipvuidle); -} - -// -------------------------------------------------------------------------------------- -// Macro mode (Phase 7.9 / M3) — native COP2 transfer ops (faithful, memory-backed) -// -------------------------------------------------------------------------------------- -// Faithful ports of microVU_Macro.inl's recCFC2/recCTC2/recQMFC2/recQMTC2, with the x86 -// register-allocator calls (_allocX86reg/_allocVFtoXMMreg/_checkXMMreg/_eeMoveGPRtoR…) -// replaced by direct, non-caching memory access: the emit loop has already flushed the EE -// GPR cache to memory before recTranslateOp runs (recTranslateOpOptimized: recCacheFlushAll), -// and recCacheApplyNativeEffects/recConstApplyNativeEffects kill the whole cache after a 0x12 -// op, so reading/writing cpuRegs.GPR and VU0.VI straight from memory is correct. They read -// the *host-side* cpuRegs.code via the _Rt_/_Rd_ macros (and cpuRegs.code & 1 for interlock), -// so the recTranslateOp dispatch must `cpuRegs.code = op` before calling. -// -// Cycle accounting (faithful to x86): the emit loop does NOT pre-commit cpuRegs.cycle. Instead -// it stashes the block's accumulated raw cycles in s_cop2RawCycles and these handlers pass it to -// the M2 sync helpers, which commit it to cpuRegs.cycle (recScaleBlockCycles, x86's -// scaleblockcycles_clear) only on a real SYNC — inside mVUSyncVU0 / the COP2_Interlock SYNC -// branch — and the emit loop clears its accumulator only then. mVUFinishVU0 (and any op that -// doesn't SYNC) commits nothing, so the accumulated cycles ride forward to the next sync / block -// tail. This is essential: _vu0FinishMicro overwrites cpuRegs.cycle with VU0.cycle (VU0.cpp), so -// pre-committing before a finish (as an earlier unconditional pre-flush did) silently lost those -// cycles; x86 keeps them uncommitted in s_nBlockCycles for exactly this reason. - -// recCFC2: VU0 control reg (VI[rd]) -> GPR[rt], with the interlock / lazy-sync prologue and -// the per-register sign/zero-extend the interpreter uses (CFC2 in VU0.cpp). -static void recCFC2() -{ - COP2_Interlock(false, s_cop2RawCycles); - - if (!_Rt_) - return; - - if (!(cpuRegs.code & 1)) - { - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(0); // vc112: cycles already committed in COP2_Interlock — no double-commit - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - } - - const u32 rt = _Rt_; - const u32 rd = _Rd_; - const a64::Register val = RXVIXLSCRATCH; // x16 — dead after the sync calls above - - if (rd == 0) - { - // why would you read vi00? -> 0 - armAsm->Mov(val, 0); - } - else if (rd == REG_I) - { - // sign-extend the 32-bit VI[REG_I] into the 64-bit GPR - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_I].UL); - armAsm->Ldr(val.W(), a64::MemOperand(RSCRATCHADDR)); - armAsm->Sxtw(val, val.W()); - } - else if (rd == REG_R) - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_R].UL); - armAsm->Ldr(val.W(), a64::MemOperand(RSCRATCHADDR)); - armAsm->Sxtw(val, val.W()); - armAsm->And(val, val, 0x7FFFFF); - } - else if (rd >= REG_STATUS_FLAG) // FixMe (x86): should R-Reg have upper 9 bits 0? - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[rd].UL); - armAsm->Ldr(val.W(), a64::MemOperand(RSCRATCHADDR)); - armAsm->Sxtw(val, val.W()); - } - else - { - // zero-extend the low 16 bits of VI[rd] (Ldrh zero-extends to W, W-write clears the - // upper 32 of the X reg -> full 64-bit zero-extend) - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[rd].UL); - armAsm->Ldrh(val.W(), a64::MemOperand(RSCRATCHADDR)); - } - - armAsm->Str(val, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// recCTC2: GPR[rt] -> VU0 control reg (VI[rd]), with the interlock(mBitSync=1)/lazy-sync -// prologue and the per-register write semantics from microVU_Macro.inl:recCTC2 (NOT the -// interpreter CTC2 — macro mode's REG_STATUS path also broadcasts the denormalized sticky -// status flag into VU0.micro_statusflags, which microVU0 reads). Memory-backed: the x86 -// register-allocator (eax/_eeMoveGPRtoR/_allocVFtoXMMreg) becomes direct GPR<->VI loads/ -// stores. _Rd_ is a compile-time constant, so only one switch arm is ever emitted. -static void recCTC2() -{ - COP2_Interlock(true, s_cop2RawCycles); - - if (!_Rd_) - return; - - if (!(cpuRegs.code & 1)) - { - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(0); // vc112: cycles already committed in COP2_Interlock — no double-commit - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - } - - const u32 rt = _Rt_; - const u32 rd = _Rd_; - - switch (rd) - { - case REG_MAC_FLAG: - case REG_TPC: - case REG_VPU_STAT: - break; // read-only regs - - case REG_R: - // VI[R] = (GPR[rt] & 0x7FFFFF) | 0x3F800000 - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(RWARG1, RWARG1, 0x7FFFFF); - armAsm->Orr(RWARG1, RWARG1, 0x3F800000); - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_R].UL); - armAsm->Str(RWARG1, a64::MemOperand(RSCRATCHADDR)); - break; - - case REG_STATUS_FLAG: - { - // VI[STATUS] = (VI[STATUS] & 0x3F) | (rt ? (GPR[rt] & 0xFC0) : 0) - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_STATUS_FLAG].UL); - armAsm->Ldr(RWARG2, a64::MemOperand(RSCRATCHADDR)); - armAsm->And(RWARG2, RWARG2, 0x3F); - if (rt) - { - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(RWARG1, RWARG1, 0xFC0); - armAsm->Orr(RWARG2, RWARG2, RWARG1); - } - armAsm->Str(RWARG2, a64::MemOperand(RSCRATCHADDR)); - - // Update microVU's sticky status flags: denormalize VI[STATUS] and broadcast it - // across all 4 lanes of VU0.micro_statusflags. Inline port of mVUallocSFLAGd - // (aVU_Alloc.inl) — pure bit-math, no microVU reg-alloc — into reg=w0,tmp1=w1,tmp2=w2. - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_STATUS_FLAG].UL); - armAsm->Ldr(RWARG3, a64::MemOperand(RSCRATCHADDR)); // tmp2 = *memAddr - armAsm->Mov(RWARG1, RWARG3); // reg - armAsm->Lsr(RWARG1, RWARG1, 3); - armAsm->And(RWARG1, RWARG1, 0x18); - armAsm->Mov(RWARG2, RWARG3); // tmp1 - armAsm->Lsl(RWARG2, RWARG2, 11); - armAsm->And(RWARG2, RWARG2, 0x1800); - armAsm->Orr(RWARG1, RWARG1, RWARG2); - armAsm->Lsl(RWARG3, RWARG3, 14); - armAsm->And(RWARG3, RWARG3, 0x3cf0000); - armAsm->Orr(RWARG1, RWARG1, RWARG3); - - armMoveAddressToReg(RSCRATCHADDR, &VU0.micro_statusflags[0]); - armAsm->Dup(RQSCRATCH.V4S(), RWARG1); - armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); - break; - } - - case REG_CMSAR1: // Execute VU1 Micro SubRoutine - armAsm->Mov(RWARG1, 1); - armEmitCall(reinterpret_cast(vu1Finish)); - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armEmitCall(reinterpret_cast(vu1ExecMicro)); - break; - - case REG_FBRST: - { - if (!rt) - { - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_FBRST].UL); - armAsm->Str(a64::wzr, a64::MemOperand(RSCRATCHADDR)); - return; - } - - // TEST_FBRST_RESET: GPR[rt] is stable in memory across the reset calls, so reload it - // each time instead of pinning a callee-saved reg (x86 allocs MODE_CALLEESAVED). - a64::Label skip0; - armAsm->Ldr(RWVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Tst(RWVIXLSCRATCH, 0x002); // VU0 Reset - armAsm->B(&skip0, a64::eq); - armEmitCall(reinterpret_cast(vu0ResetRegs)); - armAsm->Bind(&skip0); - - a64::Label skip1; - armAsm->Ldr(RWVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Tst(RWVIXLSCRATCH, 0x200); // VU1 Reset - armAsm->B(&skip1, a64::eq); - armEmitCall(reinterpret_cast(vu1ResetRegs)); - armAsm->Bind(&skip1); - - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(RWARG1, RWARG1, 0x0C0C); - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_FBRST].UL); - armAsm->Str(RWARG1, a64::MemOperand(RSCRATCHADDR)); - break; - } - - case 0: - break; // ignore writes to vi00 - - default: - // VI 1..15 are 16-bit (write US[0]); VI >= REG_STATUS_FLAG (incl. REG_I, whose - // x86 FPR mirror at VF#33 == &VU0.VI[REG_I].F collapses to this memory store with - // no VF cache) take the full 32-bit write. - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[rd].UL); - if (rd < REG_STATUS_FLAG) - { - armAsm->Ldrh(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Strh(RWARG1, a64::MemOperand(RSCRATCHADDR)); - } - else - { - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Str(RWARG1, a64::MemOperand(RSCRATCHADDR)); - } - break; - } -} - -// recQMFC2: VF[rd] (128-bit) -> GPR[rt] (128-bit). Interlock(false)/lazy-sync prologue, then a -// straight quad copy via RQSCRATCH. x86's vf00 cache special-case is moot memory-backed (no VF -// cache); reading VF[0] from memory is the real vf00. -static void recQMFC2() -{ - COP2_Interlock(false, s_cop2RawCycles); - - if (!_Rt_) - return; - - if (!(cpuRegs.code & 1)) - { - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(0); // vc112: cycles already committed in COP2_Interlock — no double-commit - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - } - - armMoveAddressToReg(RSCRATCHADDR, &VU0.VF[_Rd_]); - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); - armAsm->Str(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(_Rt_))); -} - -// recQMTC2: GPR[rt] (128-bit) -> VF[rd] (128-bit). Interlock(true)/lazy-sync prologue; vf00 is -// not writable (early-out), and rt==0 zeroes the destination. -static void recQMTC2() -{ - COP2_Interlock(true, s_cop2RawCycles); - - if (!_Rd_) - return; // can't write vf00 - - if (!(cpuRegs.code & 1)) - { - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(0); // vc112: cycles already committed in COP2_Interlock — no double-commit - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - } - - armMoveAddressToReg(RSCRATCHADDR, &VU0.VF[_Rd_]); - if (_Rt_) - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(_Rt_))); - else - armAsm->Movi(RQSCRATCH.V4S(), 0); - armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); -} - -// recLQC2: memory[GPR[rs] + imm] (128-bit, 16-byte aligned) -> VF[rt]. Unlike the COP2 -// transfer ops above there is NO COP2_Interlock (faithful to microVU_Macro.inl:recLQC2, -// which only does the analysis-driven SYNC/FINISH dispatch); the quad load reuses the -// non-cached vtlb quad path (armEmitVtlbReadQuad), the same slow path armEmitLoadQuad uses. -// Memory-backed: the EE GPR cache is flushed before recTranslateOp and killed after, so the -// effective address reads GPR[rs] straight from cpuRegs. LQC2 to vf00 (!_Rt_) discards. -static void recLQC2() -{ - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(s_cop2RawCycles); - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - - // Effective address into the read helper's first argument register, 16-byte aligned - // (the EE silently aligns 128-bit accesses; matches x86 recLQC2's xAND(arg1regd, ~0xF)). - armEmitEffectiveAddr(RWARG1, _Rs_, _Imm_); - armAsm->And(RWARG1, RWARG1, ~0x0F); - - // Perform the read even when discarding (vf00) — the access can have I/O side effects. - // The call inside ReadQuad clobbers v0-v7/v16-v31, so the Mov to RQSCRATCH is after it. - armEmitVtlbReadQuad(RQSCRATCH, RWARG1); - - if (!_Rt_) - return; // loading to vf00 -> toss away - - armMoveAddressToReg(RSCRATCHADDR, &VU0.VF[_Rt_]); - armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); -} - -// recSQC2: VF[rt] (128-bit) -> memory[GPR[rs] + imm] (16-byte aligned). No COP2_Interlock -// (faithful to microVU_Macro.inl:recSQC2 — SYNC/FINISH dispatch only). vf00 stores VU0.VF[0] -// (memory-backed: no microVU VF cache to special-case). Reuses the non-cached vtlb quad path. -static void recSQC2() -{ - if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - mVUSyncVU0(s_cop2RawCycles); - else if (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) - mVUFinishVU0(); - - // Load VF[rt] (vf00 reads VU0.VF[0]) into the quad scratch before computing the address; - // WriteQuad moves it to q0 before its call, so it only needs to live until then. - armMoveAddressToReg(RSCRATCHADDR, &VU0.VF[_Rt_]); - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); - - // Effective address into the write helper's first argument register, 16-byte aligned. - armEmitEffectiveAddr(RWARG1, _Rs_, _Imm_); - armAsm->And(RWARG1, RWARG1, ~0x0F); - - armEmitVtlbWriteQuad(RWARG1, RQSCRATCH); -} - -// Install a freshly-compiled block's self-modifying-code protection and return the pointer -// to record in its recLUT slot. Direct port of x86 memory_protect_recompiled_code -// (iR5900.cpp), adapted to this port's body-first layout: the caller has already emitted -// the block body + dispatch tail (entry `body_entry`); for a manually-protected page we -// emit a checksum prologue AFTER the body and make THAT the block entry (it verifies the -// guest code and branches into the body). -// -// The whole point: on Apple Silicon the host page is 16 KB (4 KB on x86), so a single data -// write — e.g. an FMV frame the IPU/EE streams into RAM — can sit on the same page as -// compiled code and fault it. Tier 1 (Write) re-protects read-only and recompiles on every -// such write, which thrashes. Once a page has faulted it becomes Manual: we stop -// re-protecting it and instead self-check the code bytes on each block entry, so pure data -// writes no longer fault or invalidate. Blocks are kept within a single host page (see the -// page-boundary stop in recRecompile) so one page's mode governs the whole block. -// -// Assumes `body_entry` is the start of a real compiled block (not an interpreter -// single-step block — those re-read guest memory every run and need no protection). -static u8* recEmitManualProtection(u32 startpc, u32 endpc, u8* body_entry) -{ - const u32 size_bytes = endpc - startpc; - const u32 size_words = size_bytes >> 2; - - // The kernel/EENULL thread-context pages alias one physical page across many virtual - // mappings; always treat them as manual (matches x86). - const bool contains_thread_stack = ((startpc >> 12) == 0x81) || ((startpc >> 12) == 0x80001); - const vtlb_ProtectionMode mode = contains_thread_stack ? ProtMode_Manual : mmap_GetRamPageInfo(startpc); - - // Index into manual_page/counter by host RAM page, matching the vtlb's m_PageProtectInfo. - const u32 rampage = static_cast( - (reinterpret_cast(PSM(startpc)) - reinterpret_cast(eeMem->Main)) >> __pageshift); - - switch (mode) - { - case ProtMode_NotRequired: - // ROM / unbacked — never written, nothing to protect. - return body_entry; - - case ProtMode_None: - case ProtMode_Write: - // Cheap tier: write-protect the page so a future write faults and clears us. - mmap_MarkCountedRamPage(startpc); - manual_page[rampage] = 0; - return body_entry; - - case ProtMode_Manual: - default: - break; - } - - // Manual tier: emit the runtime self-check prologue. It becomes the block's entry. - u8* const prologue = armGetCurrentCodePointer(); - - // Args for the discard / page-reset helpers, kept live across the checks below - // (the checks only touch x9/w10/w11). - armAsm->Mov(RWARG1, startpc); // x0 = startpc (guest vaddr) - armAsm->Mov(RWARG2, size_bytes); // x1 = block size in bytes - - // Compare every compiled guest word against the value captured at compile time. A - // mismatch means the code itself changed (real SMC / module reload) -> discard. - const u8* const base = static_cast(PSM(startpc)); - armMoveAddressToReg(a64::x9, base); - for (u32 i = 0; i < size_words; i++) - { - const u32 captured = *reinterpret_cast(base + i * 4); - armAsm->Ldr(a64::w10, a64::MemOperand(a64::x9, i * 4)); - armAsm->Mov(a64::w11, captured); - armAsm->Cmp(a64::w10, a64::w11); - armEmitCondBranch(a64::ne, DispatchBlockDiscard); - } - - // Counted heuristic: a Manual block that runs a lot periodically retries cheap - // write-protection (in case the write that demoted the page was a one-off). After the - // page has been retried enough times (manual_counter > 3) it stays Manual permanently. - if (!contains_thread_stack && manual_counter[rampage] <= 3) - { - armMoveAddressToReg(a64::x9, &manual_page[rampage]); - armAsm->Ldrh(a64::w10, a64::MemOperand(a64::x9)); - armAsm->Add(a64::w10, a64::w10, size_words); - armAsm->Strh(a64::w10, a64::MemOperand(a64::x9)); // truncates to 16 bits, like x86 xADD ptr16 - armAsm->Tst(a64::w10, 0x10000); // carry out of the 16-bit accumulator - armEmitCondBranch(a64::ne, DispatchPageReset); - } - - armEmitJmp(body_entry); - return prologue; -} - -// -------------------------------------------------------------------------------------- -// Dispatcher stubs (Phase 4.4) -// -------------------------------------------------------------------------------------- -// Entered when DispatcherReg looks up a guest PC whose 64 KB page has no recompiler -// slot array (scratchpad / hardware registers / TLB-mapped code we don't cover yet). -// Logs once and bails out of the rec via the exit longjmp, mirroring x86 recError(0). -static void recExitUnmapped() -{ - Console.Error("ARM64 EE rec: jump to unmapped recLUT page (PC=0x%08x)", cpuRegs.pc); - eeRecExitRequested.store(true, std::memory_order_release); - longjmp(s_jmp_buf, 1); -} - -// Emit the four dispatcher stubs into one contiguous block at the head of the code -// cache. They reference each other by label (DispatcherEvent / JITCompile / Enter / -// Unmapped all fall through to DispatcherReg) and are recorded as raw entry pointers. -// Regenerated on every reset; because recLUT, recEventTest and recRecompile live at -// fixed addresses, regeneration is byte-identical at the same location — which is why -// recRecompile can reset the cache mid-compile and safely return into JITCompile. -static void recGenDispatchers() -{ - armSetAsmPtr(recPtr, recPtrEnd - recPtr, &s_const_pool); - armStartBlock(); - - a64::Label dispatcher_reg; - - // DispatcherReg: fnptr = *(uptr*)(recLUT[pc>>16] + pc*2); br fnptr. - // - // Re-pin RESTATEPTR (x19) = &cpuRegs on every dispatch. Although EnterRecompiledCode - // establishes it once, the C++ callees we re-enter through (recEventTest -> - // _cpuEventTest_Shared in particular, which services DMA/VIF and runs other ARM64 JIT) - // do NOT preserve x19 across the call — so by the time control returns to the - // dispatcher it can hold garbage. Reloading it here (the single point every block, - // event-test and compile path funnels back through) keeps it authoritative cheaply, - // instead of relying on every external callee honouring the reservation. - DispatcherReg = armGetCurrentCodePointer(); - armAsm->Bind(&dispatcher_reg); - armMoveAddressToReg(RESTATEPTR, &cpuRegs); - armLoadPtr(REVTLBPTR, &vtlb_private::vtlbdata.vmap); - if (CHECK_FASTMEM) - armLoadPtr(RFASTMEMBASE, &vtlb_private::vtlbdata.fastmem_base); // x28 = host-MMU fastmem base - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_PC_OFFSET)); // x0 = pc (zero-extended) - armAsm->Lsr(RXARG2, RXARG1, 16); // x1 = pc >> 16 - armMoveAddressToReg(RXARG3, recLUT); // x2 = &recLUT[0] - armAsm->Ldr(RXARG3, a64::MemOperand(RXARG3, RXARG2, a64::LSL, 3)); // x2 = recLUT[page] - armAsm->Add(RXARG3, RXARG3, a64::Operand(RXARG1, a64::LSL, 1)); // x2 = base + pc*2 - armAsm->Ldr(RXARG3, a64::MemOperand(RXARG3)); // x2 = fnptr - armAsm->Br(RXARG3); - - // DispatcherEvent: run the EE event test, then fall through to DispatcherReg (which - // re-pins RESTATEPTR, since recEventTest clobbers it). - DispatcherEvent = armGetCurrentCodePointer(); - armEmitCall(reinterpret_cast(recEventTest)); - armAsm->B(&dispatcher_reg); - - // JITCompile: compile the block at cpuRegs.pc (which sets its recLUT slot), then - // re-dispatch — the slot now points at the freshly compiled block. - JITCompile = armGetCurrentCodePointer(); - armAsm->Ldr(RWARG1, a64::MemOperand(RESTATEPTR, EE_PC_OFFSET)); - armEmitCall(reinterpret_cast(recRecompile)); - armAsm->B(&dispatcher_reg); - - // EnterRecompiledCode: the C entry point. Pin RESTATEPTR (x19) = &cpuRegs once, - // then dispatch. We never return through here (exit is a longjmp out of - // recEventTest), so callee-saved registers need no preserving — longjmp restores - // recExecute's full context. Blocks therefore need no per-block prologue/epilogue. - EnterRecompiledCode = armGetCurrentCodePointer(); - armMoveAddressToReg(RESTATEPTR, &cpuRegs); - armLoadPtr(REVTLBPTR, &vtlb_private::vtlbdata.vmap); - if (CHECK_FASTMEM) - armLoadPtr(RFASTMEMBASE, &vtlb_private::vtlbdata.fastmem_base); // x28 = host-MMU fastmem base - armAsm->B(&dispatcher_reg); - - // UnmappedRecLUTPage: target for every word of an unmapped guest page. - UnmappedRecLUTPage = armGetCurrentCodePointer(); - armEmitCall(reinterpret_cast(recExitUnmapped)); - armAsm->B(&dispatcher_reg); - - // DispatchBlockDiscard / DispatchPageReset: the tails of a manually-protected block's - // entry checksum (see recEmitManualProtection). The checksum prologue has already loaded - // x0 = startpc and x1 = block size (bytes) and branches here on failure; we run the C - // helper, then re-dispatch (the slot now points back at JITCompile, so it recompiles). - DispatchBlockDiscard = armGetCurrentCodePointer(); - armEmitCall(reinterpret_cast(dyna_block_discard)); - armAsm->B(&dispatcher_reg); - - DispatchPageReset = armGetCurrentCodePointer(); - armEmitCall(reinterpret_cast(dyna_page_reset)); - armAsm->B(&dispatcher_reg); - - recPtr = armEndBlock(); -} - -// Emit a block's tail: charge the block's scaled guest cycles, then the inline event -// test. Mirrors iR5900.cpp iBranchTest (dynamic-target form): if (s64)(cycle - -// nextEventCycle) < 0 there is no event due, so jump straight back into the dispatcher -// (DispatcherReg re-reads cpuRegs.pc and chains into the next block); otherwise fall -// to DispatcherEvent to service events first. `add_cycles` is false for interpreter -// single-step blocks, which charge their own cycles inside intExecuteOneInst. -// `waitloop_selfpc`: non-zero marks this block as a detected wait/idle loop with -// the given start PC. The tail then checks whether the branch was taken back to -// the loop start and, if so, bumps cpuRegs.cycle up to nextEventCycle so the next -// event fires after one iteration instead of the EE busy-spinning host-side until -// the event (the main EE-at-99%/heat case for polling loops). -static void recEmitEventTestAndDispatch(u32 scaled_cycles, bool add_cycles, bool known_dispatch_pc, u32 dispatch_pc, - u32 waitloop_selfpc = 0) -{ - armAsm->Ldr(RXARG1, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); // x0 = cpuRegs.cycle (u64) - if (add_cycles) - armAsm->Add(RXARG1, RXARG1, scaled_cycles); - armAsm->Ldr(RXARG2, a64::MemOperand(RESTATEPTR, EE_NEXTEVENTCYCLE_OFFSET)); - if (waitloop_selfpc != 0) - { - a64::Label no_bump; - armAsm->Ldr(RWARG3, a64::MemOperand(RESTATEPTR, EE_PC_OFFSET)); - armAsm->Mov(RWARG4, waitloop_selfpc); - armAsm->Cmp(RWARG3, RWARG4); - armAsm->B(&no_bump, a64::ne); // branch not taken back to loop start: normal tail - armAsm->Cmp(RXARG1, RXARG2); - armAsm->Csel(RXARG1, RXARG2, RXARG1, a64::lt); // cycle = max(cycle, nextEventCycle) - armAsm->Bind(&no_bump); - } - if (add_cycles || waitloop_selfpc != 0) - armAsm->Str(RXARG1, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); - - a64::Label no_forced_exit; - armMoveAddressToReg(RXARG3, const_cast(&eeRecExitSignal)); - armAsm->Ldrb(RWARG3, a64::MemOperand(RXARG3)); - armAsm->Cbz(RWARG3, &no_forced_exit); - armEmitCall(reinterpret_cast(recCheckExitAfterInterp)); - armAsm->Ldr(RXARG1, a64::MemOperand(RESTATEPTR, EE_CYCLE_OFFSET)); - armAsm->Ldr(RXARG2, a64::MemOperand(RESTATEPTR, EE_NEXTEVENTCYCLE_OFFSET)); - armAsm->Bind(&no_forced_exit); - - armAsm->Cmp(RXARG1, RXARG2); - - if (known_dispatch_pc) - { - armEmitCondBranch(a64::pl, DispatcherEvent); // event due => service before continuing - if (s_eeBlockLinkEnabled) - recEmitLinkableExitToKnownPc(dispatch_pc); // patchable direct B, staged for linking - else - recEmitDispatchToKnownPc(dispatch_pc); // LUT-indirect fallback - return; - } - - armEmitCondBranch(a64::mi, DispatcherReg); // N set => (cycle - nextEvent) < 0 => continue - armEmitJmp(DispatcherEvent); -} - -// -------------------------------------------------------------------------------------- -// EEINST inst-cache (Phase 7.9 M0.2 — macro-mode analysis substrate) -// -------------------------------------------------------------------------------------- -// Per-block instruction-info array, mirroring the x86 rec's s_pInstCache. The M1 COP2 -// analysis passes write the EEINST_COP2_* bits here per instruction, and the macro-mode -// emit (M2/M3) reads them off g_pCurInstInfo. Indexed by (pc - startpc) >> 2. -// -// Unlike x86 (whose blocks run unbounded until a branch, so it mallocs+grows the cache), -// ARM64 blocks are capped to MAX_BLOCK_INSTS guest ops and one host page, so a fixed -// array suffices: + a branch delay slot + the x86-style end sentinel. -static constexpr u32 EE_INST_CACHE_SIZE = MAX_BLOCK_INSTS + 4; -static EEINST s_instCache[EE_INST_CACHE_SIZE]; -static u32 s_eeEndBlock = 0; // first pc past the current block (x86 s_nEndBlock equiv.) - -// Forward pre-scan of the block range, mirroring the x86 rec's s_nEndBlock walk -// (ix86-32/iR5900.cpp:2292) but matching THIS rec's actual block boundaries so the -// EEINST indices line up with what the emit loop below compiles. It over-approximates -// safely: it ends only at a control-flow op (branch/jump + delay slot), a host-page -// boundary, or the instruction cap — exactly the emit loop's terminators *except* the -// "un-compilable op ends the block early" case, which only makes the real block shorter. -// So the scanned range is always >= the emitted range, keeping every g_pCurInstInfo -// index in bounds. (No compile is attempted here — it is pure opcode inspection.) -static u32 recScanBlockEnd(u32 startpc) -{ - u32 pc = startpc; - u32 count = 0; - for (;;) - { - // Host-page boundary — same single-page-per-block rule as the emit loop. - if (pc != startpc && (pc & ~__pagemask) != (startpc & ~__pagemask)) - break; - - if (count >= MAX_BLOCK_INSTS) - break; - - const u32 op = memRead32(pc); - count++; - - // Branch / branch-likely: the block ends after the delay slot (pc += 8), exactly - // as the emit loop terminates. (A J/JAL/JR/JALR/Bcc or a likely Bccl.) - if (recIsHandledBranch(op) || recIsLikelyBranch(op)) - { - pc += 8; - break; - } - - pc += 4; - } - return pc; -} - -// Skip MPEG game-fix (CHECK_SKIPMPEGHACK) — ported from the x86 rec's -// skipMPEG_By_Pattern (ix86-32/iR5900.cpp). It was previously x86-only, so the -// "Skip MPEG" toggle did nothing on Android (this ARM64 EE backend). The PS2 -// sceMpegIsEnd routine is a tiny 3-instruction leaf: -// lw reg, 0x40(a0) ; jr ra ; lw v0, 0(reg) -// When the fix is on and a block starts exactly on that pattern, we don't -// recompile it — we stub it to force v0 = 1 ("movie finished") and return to ra, -// so games that spin waiting for an FMV to end skip it (Katamari et al.). The -// pattern detection is architecture-independent; the host state change is done by -// this C helper (called via armEmitCall) so no hand-emitted field writes are -// needed, and the normal block finalize then event-tests + dispatches from the -// cpuRegs.pc the helper set. -static void eeSkipMpegIsEnd() -{ - cpuRegs.GPR.n.v0.UD[0] = 1; // v0 = 1 (UL[0] = 1, UL[1] = 0) - cpuRegs.pc = cpuRegs.GPR.n.ra.UL[0]; // jr ra -} - -// Returns true (and emits the stub call) when [startpc] is the sceMpegIsEnd leaf -// and the fix is enabled. s_eeEndBlock must already be set (recScanBlockEnd). -static bool recTrySkipMpeg(u32 startpc) -{ - if (!CHECK_SKIPMPEGHACK) - return false; - - // Exactly three words, middle op == `jr ra` (0x03e00008). - if (s_eeEndBlock != startpc + 12 || memRead32(startpc + 4) != 0x03e00008u) - return false; - - const u32 code = memRead32(startpc); - const u32 p1 = 0x8c800040u; // lw ?, 0x40(a0) - const u32 p2 = 0x8c020000u | ((code & 0x1f0000u) << 5); // lw v0, 0(reg) - if ((code & 0xffe0ffffu) != p1) - return false; - if (memRead32(startpc + 8) != p2) - return false; - - armEmitCall(reinterpret_cast(eeSkipMpegIsEnd)); - Console.WriteLn("sceMpegIsEnd pattern found! Recompiling skip video fix... [ARM64]"); - return true; -} - -// -------------------------------------------------------------------------------------- -// Block compiler (Phase 4.3 / 4.4) -// -------------------------------------------------------------------------------------- -// Compile a straight-line run starting at startpc into one host block and install its -// entry into the recLUT slot for startpc. Unlike the Phase 4.3 version this block has -// NO prologue/epilogue and never RETs — RESTATEPTR is pinned once by EnterRecompiledCode -// and the block ends by branching into the dispatcher (via recEmitEventTestAndDispatch). -// The run: -// - emits straight-line ops we can codegen inline; -// - stops at the first control-flow op we have a generator for (branch + delay slot), -// the branch generator having written cpuRegs.pc; -// - if the *first* op is one we can't codegen, emits a one-shot block that single-steps -// it through the interpreter (intExecuteOneInst handles its own PC/delay/cycles); -// - otherwise ends at the next un-compilable op (or the length cap), writing cpuRegs.pc -// so the next dispatch resumes there. -// Thunk carving for fastmem backpatch (@@MAC_FASTMEM_BACKPATCH@@) — ported from the stock -// arm64 backend (arm64/aR5900.cpp). Carves a scratch code region from the EE code buffer -// with NO const pool, so armEmitJmp/armEmitCall inside the thunk inline the target through -// x16 rather than routing via a trampoline (x16 is scratch, clobbered by the call anyway). -u8* recBeginThunk() -{ - if (recPtr >= recPtrEnd) - eeRecNeedsReset = true; - armSetAsmPtr(recPtr, recPtrEnd - recPtr, nullptr); - recPtr = armStartBlock(); - return recPtr; -} - -u8* recEndThunk() -{ - u8* block_end = armEndBlock(); - pxAssert(block_end < recPtrEnd); - recPtr = block_end; - return block_end; -} - -static void recRecompile(u32 startpc) -{ - const u32 hw_startpc = recHWAddr(startpc); - - // Reset the whole cache if the emit cursor has run within one block's worth of the - // constant-pool tail. Doing it here (before emitting) is safe: the dispatcher stubs - // regenerate byte-identically at the same addresses, so the JITCompile stub this - // call returns into is unchanged. Mirrors x86 recRecompile. - if (recPtr >= recPtrEnd - RECOMPILE_HEADROOM) - eeRecNeedsReset = true; - - if (hw_startpc == VMManager::Internal::GetCurrentELFEntryPoint()) - { - VMManager::Internal::EntryPointCompilingOnCPUThread(); - } - - if (eeRecNeedsReset) - recResetRaw(); - - // Each block starts with no staged link exit; only the known-target tail sets one. - s_eeLinkStaged = false; - - armSetAsmPtr(recPtr, recPtrEnd - recPtr, &s_const_pool); - u8* const entry = armStartBlock(); - - if (hw_startpc == EELOAD_START) - { - const u32 mainjump = memRead32(EELOAD_START + 0x9c); - if (mainjump >> 26 == 3) // JAL - g_eeloadMain = ((EELOAD_START + 0xa0) & 0xf0000000U) | ((mainjump << 2) & 0x0fffffffu); - } - - if (g_eeloadMain && hw_startpc == recHWAddr(g_eeloadMain)) - { - armEmitCall(reinterpret_cast(eeloadHook)); - if (VMManager::Internal::IsFastBootInProgress()) - { - const u32 typeAexecjump = memRead32(EELOAD_START + 0x470); - const u32 typeBexecjump = memRead32(EELOAD_START + 0x5b0); - const u32 typeCexecjump = memRead32(EELOAD_START + 0x618); - const u32 typeDexecjump = memRead32(EELOAD_START + 0x600); - if ((typeBexecjump >> 26 == 3) || (typeCexecjump >> 26 == 3) || (typeDexecjump >> 26 == 3)) - g_eeloadExec = EELOAD_START + 0x2b8; - else if (typeAexecjump >> 26 == 3) - g_eeloadExec = EELOAD_START + 0x170; - else - Console.WriteLn("recRecompile: Could not enable launch arguments for fast boot mode; unidentified BIOS version! Please report this to the PCSX2 developers."); - } - } - - if (g_eeloadExec && hw_startpc == recHWAddr(g_eeloadExec)) - { - armEmitCall(reinterpret_cast(eeloadHook2)); - } - - u32 pc = startpc; - u32 endpc = startpc; - u32 raw_cycles = 0; - // EE memory-speed multiplier: when the COP0 Config.DIE (i-cache enable) bit is clear, every - // EE instruction costs double cycles. Read at compile time, matching the x86 rec's per-op - // accounting in recompileNextInstruction (iR5900.cpp). Without it cpuRegs.cycle advances at - // half rate whenever DIE is clear, so the EE runs ahead of the GS/VU/IOP/VBlank schedule. - const u32 ee_cycle_mult = 2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1); - // Per-op cycle cost incl. the x86 rec's NOP special-case (a real NOP is treated as ~9 cycles). - const auto eeOpCycles = [ee_cycle_mult](u32 opc) -> u32 { - return (opc == 0 ? 9u : static_cast(::R5900::GetInstruction(opc).cycles)) * ee_cycle_mult; - }; - u32 compiled = 0; - bool interp_step = false; - bool known_dispatch_pc = false; - u32 dispatch_pc = 0; - u32 waitloop_selfpc = 0; - u32 waitloop_ops[REC_WAITLOOP_MAX_OPS]; - u32 waitloop_num_ops = 0; - bool waitloop_possible = true; - RecGprConstState const_state; - RecGprCacheState cache_state; - - // @@EEDIFF@@ Snapshot the diff-verify enable ONCE per block. When set, every op is - // wrapped with a pre/post interpreter compare and the GPR/const cache is forced off - // (memory stays authoritative between ops so the snapshot/compare is exact). The - // toggle clears the whole EE block cache, so all blocks compiled while it is on carry - // the hooks and all compiled while off are hook-free (zero overhead). - const bool ee_diff = g_ee_diff_verify; - - // Macro mode (M2): reset the per-block "contains an interlocked COP2 op" flag. Set by - // COP2_Interlock during emit, baked into the VU0 ExecuteBlockJIT `interlocked` arg. - s_nBlockInterlocked = false; - - // Build the per-block EEINST inst-cache (Phase 7.9 M0.2). Pre-scan the block range - // and clear one EEINST slot per instruction so the M1 COP2 analysis passes have a - // place to write and the emit loop can expose a g_pCurInstInfo per op. No emit/ - // behavior change yet — the flags computed here are not consumed until M3. - s_eeEndBlock = recScanBlockEnd(startpc); - { - u32 ninst = (s_eeEndBlock - startpc) >> 2; - if (ninst >= EE_INST_CACHE_SIZE) - ninst = EE_INST_CACHE_SIZE - 1; // can't happen (range is capped) — defensive - std::memset(s_instCache, 0, sizeof(EEINST) * (ninst + 1)); // +1: end sentinel - } - - // Phase 7.9 M1 — COP2 macro-mode analysis passes. Only worth running when the block - // actually contains COP2 / LQC2 / SQC2 ops (mirrors the x86 rec's has_cop2_instructions - // gate). The passes write the EEINST_COP2_* bits into s_instCache using the no-offset - // convention (base = s_instCache, instruction at pc -> s_instCache[(pc-startpc)>>2]), - // matching the per-op g_pCurInstInfo the emit loop hands out below. The flags are - // computed-ready but NOT consumed yet (consumption starts in M3) — no behavior change. - // Call order matches x86 (MicroFinish then, under vuFlagHack, FlagHack). - { - bool has_cop2 = false; - for (u32 i = startpc; i < s_eeEndBlock; i += 4) - { - const u32 op26 = memRead32(i) >> 26; - if (op26 == 022 || op26 == 066 || op26 == 076) // COP2 / LQC2 / SQC2 - { - has_cop2 = true; - break; - } - } - if (has_cop2) - { - R5900::COP2MicroFinishPass().Run(startpc, s_eeEndBlock, s_instCache); - if (EmuConfig.Speedhacks.vuFlagHack) - R5900::COP2FlagHackPass().Run(startpc, s_eeEndBlock, s_instCache); - - eeDumpCOP2AnnotatedBlock(startpc, s_eeEndBlock, s_instCache); // M1.3 (env-gated) - } - } - - // Skip MPEG game-fix: if enabled and this block IS the sceMpegIsEnd leaf, stub it - // (force v0=1 + jr ra) instead of recompiling, then fall straight into the normal - // block finalize/dispatch below — which resumes at cpuRegs.pc (= ra) the stub set. - const bool skipped_mpeg = recTrySkipMpeg(startpc); - if (skipped_mpeg) - { - endpc = s_eeEndBlock; // the 3-word leaf [startpc, startpc+12) - raw_cycles = eeOpCycles(memRead32(startpc)) + eeOpCycles(0x03e00008u) + - eeOpCycles(memRead32(startpc + 8)); - // known_dispatch_pc stays false -> dispatch dynamically from cpuRegs.pc. - } - - for (; !skipped_mpeg;) - { - // Keep every block within a single host RAM page so its SMC protection mode (see - // recEmitManualProtection) governs the whole block, and so a page-fault clear of - // the block's page always hits the block's start slot. (A branch's delay slot may - // still spill one word into the next page — an accepted corner, as on x86.) - if (pc != startpc && (pc & ~__pagemask) != (startpc & ~__pagemask)) - { - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - - const u32 op = memRead32(pc); - - // Point g_pCurInstInfo at this instruction's EEINST slot (M0.2). The pre-scan - // guarantees the index is in bounds (its range >= the emitted range); clamp - // defensively all the same. Consumed by the macro-mode COP2 emit from M3 on. - { - u32 idx = (pc - startpc) >> 2; - if (idx >= EE_INST_CACHE_SIZE) - idx = EE_INST_CACHE_SIZE - 1; - g_pCurInstInfo = &s_instCache[idx]; - } - - // @@EEDIFF@@ Force the guest state committed to memory before EVERY op so the - // diff verifier's snapshot/compare (and the interpreter re-run, which reads - // cpuRegs) sees authoritative state. Writes back dirty cached GPRs, drops the - // cache, and clears const tracking so no op's inputs live only in a host reg or - // the compiler's const map. Cheap: only when the diagnostic is on. - if (ee_diff) - { - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recConstKillAll(const_state); - } - - // COP0 DI — the interrupt-disable must take effect one instruction LATE, exactly as - // the x86 recDI (iCOP0.cpp): emit the *following* guest instruction first, then the - // Status.EIE clear. Without this delay several games disable IRQs one op too early - // and hang at boot (Jak X, Namco 50th Anniversary, SpongeBob the Movie / Battle for - // Bikini Bottom, The Incredibles (+ Rise of the Underminer), Soukou Kihei Armodyne, - // Garfield: Saving Arlene, Tales of Fandom Vol. 2). The delayed op is emitted - // straight-line in program order (recEmitOp), then recEmitCop0DI; the pair advances - // pc by 8. A DI in a branch delay slot never reaches here (delay slots go through - // recEmitOp, where DI inline-interprets immediately — matching x86's - // g_recompilingDelaySlot path). If the following op can't be safely spliced inline - // (control-flow / PC-writing / exception / cycle-sensitive), fall through to end the - // block at DI and single-step it (rare; DI then applies immediately). - if (recIsCop0DI(op)) - { - const u32 next_op = memRead32(pc + 4); - if (!recIsHandledBranch(next_op) && !recIsLikelyBranch(next_op) && - !recCop0DelayOpUnsafe(next_op)) - { - raw_cycles += eeOpCycles(op); - - // Compile the delayed instruction (point g_pCurInstInfo at its slot for any - // analysis-driven emit), then apply DI after it has executed. - { - u32 nidx = ((pc + 4) - startpc) >> 2; - if (nidx >= EE_INST_CACHE_SIZE) - nidx = EE_INST_CACHE_SIZE - 1; - g_pCurInstInfo = &s_instCache[nidx]; - } - recEmitOp(next_op, const_state, cache_state, pc + 4); - recEmitCop0DI(); - raw_cycles += eeOpCycles(next_op); - - // A block containing a DI is not a poll loop. - waitloop_possible = false; - pc += 8; - endpc = pc; - compiled += 2; - if (compiled >= MAX_BLOCK_INSTS) - { - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - continue; - } - // else: fall through — recTranslateOpOptimized(DI) returns false below, so the - // block ends here / single-steps DI (no cycles charged for DI on this path). - } - - // MIPS trap ops (TGE/TLT/TEQ/TNE + immediate forms). Native, block-conditional: - // emit a compare and, when the trap is NOT taken (the overwhelmingly common case), - // branch over the raise block and STAY in-block — no block-terminate, no dispatch - // round-trip (the regression that made these single-step every execution). On the - // rare taken path we run the interpreter op (which raises via cpuException, setting - // cpuRegs.pc to the exception vector), commit the block's cycles, and tail into the - // dispatcher — mirroring x86's recBranchCall, but only for the taken path. We must - // make memory authoritative first (the compare reads guest GPRs; the taken path's - // interpreter reads cpuRegs), so flush + kill the GPR/const cache exactly as a - // block-terminating branch does. - if (recIsTrap(op)) - { - raw_cycles += eeOpCycles(op); - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recConstKillAll(const_state); - - a64::Label skip; - recEmitTrapCompareIfTrap(op, &skip); // compare + B(skip) when NOT taken - recEmitWritePc(pc + 4); // trap() does pc-=4 -> EPC = trap pc - recEmitInterpInline(op); // trap taken: raise -> cpuRegs.pc = vector - recEmitCommitBlockCycles(raw_cycles); // commit cycles incl. the trap - armEmitJmp(DispatcherEvent); // event test + re-dispatch from cpuRegs.pc - armAsm->Bind(&skip); // NOT taken: fall through, stay in-block - - waitloop_possible = false; // a block with a trap is not a poll loop - pc += 4; - endpc = pc; - if (++compiled >= MAX_BLOCK_INSTS) - { - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - continue; - } - - if (recIsHandledBranch(op)) - { - // Terminate the block: branch generator + delay slot + dispatch tail. - raw_cycles += eeOpCycles(op); - known_dispatch_pc = recGetKnownBranchTarget(op, pc, const_state, &dispatch_pc); - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recEmitBranch(op, pc); // writes cpuRegs.pc (taken/fallthrough/link) - recConstApplyBranchLink(op, pc, const_state); - - const u32 delay_op = memRead32(pc + 4); - raw_cycles += eeOpCycles(delay_op); - recEmitOp(delay_op, const_state, cache_state, pc + 4); // delay slot — must not write cpuRegs.pc - endpc = pc + 8; - - // Wait-loop detection: does this branch loop back to the block start with a - // body that carries no register state between iterations? Non-linking forms - // only (J/BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ). Unconditional self-loops can only - // exit via an event, so the skip is exact and always enabled; conditional - // (polling) loops follow the WaitLoop speedhack toggle like the x86 rec. - { - const u32 opc = op >> 26; - const u32 looptarget = (opc == 0x02) ? - (((op & 0x03ffffff) << 2) | ((pc + 4) & 0xf0000000u)) : - ((pc + 4) + (static_cast(static_cast(static_cast(op))) << 2)); - const u32 cond_reads = recBranchConditionReads(op); - const bool unconditional = recBranchIsUnconditional(op); - - if (looptarget == startpc && waitloop_possible && waitloop_num_ops == compiled && - cond_reads != 0xffffffffu && (unconditional || EmuConfig.Speedhacks.WaitLoop) && - recWaitLoopBodyIsPure(waitloop_ops, waitloop_num_ops, cond_reads, delay_op)) - { - waitloop_selfpc = startpc; - } - } - break; - } - - if (recIsLikelyBranch(op)) - { - // Branch-likely: delay slot executes ONLY when taken. Emit the condition - // test + PC select, then jump over the delay-slot code when not taken. - // The cache/const state diverges across the two paths, so it is flushed - // and discarded inside the taken path before the skip label. - raw_cycles += eeOpCycles(op); - - const u32 btarget = (pc + 4) + (static_cast(static_cast(static_cast(op))) << 2); - const u32 fallthrough = pc + 8; - - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - - const a64::Condition taken = armEmitBranchLikelyTest(op, btarget, fallthrough); - a64::Label skip_delay; - armAsm->B(&skip_delay, a64::InvertCondition(taken)); - - const u32 delay_op = memRead32(pc + 4); - raw_cycles += eeOpCycles(delay_op); - recEmitOp(delay_op, const_state, cache_state, pc + 4); - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recConstKillAll(const_state); - - armAsm->Bind(&skip_delay); - endpc = pc + 8; - break; - } - - // COP2 / VU0-macro ops: the cycle commit happens INSIDE the macro-mode sync helpers, - // exactly where x86 does it (mVUSyncVU0 / the COP2_Interlock SYNC branch) and only for - // ops that actually SYNC VU0. Stash the block's accumulated cycles (incl. this op's, - // matching x86 order) for the handler to pass to the helper; clear the accumulator only - // when a commit is emitted — iff the op syncs (EEINST_COP2_SYNC_VU0), which is the union - // of both helpers' compile-time commit gate. FINISH-only / no-sync ops leave the cycles - // in the accumulator so they ride forward (x86 keeps them in s_nBlockCycles), surviving - // the _vu0FinishMicro cpuRegs.cycle = VU0.cycle collapse a pre-commit would have lost. - const bool needs_cycle_flush = recOpNeedsCycleFlush(op); - if (needs_cycle_flush) - { - raw_cycles += eeOpCycles(op); - s_cop2RawCycles = raw_cycles; - if (recCop2IsCallms(op) || recCop2ForceInterp(op)) - { - // CALLMS/CALLMSR are x86's only INTERPRETATE_COP2_FUNC ops: they commit the - // scaled block cycles to cpuRegs.cycle and clear the accumulator - // (scaleblockcycles_clear) BEFORE the inline interpreter runs vu0ExecMicro, - // which sets VU0.cycle = cpuRegs.cycle — so the launched VU0 microprogram sees - // the committed EE time. The native FINISH macro ops correctly ride cycles - // forward (mVUFinishVU0 commits nothing; _vu0FinishMicro collapses cpuRegs.cycle), - // but a LAUNCH does not collapse it, so for these two ops the cycles must be - // committed here. Emitted before recTranslateOpOptimized's cache flush + interp - // call below, mirroring x86's order (commit, then recCall(V##f)). - // vc105: force-interp COP2 ops (FullVU0SyncHack) are ALSO run via the inline - // interpreter, whose vu0Sync/_vu0FinishMicro read cpuRegs.cycle — same live-clock - // requirement as CALLMS, so they take this commit-then-inline path too. - recEmitCommitBlockCycles(s_cop2RawCycles); - raw_cycles = 0; - } - else if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) - raw_cycles = 0; - } - - // MFC0/MTC0 of Count(rd9)/PERF(rd25): commit the block's cycles (incl. this op) so - // the read is live, clear the accumulator, then INLINE-interp in-block — instead of - // the expensive single-step path (these were ~80% of Jackie Chan's EE fallbacks, a - // Count busy-poll). Same commit-then-inline shape as the CALLMS launch above; the - // per-op commit makes consecutive Count reads see an advancing cpuRegs.cycle (no - // stale-value lock-up). See recCop0NeedsLiveCycle + the COP0 note in recTranslateOpOptimized. - if (recCop0NeedsLiveCycle(op)) - { - raw_cycles += eeOpCycles(op); - recEmitCommitBlockCycles(raw_cycles); - raw_cycles = 0; - // Flush + kill the GPR register cache / const tracking before the inline interp, - // exactly like the trap path (recCacheFlushAll/KillAll/ConstKillAll above): MTC0 - // reads cpuRegs.GPR.r[rt] from memory (must be authoritative) and MFC0 WRITES it — - // a stale cached copy in a callee-saved host reg would survive the C call and shadow - // the Count value, silently breaking the very poll loop this targets. - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recConstKillAll(const_state); - recEmitInterpInline(op); - waitloop_possible = false; // inline live-cycle op — not a wait-loop body - pc += 4; - endpc = pc; - if (++compiled >= MAX_BLOCK_INSTS) - { - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - continue; - } - - // COP0 EI / ERET (CO ops, rs==0x10; funct 0x38 / 0x18). Faithful to x86 - // recEI/recERET (recBranchCall): run the interpreter handler in-block, then END - // the block so the tail event-tests + dispatches from cpuRegs.pc. EI: a now- - // unmasked pending IRQ gets serviced by that event test (x86 "must branch after - // enabling interrupts"); ERET: it writes cpuRegs.pc, so the dispatch must read it. - // Like the handled-branch path, the block's cycles ride in raw_cycles and the - // post-loop commits them (x86's iBranchTest also commits AFTER the Interp call) — - // no early commit, so no spurious tail +1. Replaces the per-op single-step (the - // top EE interpreter fallback after the BC0/MADD work). - if (recIsCop0EIorERET(op)) - { - raw_cycles += eeOpCycles(op); - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - recConstKillAll(const_state); - if (!recIsCop0ERET(op)) - recEmitWritePc(pc + 4); // EI doesn't write PC; resume after it unless the event test diverts - recEmitInterpInline(op); // Interp::EI sets Status.EIE / Interp::ERET sets cpuRegs.pc - waitloop_possible = false; - known_dispatch_pc = false; // dispatch from cpuRegs.pc: an IRQ raised by the tail event test, or ERET's new pc - endpc = pc + 4; - break; - } - - // @@EEDIFF@@ Diagnostic path: wrap the op with snapshot + interpreter re-run + - // compare. Uses the raw recTranslateOp (memory-committed generators) because the - // cache/const were just killed for this op. Wait-loop detection is disabled here - // (waitloop_possible is forced false below) so the extra hook calls never sit in a - // "pure" body. Falls through to the normal un-compilable handling if there is no - // native generator (an interp-fallback op can't diverge from itself). - if (ee_diff) - { - if (recEmitDiffVerifyOp(op, pc)) - { - waitloop_possible = false; // verified block is never a wait-loop - if (!needs_cycle_flush) - raw_cycles += eeOpCycles(op); - pc += 4; - endpc = pc; - if (++compiled >= MAX_BLOCK_INSTS) - { - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - continue; - } - // else: no native generator — fall through to the shared un-compilable path, - // which single-steps the op on the interpreter (no verify needed). - } - // Straight-line op we can codegen? (Generators decode from `op` directly; - // they never read cpuRegs.code, so nothing to set here at compile time.) - else if (recTranslateOpOptimized(op, const_state, cache_state, pc)) - { - // Record the body for wait-loop analysis (only short blocks qualify). - if (waitloop_num_ops < REC_WAITLOOP_MAX_OPS) - waitloop_ops[waitloop_num_ops++] = op; - else - waitloop_possible = false; - - if (!needs_cycle_flush) - raw_cycles += eeOpCycles(op); - pc += 4; - endpc = pc; - if (++compiled >= MAX_BLOCK_INSTS) - { - recEmitWritePc(pc); // resume at the next instruction - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - continue; - } - - // Un-compilable op (likely branch / syscall / COP0 / MMI SIMD / ...). - if (compiled == 0) - { - // Block starts on it — emit a one-shot interpreter single-step block. It - // runs exactly one guest instruction (handling its own PC, delay slot and - // cycle accounting), then re-dispatches via the tail. No compiled cycles to - // charge (intExecuteOneInst does that itself). - armEmitCall(reinterpret_cast(intExecuteOneInst)); - armEmitCall(reinterpret_cast(recCheckExitAfterInterp)); - endpc = pc + 4; - interp_step = true; - break; - } - - // End the block here; the next dispatch will single-step this op. - recEmitWritePc(pc); - known_dispatch_pc = true; - dispatch_pc = pc; - break; - } - - recCacheFlushAll(cache_state); - recCacheKillAll(cache_state); - - recEmitEventTestAndDispatch(interp_step ? 0 : recScaleBlockCycles(raw_cycles), !interp_step, - !interp_step && known_dispatch_pc, dispatch_pc, waitloop_selfpc); - - // Apply SMC protection (must emit any checksum prologue into this block's stream before - // armEndBlock flushes it). `block_entry` is what subsequent dispatches jump to. - u8* block_entry = entry; - if (interp_step) - { - // Single-step interp blocks re-read guest memory each run -> no checksum needed. - // Still keep the page's protection state consistent: mark a fresh page counted, but - // never re-protect a page that's already Manual (that would revive the write-fault - // thrash the Manual tier exists to avoid). - const vtlb_ProtectionMode mode = mmap_GetRamPageInfo(startpc); - if (mode == ProtMode_None || mode == ProtMode_Write) - mmap_MarkCountedRamPage(startpc); - } - else - { - block_entry = recEmitManualProtection(startpc, endpc, entry); - } - - recPtr = armEndBlock(); - - // Install the block so subsequent dispatches to startpc (and its address mirrors) - // branch straight into it instead of recompiling. - *recPtrToBlock(startpc) = reinterpret_cast(block_entry); - - // Register for direct-B block chaining: resolve forward links (target already - // compiled) and back-patch any predecessors that were waiting on this block. - if (s_eeBlockLinkEnabled) - recRegisterBlockLinks(startpc, block_entry); -} - -static void recEventTest() -{ - const auto exit_execution = []() { - eeRecExitRequested.store(false, std::memory_order_release); - eeRecExitSignal = 0; - longjmp(s_jmp_buf, 1); - }; - - VMState st = VMManager::GetState(); - if (st == VMState::Stopping || st == VMState::Shutdown) - exit_execution(); - - _cpuEventTest_Shared(); - - st = VMManager::GetState(); - if (eeRecExitRequested.load(std::memory_order_acquire) || - st == VMState::Stopping || st == VMState::Shutdown) - exit_execution(); -} - -static void recCheckExitAfterInterp() -{ - const VMState st = VMManager::GetState(); - if (eeRecExitRequested.load(std::memory_order_acquire) || - st == VMState::Stopping || st == VMState::Shutdown) - { - eeRecExitRequested.store(false, std::memory_order_release); - eeRecExitSignal = 0; - longjmp(s_jmp_buf, 1); - } -} - -// C entry point. Pins the exit longjmp target, then jumps into the generated -// EnterRecompiledCode stub, which establishes RESTATEPTR and runs blocks chained -// entirely in host code (block -> DispatcherReg -> block ...). Control only returns -// here via the longjmp in recEventTest (state-check / exit request). -static void recExecute() -{ - if (eeRecNeedsReset || !EnterRecompiledCode) - recResetRaw(); - - if (setjmp(s_jmp_buf) != 0) - { - eeRecExecuting = false; - return; - } - - eeRecExecuting = true; - - // Cancel-instruction landing pad. An inline-interpreted op (recEmitInterpInline) that - // aborts the in-flight guest instruction — a vtlb TLB miss (vtlb.cpp), an address error - // (R5900OpcodeImpl RaiseAddressError), or a met MIPS trap raised by the native trap - // block's interpreter call — reaches Cpu->CancelInstruction() -> recCancelInstruction() - // -> longjmp(s_cancel_jmp_buf). We land here (NOT the s_jmp_buf exit path), then - // re-dispatch into the recompiled code from cpuRegs.pc, which cpuException already set to - // the exception vector. The faulting op never reached intUpdateCPUCycles, so charge a - // small fixed cycle (matching the old backend's +8) to guarantee forward progress and let - // any due event fire before re-entry. Then recEventTest() runs the shared event test - // AND honors a pending exit (VMState Stopping/Shutdown or eeRecExitRequested) by - // longjmp'ing to s_jmp_buf above, so Stop/Shutdown still unwinds. (Native traps that - // DON'T longjmp commit cycles + tail to DispatcherEvent themselves; this pad only - // catches the interp calls that abort via cpuException.) Mirrors trak's cancel pad. - if (setjmp(s_cancel_jmp_buf) != 0) - { - cpuRegs.cycle += 8; - recEventTest(); - } - - reinterpret_cast(reinterpret_cast(EnterRecompiledCode))(); - // EnterRecompiledCode never returns; the only way out is one of the longjmps above. -} - -static void recSafeExitExecution() -{ - // Ask the dispatcher loop to fastjmp out at the next event test. Forcing the - // event cycle to 0 guarantees the test fires after the current block. - eeRecExitRequested.store(true, std::memory_order_release); - eeRecExitSignal = 1; - cpuRegs.nextEventCycle = 0; -} - -static void recCancelInstruction() -{ - // Raised when an inline-interpreted op aborts the in-flight guest instruction: a vtlb - // TLB miss (vtlb.cpp), an address error (R5900OpcodeImpl RaiseAddressError), or a met - // MIPS trap (trap() -> cpuException(0x34) -> Cpu->CancelInstruction()). cpuException has - // already rewritten cpuRegs.pc to the exception vector; we must NOT exit recExecute (that - // would stop the EE), only unwind the in-flight interp call and re-dispatch from the new - // PC. This matches the old arm64 backend and the interpreter's intCancelInstruction, both - // of which longjmp back into their loop. - longjmp(s_cancel_jmp_buf, 1); -} - -static void recClear(u32 addr, u32 size) -{ - // Targeted invalidation (Phase 4.5): reset only the recLUT slots covering - // [addr, addr+size) back to JITCompile, so the next dispatch to any of those guest - // words recompiles fresh. The orphaned host code for the discarded blocks is - // reclaimed at the next full cache reset (when recPtr wraps past recPtrEnd in - // recRecompile). This mirrors the x86 rec's per-range clear instead of the old - // bring-up whole-cache reset: recResetRaw rebuilds the dispatchers AND rewrites the - // entire multi-million-entry recLUT, and Cpu->Clear is called a page at a time - // (MapTLB issues one 0x400 clear per mapped TLB page during BIOS setup), so a - // whole-cache reset per call made boot effectively hang. - // - // Safe while executing: recClear is always invoked synchronously on the EE thread - // (a store page-fault or an interpreted TLBWI), so there is no concurrent block. An - // in-flight block whose slot we clear keeps running its still-valid host code to - // completion, then re-dispatches through DispatcherReg, which recompiles the slot. - if (!JITCompile) - return; // rec not yet generated — nothing compiled to invalidate. - - const u32 end = addr + size; - for (u32 pc = addr & ~3u; pc < end; pc += 4) - { - uptr* const slot = recPtrToBlock(pc); - // Skip unmapped guest pages: their slots all alias one shared page pointing at - // UnmappedRecLUTPage; don't turn an unmapped word into a compile-on-jump word. - if (*slot != reinterpret_cast(UnmappedRecLUTPage)) - *slot = reinterpret_cast(JITCompile); - } - - // Unpatch any direct-B links whose target is in the cleared range BEFORE that - // host code is recycled, so no predecessor can branch into a stale block. - // recHWAddr is linear over the (small, intra-mirror) cleared range. - if (s_eeBlockLinkEnabled) - { - const u32 start_pc = addr & ~3u; - const u32 span = (addr + size) - start_pc; - const u32 start_hw = recHWAddr(start_pc); - // Tripwire: the flat [start_hw, start_hw+span) range assumes recHWAddr is - // linear across the cleared span (no RAM/BIOS mirror-fold crossing) — true for - // every current caller (page-aligned RAM, 0x400 TLB spans). Catch a future one. - pxAssert(span < 4 || recHWAddr(start_pc) + span == recHWAddr(start_pc + span - 4) + 4); - eeInvalidateLinks(start_hw, start_hw + span); - } -} - -// Called (via the DispatchBlockDiscard stub) when a manually-protected block fails its -// entry checksum: the guest code really changed, so throw the block away and recompile. -// `start` is the guest startpc, `sz` the block size in bytes. Mirrors x86 dyna_block_discard. -static void dyna_block_discard(u32 start, u32 sz) -{ - recClear(start, sz); -} - -// Called (via the DispatchPageReset stub) when a counted manual block has run enough times -// to be worth retrying cheap write-protection: clear the whole page's blocks, bump the -// per-page retry counter, and re-arm vtlb write protection. Mirrors x86 dyna_page_reset. -static void dyna_page_reset(u32 start, u32 sz) -{ - recClear(start & ~__pagemask, __pagesize); - const u32 rampage = static_cast( - (reinterpret_cast(PSM(start)) - reinterpret_cast(eeMem->Main)) >> __pageshift); - manual_counter[rampage]++; - mmap_MarkCountedRamPage(start); -} - -R5900cpu recCpu = { - recReserve, - recShutdown, - - recResetEE, - recStep, - recExecute, - - recSafeExitExecution, - recCancelInstruction, - recClear}; - diff --git a/pcsx2/arm64/aR5900Analysis.android.cpp b/pcsx2/arm64/aR5900Analysis.android.cpp deleted file mode 100644 index 362ba67e9b..0000000000 --- a/pcsx2/arm64/aR5900Analysis.android.cpp +++ /dev/null @@ -1,466 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) macro-mode analysis — Phase 7.9 (M0). -// -// Arch-neutral analysis ported faithfully from the x86 recompiler. This file is -// only compiled on ARM64 (pcsx2arm64Sources). It holds no VIXL / NEON emission; -// it is pure decode/analysis over EE opcodes, so the logic is copied verbatim -// from pcsx2/x86/ix86-32/iR5900.cpp (cop2flags) and pcsx2/x86/iR5900Analysis.cpp -// (the COP2 passes, ported in M1). Keeping it in its own TU mirrors the x86 -// split and keeps aR5900.cpp focused on codegen. - -#include "arm64/aR5900Analysis.h" - -#include "Config.h" -#include "Memory.h" -#include "R5900.h" -#include "VU.h" -#include "DebugTools/Debug.h" - -#include "common/Console.h" - -#include -#include - - -using namespace R5900; - -// Set per-op in recRecompile (aR5900.cpp); the macro-mode emit reads the COP2_* -// bits on g_pCurInstInfo->info to drive lazy VU0 sync. Defined here so both the -// analysis passes and the rec share one definition. -EEINST* g_pCurInstInfo = nullptr; - -// opcode 'code' modifies: -// 1: status -// 2: MAC -// 4: clip -// -// Verbatim port of cop2flags() from pcsx2/x86/ix86-32/iR5900.cpp:1384 — pure bit -// decode, no x86 specifics. (022 == 0x12 == COP2 primary opcode.) -int cop2flags(u32 code) -{ - if (code >> 26 != 022) - return 0; // not COP2 - if ((code >> 25 & 1) == 0) - return 0; // a branch or transfer instruction - - switch (code >> 2 & 15) - { - case 15: - switch (code >> 6 & 0x1f) - { - case 4: // ITOF* - case 5: // FTOI* - case 12: // MOVE MR32 - case 13: // LQI SQI LQD SQD - case 15: // MTIR MFIR ILWR ISWR - case 16: // RNEXT RGET RINIT RXOR - return 0; - case 7: // MULAq, ABS, MULAi, CLIP - if ((code & 3) == 1) // ABS - return 0; - if ((code & 3) == 3) // CLIP - return 4; - return 3; - case 11: // SUBA, MSUBA, OPMULA, NOP - if ((code & 3) == 3) // NOP - return 0; - return 3; - case 14: // DIV, SQRT, RSQRT, WAITQ - if ((code & 3) == 3) // WAITQ - return 0; - return 1; // but different timing, ugh - default: - break; - } - break; - case 4: // MAXbc - case 5: // MINbc - case 12: // IADD, ISUB, IADDI - case 13: // IAND, IOR - case 14: // VCALLMS, VCALLMSR - return 0; - case 7: - if ((code & 1) == 1) // MAXi, MINIi - return 0; - return 3; - case 10: - if ((code & 3) == 3) // MAX - return 0; - return 3; - case 11: - if ((code & 3) == 3) // MINI - return 0; - return 3; - default: - break; - } - return 3; -} - -// -------------------------------------------------------------------------------------- -// COP2 analysis passes — faithful port of pcsx2/x86/iR5900Analysis.cpp -// -------------------------------------------------------------------------------------- -// Ported verbatim; the only ARM64 difference is the inst-cache indexing convention -// (no-offset map — see aR5900Analysis.h and MACRO_MODE_PLAN.md Phase M1). The backward- -// liveness back-prop tables (recBackpropBSC etc.) are NOT ported — ARM64 has no EE -// register allocator/liveness pass yet, and these two COP2 passes don't depend on them. - -AnalysisPass::AnalysisPass() = default; - -AnalysisPass::~AnalysisPass() = default; - -void AnalysisPass::Run(u32 start, u32 end, EEINST* inst_cache) -{ -} - -template -void __fi AnalysisPass::ForEachInstruction(u32 start, u32 end, EEINST* inst_cache, const F& func) -{ - EEINST* eeinst = inst_cache; - for (u32 apc = start; apc < end; apc += 4, eeinst++) - { - cpuRegs.code = memRead32(apc); - if (!func(apc, eeinst)) - break; - } -} - -template -void __fi R5900::AnalysisPass::DumpAnnotatedBlock(u32 start, u32 end, EEINST* inst_cache, const F& func) -{ - std::string d; - EEINST* eeinst = inst_cache; - for (u32 apc = start; apc < end; apc += 4, eeinst++) - { - const u32 code = memRead32(apc); - d.clear(); - ::R5900::disR5900Fasm(d, code, apc, false); - func(apc, eeinst, d); - Console.WriteLn(" %08X %08X %s", apc, code, d.c_str()); - } -} - -COP2FlagHackPass::COP2FlagHackPass() - : AnalysisPass() -{ -} - -COP2FlagHackPass::~COP2FlagHackPass() = default; - -void COP2FlagHackPass::Run(u32 start, u32 end, EEINST* inst_cache) -{ - m_status_denormalized = false; - m_last_status_write = nullptr; - m_last_mac_write = nullptr; - m_last_clip_write = nullptr; - m_cfc2_pc = start; - - ForEachInstruction(start, end, inst_cache, [this, end](u32 apc, EEINST* inst) { - // catch SB/SH/SW to potential DMA->VIF0->VU0 exec. - // this is very unlikely in a cop2 chain. - if (_Opcode_ == 050 || _Opcode_ == 051 || _Opcode_ == 053) - { - CommitAllFlags(); - return true; - } - else if (_Opcode_ != 022) - { - // not COP2 - return true; - } - - // Detect ctc2 Status, zero, ..., cfc2 v0, Status pattern where we need accurate sticky bits. - // Test case: Tekken Tag Tournament. - if (_Rs_ == 6 && _Rd_ == REG_STATUS_FLAG) - { - // Read ahead, looking for cfc2. - m_cfc2_pc = apc; - ForEachInstruction(apc, end, inst, [this](u32 capc, EEINST*) { - if (_Opcode_ == 022 && _Rs_ == 2 && _Rd_ == REG_STATUS_FLAG) - { - m_cfc2_pc = capc; - return false; - } - return true; - }); -#ifdef PCSX2_DEVBUILD - if (m_cfc2_pc != apc) - DevCon.WriteLn("CTC2 at %08X paired with CFC2 %08X", apc, m_cfc2_pc); -#endif - } - - // CFC2/CTC2 - if (_Rs_ == 6 || _Rs_ == 2) - { - switch (_Rd_) - { - case REG_STATUS_FLAG: - CommitStatusFlag(); - break; - case REG_MAC_FLAG: - CommitMACFlag(); - break; - case REG_CLIP_FLAG: - CommitClipFlag(); - break; - case REG_FBRST: - { - // only apply to CTC2, is FBRST readable? - if (_Rs_ == 2) - CommitAllFlags(); - } - break; - } - } - - if (((cpuRegs.code >> 25 & 1) == 1) && ((cpuRegs.code >> 2 & 15) == 14)) - { - // VCALLMS, everything needs to be up to date - CommitAllFlags(); - } - - // 1 - status, 2 - mac, 3 - clip - const int flags = cop2flags(cpuRegs.code); - if (flags == 0) - return true; - - // STATUS - if (flags & 1) - { - if (!m_status_denormalized) - { - inst->info |= EEINST_COP2_DENORMALIZE_STATUS_FLAG; - m_status_denormalized = true; - } - - // If we're still behind the next CFC2 after the sticky bits got cleared, we need to update flags. - // Also do this if we're a vsqrt/vrsqrt/vdiv, these update status unconditionally. - const u32 sub_opcode = (cpuRegs.code & 3) | ((cpuRegs.code >> 4) & 0x7c); - if (apc < m_cfc2_pc || (_Rs_ >= 020 && _Funct_ >= 074 && sub_opcode >= 070 && sub_opcode <= 072)) - inst->info |= EEINST_COP2_STATUS_FLAG; - - m_last_status_write = inst; - } - - // MAC - if (flags & 2) - { - m_last_mac_write = inst; - } - - // CLIP - if (flags & 4) - { - // we don't track the clip flag yet.. - // but it's unlikely that we'll have more than 4 clip flags in a row, because that would be pointless? - inst->info |= EEINST_COP2_CLIP_FLAG; - m_last_clip_write = inst; - } - - return true; - }); - - CommitAllFlags(); - -#if 0 - if (m_cfc2_pc != start) - DumpAnnotatedBlock(start, end, inst_cache); -#endif -} - -void COP2FlagHackPass::DumpAnnotatedBlock(u32 start, u32 end, EEINST* inst_cache) -{ - AnalysisPass::DumpAnnotatedBlock(start, end, inst_cache, [](u32, EEINST* eeinst, std::string& d) { - if (eeinst->info & EEINST_COP2_DENORMALIZE_STATUS_FLAG) - d.append(" COP2_DENORMALIZE_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_NORMALIZE_STATUS_FLAG) - d.append(" COP2_NORMALIZE_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_STATUS_FLAG) - d.append(" COP2_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_MAC_FLAG) - d.append(" COP2_MAC_FLAG"); - if (eeinst->info & EEINST_COP2_CLIP_FLAG) - d.append(" COP2_CLIP_FLAG"); - }); -} - -void COP2FlagHackPass::CommitStatusFlag() -{ - if (m_last_status_write) - { - m_last_status_write->info |= EEINST_COP2_STATUS_FLAG | EEINST_COP2_NORMALIZE_STATUS_FLAG; - m_status_denormalized = false; - } -} - -void COP2FlagHackPass::CommitMACFlag() -{ - if (m_last_mac_write) - m_last_mac_write->info |= EEINST_COP2_MAC_FLAG; -} - -void COP2FlagHackPass::CommitClipFlag() -{ - if (m_last_clip_write) - m_last_clip_write->info |= EEINST_COP2_CLIP_FLAG; -} - -void COP2FlagHackPass::CommitAllFlags() -{ - CommitStatusFlag(); - CommitMACFlag(); - CommitClipFlag(); -} - -COP2MicroFinishPass::COP2MicroFinishPass() = default; - -COP2MicroFinishPass::~COP2MicroFinishPass() = default; - -void COP2MicroFinishPass::Run(u32 start, u32 end, EEINST* inst_cache) -{ - bool needs_vu0_sync = true; - bool needs_vu0_finish = true; - bool block_interlocked = CHECK_FULLVU0SYNCHACK; - - // First pass through the block to find out if it's interlocked or not. If it is, we need to use tighter - // synchronization on all COP2 instructions, otherwise Crash Twinsanity breaks. - ForEachInstruction(start, end, inst_cache, [&block_interlocked](u32 apc, EEINST* inst) { - if (_Opcode_ == 022 && (_Rs_ == 001 || _Rs_ == 002 || _Rs_ == 005 || _Rs_ == 006) && cpuRegs.code & 1) - { - block_interlocked = true; - return false; - } - return true; - }); - - ForEachInstruction(start, end, inst_cache, [this, start, end, inst_cache, &needs_vu0_sync, &needs_vu0_finish, block_interlocked](u32 apc, EEINST* inst) { - // Catch SQ/SB/SH/SW/SD to potential DMA->VIF0->VU0 exec. - // Also VCALLMS/VCALLMSR, that can start a micro, so the next instruction needs to finish it. - // This is very unlikely in a cop2 chain. - if (_Opcode_ == 050 || _Opcode_ == 051 || _Opcode_ == 053 || _Opcode_ == 077 || (_Opcode_ == 022 && _Rs_ >= 020 && (_Funct_ == 070 || _Funct_ == 071))) - { - // If we started a micro, we'll need to finish it before the first COP2 instruction. - needs_vu0_sync = true; - needs_vu0_finish = true; - inst->info |= EEINST_COP2_FLUSH_VU0_REGISTERS; - return true; - } - - // LQC2/SQC2 - these don't interlock with VU0, but still sync, so we can persist the cached registers - // for a LQC2..COP2 sequence. If there's no COP2 instructions following, don't bother, just yolo it. - // We do either a sync or a finish here depending on which COP2 instruction follows - we don't want - // to run the program until end if there's nothing which would actually trigger that. - // - // In essence, what we're doing is moving the finish from the COP2 instruction to the LQC2 in a LQC2..COP2 - // chain, so that we can preserve the cached registers and not need to reload them. - // - const bool is_lqc_sqc = (_Opcode_ == 066 || _Opcode_ == 076); - const bool is_non_interlocked_move = (_Opcode_ == 022 && _Rs_ < 020 && ((cpuRegs.code & 1) == 0)); - // Moving zero to the VU registers, so likely removing a loop/lock. - const bool likely_clear = _Opcode_ == 022 && _Rs_ < 020 && _Rs_ > 004 && _Rt_ == 000; - if ((needs_vu0_sync && (is_lqc_sqc || is_non_interlocked_move)) || likely_clear) - { - bool following_needs_finish = false; - // No-offset look-ahead: instruction at (apc + 4) maps to &inst_cache[(apc + 4 - start) >> 2] - // (x86 used the placeholder inst_cache + 1 here; the EEINST* is unused in this lambda, but - // we keep the producer on the same convention as the consumer — see header note). - ForEachInstruction(apc + 4, end, &inst_cache[(apc + 4 - start) >> 2], [&following_needs_finish](u32 apc2, EEINST* inst2) { - if (_Opcode_ == 022) - { - // For VCALLMS/VCALLMSR, we only sync, because the VCALLMS in itself will finish. - // Since we're paying the cost of syncing anyway, better to be less risky. - if (_Rs_ >= 020 && (_Funct_ == 070 || _Funct_ == 071)) - return false; - - // Allow the finish from COP2 to be moved to the first LQC2 of LQC2..QMTC2..COP2. - // Otherwise, keep searching for a finishing COP2. - following_needs_finish = _Rs_ >= 020; - if (following_needs_finish) - return false; - } - - return true; - }); - if (following_needs_finish && !block_interlocked) - { - inst->info |= EEINST_COP2_FLUSH_VU0_REGISTERS | EEINST_COP2_FINISH_VU0; - needs_vu0_sync = false; - needs_vu0_finish = false; - } - else - { - inst->info |= EEINST_COP2_FLUSH_VU0_REGISTERS | EEINST_COP2_SYNC_VU0; - needs_vu0_sync = block_interlocked || (is_non_interlocked_move && likely_clear); - needs_vu0_finish = true; - } - - return true; - } - - // Look for COP2 instructions. - if (_Opcode_ != 022) - return true; - - // Set the flag on the current instruction, and clear it for the next. - if (_Rs_ >= 020 && needs_vu0_finish) - { - inst->info |= EEINST_COP2_FLUSH_VU0_REGISTERS | EEINST_COP2_FINISH_VU0; - needs_vu0_finish = false; - needs_vu0_sync = false; - } - else if (needs_vu0_sync) - { - // Starting a sync-free block! - inst->info |= EEINST_COP2_FLUSH_VU0_REGISTERS | EEINST_COP2_SYNC_VU0; - needs_vu0_sync = block_interlocked; - } - - return true; - }); -} - -// -------------------------------------------------------------------------------------- -// M1.3 — env-gated annotated dump (ARM64 verification aid) -// -------------------------------------------------------------------------------------- -// x86 gates its DumpAnnotatedBlock behind compile-time #if 0; on ARM64 we expose a -// runtime env switch (EE_COP2_DUMP=1) so a block's computed COP2 flags can be spot- -// checked against an x86 build without a recompile. Pure diagnostic — no behavior change. -// Iterates the same no-offset inst-cache the passes wrote (instruction at pc ↔ -// inst_cache[(pc-start)>>2]). -void eeDumpCOP2AnnotatedBlock(u32 start, u32 end, EEINST* inst_cache) -{ - static const bool enabled = (std::getenv("EE_COP2_DUMP") != nullptr); - if (!enabled) - return; - - Console.WriteLn("-- COP2 block %08X - %08X --", start, end); - std::string d; - EEINST* eeinst = inst_cache; - for (u32 apc = start; apc < end; apc += 4, eeinst++) - { - const u32 code = memRead32(apc); - d.clear(); - ::R5900::disR5900Fasm(d, code, apc, false); - if (eeinst->info & EEINST_COP2_DENORMALIZE_STATUS_FLAG) - d.append(" COP2_DENORMALIZE_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_NORMALIZE_STATUS_FLAG) - d.append(" COP2_NORMALIZE_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_STATUS_FLAG) - d.append(" COP2_STATUS_FLAG"); - if (eeinst->info & EEINST_COP2_MAC_FLAG) - d.append(" COP2_MAC_FLAG"); - if (eeinst->info & EEINST_COP2_CLIP_FLAG) - d.append(" COP2_CLIP_FLAG"); - if (eeinst->info & EEINST_COP2_SYNC_VU0) - d.append(" COP2_SYNC_VU0"); - if (eeinst->info & EEINST_COP2_FINISH_VU0) - d.append(" COP2_FINISH_VU0"); - if (eeinst->info & EEINST_COP2_FLUSH_VU0_REGISTERS) - d.append(" COP2_FLUSH_VU0_REGISTERS"); - Console.WriteLn(" %08X %08X %s", apc, code, d.c_str()); - } -} - diff --git a/pcsx2/arm64/aR5900Arith.android.cpp b/pcsx2/arm64/aR5900Arith.android.cpp deleted file mode 100644 index d67967c40f..0000000000 --- a/pcsx2/arm64/aR5900Arith.android.cpp +++ /dev/null @@ -1,775 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — integer arithmetic codegen (Phase 3.2). -// -// Generates ARM64 for the MIPS I-type and R-type integer arithmetic opcodes: -// I-type: ADDI/ADDIU/SLTI/SLTIU/ANDI/ORI/XORI/LUI/DADDI/DADDIU (Phase 3.1) -// R-type: ADD/ADDU/SUB/SUBU/SLT/SLTU/AND/OR/XOR/NOR/DADD/DADDU/DSUB/DSUBU -// -// No register allocator yet — every source GPR is loaded from cpuRegs in memory -// (via RESTATEPTR = &cpuRegs), computed in a scratch register, and stored back. -// $zero writes are silently discarded, matching interpreter semantics. - -#include "aR5900.h" - -#include "R5900.h" - -#include - - - -namespace a64 = vixl::aarch64; - -// Scratch register for arithmetic ops (caller-saved, not used by any helper). -static const a64::Register RSCRATCH = RSCRATCHADDR; -static const a64::Register RSCRATCHW = RSCRATCHADDR.W(); - -// ------------------------------------------------------------------------ -// ADDI / ADDIU (primary opcodes 0x08 / 0x09) -// Rt = (s32)(GPR[rs].UL[0] + imm) -// The x86 JIT treats ADDI identically to ADDIU (skips the overflow trap); -// we follow that model. -// ------------------------------------------------------------------------ -void armEmitADDI(u32 rt, u32 rs, s32 imm) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm != 0) - armAsm->Add(RSCRATCHW, RSCRATCHW, imm); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -void armEmitADDIU(u32 rt, u32 rs, s32 imm) -{ - armEmitADDI(rt, rs, imm); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// DADDI / DADDIU (primary opcodes 0x18 / 0x19) -// Rt = GPR[rs].UD[0] + (s64)imm -// ------------------------------------------------------------------------ -void armEmitDADDI(u32 rt, u32 rs, s32 imm) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm != 0) - armAsm->Add(RSCRATCH, RSCRATCH, imm); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -void armEmitDADDIU(u32 rt, u32 rs, s32 imm) -{ - armEmitDADDI(rt, rs, imm); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// SLTI (primary opcode 0x0A) -// Rt = (GPR[rs].SD[0] < (s64)imm) ? 1 : 0 -// ------------------------------------------------------------------------ -void armEmitSLTI(u32 rt, u32 rs, s32 imm) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cmp(RSCRATCH, imm); - armAsm->Cset(RSCRATCH, a64::lt); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// SLTIU (primary opcode 0x0B) -// Rt = (GPR[rs].UD[0] < (u64)(s64)imm) ? 1 : 0 -// ------------------------------------------------------------------------ -void armEmitSLTIU(u32 rt, u32 rs, s32 imm) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cmp(RSCRATCH, imm); - armAsm->Cset(RSCRATCH, a64::lo); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// ANDI (primary opcode 0x0C) -// Rt = GPR[rs].UD[0] & (u64)imm_u (imm_u is zero-extended 16-bit) -// ------------------------------------------------------------------------ -void armEmitANDI(u32 rt, u32 rs, u32 imm_u) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm_u == 0) - { - armAsm->Mov(RSCRATCH, 0); - } - else - { - // Zero-extended 16-bit immediates are not always valid ARM64 logical - // immediates; materialize into a scratch register first. - armAsm->Mov(RXVIXLSCRATCH, imm_u); - armAsm->And(RSCRATCH, RSCRATCH, RXVIXLSCRATCH); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// ORI (primary opcode 0x0D) -// Rt = GPR[rs].UD[0] | (u64)imm_u -// ------------------------------------------------------------------------ -void armEmitORI(u32 rt, u32 rs, u32 imm_u) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm_u != 0) - { - armAsm->Mov(RXVIXLSCRATCH, imm_u); - armAsm->Orr(RSCRATCH, RSCRATCH, RXVIXLSCRATCH); - } - // imm_u == 0: identity; RSCRATCH already holds GPR[rs] - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// XORI (primary opcode 0x0E) -// Rt = GPR[rs].UD[0] ^ (u64)imm_u -// ------------------------------------------------------------------------ -void armEmitXORI(u32 rt, u32 rs, u32 imm_u) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm_u != 0) - { - armAsm->Mov(RXVIXLSCRATCH, imm_u); - armAsm->Eor(RSCRATCH, RSCRATCH, RXVIXLSCRATCH); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// LUI (primary opcode 0x0F) -// Rt = (s32)(imm << 16) -// The 16-bit immediate is shifted left 16 and sign-extended from 32 to 64. -// ------------------------------------------------------------------------ -void armEmitLUI(u32 rt, u32 imm) -{ - if (rt == 0) - return; - - const s32 val = static_cast(static_cast(imm) << 16); - if (val == 0) - { - armAsm->Mov(RSCRATCH, 0); - } - else - { - armAsm->Mov(RSCRATCHW, val); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// R-type register-register arithmetic (FUNCT field in bits [5:0]). -// Format: OP rd, rs, rt -// ------------------------------------------------------------------------ - -// Second scratch register for R-type ops that need two source values. -static const a64::Register RSCRATCH2 = RXVIXLSCRATCH; -static const a64::Register RSCRATCH2W = RXVIXLSCRATCH.W(); - -// ------------------------------------------------------------------------ -// ADD / ADDU (funct 0x20 / 0x21) -// Rd = (s32)(GPR[rs].UL[0] + GPR[rt].UL[0]) -// The x86 JIT skips the 32-bit overflow trap for ADD; ADDU is identical. -// ------------------------------------------------------------------------ -void armEmitADD(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - // rs + rs in 32-bit, then sign-extend - armAsm->Add(RSCRATCHW, RSCRATCHW, RSCRATCHW); - } - else - { - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Add(RSCRATCHW, RSCRATCHW, RSCRATCH2W); - } - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitADDU(u32 rd, u32 rs, u32 rt) -{ - armEmitADD(rd, rs, rt); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// DADD / DADDU (funct 0x2C / 0x2D) -// Rd = GPR[rs].UD[0] + GPR[rt].UD[0] -// ------------------------------------------------------------------------ -void armEmitDADD(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - armAsm->Add(RSCRATCH, RSCRATCH, RSCRATCH); - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Add(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitDADDU(u32 rd, u32 rs, u32 rt) -{ - armEmitDADD(rd, rs, rt); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// SUB / SUBU (funct 0x22 / 0x23) -// Rd = (s32)(GPR[rs].UL[0] - GPR[rt].UL[0]) -// ------------------------------------------------------------------------ -void armEmitSUB(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - armAsm->Mov(RSCRATCHW, 0); - } - else - { - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Sub(RSCRATCHW, RSCRATCHW, RSCRATCH2W); - } - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitSUBU(u32 rd, u32 rs, u32 rt) -{ - armEmitSUB(rd, rs, rt); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// DSUB / DSUBU (funct 0x2E / 0x2F) -// Rd = GPR[rs].UD[0] - GPR[rt].UD[0] -// ------------------------------------------------------------------------ -void armEmitDSUB(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - armAsm->Mov(RSCRATCH, 0); - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Sub(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitDSUBU(u32 rd, u32 rs, u32 rt) -{ - armEmitDSUB(rd, rs, rt); // identical in the JIT -} - -// ------------------------------------------------------------------------ -// AND (funct 0x24) -// Rd = GPR[rs].UD[0] & GPR[rt].UD[0] -// ------------------------------------------------------------------------ -void armEmitAND(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - // rs & rs == rs; RSCRATCH already holds the value. - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// OR (funct 0x25) -// Rd = GPR[rs].UD[0] | GPR[rt].UD[0] -// ------------------------------------------------------------------------ -void armEmitOR(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - // rs | rs == rs - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Orr(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// XOR (funct 0x26) -// Rd = GPR[rs].UD[0] ^ GPR[rt].UD[0] -// ------------------------------------------------------------------------ -void armEmitXOR(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - armAsm->Mov(RSCRATCH, 0); - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Eor(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// NOR (funct 0x27) -// Rd = ~(GPR[rs].UD[0] | GPR[rt].UD[0]) -// ------------------------------------------------------------------------ -void armEmitNOR(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (rs == rt) - { - // ~(rs | rs) == ~rs - armAsm->Orr(RSCRATCH, RSCRATCH, RSCRATCH); - } - else - { - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Orr(RSCRATCH, RSCRATCH, RSCRATCH2); - } - armAsm->Mvn(RSCRATCH, RSCRATCH); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SLT (funct 0x2A) -// Rd = (GPR[rs].SD[0] < GPR[rt].SD[0]) ? 1 : 0 -// ------------------------------------------------------------------------ -void armEmitSLT(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, RSCRATCH2); - armAsm->Cset(RSCRATCH, a64::lt); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SLTU (funct 0x2B) -// Rd = (GPR[rs].UD[0] < GPR[rt].UD[0]) ? 1 : 0 -// ------------------------------------------------------------------------ -void armEmitSLTU(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, RSCRATCH2); - armAsm->Cset(RSCRATCH, a64::lo); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// Shift operations (Phase 3.3) -// ------------------------------------------------------------------------ -// -// MIPS shift amounts: -// - 32-bit shifts: amount masked to 5 bits (0-31). ARM64 variable shifts on -// W-registers natively use the low 5 bits of the amount reg. -// - 64-bit shifts: amount masked to 6 bits (0-63). ARM64 variable shifts on -// X-registers natively use the low 6 bits of the amount reg. -// -// All 32-bit results (SLL/SRL/SRA/SLLV/SRLV/SRAV) are sign-extended to 64, -// matching MIPS semantics and the x86 JIT (xMOVSX(xRegister64, xRegister32)). - -// ------------------------------------------------------------------------ -// SLL (funct 0x00) -// Rd = (s32)(GPR[rt].UL[0] << sa) -// ------------------------------------------------------------------------ -void armEmitSLL(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsl(RSCRATCHW, RSCRATCHW, sa); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SRL (funct 0x02) -// Rd = (s32)(GPR[rt].UL[0] >> sa) -// ------------------------------------------------------------------------ -void armEmitSRL(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsr(RSCRATCHW, RSCRATCHW, sa); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SRA (funct 0x03) -// Rd = (s32)(GPR[rt].SL[0] >> sa) -// ------------------------------------------------------------------------ -void armEmitSRA(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Asr(RSCRATCHW, RSCRATCHW, sa); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SLLV (funct 0x04) -// Rd = (s32)(GPR[rt].UL[0] << (GPR[rs].UL[0] & 0x1f)) -// ------------------------------------------------------------------------ -void armEmitSLLV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Lsl(RSCRATCHW, RSCRATCHW, RSCRATCH2W); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SRLV (funct 0x06) -// Rd = (s32)(GPR[rt].UL[0] >> (GPR[rs].UL[0] & 0x1f)) -// ------------------------------------------------------------------------ -void armEmitSRLV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Lsr(RSCRATCHW, RSCRATCHW, RSCRATCH2W); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// SRAV (funct 0x07) -// Rd = (s32)(GPR[rt].SL[0] >> (GPR[rs].UL[0] & 0x1f)) -// ------------------------------------------------------------------------ -void armEmitSRAV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Asr(RSCRATCHW, RSCRATCHW, RSCRATCH2W); - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSLLV (funct 0x14) -// Rd = GPR[rt].UD[0] << (GPR[rs].UL[0] & 0x3f) -// ------------------------------------------------------------------------ -void armEmitDSLLV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Lsl(RSCRATCH, RSCRATCH, RSCRATCH2); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRLV (funct 0x16) -// Rd = GPR[rt].UD[0] >> (GPR[rs].UL[0] & 0x3f) -// ------------------------------------------------------------------------ -void armEmitDSRLV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Lsr(RSCRATCH, RSCRATCH, RSCRATCH2); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRAV (funct 0x17) -// Rd = (s64)(GPR[rt].SD[0] >> (GPR[rs].UL[0] & 0x3f)) -// ------------------------------------------------------------------------ -void armEmitDSRAV(u32 rd, u32 rt, u32 rs) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Asr(RSCRATCH, RSCRATCH, RSCRATCH2); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSLL (funct 0x38) -// Rd = GPR[rt].UD[0] << sa -// ------------------------------------------------------------------------ -void armEmitDSLL(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsl(RSCRATCH, RSCRATCH, sa); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRL (funct 0x3A) -// Rd = GPR[rt].UD[0] >> sa -// ------------------------------------------------------------------------ -void armEmitDSRL(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsr(RSCRATCH, RSCRATCH, sa); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRA (funct 0x3B) -// Rd = (s64)(GPR[rt].SD[0] >> sa) -// ------------------------------------------------------------------------ -void armEmitDSRA(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Asr(RSCRATCH, RSCRATCH, sa); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSLL32 (funct 0x3C) -// Rd = GPR[rt].UD[0] << (sa + 32) -// ------------------------------------------------------------------------ -void armEmitDSLL32(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsl(RSCRATCH, RSCRATCH, sa + 32); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRL32 (funct 0x3E) -// Rd = GPR[rt].UD[0] >> (sa + 32) -// ------------------------------------------------------------------------ -void armEmitDSRL32(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsr(RSCRATCH, RSCRATCH, sa + 32); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// DSRA32 (funct 0x3F) -// Rd = (s64)(GPR[rt].SD[0] >> (sa + 32)) -// ------------------------------------------------------------------------ -void armEmitDSRA32(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Asr(RSCRATCH, RSCRATCH, sa + 32); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// Phase 3.4 — Move operations -// ------------------------------------------------------------------------ -// Offset helpers for HI/LO registers in cpuRegs (they follow GPR[31]). -static constexpr u32 EE_HI_OFFSET() { return 32 * 16u; } -static constexpr u32 EE_LO_OFFSET() { return 33 * 16u; } - -// ------------------------------------------------------------------------ -// MOVZ (funct 0x0A) -// Rd = (GPR[rt].UD[0] == 0) ? GPR[rs].UD[0] : GPR[rd].UD[0] -// ------------------------------------------------------------------------ -void armEmitMOVZ(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - // If rs == rd, the move is a no-op regardless of the condition. - if (rs == rd) - return; - - // Load Rt and test if zero. - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, 0); - - // Load Rd (destination current value) into scratch2. - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - // Load Rs into scratch. - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - - // Conditional select: if Rt == 0 (eq), use Rs; otherwise keep Rd. - armAsm->Csel(RSCRATCH2, RSCRATCH, RSCRATCH2, a64::eq); - - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// MOVN (funct 0x0B) -// Rd = (GPR[rt].UD[0] != 0) ? GPR[rs].UD[0] : GPR[rd].UD[0] -// ------------------------------------------------------------------------ -void armEmitMOVN(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - // If rs == rd, the move is a no-op regardless of the condition. - if (rs == rd) - return; - - // Load Rt and test if zero. - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, 0); - - // Load Rd (destination current value) into scratch2. - armAsm->Ldr(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - // Load Rs into scratch. - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - - // Conditional select: if Rt != 0 (ne), use Rs; otherwise keep Rd. - armAsm->Csel(RSCRATCH2, RSCRATCH, RSCRATCH2, a64::ne); - - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// MFHI (funct 0x10) -// Rd = HI -// ------------------------------------------------------------------------ -void armEmitMFHI(u32 rd) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET())); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// MTHI (funct 0x11) -// HI = Rs -// ------------------------------------------------------------------------ -void armEmitMTHI(u32 rs) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET())); -} - -// ------------------------------------------------------------------------ -// MFLO (funct 0x12) -// Rd = LO -// ------------------------------------------------------------------------ -void armEmitMFLO(u32 rd) -{ - if (rd == 0) - return; - - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET())); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -// ------------------------------------------------------------------------ -// MTLO (funct 0x13) -// LO = Rs -// ------------------------------------------------------------------------ -void armEmitMTLO(u32 rs) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET())); -} - - diff --git a/pcsx2/arm64/aR5900Branch.android.cpp b/pcsx2/arm64/aR5900Branch.android.cpp deleted file mode 100644 index 0a950415b7..0000000000 --- a/pcsx2/arm64/aR5900Branch.android.cpp +++ /dev/null @@ -1,326 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — branch/jump codegen (Phase 4.1 / 4.2). -// -// These generators emit only the control-flow effect of a branch or jump: the -// next-PC write into cpuRegs.pc and, for the linking forms, the return-address -// write into a GPR. They do NOT compile the delay-slot instruction or terminate -// the block — that is the block compiler's job (it compiles the delay slot after -// invoking the generator, then RETs back to the dispatcher loop, which re-reads -// cpuRegs.pc to find the next block). -// -// Why writing cpuRegs.pc *before* the delay slot is safe: no EE delay-slot -// instruction writes cpuRegs.pc, so the early write survives unchanged. For the -// register-target forms (JR/JALR) this is also *required* for correctness — the -// jump target must be the value of GPR[rs] as it was before the delay slot, which -// may overwrite rs. Reading rs into pc here captures it at the right time. -// -// No register allocator yet — sources are read from / results written to cpuRegs -// in memory via RESTATEPTR. The only scratch used is RSCRATCHADDR (x17); the -// immediates materialized here (Mov of a 32-bit constant) go straight into the -// destination register, so VIXL never needs RXVIXLSCRATCH (x16) as a temp. - -#include "aR5900.h" - -#include "R5900.h" -#include "VU.h" // VU0 / REG_VPU_STAT for the COP2 (BC2) branch condition -#include "Memory.h" // eeHw — the backing store the DMAC registers alias into -#include "Hw.h" // D0_CHCR.. HW-register address enum (Dmac.h prerequisite) -#include "Dmac.h" // dmacRegs / DMACregisters for the COP0 (BC0) CPCOND0 condition - -#include "common/Assertions.h" - - -namespace a64 = vixl::aarch64; - -// Scratch register (caller-saved; clobbered freely by these generators). -static const a64::Register RSCRATCH = RSCRATCHADDR; -static const a64::Register RSCRATCHW = RSCRATCHADDR.W(); - -// Store a 32-bit value into cpuRegs.pc. -static void emitWritePcReg(const a64::Register& src_w) -{ - armAsm->Str(src_w, a64::MemOperand(RESTATEPTR, EE_PC_OFFSET)); -} - -// cpuRegs.pc = imm -static void emitWritePcImm(u32 pc) -{ - armAsm->Mov(RSCRATCHW, pc); - emitWritePcReg(RSCRATCHW); -} - -// GPR[reg].UD[0] = linkpc (zero-extended 32->64; upper 64 bits of the 128-bit reg -// are left untouched, matching the x86 JIT / interpreter _SetLink). -static void emitWriteLink(u32 reg, u32 linkpc) -{ - armAsm->Mov(RSCRATCHW, linkpc); // X upper 32 bits zeroed - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(reg))); // store 64-bit => UD[0] -} - -// ------------------------------------------------------------------------ -// J / JAL (primary opcodes 0x02 / 0x03) — immediate (region) target. -// ------------------------------------------------------------------------ -void armEmitJ(u32 target) -{ - emitWritePcImm(target); -} - -void armEmitJAL(u32 target, u32 linkpc) -{ - emitWriteLink(31, linkpc); - emitWritePcImm(target); -} - -// ------------------------------------------------------------------------ -// JR / JALR (SPECIAL funct 0x08 / 0x09) — register target. -// The target is GPR[rs].UL[0] read *before* the delay slot. -// ------------------------------------------------------------------------ -void armEmitJR(u32 rs) -{ - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - emitWritePcReg(RSCRATCHW); -} - -void armEmitJALR(u32 rd, u32 rs, u32 linkpc) -{ - // Read rs and commit the target first, so that rd==rs (link overwriting the - // target source) still jumps to the original GPR[rs]. - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - emitWritePcReg(RSCRATCHW); - if (rd != 0) - emitWriteLink(rd, linkpc); -} - -// ------------------------------------------------------------------------ -// Conditional branches (Phase 4.2). -// ------------------------------------------------------------------------ -// Given that a preceding Cmp has set the condition flags, write -// cpuRegs.pc = cond ? target : fallthrough. -// Both constants are materialized straight into their destination registers -// (x17 = fallthrough, x16 = target), so neither Mov needs a VIXL temp and the -// flags from the Cmp survive into the Csel. -static void emitSelectPc(u32 target, u32 fallthrough, a64::Condition cond) -{ - armAsm->Mov(RSCRATCHW, fallthrough); - armAsm->Mov(RXVIXLSCRATCH.W(), target); - armAsm->Csel(RSCRATCHW, RXVIXLSCRATCH.W(), RSCRATCHW, cond); - emitWritePcReg(RSCRATCHW); -} - -void armEmitBEQ(u32 rs, u32 rt, u32 target, u32 fallthrough) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // GPR[rs].UD[0] - armAsm->Ldr(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // GPR[rt].UD[0] - armAsm->Cmp(RSCRATCH, RXVIXLSCRATCH); - emitSelectPc(target, fallthrough, a64::eq); -} - -void armEmitBNE(u32 rs, u32 rt, u32 target, u32 fallthrough) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, RXVIXLSCRATCH); - emitSelectPc(target, fallthrough, a64::ne); -} - -// Single-operand forms compare signed 64-bit GPR[rs] against zero. -static void emitBranchZero(u32 rs, u32 target, u32 fallthrough, a64::Condition cond) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cmp(RSCRATCH, 0); - emitSelectPc(target, fallthrough, cond); -} - -void armEmitBLTZ(u32 rs, u32 target, u32 fallthrough) -{ - emitBranchZero(rs, target, fallthrough, a64::lt); // rs < 0 -} - -void armEmitBGEZ(u32 rs, u32 target, u32 fallthrough) -{ - emitBranchZero(rs, target, fallthrough, a64::ge); // rs >= 0 -} - -void armEmitBLEZ(u32 rs, u32 target, u32 fallthrough) -{ - emitBranchZero(rs, target, fallthrough, a64::le); // rs <= 0 -} - -void armEmitBGTZ(u32 rs, u32 target, u32 fallthrough) -{ - emitBranchZero(rs, target, fallthrough, a64::gt); // rs > 0 -} - -// *AL forms: the link is written unconditionally and *before* rs is read, matching -// the interpreter's _SetLink ordering (so a degenerate rs==31 compares the link). -void armEmitBLTZAL(u32 rs, u32 target, u32 fallthrough, u32 linkpc) -{ - emitWriteLink(31, linkpc); - emitBranchZero(rs, target, fallthrough, a64::lt); -} - -void armEmitBGEZAL(u32 rs, u32 target, u32 fallthrough, u32 linkpc) -{ - emitWriteLink(31, linkpc); - emitBranchZero(rs, target, fallthrough, a64::ge); -} - -// ------------------------------------------------------------------------ -// COP1 conditional branches BC1F/BC1T (opcode 0x11, rs==0x08, rt 0x00/0x01). -// Branch on the FCR31 C (condition) bit set by the C.* compares. The likely -// forms BC1FL/BC1TL (rt 0x02/0x03) are handled by armEmitBranchLikelyTest below. -static constexpr u32 FPUflagC = 0x00800000; - -void armEmitBC1F(u32 target, u32 fallthrough) -{ - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Tst(RSCRATCHW, FPUflagC); - emitSelectPc(target, fallthrough, a64::eq); // C == 0 -> branch -} - -void armEmitBC1T(u32 target, u32 fallthrough) -{ - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Tst(RSCRATCHW, FPUflagC); - emitSelectPc(target, fallthrough, a64::ne); // C != 0 -> branch -} - -// ------------------------------------------------------------------------ -// COP2 conditional branches BC2F/BC2T (opcode 0x12, rs==0x08 (BC), rt 0x00/0x01). -// Branch on the VU0 macro-mode condition bit VBS0: VU0.VI[REG_VPU_STAT].UL & 0x100 -// (CP2COND = bit 8). BC2F branches when the bit is CLEAR (CP2COND==0), BC2T when -// SET (CP2COND==1) — matching the interpreter (COP2.cpp BC2F/BC2T) and x86 -// microVU_Macro.inl recBC2F/T (_setupBranchTest: TEST VPU_STAT,0x100 then -// recBC2F=JNZ32 / recBC2T=JZ32, where the jmpType skips the taken path on the -// opposite condition). This is purely a bit-test branch — x86 BC2 emits NO VU -// sync / interlock / cycle commit, so neither do we (unlike the M3 transfer ops). -// VU0.VI is global state (not RESTATEPTR-relative), so the address is materialized. -// The likely forms BC2FL/BC2TL (rt 0x02/0x03) are in armEmitBranchLikelyTest below. -static constexpr u32 VU0_VBS0 = 0x100; - -void armEmitBC2F(u32 target, u32 fallthrough) -{ - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RSCRATCHADDR)); - armAsm->Tst(RSCRATCHW, VU0_VBS0); - emitSelectPc(target, fallthrough, a64::eq); // bit clear (CP2COND==0) -> branch -} - -void armEmitBC2T(u32 target, u32 fallthrough) -{ - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RSCRATCHADDR)); - armAsm->Tst(RSCRATCHW, VU0_VBS0); - emitSelectPc(target, fallthrough, a64::ne); // bit set (CP2COND==1) -> branch -} - -// ------------------------------------------------------------------------ -// COP0 conditional branches BC0F/BC0T (opcode 0x10, rs==0x08 (BC), rt 0x00/0x01). -// Branch on CPCOND0, the DMA-ready flag the EE polls while waiting for DMA to finish: -// CPCOND0 = (((dmacRegs.stat.CIS | ~dmacRegs.pcr.CPC) & 0x3FF) == 0x3FF) (COP0.cpp:595) -// i.e. true once every PCR-enabled DMA channel has its STAT interrupt bit set. The OR -// form is emitted verbatim from CPCOND0() so the polarity is self-evident; CIS/CPC are -// bits[9:0] of stat/pcr, so masking the 32-bit loads with 0x3FF is exact. dmacRegs is -// global HW state (a reference into eeHw[0xE000]); materialize its base once and load -// stat+pcr by offset — the second Ldr's destination (w17) reuses the base register, so -// it must come last (the base is consumed for address-gen before being overwritten). -// BC0T branches when CPCOND0==1, BC0F when ==0, matching the interpreter (COP0.cpp -// BC0F/BC0T) and x86 iCOP0.cpp _setupBranchTest. No cycle commit / VU sync — a plain -// HW-register-test branch like BC1/BC2. The likely forms BC0FL/BC0TL (rt 0x02/0x03) -// are in armEmitBranchLikelyTest below. -static void emitCpcond0Test() -{ - armMoveAddressToReg(RSCRATCHADDR, &dmacRegs); // x17 = &dmacRegs - armAsm->Ldr(RXVIXLSCRATCH.W(), a64::MemOperand(RSCRATCHADDR, offsetof(DMACregisters, pcr))); // w16 = PCR (CPC) - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RSCRATCHADDR, offsetof(DMACregisters, stat))); // w17 = STAT (CIS); clobbers base last - armAsm->Mvn(RXVIXLSCRATCH.W(), RXVIXLSCRATCH.W()); // ~CPC - armAsm->Orr(RXVIXLSCRATCH.W(), RXVIXLSCRATCH.W(), RSCRATCHW); // CIS | ~CPC - armAsm->And(RXVIXLSCRATCH.W(), RXVIXLSCRATCH.W(), 0x3FF); // & 0x3FF (bits[9:0]) - armAsm->Cmp(RXVIXLSCRATCH.W(), 0x3FF); // eq <=> CPCOND0 == 1 -} - -void armEmitBC0F(u32 target, u32 fallthrough) -{ - emitCpcond0Test(); - emitSelectPc(target, fallthrough, a64::ne); // CPCOND0 == 0 -> branch -} - -void armEmitBC0T(u32 target, u32 fallthrough) -{ - emitCpcond0Test(); - emitSelectPc(target, fallthrough, a64::eq); // CPCOND0 == 1 -> branch -} - -// ------------------------------------------------------------------------ -// Branch-likely forms. Evaluate the condition, write -// cpuRegs.pc = taken ? target : fallthrough, and return the "taken" condition -// with the flags still live (the Mov/Csel/Str of the PC select don't touch -// flags), so the block compiler can branch around the nullified delay slot. -// Forms: 0x14 BEQL, 0x15 BNEL, 0x16 BLEZL, 0x17 BGTZL, -// REGIMM rt 0x02 BLTZL / 0x03 BGEZL, -// COP1 rs==0x08, rt 0x02 BC1FL / 0x03 BC1TL, -// COP2 rs==0x08, rt 0x02 BC2FL / 0x03 BC2TL. -// ------------------------------------------------------------------------ -vixl::aarch64::Condition armEmitBranchLikelyTest(u32 op, u32 target, u32 fallthrough) -{ - const u32 opcode = op >> 26; - const u32 rs = (op >> 21) & 0x1f; - const u32 rt = (op >> 16) & 0x1f; - - a64::Condition taken = a64::nv; - switch (opcode) - { - case 0x14: // BEQL - case 0x15: // BNEL - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RXVIXLSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH, RXVIXLSCRATCH); - taken = (opcode == 0x14) ? a64::eq : a64::ne; - break; - - case 0x16: // BLEZL - case 0x17: // BGTZL - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cmp(RSCRATCH, 0); - taken = (opcode == 0x16) ? a64::le : a64::gt; - break; - - case 0x01: // REGIMM: BLTZL (0x02) / BGEZL (0x03) - pxAssert(rt == 0x02 || rt == 0x03); - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cmp(RSCRATCH, 0); - taken = (rt == 0x02) ? a64::lt : a64::ge; - break; - - case 0x11: // COP1: BC1FL (rt 0x02) / BC1TL (rt 0x03) - pxAssert(rs == 0x08 && (rt == 0x02 || rt == 0x03)); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Tst(RSCRATCHW, FPUflagC); - taken = (rt == 0x02) ? a64::eq : a64::ne; - break; - - case 0x12: // COP2: BC2FL (rt 0x02) / BC2TL (rt 0x03) - pxAssert(rs == 0x08 && (rt == 0x02 || rt == 0x03)); - armMoveAddressToReg(RSCRATCHADDR, &VU0.VI[REG_VPU_STAT].UL); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RSCRATCHADDR)); - armAsm->Tst(RSCRATCHW, VU0_VBS0); - taken = (rt == 0x02) ? a64::eq : a64::ne; // FL: bit clear; TL: bit set - break; - - case 0x10: // COP0: BC0FL (rt 0x02) / BC0TL (rt 0x03) - pxAssert(rs == 0x08 && (rt == 0x02 || rt == 0x03)); - emitCpcond0Test(); // Cmp sets eq <=> CPCOND0 == 1 - taken = (rt == 0x03) ? a64::eq : a64::ne; // TL: CPCOND0==1; FL: CPCOND0==0 - break; - - default: - pxFailRel("armEmitBranchLikelyTest: not a likely branch"); - } - - emitSelectPc(target, fallthrough, taken); - return taken; -} - diff --git a/pcsx2/arm64/aR5900FPU.android.cpp b/pcsx2/arm64/aR5900FPU.android.cpp deleted file mode 100644 index 56613c20a6..0000000000 --- a/pcsx2/arm64/aR5900FPU.android.cpp +++ /dev/null @@ -1,1143 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — FPU (COP1) exact-semantics opcode generators -// (Phase 5.2a). -// -// This file implements the COP1 instructions that are pure bit/integer movement: -// register transfers (MFC1/MTC1/CFC1/CTC1), the bit-twiddling "arithmetic" ops -// (MOV_S/ABS_S/NEG_S), and the FPU load/store ops (LWC1/SWC1). None of these -// involve the EE FPU's non-IEEE float rounding/clamping, so the ARM64 codegen is -// bit-exact against the interpreter (the ground truth — see pcsx2/FPU.cpp). -// -// The remaining float arithmetic (MADD/MSUB, MAX/MIN, CVT, the C.*.S compares, -// and the BC1* branches) needs the EE's denormal-flush / -// infinity-clamp / overflow-underflow behaviour (fpuDouble + checkOverflow/ -// checkUnderflow). Those remain interpreter fallbacks until a later increment. - -#include "aR5900.h" - -#include "Config.h" -#include "R5900.h" - -#include "common/Assertions.h" - -#include - - - -namespace a64 = vixl::aarch64; - -// FCR31 (fprc[31]) flag bits — see pcsx2/FPU.cpp. -static constexpr u32 FPUflagO = 0x00008000; // overflow (cause) -static constexpr u32 FPUflagU = 0x00004000; // underflow (cause) -static constexpr u32 FPUflagI = 0x00020000; // invalid operation (cause) -static constexpr u32 FPUflagD = 0x00010000; // divide by zero (cause) -static constexpr u32 FPUflagSO = 0x00000010; // overflow (sticky) -static constexpr u32 FPUflagSU = 0x00000008; // underflow (sticky) -static constexpr u32 FPUflagSI = 0x00000040; // invalid operation (sticky) -static constexpr u32 FPUflagSD = 0x00000020; // divide by zero (sticky) -static constexpr u32 FPUflagC = 0x00800000; // compare condition bit - -// IEEE-754 single-precision sentinel bit patterns used by the EE clamp logic. -static constexpr u32 kPosFmax = 0x7f7fffff; // largest finite magnitude -static constexpr u32 kSignBit = 0x80000000; -static constexpr u32 kExpMask = 0x7f800000; -static constexpr u32 kMantMask = 0x007fffff; - -// Clear the given FCR31 cause flags (read-modify-write fprc[31]). Used by the -// MOV-family ops that the interpreter documents as clearing O|U every execution. -static void emitClearFCR31Flags(u32 flags) -{ - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(RSCRATCHADDR.W(), RSCRATCHADDR.W(), ~flags); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); -} - -// ------------------------------------------------------------------------ -// MFC1: move FPR -> GPR. The interpreter sign-extends the 32-bit FPR into the -// low 64-bit doubleword of the GPR (GPR[rt].SD[0] = (s32)fpr[fs]); the upper -// doubleword is left untouched, matching the EE's scalar-write semantics. -void armEmitMFC1(u32 rt, u32 fs) -{ - if (rt == 0) - return; - - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Sxtw(RSCRATCHADDR, RSCRATCHADDR.W()); - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -// MTC1: move GPR -> FPR (low word only). GPR[0] reads as zero straight from the -// register file, so rt==0 needs no special case. -void armEmitMTC1(u32 fs, u32 rt) -{ - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); -} - -// ------------------------------------------------------------------------ -// CFC1: move FPU control register -> GPR. `fs` is a compile-time constant, so the -// interpreter's three-way select collapses to a single emitted path. fprc[31] is -// sign-extended; the other defined values (0x2E00 for fs==0, else 0) are constants. -void armEmitCFC1(u32 rt, u32 fs) -{ - if (rt == 0) - return; - - if (fs == 31) - { - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Sxtw(RSCRATCHADDR, RSCRATCHADDR.W()); - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - } - else if (fs == 0) - { - armAsm->Mov(RSCRATCHADDR, 0x2E00); - armAsm->Str(RSCRATCHADDR, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - } - else - { - armAsm->Str(a64::xzr, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - } -} - -// ------------------------------------------------------------------------ -// CTC1: move GPR -> FPU control register. The interpreter only honours writes to -// fprc[31]; writes to any other control register are ignored, so for a compile-time -// fs != 31 this generator emits nothing. -void armEmitCTC1(u32 fs, u32 rt) -{ - if (fs != 31) - return; - - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); -} - -// ------------------------------------------------------------------------ -// MOV_S: fpr[fd] = fpr[fs] (pure 32-bit copy, no flags touched). -void armEmitMOV_S(u32 fd, u32 fs) -{ - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); -} - -// ------------------------------------------------------------------------ -// ABS_S: fpr[fd] = |fpr[fs]|. x86 recABS_S_xmm (iFPU.cpp): clear the sign, then at -// eeClampMode>=1 clamp +Inf/+NaN -> +fmax (MIN.SS with +fmax; result is already positive). -// Does NOT clear the FCR31 O/U flags — that clear is commented out in x86 (only the -// interpreter did it), so match the recompiler and leave them untouched. -void armEmitABS_S(u32 fd, u32 fs) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->And(a64::w9, a64::w9, 0x7fffffff); // clear sign - if (EmuConfig.Cpu.Recompiler.fpuOverflow) - { - const a64::Register wexp = a64::w10; - const a64::Register wfmax = a64::w11; - armAsm->Ubfx(wexp, a64::w9, 23, 8); - armAsm->Mov(wfmax, kPosFmax); - armAsm->Cmp(wexp, 0xFF); - armAsm->Csel(a64::w9, wfmax, a64::w9, a64::eq); // +Inf/+NaN -> +fmax - } - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); -} - -// ------------------------------------------------------------------------ -// NEG_S: fpr[fd] = fpr[fs] with the sign bit flipped. x86 recNEG_S_xmm does not clear the -// FCR31 O/U flags (commented out); match the recompiler. -void armEmitNEG_S(u32 fd, u32 fs) -{ - armAsm->Ldr(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Eor(RSCRATCHADDR.W(), RSCRATCHADDR.W(), 0x80000000); - armAsm->Str(RSCRATCHADDR.W(), a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); -} - -// ------------------------------------------------------------------------ -// LWC1: fpr[ft].UL = mem32[GPR[rs] + imm]. Routed through the same slow-path vtlb -// helper as the GPR loads; the 32-bit result is written to the FPR's low word. -// (FPR0 is a real register, so there is no rt==0 discard as there is for GPRs.) -void armEmitLWC1(u32 ft, u32 rs, s32 imm, u32 pc) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - // Single-instruction backpatch fastmem 32-bit load into RXRET; falls back to inline vmap - // for faulting PCs / when fastmem is off. The load's memory access is a plain GPR access. - if (!armTryEmitFastmemScalar32(pc, /*is_load*/ true, RXRET)) - armEmitVtlbRead(32, /*sign*/ false, RXRET, RWARG1); - armAsm->Str(RWRET, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); -} - -// ------------------------------------------------------------------------ -// SWC1: mem32[GPR[rs] + imm] = fpr[ft].UL. -void armEmitSWC1(u32 ft, u32 rs, s32 imm, u32 pc) -{ - armAsm->Ldr(RWARG2, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armEmitEffectiveAddr(RWARG1, rs, imm); - // Single-instruction backpatch fastmem 32-bit store of the FPR value (in RWARG2); falls - // back to inline vmap for faulting PCs / when fastmem is off. - if (!armTryEmitFastmemScalar32(pc, /*is_load*/ false, RWARG2)) - armEmitVtlbWrite(32, RWARG1, RWARG2); -} - -// ======================================================================== -// Float arithmetic (Phase 5.2b) -// -// The EE FPU is not IEEE-754. The interpreter (pcsx2/FPU.cpp, ground truth) -// implements the quirks with two pieces we reproduce here: -// - fpuDouble(f): clamp each operand before the op (denormal/zero -> signed 0, -// inf/NaN -> signed fmax); -// - checkOverflow/checkUnderflow: clamp the result (inf -> signed fmax, -// denormal -> signed 0) and set the FCR31 O/U cause+sticky flags. -// The op itself is host single-precision NEON (Fadd/Fsub/Fmul), bit-identical to -// the interpreter's `float OP float` (both IEEE round-to-nearest-even). Generators -// have no register allocator and make no calls, so they freely use the caller-saved -// w9-w13 as integer scratch and s29-s31 (RSSCRATCH*) for the float operands. -// ======================================================================== - -// NOTE: the interpreter-matching helpers that used to live here (emitClampFpuDoubleBits / -// emitLoadFpuDouble / emitStoreClampedResult — fpuDouble input clamp + checkOverflow/ -// checkUnderflow output clamp with O/U flags) were removed: the non-full paths now match -// the x86 recompiler via emitLoadOperandX86 / emitStoreResultX86 (defined below). The x86 -// JIT, not the interpreter, is the port's ground truth. - -// ======================================================================== -// Full clamp mode (eeClampMode 3 / fpuFullMode) — faithful port of the x86 -// DOUBLE path (pcsx2/x86/iFPUd.cpp). -// -// The PS2 FPU has NO inf/NaN: a single with exponent 0xFF is just a very large -// *finite* number. The default (single-precision) path mirrors the interpreter, -// which clamps those to ±fmax via fpuDouble() *before* every op — fine for most -// games, but it throws away magnitude. Full mode instead promotes operands to -// IEEE double WITHOUT that clamp (emitToDouble), so over-range intermediates -// survive the computation as real numbers, then converts the result back with -// proper overflow/underflow thresholds (emitToPS2FPUFull). Games whose GameIndex -// sets eeClampMode=3 (e.g. NFS Carbon) depend on this. -// ======================================================================== - -// emitToDoubleFromBits: PS2 single bits already in `wbits` -> IEEE double in dstD, -// with NO fmax clamp. exp != 0xFF converts exactly (incl. denormals/zero). exp == -// 0xFF is reconstructed as the equivalent large finite double: sign<<63 | 1151<<52 -// | mant<<29 (mirrors x86 ToDouble's lower-exp / convert / raise-exp dance). -// Clobbers wbits (and its X alias) plus the scratch X reg `xtmp`. dstD must not be -// d29/d31; callers pass RDSCRATCH/RDSCRATCH2. -static void emitToDoubleFromBits(const a64::VRegister& dstD, const a64::Register& wbits, - const a64::Register& xtmp) -{ - const a64::Register xbits = wbits.X(); - - a64::Label special, done; - - armAsm->Ubfx(xtmp.W(), wbits, 23, 8); - armAsm->Cmp(xtmp.W(), 0xFF); - armAsm->B(&special, a64::eq); - - // Normal / denormal / zero: single -> double is exact. - armAsm->Fmov(dstD.S(), wbits); - armAsm->Fcvt(dstD, dstD.S()); - armAsm->B(&done); - - armAsm->Bind(&special); - armAsm->Ubfx(xtmp, xbits, 0, 23); // mantissa - armAsm->Lsl(xtmp, xtmp, 29); - armAsm->And(xbits, xbits, 0x80000000); // sign bit (bit31) - armAsm->Lsl(xbits, xbits, 32); // -> bit63 - armAsm->Orr(xbits, xbits, xtmp); - armAsm->Mov(xtmp, 0x47F0000000000000); // 1151 << 52 - armAsm->Orr(xbits, xbits, xtmp); - armAsm->Fmov(dstD, xbits); - - armAsm->Bind(&done); -} - -// emitToDouble: load PS2 single at byteOffset and convert (no clamp). Uses w9/x9 -// and x11 as scratch. -static void emitToDouble(const a64::VRegister& dstD, u32 byteOffset) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, byteOffset)); - emitToDoubleFromBits(dstD, a64::w9, a64::x11); -} - -// emitFpuAddSub: x86 FPU_ADD_SUB (iFPUd.cpp) ported in-place on the two raw single -// bit-patterns wA/wB. The EE FPU lacks IEEE guard bits, so before an add/sub the -// low mantissa bits of the *smaller* operand (the one shifted right during -// alignment) are masked off according to the exponent difference. Runs on the raw -// bits BEFORE emitToDoubleFromBits (an exp-0xFF operand isn't IEEE-inf here, it's a -// real number). Scratch: w11-w14. -static void emitFpuAddSub(const a64::Register& wA, const a64::Register& wB) -{ - const a64::Register wdiff = a64::w11; - const a64::Register wmask = a64::w12; - const a64::Register wexpA = a64::w13; - const a64::Register wexpB = a64::w14; - - a64::Label bigPos, posDiff, bigNeg, done; - - armAsm->Ubfx(wexpA, wA, 23, 8); - armAsm->Ubfx(wexpB, wB, 23, 8); - armAsm->Sub(wdiff, wexpA, wexpB); // signed exponent difference - - armAsm->Cmp(wdiff, 25); - armAsm->B(&bigPos, a64::ge); // expA >> expB: flush B to its sign - armAsm->Cmp(wdiff, 0); - armAsm->B(&posDiff, a64::gt); // 1..24: mask low bits of B - armAsm->B(&done, a64::eq); // equal exponents: nothing to mask - armAsm->Cmp(wdiff, -25); - armAsm->B(&bigNeg, a64::le); // expB >> expA: flush A to its sign - - // diff in -24..-1: mask low (|diff|-1) bits of A. - armAsm->Neg(wdiff, wdiff); - armAsm->Sub(wdiff, wdiff, 1); - armAsm->Mov(wmask, 0xffffffff); - armAsm->Lsl(wmask, wmask, wdiff); - armAsm->And(wA, wA, wmask); - armAsm->B(&done); - - armAsm->Bind(&bigPos); - armAsm->And(wB, wB, kSignBit); - armAsm->B(&done); - - armAsm->Bind(&posDiff); - armAsm->Sub(wdiff, wdiff, 1); - armAsm->Mov(wmask, 0xffffffff); - armAsm->Lsl(wmask, wmask, wdiff); - armAsm->And(wB, wB, wmask); - armAsm->B(&done); - - armAsm->Bind(&bigNeg); - armAsm->And(wA, wA, kSignBit); - - armAsm->Bind(&done); -} - -// emitToPS2FPUFullCore: IEEE double result in srcD -> PS2 single bits left in w9, -// with the EE overflow/underflow behaviour (x86 ToPS2FPU_Full). When setFlags, -// FCR31 is updated and stored: O|U cleared up front, then O|SO on true overflow / -// U|SU on underflow. When also `acc` (the op writes ACC: ADDA/SUBA/MULA and the -// MADDA/MSUBA accumulate), fpuRegs.ACCflag bit0 is cleared up front and set on -// overflow — recMaddsub tests it to propagate an overflowed ACC. `addsub` selects -// the EE ADD/SUB underflow behaviour: the normalized mantissa bits are kept with -// exp=0 instead of being flushed (MUL/DIV-style ops flush to signed zero). -// srcD must be RDSCRATCH (d30); d29/d31 are used as scratch. Integer scratch: w9-w14. -static void emitToPS2FPUFullCore(const a64::VRegister& srcD, bool setFlags, bool acc, bool addsub) -{ - const a64::Register w = a64::w9; // result single bits - const a64::Register wtmp = a64::w10; - const a64::Register wsign = a64::w11; // aliases x11 - const a64::Register xbits = a64::x12; - const a64::Register wflags = a64::w13; - const a64::Register xc = a64::x14; - const a64::VRegister absD = RDSCRATCH3; // d29 - const a64::VRegister dC = RDSCRATCH2; // d31 - - // Match x86 ToPS2FPU_Full: clear both O and U cause bits (and the ACC overflow - // flag for ACC-writing ops) up front, then only set them on the relevant paths. - if (setFlags) - { - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagO | FPUflagU)); - if (acc) - { - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - armAsm->And(wtmp, wtmp, ~1); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - } - } - - armAsm->Fabs(absD, srcD); - - a64::Label toComplex, toOverflow, toUnderflow, uflush, done; - - // |x| >= 2^128 (single exp 0xFF territory) -> complex / overflow handling. - armAsm->Mov(xc, 0x47F0000000000000); // 2^128 - armAsm->Fmov(dC, xc); - armAsm->Fcmp(absD, dC); - armAsm->B(&toComplex, a64::ge); - - // |x| < 2^-126 (smallest normal single) -> underflow / flush. - armAsm->Mov(xc, 0x3810000000000000); // 2^-126 - armAsm->Fmov(dC, xc); - armAsm->Fcmp(absD, dC); - armAsm->B(&toUnderflow, a64::lt); - - // Normal range: plain double -> single. (O/U already cleared up front.) - armAsm->Fcvt(srcD.S(), srcD); - armAsm->Fmov(w, srcD.S()); - armAsm->B(&done); - - armAsm->Bind(&toComplex); - // 2^128 <= |x| < 2^129 : representable as a PS2 exp-0xFF single (not overflow). - armAsm->Mov(xc, 0x4800000000000000); // 2^129 - armAsm->Fmov(dC, xc); - armAsm->Fcmp(absD, dC); - armAsm->B(&toOverflow, a64::ge); - armAsm->Fmov(xbits, srcD); - armAsm->Mov(xc, 0x0010000000000000); // lower double exp by one - armAsm->Sub(xbits, xbits, xc); - armAsm->Fmov(dC, xbits); - armAsm->Fcvt(dC.S(), dC); // -> single, exp 0xFE - armAsm->Fmov(w, dC.S()); - armAsm->Add(w, w, 0x00800000); // raise single exp -> 0xFF (O/U already cleared) - armAsm->B(&done); - - armAsm->Bind(&toOverflow); - // True overflow: result = sign | 0x7FFFFFFF — the PS2 maximum (exp 0xFF, full - // mantissa; x86 SetMaxValue / s_const.pos), NOT the IEEE fmax 0x7F7FFFFF. - armAsm->Fmov(xbits, srcD); - armAsm->Lsr(a64::x11, xbits, 32); - armAsm->And(wsign, wsign, kSignBit); - armAsm->Mov(w, 0x7FFFFFFF); - armAsm->Orr(w, w, wsign); - if (setFlags) - { - armAsm->Orr(wflags, wflags, FPUflagO | FPUflagSO); - if (acc) - { - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - armAsm->Orr(wtmp, wtmp, 1); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - } - } - armAsm->B(&done); - - armAsm->Bind(&toUnderflow); - // x86 tests the *double* against zero (the converted single could flush under - // the host FZ bit and hide the underflow): exact zero -> plain convert, no - // flags. Nonzero -> U|SU; ADD/SUB keep the normalized mantissa bits with exp=0 - // (the EE doesn't flush the mantissa on add/sub), other ops flush to signed 0. - if (setFlags) - { - armAsm->Fcmp(srcD, 0.0); - armAsm->B(&uflush, a64::eq); - armAsm->Orr(wflags, wflags, FPUflagU | FPUflagSU); - if (addsub) - { - armAsm->Fmov(xbits, srcD); - armAsm->Ubfx(a64::x9, xbits, 29, 23); // double mantissa[51:29] -> single mantissa, exp=0 - armAsm->Lsr(xbits, xbits, 63); // sign bit - armAsm->Orr(a64::x9, a64::x9, a64::Operand(xbits, a64::LSL, 31)); - armAsm->B(&done); - } - } - armAsm->Bind(&uflush); - armAsm->Fcvt(srcD.S(), srcD); - armAsm->Fmov(w, srcD.S()); - armAsm->And(w, w, kSignBit); // flush to signed zero - - armAsm->Bind(&done); - if (setFlags) - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); -} - -// emitToPS2FPUFull: core + store of the result single to the fpr/ACC slot. -static void emitToPS2FPUFull(const a64::VRegister& srcD, u32 dstByteOffset, bool setFlags, - bool acc, bool addsub) -{ - emitToPS2FPUFullCore(srcD, setFlags, acc, addsub); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, dstByteOffset)); -} - -// Write a compile-time FPCR bitmask to the host FPCR (the x86 analogue is -// xLDMXCSR). The EE FPCR config is fixed for the lifetime of compiled blocks -// (recs are reset when CPU config changes), so embedding the constant is safe. -// Clobbers x16 (vixl scratch), like aVU's FPCR swap. -static void emitSetHostFPCR(u64 bitmask) -{ - armAsm->Mov(RXVIXLSCRATCH, bitmask); - armAsm->Msr(a64::FPCR, RXVIXLSCRATCH); -} - -// ======================================================================== -// x86-recompiler-faithful clamp helpers (pcsx2/x86/iFPU.cpp, non-full modes 0/1/2). -// -// The non-full paths used to reproduce the INTERPRETER (fpuDouble: denormal-flush on -// input; checkOverflow+checkUnderflow+O/U flags on output). That is NOT what the x86 -// recompiler — the port's ground truth — does, so those helpers were removed in favour -// of the ones below. For eeClampMode 0/1/2 the x86 rec: -// * clamps OPERANDS only when eeClampMode>=2 (fpuExtraOverflow), and only inf/NaN -> -// ±fmax, sign-preserving, with NO denormal flush (fpuFloat2 -> fpuFloat3); -// * clamps the RESULT only when eeClampMode>=1 (fpuOverflow): NaN -> +fmax, +Inf -> -// +fmax, -Inf -> -fmax (ClampValues -> fpuFloat = MIN.SS(+fmax) then MAX.SS(-fmax)); -// a finite single is already within ±fmax so it is untouched, and there is NO -// underflow handling; -// * does NOT maintain the FCR31 O/U cause/sticky flags for arithmetic (the flag -// writes are commented out in iFPU.cpp). -// eeClampMode==3 keeps the dedicated full-mode (iFPUd) path. See the "x86 JIT is ground -// truth" note: match the recompiler, not the interpreter. - -// x86 fpuFloat3 operand clamp, in place on the raw single `w` (w9/w10): exp field == -// 0xFF (inf or NaN) -> sign | fmax, sign-preserving. No denormal flush. Scratch: w11,w12. -static void emitClampOperandBits(const a64::Register& w) -{ - const a64::Register wexp = a64::w11; - const a64::Register wcand = a64::w12; - armAsm->Ubfx(wexp, w, 23, 8); - armAsm->And(wcand, w, kSignBit); - armAsm->Orr(wcand, wcand, kPosFmax); // sign | fmax - armAsm->Cmp(wexp, 0xFF); - armAsm->Csel(w, wcand, w, a64::eq); -} - -// x86 fpuFloat / ClampValues result clamp, in place on the raw single `w` (w9/w10): -// NaN -> +fmax (positive, MIN.SS drops the sign), +Inf -> +fmax, -Inf -> -fmax. A finite -// single is within ±fmax and left untouched. Scratch: w11,w12,w13. -static void emitClampResultBits(const a64::Register& w) -{ - const a64::Register wexp = a64::w11; - const a64::Register wmant = a64::w12; - const a64::Register wcand = a64::w13; - a64::Label isInf, skip; - armAsm->Ubfx(wexp, w, 23, 8); - armAsm->Cmp(wexp, 0xFF); - armAsm->B(&skip, a64::ne); // finite -> untouched - armAsm->And(wmant, w, kMantMask); - armAsm->Mov(wcand, kPosFmax); - armAsm->Cbz(wmant, &isInf); // mantissa == 0 -> Inf - armAsm->Mov(w, wcand); // NaN -> +fmax (positive) - armAsm->B(&skip); - armAsm->Bind(&isInf); - armAsm->And(wmant, w, kSignBit); - armAsm->Orr(w, wmant, wcand); // Inf -> sign | fmax - armAsm->Bind(&skip); -} - -// Load PS2 single at byteOffset into dstS, applying the x86 operand clamp when `clamp`. -// Uses w9 (+ emitClampOperandBits scratch). -static void emitLoadOperandX86(const a64::VRegister& dstS, u32 byteOffset, bool clamp) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, byteOffset)); - if (clamp) - emitClampOperandBits(a64::w9); - armAsm->Fmov(dstS, a64::w9); -} - -// x86 ClampValues result clamp (when fpuOverflow) + store of srcS to byteOffset. No -// underflow, no O/U flags. Uses w9 (+ emitClampResultBits scratch). -static void emitStoreResultX86(const a64::VRegister& srcS, u32 byteOffset) -{ - armAsm->Fmov(a64::w9, srcS); - if (EmuConfig.Cpu.Recompiler.fpuOverflow) - emitClampResultBits(a64::w9); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, byteOffset)); -} - -enum class FpuBinOp -{ - Add, - Sub, - Mul -}; - -// Shared body for the ADD/SUB/MUL (-> fpr[fd]) and ADDA/SUBA/MULA (-> ACC) family. -// When fpuFullMode: promote both operands to IEEE double before the arithmetic, then -// convert back. This prevents intermediate overflow that single-precision can hit on -// games like NFS Carbon (eeClampMode=3 in GameIndex). -static void emitFpuBinary(FpuBinOp op, u32 dstByteOffset, u32 fs, u32 ft) -{ - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - if (op == FpuBinOp::Mul) - { - emitToDouble(RDSCRATCH, EE_FPR_OFFSET(fs)); // d30 = ToDouble(fs), no clamp - if (fs == ft) - { - // fs*fs (squares are common): one load+convert feeds both operands. - armAsm->Fmul(RDSCRATCH, RDSCRATCH, RDSCRATCH); - } - else - { - emitToDouble(RDSCRATCH2, EE_FPR_OFFSET(ft)); // d31 = ToDouble(ft), no clamp - armAsm->Fmul(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - } - } - else if (fs == ft) - { - // Equal operands: the guard-bit masking (emitFpuAddSub) is an exact no-op - // (exponent difference 0 takes its untouched early-out), and both - // conversions yield the same double -- convert once and reuse it. - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - emitToDoubleFromBits(RDSCRATCH, a64::w9, a64::x11); - if (op == FpuBinOp::Add) - armAsm->Fadd(RDSCRATCH, RDSCRATCH, RDSCRATCH); - else - armAsm->Fsub(RDSCRATCH, RDSCRATCH, RDSCRATCH); - } - else - { - // ADD/SUB: apply the EE guard-bit mantissa masking (x86 FPU_ADD_SUB) on - // the raw single operands first, then promote and add/sub in double. - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - emitFpuAddSub(a64::w9, a64::w10); - emitToDoubleFromBits(RDSCRATCH, a64::w9, a64::x11); - emitToDoubleFromBits(RDSCRATCH2, a64::w10, a64::x12); - if (op == FpuBinOp::Add) - armAsm->Fadd(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - else - armAsm->Fsub(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - } - // ADDA/SUBA/MULA write ACC and must track the ACC overflow flag; ADD/SUB get - // the EE's underflow mantissa-preserve behaviour (x86 recFPUOp/FPU_MUL). - emitToPS2FPUFull(RDSCRATCH, dstByteOffset, /*setFlags*/ true, - /*acc*/ dstByteOffset == EE_ACC_OFFSET, /*addsub*/ op != FpuBinOp::Mul); - return; - } - - // x86 non-full path (iFPU.cpp recCommutativeOp / FPU_MUL / FPU_ADD_SUB) — ground truth. - // Operand inf/NaN clamp only at eeClampMode>=2 (sign-preserving, no denormal flush); - // ADD/SUB additionally apply the EE guard-bit mantissa mask (always); result overflow - // clamp only at eeClampMode>=1. No underflow, no O/U flags. - const bool clampOperands = EmuConfig.Cpu.Recompiler.fpuExtraOverflow; - if (op == FpuBinOp::Mul) - { - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(fs), clampOperands); - if (fs != ft) - { - emitLoadOperandX86(RSSCRATCH2, EE_FPR_OFFSET(ft), clampOperands); - armAsm->Fmul(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - } - else - armAsm->Fmul(RSSCRATCH, RSSCRATCH, RSSCRATCH); - } - else - { - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - if (clampOperands) - { - emitClampOperandBits(a64::w9); - emitClampOperandBits(a64::w10); - } - emitFpuAddSub(a64::w9, a64::w10); // EE guard-bit mantissa mask (always, x86 FPU_ADD_SUB) - armAsm->Fmov(RSSCRATCH, a64::w9); - armAsm->Fmov(RSSCRATCH2, a64::w10); - if (op == FpuBinOp::Add) - armAsm->Fadd(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - else - armAsm->Fsub(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - } - emitStoreResultX86(RSSCRATCH, dstByteOffset); -} - -void armEmitADD_S(u32 fd, u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Add, EE_FPR_OFFSET(fd), fs, ft); } -void armEmitSUB_S(u32 fd, u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Sub, EE_FPR_OFFSET(fd), fs, ft); } -void armEmitMUL_S(u32 fd, u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Mul, EE_FPR_OFFSET(fd), fs, ft); } -void armEmitADDA_S(u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Add, EE_ACC_OFFSET, fs, ft); } -void armEmitSUBA_S(u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Sub, EE_ACC_OFFSET, fs, ft); } -void armEmitMULA_S(u32 fs, u32 ft) { emitFpuBinary(FpuBinOp::Mul, EE_ACC_OFFSET, fs, ft); } - -void armEmitDIV_S(u32 fd, u32 fs, u32 ft) -{ - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - // Faithful port of x86 recDIV_S_xmm/recDIVhelper1 (iFPUd.cpp). The whole op - // (including the double conversions) runs under the dedicated DIV round mode - // (FPUDivFPCR, default nearest), and I|D are cleared every DIV. - const bool swapRound = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; - if (swapRound) - emitSetHostFPCR(EmuConfig.Cpu.FPUDivFPCR.bitmask); - - const a64::Register ws = a64::w9; - const a64::Register wt = a64::w10; - const a64::Register wtmp = a64::w11; - const a64::Register wflags = a64::w13; - - a64::Label normal, fsZero, byZeroDone, end; - - armAsm->Ldr(ws, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Ldr(wt, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagI | FPUflagD)); - - // Divisor zero/denormal (the x86 compare runs under DAZ, so exp==0 == zero). - armAsm->And(wtmp, wt, kExpMask); - armAsm->Cbnz(wtmp, &normal); - - // 0/0 -> I|SI, x/0 -> D|SD; result = sign(fs^ft) | 0x7FFFFFFF - // (x86: regd ^= regt, then SetMaxValue ORs in 0x7FFFFFFF). - armAsm->And(wtmp, ws, kExpMask); - armAsm->Cbz(wtmp, &fsZero); - armAsm->Orr(wflags, wflags, FPUflagD | FPUflagSD); - armAsm->B(&byZeroDone); - armAsm->Bind(&fsZero); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->Bind(&byZeroDone); - armAsm->Eor(wtmp, ws, wt); - armAsm->And(wtmp, wtmp, kSignBit); - armAsm->Orr(wtmp, wtmp, 0x7FFFFFFF); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - armAsm->B(&end); - - armAsm->Bind(&normal); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - emitToDoubleFromBits(RDSCRATCH, ws, a64::x11); - emitToDoubleFromBits(RDSCRATCH2, wt, a64::x11); - armAsm->Fdiv(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - emitToPS2FPUFull(RDSCRATCH, EE_FPR_OFFSET(fd), /*setFlags*/ false, false, false); - - armAsm->Bind(&end); - if (swapRound) - emitSetHostFPCR(EmuConfig.Cpu.FPUFPCR.bitmask); - return; - } - - const a64::Register wdivisor = a64::w9; - const a64::Register wdividend = a64::w10; - const a64::Register wtmp = a64::w11; - const a64::Register wflags = a64::w13; - - a64::Label normal, done, dividendZero; - - armAsm->Ldr(wdivisor, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wdividend, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->And(wtmp, wdivisor, kExpMask); - armAsm->Cbnz(wtmp, &normal); - - // checkDivideByZero(): denormal divisors count as zero. z/0 sets D|SD, - // 0/0 sets I|SI, and the result is sign(divisor ^ dividend) | +fmax. - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wtmp, wdividend, kExpMask); - armAsm->Cbz(wtmp, ÷ndZero); - armAsm->Orr(wflags, wflags, FPUflagD | FPUflagSD); - armAsm->B(&done); - armAsm->Bind(÷ndZero); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - - armAsm->Bind(&done); - armAsm->Eor(wtmp, wdivisor, wdividend); - armAsm->And(wtmp, wtmp, kSignBit); - armAsm->Orr(wtmp, wtmp, kPosFmax); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - a64::Label end; - armAsm->B(&end); - - armAsm->Bind(&normal); - // x86 recDIVhelper1 (iFPU.cpp): operand clamp at eeClampMode>=2, ClampValues result - // clamp at eeClampMode>=1. (The I/D divide-by-zero flags are handled above.) - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(fs), EmuConfig.Cpu.Recompiler.fpuExtraOverflow); - emitLoadOperandX86(RSSCRATCH2, EE_FPR_OFFSET(ft), EmuConfig.Cpu.Recompiler.fpuExtraOverflow); - armAsm->Fdiv(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - emitStoreResultX86(RSSCRATCH, EE_FPR_OFFSET(fd)); - - armAsm->Bind(&end); -} - -void armEmitSQRT_S(u32 fd, u32 ft) -{ - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - // Faithful port of x86 recSQRT_S_xmm (iFPUd.cpp): runs under round-to-nearest; - // clears I|D; a negative input (including -0) sets I|SI and is made positive. - // No zero/denormal shortcut — sqrt of a denormal goes through the double path - // and underflows back to +0 in emitToPS2FPUFull. - const bool swapRound = EmuConfig.Cpu.FPUFPCR.GetRoundMode() != FPRoundMode::Nearest; - if (swapRound) - { - FPControlRegister nearest = EmuConfig.Cpu.FPUFPCR; - nearest.SetRoundMode(FPRoundMode::Nearest); - emitSetHostFPCR(nearest.bitmask); - } - - const a64::Register wraw = a64::w9; - const a64::Register wflags = a64::w13; - - a64::Label positive; - - armAsm->Ldr(wraw, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagI | FPUflagD)); - armAsm->Tbz(wraw, 31, &positive); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->And(wraw, wraw, ~kSignBit); // make positive - armAsm->Bind(&positive); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - - emitToDoubleFromBits(RDSCRATCH, wraw, a64::x11); - armAsm->Fsqrt(RDSCRATCH, RDSCRATCH); - emitToPS2FPUFull(RDSCRATCH, EE_FPR_OFFSET(fd), /*setFlags*/ false, false, false); - - if (swapRound) - emitSetHostFPCR(EmuConfig.Cpu.FPUFPCR.bitmask); - return; - } - - const a64::Register wraw = a64::w9; - const a64::Register wtmp = a64::w10; - const a64::Register wflags = a64::w13; - - a64::Label nonzero, positive, done; - - armAsm->Ldr(wraw, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagI | FPUflagD)); - armAsm->And(wtmp, wraw, kExpMask); - armAsm->Cbnz(wtmp, &nonzero); - - // +/-0 and denormals produce signed zero with I/D cause flags cleared. - armAsm->And(wraw, wraw, kSignBit); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Str(wraw, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - armAsm->B(&done); - - armAsm->Bind(&nonzero); - armAsm->Tbz(wraw, 31, &positive); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->Bind(&positive); - // Commit the I flag before the result clamp (emitStoreResultX86 may use w13=wflags as - // scratch). sqrt of a positive finite never overflows, so the clamp is effectively a - // no-op, but keep the x86-faithful operand-clamp(>=2)/result-clamp(>=1) shape. - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(ft), EmuConfig.Cpu.Recompiler.fpuExtraOverflow); - armAsm->Fabs(RSSCRATCH, RSSCRATCH); - armAsm->Fsqrt(RSSCRATCH, RSSCRATCH); - emitStoreResultX86(RSSCRATCH, EE_FPR_OFFSET(fd)); - - armAsm->Bind(&done); -} - -void armEmitRSQRT_S(u32 fd, u32 fs, u32 ft) -{ - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - // Faithful port of x86 recRSQRT_S_xmm/recRSQRThelper1 (iFPUd.cpp): runs under - // round-to-nearest; clears I|D; negative ft (incl. -0) sets I|SI and is made - // positive; ft==0 (or denormal, DAZ semantics) sets I|SI on 0/0 else D|SD and - // the result is sign(fs) | 0x7FFFFFFF (SetMaxValue on the untouched fs). - const bool swapRound = EmuConfig.Cpu.FPUFPCR.GetRoundMode() != FPRoundMode::Nearest; - if (swapRound) - { - FPControlRegister nearest = EmuConfig.Cpu.FPUFPCR; - nearest.SetRoundMode(FPRoundMode::Nearest); - emitSetHostFPCR(nearest.bitmask); - } - - const a64::Register ws = a64::w9; - const a64::Register wt = a64::w10; - const a64::Register wtmp = a64::w11; - const a64::Register wflags = a64::w13; - - a64::Label tPositive, tNonzero, fsZero, zeroDone, end; - - armAsm->Ldr(ws, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Ldr(wt, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagI | FPUflagD)); - - armAsm->Tbz(wt, 31, &tPositive); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->And(wt, wt, ~kSignBit); // make positive - armAsm->Bind(&tPositive); - - armAsm->And(wtmp, wt, kExpMask); - armAsm->Cbnz(wtmp, &tNonzero); - - // ft == 0: 0/0 -> I|SI, x/0 -> D|SD; result = sign(fs) | 0x7FFFFFFF. - armAsm->And(wtmp, ws, kExpMask); - armAsm->Cbz(wtmp, &fsZero); - armAsm->Orr(wflags, wflags, FPUflagD | FPUflagSD); - armAsm->B(&zeroDone); - armAsm->Bind(&fsZero); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->Bind(&zeroDone); - armAsm->And(wtmp, ws, kSignBit); - armAsm->Orr(wtmp, wtmp, 0x7FFFFFFF); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - armAsm->B(&end); - - armAsm->Bind(&tNonzero); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - emitToDoubleFromBits(RDSCRATCH2, wt, a64::x11); // d31 = ft - emitToDoubleFromBits(RDSCRATCH, ws, a64::x11); // d30 = fs - armAsm->Fsqrt(RDSCRATCH2, RDSCRATCH2); - armAsm->Fdiv(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - emitToPS2FPUFull(RDSCRATCH, EE_FPR_OFFSET(fd), /*setFlags*/ false, false, false); - - armAsm->Bind(&end); - if (swapRound) - emitSetHostFPCR(EmuConfig.Cpu.FPUFPCR.bitmask); - return; - } - - const a64::Register wraw = a64::w9; - const a64::Register wtmp = a64::w10; - const a64::Register wflags = a64::w13; - - a64::Label nonzero, positive, done; - - armAsm->Ldr(wraw, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wflags, wflags, ~(FPUflagI | FPUflagD)); - armAsm->And(wtmp, wraw, kExpMask); - armAsm->Cbnz(wtmp, &nonzero); - - // Interpreter RSQRT zero path: set D|SD and return sign(ft) | +fmax. - armAsm->Orr(wflags, wflags, FPUflagD | FPUflagSD); - armAsm->And(wtmp, wraw, kSignBit); - armAsm->Orr(wtmp, wtmp, kPosFmax); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - armAsm->B(&done); - - armAsm->Bind(&nonzero); - armAsm->Tbz(wraw, 31, &positive); - armAsm->Orr(wflags, wflags, FPUflagI | FPUflagSI); - armAsm->Bind(&positive); - // Commit flags before the result clamp (emitStoreResultX86 may use w13=wflags as scratch). - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(fs), EmuConfig.Cpu.Recompiler.fpuExtraOverflow); - emitLoadOperandX86(RSSCRATCH2, EE_FPR_OFFSET(ft), EmuConfig.Cpu.Recompiler.fpuExtraOverflow); - armAsm->Fabs(RSSCRATCH2, RSSCRATCH2); - armAsm->Fsqrt(RSSCRATCH2, RSSCRATCH2); - armAsm->Fdiv(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - emitStoreResultX86(RSSCRATCH, EE_FPR_OFFSET(fd)); - - armAsm->Bind(&done); -} - -// ------------------------------------------------------------------------ -// MADD/MSUB (-> fpr[fd]) and MADDA/MSUBA (-> ACC). The interpreter has a subtle -// asymmetry that we reproduce exactly: -// MADD_S : temp = clamp(fs)*clamp(ft); fd = fpuDouble(ACC) (+/-) fpuDouble(temp) -// MADDA_S: ACC.f (+/-)= clamp(fs)*clamp(ft) (raw ACC, unclamped product) -// i.e. the fd-form re-clamps both the accumulator and the product before the add, -// while the ACC-form uses the raw stored ACC and the unclamped product. Both then -// run checkOverflow/checkUnderflow with the O|SO / U|SU flag side-effects. -static void emitFpuMulAcc(bool subtract, bool toAcc, u32 fd, u32 fs, u32 ft) -{ - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - // Faithful port of x86 recMaddsub (iFPUd.cpp). The product is computed in - // double and rounded back to a PS2 single FIRST (setting the O/U flags), then - // the accumulate runs with overflow propagation: a product overflow forces - // ±MAX (sign-flipped for MSUB) and skips the add entirely; a previously - // overflowed ACC (fpuRegs.ACCflag, set by the ACC-writing ops) forces the - // clamped ACC. Only then is the add/sub done in double and re-rounded. - const a64::Register wprod = a64::w9; // the core's result register - const a64::Register wacc = a64::w10; - const a64::Register wtmp = a64::w11; - - // FPU_MUL: product = ToPS2FPU(ToDouble(fs) * ToDouble(ft)) -> w9, flags set. - emitToDouble(RDSCRATCH, EE_FPR_OFFSET(fs)); - if (fs == ft) - armAsm->Fmul(RDSCRATCH, RDSCRATCH, RDSCRATCH); // fs*fs: reuse the conversion - else - { - emitToDouble(RDSCRATCH2, EE_FPR_OFFSET(ft)); - armAsm->Fmul(RDSCRATCH, RDSCRATCH, RDSCRATCH2); - } - emitToPS2FPUFullCore(RDSCRATCH, /*setFlags*/ true, /*acc*/ false, /*addsub*/ false); - - armAsm->Ldr(wacc, a64::MemOperand(RESTATEPTR, EE_ACC_OFFSET)); - emitFpuAddSub(wacc, wprod); // EE guard-bit masking on (ACC, product) - - a64::Label mulOvf, ovfCommon, end; - - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Tst(wtmp, FPUflagO); - armAsm->B(&mulOvf, a64::ne); // the product overflowed - emitToDoubleFromBits(RDSCRATCH, wprod, a64::x11); // d30 = product - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - armAsm->Tbnz(wtmp, 0, &ovfCommon); // ACC overflowed earlier -> clamped ACC - emitToDoubleFromBits(RDSCRATCH2, wacc, a64::x11); // d31 = ACC - if (subtract) - armAsm->Fsub(RDSCRATCH, RDSCRATCH2, RDSCRATCH); // ACC - product - else - armAsm->Fadd(RDSCRATCH, RDSCRATCH2, RDSCRATCH); // ACC + product - emitToPS2FPUFull(RDSCRATCH, toAcc ? EE_ACC_OFFSET : EE_FPR_OFFSET(fd), - /*setFlags*/ true, /*acc*/ toAcc, /*addsub*/ true); - armAsm->B(&end); - - armAsm->Bind(&mulOvf); - if (subtract) - armAsm->Eor(wprod, wprod, kSignBit); // MSUB propagates -product - armAsm->Mov(wacc, wprod); - armAsm->Bind(&ovfCommon); - // Result = sign | 0x7FFFFFFF (x86 SetMaxValue); O|SO set, U cleared, and the - // ACC-writing forms mark the ACC as overflowed. - armAsm->And(wprod, wacc, kSignBit); - armAsm->Orr(wprod, wprod, 0x7FFFFFFF); - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->And(wtmp, wtmp, ~(FPUflagO | FPUflagU)); - armAsm->Orr(wtmp, wtmp, FPUflagO | FPUflagSO); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - if (toAcc) - { - armAsm->Ldr(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - armAsm->Orr(wtmp, wtmp, 1); - armAsm->Str(wtmp, a64::MemOperand(RESTATEPTR, EE_ACCFLAG_OFFSET)); - } - armAsm->Str(wprod, a64::MemOperand(RESTATEPTR, toAcc ? EE_ACC_OFFSET : EE_FPR_OFFSET(fd))); - armAsm->Bind(&end); - return; - } - - // x86 non-full recMADDtemp/recMSUBtemp (iFPU.cpp), the port's ground truth — uniform - // for the fd- and ACC-writing forms (no interpreter fd/ACC asymmetry, no ACCflag): - // product = clamp2(fs) * clamp2(ft) [operand clamp at >=2] - // if >=2: product = fpuFloat(product); acc = fpuFloat(acc) [intermediate clamp] - // result = acc (+/-) product [EE guard-bit mask, single precision, ADD/SUB order] - // result = ClampValues(result) [result clamp at >=1] - // No underflow, no O/U flags. - const bool clampOperands = EmuConfig.Cpu.Recompiler.fpuExtraOverflow; - - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(fs), clampOperands); - if (fs == ft) - armAsm->Fmul(RSSCRATCH, RSSCRATCH, RSSCRATCH); - else - { - emitLoadOperandX86(RSSCRATCH2, EE_FPR_OFFSET(ft), clampOperands); - armAsm->Fmul(RSSCRATCH, RSSCRATCH, RSSCRATCH2); - } - armAsm->Fmov(a64::w9, RSSCRATCH); // product bits - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_ACC_OFFSET)); // ACC bits - if (clampOperands) // x86 intermediate fpuFloat clamp on product + ACC (eeClampMode>=2) - { - emitClampResultBits(a64::w9); - emitClampResultBits(a64::w10); - } - emitFpuAddSub(a64::w9, a64::w10); // EE guard-bit mantissa mask (x86 FPU_ADD/FPU_SUB) - armAsm->Fmov(RSSCRATCH, a64::w9); // product - armAsm->Fmov(RSSCRATCH2, a64::w10); // ACC - if (subtract) - armAsm->Fsub(RSSCRATCH, RSSCRATCH2, RSSCRATCH); // ACC - product - else - armAsm->Fadd(RSSCRATCH, RSSCRATCH2, RSSCRATCH); // ACC + product - emitStoreResultX86(RSSCRATCH, toAcc ? EE_ACC_OFFSET : EE_FPR_OFFSET(fd)); -} - -void armEmitMADD_S(u32 fd, u32 fs, u32 ft) { emitFpuMulAcc(/*sub*/ false, /*toAcc*/ false, fd, fs, ft); } -void armEmitMSUB_S(u32 fd, u32 fs, u32 ft) { emitFpuMulAcc(/*sub*/ true, /*toAcc*/ false, fd, fs, ft); } -void armEmitMADDA_S(u32 fs, u32 ft) { emitFpuMulAcc(/*sub*/ false, /*toAcc*/ true, 0, fs, ft); } -void armEmitMSUBA_S(u32 fs, u32 ft) { emitFpuMulAcc(/*sub*/ true, /*toAcc*/ true, 0, fs, ft); } - -// ------------------------------------------------------------------------ -// MAX_S/MIN_S: integer-domain fp_max/fp_min on the raw bit patterns (no fpuDouble -// clamp, no rounding), then clear the O|U cause flags. The interpreter: -// fp_max(a,b) = (s32a<0 && s32b<0) ? min(a,b) : max(a,b) -// fp_min(a,b) = (s32a<0 && s32b<0) ? max(a,b) : min(a,b) -// "both negative" is detected by sign bit 31 of (a & b). -static void emitFpuMinMax(bool isMax, u32 fd, u32 fs, u32 ft) -{ - const a64::Register wa = a64::w9; - const a64::Register wb = a64::w10; - const a64::Register wmax = a64::w11; - const a64::Register wmin = a64::w12; - const a64::Register wtmp = a64::w13; - - armAsm->Ldr(wa, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Ldr(wb, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(ft))); - // x86 recMAX/recMIN (recCommutativeOp op>=2) clamp both operands (fpuFloat2) at - // eeClampMode>=1, then SSE min/max. On clamped-finite operands the integer-domain - // min/max below is exact. (x86 uses w11,w12 as clamp scratch here — reused as - // wmax/wmin only after the clamps, so no clobber.) - if (EmuConfig.Cpu.Recompiler.fpuOverflow) - { - emitClampOperandBits(wa); - emitClampOperandBits(wb); - } - armAsm->Cmp(wa, wb); // signed - armAsm->Csel(wmax, wa, wb, a64::gt); - armAsm->Csel(wmin, wa, wb, a64::lt); - // both negative <=> bit 31 of (a & b) set -> Tst leaves NE in that case - armAsm->And(wtmp, wa, wb); - armAsm->Tst(wtmp, kSignBit); - if (isMax) - armAsm->Csel(wa, wmin, wmax, a64::ne); // both neg -> min, else max - else - armAsm->Csel(wa, wmax, wmin, a64::ne); // both neg -> max, else min - armAsm->Str(wa, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); - // x86 recMAX/recMIN do NOT clear the FCR31 O/U flags (the clears are commented out in - // iFPU.cpp); only the interpreter did — so leave them untouched. -} - -void armEmitMAX_S(u32 fd, u32 fs, u32 ft) { emitFpuMinMax(/*isMax*/ true, fd, fs, ft); } -void armEmitMIN_S(u32 fd, u32 fs, u32 ft) { emitFpuMinMax(/*isMax*/ false, fd, fs, ft); } - -// ------------------------------------------------------------------------ -// C.EQ/C.LT/C.LE: set or clear the FCR31 C condition bit from a clamped compare. -// _ContVal_ = (fpuDouble(fs) cond fpuDouble(ft)) ? (_ContVal_ | C) : (_ContVal_ & ~C) -// Both operands are fpuDouble-clamped (so finite, never NaN/inf), making the ARM -// float condition codes a direct match for the interpreter's C++ float comparison. -static void emitFpuCompare(a64::Condition cond, u32 fs, u32 ft) -{ - const a64::Register wflags = a64::w13; - const a64::Register wset = a64::w14; - const a64::Register wclr = a64::w15; - - if (EmuConfig.Cpu.Recompiler.fpuFullMode) - { - emitToDouble(RDSCRATCH, EE_FPR_OFFSET(fs)); // no fmax clamp - emitToDouble(RDSCRATCH2, EE_FPR_OFFSET(ft)); - armAsm->Fcmp(RDSCRATCH, RDSCRATCH2); - } - else - { - // x86 recC_EQ/LT/LE (iFPU.cpp) clamp both operands with fpuFloat3 unconditionally - // (sign-preserving inf/NaN -> ±fmax, NO denormal flush), then an ordered compare. - emitLoadOperandX86(RSSCRATCH, EE_FPR_OFFSET(fs), /*clamp*/ true); - emitLoadOperandX86(RSSCRATCH2, EE_FPR_OFFSET(ft), /*clamp*/ true); - armAsm->Fcmp(RSSCRATCH, RSSCRATCH2); - } - armAsm->Ldr(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); - armAsm->Orr(wset, wflags, FPUflagC); - armAsm->And(wclr, wflags, ~FPUflagC); - armAsm->Csel(wflags, wset, wclr, cond); - armAsm->Str(wflags, a64::MemOperand(RESTATEPTR, EE_FPRC_OFFSET(31))); -} - -void armEmitC_F(u32 fs, u32 ft) { (void)fs; (void)ft; emitClearFCR31Flags(FPUflagC); } -void armEmitC_EQ(u32 fs, u32 ft) { emitFpuCompare(a64::eq, fs, ft); } -void armEmitC_LT(u32 fs, u32 ft) { emitFpuCompare(a64::lt, fs, ft); } -void armEmitC_LE(u32 fs, u32 ft) { emitFpuCompare(a64::le, fs, ft); } - -// ------------------------------------------------------------------------ -// CVT_W: float -> signed int32 with the EE's saturation, no fpuDouble clamp: -// if (exp field <= 0x4E800000) fd = (s32)float (round toward zero) -// else fd = (sign) ? 0x80000000 : 0x7fffffff -void armEmitCVT_W(u32 fd, u32 fs) -{ - const a64::Register w = a64::w9; - const a64::Register wtmp = a64::w10; - const a64::Register wcmp = a64::w11; - const a64::Register wres = a64::w12; - const a64::Register wneg = a64::w13; - - a64::Label convert, store; - - armAsm->Ldr(w, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->And(wtmp, w, kExpMask); - armAsm->Mov(wcmp, 0x4E800000); - armAsm->Cmp(wtmp, wcmp); - armAsm->B(&convert, a64::ls); // in range (unsigned <=) - - // out of range -> saturate by sign - armAsm->Mov(wres, 0x7fffffff); - armAsm->Mov(wneg, 0x80000000); - armAsm->Tst(w, kSignBit); - armAsm->Csel(wres, wneg, wres, a64::ne); - armAsm->B(&store); - - armAsm->Bind(&convert); - armAsm->Fmov(RSSCRATCH, w); - armAsm->Fcvtzs(wres, RSSCRATCH); // round toward zero, matches (s32)float - - armAsm->Bind(&store); - armAsm->Str(wres, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); -} - -// ------------------------------------------------------------------------ -// CVT_S: signed int32 (raw fpr bits) -> float. fd = (float)(s32)fpr[fs]. -void armEmitCVT_S(u32 fd, u32 fs) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fs))); - armAsm->Scvtf(RSSCRATCH, a64::w9); - armAsm->Fmov(a64::w9, RSSCRATCH); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_FPR_OFFSET(fd))); -} - - diff --git a/pcsx2/arm64/aR5900LoadStore.android.cpp b/pcsx2/arm64/aR5900LoadStore.android.cpp deleted file mode 100644 index 23363bb091..0000000000 --- a/pcsx2/arm64/aR5900LoadStore.android.cpp +++ /dev/null @@ -1,497 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — slow-path load/store codegen. -// -// This is the ARM64 counterpart to the inline-vtlb portion of -// pcsx2/x86/ix86-32/recVTLB.cpp, but for bring-up it takes the simplest correct -// route: every guest memory access is emitted as a direct call to the C++ -// vtlb_memRead / vtlb_memWrite helpers — exactly the path the interpreter uses. -// That makes these helpers correct by construction (interpreter is ground truth) -// and avoids needing the register allocator / indirect dispatchers yet. -// -// The vtlb vmap path through REVTLBPTR is the fast path for this backend. - -#include "aR5900.h" - -#include "Memory.h" -#include "R5900.h" -#include "vtlb.h" - -#include "common/Assertions.h" - -#include - -// Inline VTLB vmap fast path (finishing the mac backend's reserved-but-unbuilt -// "Phase 2": REVTLBPTR=x21 is already pinned to vtlbdata.vmap in EnterRecompiledCode). -// When on, scalar loads decode the vmap entry inline and do a direct host access for -// RAM pages, falling back to the existing vtlb_memRead C call only for handler/MMIO -// pages — no signal handler, no backpatch (unlike the SIGSEGV-fastmem in arm64/). -// Enabled after initial on-device validation. Marker: @@MAC_FASTMEM@@. -#ifndef ARMSX2_MAC_FASTMEM -#define ARMSX2_MAC_FASTMEM 1 -#endif - - -namespace a64 = vixl::aarch64; - -// VTLB page shift (vtlb.h VTLBVirtual::VTLB_PAGE_BITS == 12). vmap is an array of -// 8-byte VTLBVirtual entries; for guest vaddr v, host = vmap[v>>12].value + v, and -// the access is a handler/MMIO page iff that sum is negative (sign bit set). -static constexpr int MAC_VTLB_PAGE_BITS = 12; - -// The effective-address codegen assumes guest GPRs are laid out as 16-byte -// GPR_reg slots starting at the base of cpuRegs (so GPR[n].UL[0] is at n*16). -static_assert(sizeof(GPR_reg) == 16, "GPR_reg must be 128 bits for EE_GPR_OFFSET"); -static_assert(offsetof(cpuRegisters, GPR) == 0, "GPR must be the first member of cpuRegs"); - -// ------------------------------------------------------------------------ -void armEmitVtlbRead(u32 bits, bool sign, const a64::Register& dst, const a64::Register& addr) -{ - // 32-bit guest address goes in the first argument register (zero-extended into - // RXARG1/x0, so the 64-bit views below see the full guest vaddr). - if (!addr.W().Is(RWARG1)) - armAsm->Mov(RWARG1, addr.W()); - -#if ARMSX2_MAC_FASTMEM - // Inline vmap fast path: host = vmap[vaddr>>12].value + vaddr; handler iff host<0. - // On a RAM hit, do the direct host load (with the same extension the C path uses) - // and skip the call; otherwise fall through to the vtlb_memRead helper. - // x16/x17 are emit scratch; x0 (RXARG1) keeps the vaddr for the slow path because - // the direct load (which would overwrite dst==x0) only runs after the handler test. - a64::Label fastmem_slow, fastmem_done; - armAsm->Lsr(RXVIXLSCRATCH, RXARG1, MAC_VTLB_PAGE_BITS); // x16 = vaddr >> 12 - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(REVTLBPTR, RXVIXLSCRATCH, a64::LSL, 3)); // x17 = vmap[page].value - armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXARG1); // x17 = value + vaddr (host, or <0) - armAsm->Tbnz(RSCRATCHADDR, 63, &fastmem_slow); // negative => handler/MMIO - switch (bits) - { - case 8: sign ? armAsm->Ldrsb(dst.X(), a64::MemOperand(RSCRATCHADDR)) : armAsm->Ldrb(dst.W(), a64::MemOperand(RSCRATCHADDR)); break; - case 16: sign ? armAsm->Ldrsh(dst.X(), a64::MemOperand(RSCRATCHADDR)) : armAsm->Ldrh(dst.W(), a64::MemOperand(RSCRATCHADDR)); break; - case 32: sign ? armAsm->Ldrsw(dst.X(), a64::MemOperand(RSCRATCHADDR)) : armAsm->Ldr(dst.W(), a64::MemOperand(RSCRATCHADDR)); break; - case 64: armAsm->Ldr(dst.X(), a64::MemOperand(RSCRATCHADDR)); break; - jNO_DEFAULT - } - armAsm->B(&fastmem_done); - armAsm->Bind(&fastmem_slow); -#endif - - const void* fn; - switch (bits) - { - case 8: fn = reinterpret_cast(&vtlb_memRead); break; - case 16: fn = reinterpret_cast(&vtlb_memRead); break; - case 32: fn = reinterpret_cast(&vtlb_memRead); break; - case 64: fn = reinterpret_cast(&vtlb_memRead); break; - jNO_DEFAULT - } - armEmitCall(fn); - - // Extend the returned value into the full 64-bit destination, matching the - // interpreter's load semantics. The C ABI leaves the high bits of a sub-word - // return undefined, so the extension here is mandatory, not an optimisation. - switch (bits) - { - case 8: sign ? armAsm->Sxtb(dst.X(), RWRET) : armAsm->Uxtb(dst.W(), RWRET); break; - case 16: sign ? armAsm->Sxth(dst.X(), RWRET) : armAsm->Uxth(dst.W(), RWRET); break; - case 32: sign ? armAsm->Sxtw(dst.X(), RWRET) : armAsm->Mov(dst.W(), RWRET); break; - case 64: if (!dst.X().Is(RXRET)) armAsm->Mov(dst.X(), RXRET); break; - jNO_DEFAULT - } - -#if ARMSX2_MAC_FASTMEM - armAsm->Bind(&fastmem_done); -#endif -} - -// ------------------------------------------------------------------------ -void armEmitVtlbWrite(u32 bits, const a64::Register& addr, const a64::Register& data) -{ - const void* fn; - switch (bits) - { - case 8: fn = reinterpret_cast(&vtlb_memWrite); break; - case 16: fn = reinterpret_cast(&vtlb_memWrite); break; - case 32: fn = reinterpret_cast(&vtlb_memWrite); break; - case 64: fn = reinterpret_cast(&vtlb_memWrite); break; - jNO_DEFAULT - } - - // vtlb_memWrite(u32 addr, T data): addr -> arg1, data -> arg2. Stage the value - // through VIXLSCRATCH (x16) so addr/data can't alias, and put the address in RWARG1 - // (x0 zero-extended) — shared by the inline fast store and the slow C call. - if (bits == 64) - armAsm->Mov(RXVIXLSCRATCH, data.X()); - else - armAsm->Mov(RWVIXLSCRATCH, data.W()); - if (!addr.W().Is(RWARG1)) - armAsm->Mov(RWARG1, addr.W()); - -#if ARMSX2_MAC_FASTMEM - // Inline vmap fast path: host = vmap[vaddr>>12].value + vaddr; handler iff host<0. - // x17 holds page→vmap→host in sequence (x16 keeps the staged data). A direct store - // that faults on an SMC-protected code page is handled by the shared page-fault - // handler (mmap_ClearCpuBlock + retry), exactly like a faulting vtlb_memWrite. - a64::Label fastmem_slow, fastmem_done; - armAsm->Lsr(RSCRATCHADDR, RXARG1, MAC_VTLB_PAGE_BITS); // x17 = vaddr >> 12 - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(REVTLBPTR, RSCRATCHADDR, a64::LSL, 3)); // x17 = vmap[page].value - armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXARG1); // x17 = value + vaddr (host, or <0) - armAsm->Tbnz(RSCRATCHADDR, 63, &fastmem_slow); // negative => handler/MMIO - switch (bits) - { - case 8: armAsm->Strb(RWVIXLSCRATCH, a64::MemOperand(RSCRATCHADDR)); break; - case 16: armAsm->Strh(RWVIXLSCRATCH, a64::MemOperand(RSCRATCHADDR)); break; - case 32: armAsm->Str(RWVIXLSCRATCH, a64::MemOperand(RSCRATCHADDR)); break; - case 64: armAsm->Str(RXVIXLSCRATCH, a64::MemOperand(RSCRATCHADDR)); break; - jNO_DEFAULT - } - armAsm->B(&fastmem_done); - armAsm->Bind(&fastmem_slow); -#endif - - if (bits == 64) - armAsm->Mov(RXARG2, RXVIXLSCRATCH); - else - armAsm->Mov(RWARG2, RWVIXLSCRATCH); - armEmitCall(fn); - -#if ARMSX2_MAC_FASTMEM - armAsm->Bind(&fastmem_done); -#endif -} - -// ------------------------------------------------------------------------ -void armEmitVtlbReadQuad(const a64::VRegister& dst, const a64::Register& addr) -{ - if (!addr.W().Is(RWARG1)) - armAsm->Mov(RWARG1, addr.W()); - -#if ARMSX2_MAC_FASTMEM - // Inline vmap fast path (addr is already 16-byte aligned by the caller). RAM hit: - // a single 128-bit host load; handler/MMIO falls through to vtlb_memRead128. - a64::Label fastmem_slow, fastmem_done; - armAsm->Lsr(RSCRATCHADDR, RXARG1, MAC_VTLB_PAGE_BITS); // x17 = vaddr >> 12 - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(REVTLBPTR, RSCRATCHADDR, a64::LSL, 3)); // x17 = vmap[page].value - armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXARG1); // x17 = host (or <0) - armAsm->Tbnz(RSCRATCHADDR, 63, &fastmem_slow); - armAsm->Ldr(dst.Q(), a64::MemOperand(RSCRATCHADDR)); // direct 128-bit load - armAsm->B(&fastmem_done); - armAsm->Bind(&fastmem_slow); -#endif - - // vtlb_memRead128 returns r128 (uint32x4_t) in q0. - armEmitCall(reinterpret_cast(&vtlb_memRead128)); - - if (!dst.Q().Is(RQRET)) - armAsm->Mov(dst.Q(), RQRET); - -#if ARMSX2_MAC_FASTMEM - armAsm->Bind(&fastmem_done); -#endif -} - -// ------------------------------------------------------------------------ -void armEmitVtlbWriteQuad(const a64::Register& addr, const a64::VRegister& data) -{ - // Address into RWARG1 (x0 zero-extended) for both the inline decode and the C call. - // data is a vector reg, so it can't alias the GPR address. - if (!addr.W().Is(RWARG1)) - armAsm->Mov(RWARG1, addr.W()); - -#if ARMSX2_MAC_FASTMEM - // Inline vmap fast path (addr is already 16-byte aligned by the caller). RAM hit: - // a single 128-bit host store (SMC-protected code pages fault → shared handler → - // mmap_ClearCpuBlock + retry, same as vtlb_memWrite128). Else fall through to C. - a64::Label fastmem_slow, fastmem_done; - armAsm->Lsr(RSCRATCHADDR, RXARG1, MAC_VTLB_PAGE_BITS); // x17 = vaddr >> 12 - armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(REVTLBPTR, RSCRATCHADDR, a64::LSL, 3)); // x17 = vmap[page].value - armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXARG1); // x17 = host (or <0) - armAsm->Tbnz(RSCRATCHADDR, 63, &fastmem_slow); - armAsm->Str(data.Q(), a64::MemOperand(RSCRATCHADDR)); // direct 128-bit store - armAsm->B(&fastmem_done); - armAsm->Bind(&fastmem_slow); -#endif - - // vtlb_memWrite128(u32 mem, r128 value): mem -> w0, value (uint32x4_t) -> q0. - if (!data.Q().Is(RQRET)) - armAsm->Mov(RQRET, data.Q()); - armEmitCall(reinterpret_cast(&vtlb_memWrite128)); - -#if ARMSX2_MAC_FASTMEM - armAsm->Bind(&fastmem_done); -#endif -} - -// ======================================================================== -// EE GPR load/store opcode generators (Phase 2.3) -// ======================================================================== - -// ------------------------------------------------------------------------ -void armEmitEffectiveAddr(const a64::Register& dst, u32 rs, s32 imm) -{ - // addr = GPR[rs].UL[0] + imm. GPR[0] is hardwired to zero, so for rs==0 the - // address is just the (sign-extended) immediate. - if (rs == 0) - { - armAsm->Mov(dst.W(), imm); - return; - } - - armAsm->Ldr(dst.W(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - if (imm != 0) - armAsm->Add(dst.W(), dst.W(), imm); // MacroAssembler materializes any s16 imm -} - -// ------------------------------------------------------------------------ -void armEmitLoadGpr(u32 bits, bool sign, u32 rt, u32 rs, s32 imm) -{ - // Effective address into the read helper's first argument register. - armEmitEffectiveAddr(RWARG1, rs, imm); - - // Perform the load even when rt==0 (the access can have I/O side effects); - // the extended 64-bit result lands in RXRET. Use it as the scratch dst. - armEmitVtlbRead(bits, sign, RXRET, RWARG1); - - if (rt == 0) - return; - - // Write the full 64-bit (sign/zero-extended) result to GPR[rt].UD[0]. The - // upper doubleword (UD[1]) is left untouched, matching the interpreter — EE - // scalar loads only define the low 64 bits of the 128-bit register. - armAsm->Str(RXRET, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -void armEmitStoreGpr(u32 bits, u32 rt, u32 rs, s32 imm) -{ - // Load the value to store first (GPR[rt], low `bits` bits) into the write - // helper's data argument. GPR[0] reads as zero straight from cpuRegs, so no - // special case is needed for rt==0. - if (bits == 64) - armAsm->Ldr(RXARG2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - else - armAsm->Ldr(RWARG2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - - // Effective address into the write helper's first argument register. - armEmitEffectiveAddr(RWARG1, rs, imm); - - armEmitVtlbWrite(bits, RWARG1, (bits == 64) ? RXARG2 : RWARG2); -} - -// ------------------------------------------------------------------------ -void armEmitLoadQuad(u32 rt, u32 rs, s32 imm) -{ - // Effective address into the read helper's first argument register, then - // force 16-byte alignment (the EE silently aligns 128-bit accesses, matching - // the x86 `xAND(arg1regd, ~0x0F)` in recLoadQuad). - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x0F); - - // Read the full 128-bit quadword into a vector scratch (the call inside - // ReadQuad clobbers v0-v7/v16-v31, so the Mov to RQSCRATCH happens after it). - armEmitVtlbReadQuad(RQSCRATCH, RWARG1); - - if (rt == 0) - return; - - // Quad loads define the entire 128-bit register (both doublewords). - armAsm->Str(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -// ------------------------------------------------------------------------ -void armEmitStoreQuad(u32 rt, u32 rs, s32 imm) -{ - // Load the full 128-bit GPR[rt] into a vector scratch (GPR[0] reads as zero - // straight from cpuRegs, so rt==0 needs no special case). WriteQuad moves it - // to q0 before its call, so the scratch only needs to live until then. - armAsm->Ldr(RQSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - - // Effective address into the write helper's first argument register, 16-byte - // aligned to match EE 128-bit store semantics. - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x0F); - - armEmitVtlbWriteQuad(RWARG1, RQSCRATCH); -} - -// ======================================================================== -// Unaligned load/store (LWL/LWR/SWL/SWR, LDL/LDR/SDL/SDR) -// ======================================================================== -// Bit-exact ports of the interpreter's mask/shift table semantics, computed -// from the low runtime address bits. The vtlb call clobbers caller-saved regs, -// so the effective address is recomputed after the call from guest state. - -static void emitUnalignedShift(u32 rs, s32 imm, u32 addr_mask) -{ - armEmitEffectiveAddr(a64::w9, rs, imm); - armAsm->And(a64::w10, a64::w9, addr_mask); - armAsm->Lsl(a64::w10, a64::w10, 3); -} - -void armEmitLWL(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x03); - armEmitVtlbRead(32, false, RXRET, RWARG1); - if (rt == 0) - return; - - emitUnalignedShift(rs, imm, 3); - armAsm->Mov(a64::w11, 0x00ffffff); - armAsm->Lsr(a64::w11, a64::w11, a64::w10); - armAsm->Mov(a64::w12, 24); - armAsm->Sub(a64::w12, a64::w12, a64::w10); - armAsm->Lsl(a64::w13, RWRET, a64::w12); - armAsm->Ldr(a64::w14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(a64::w14, a64::w14, a64::w11); - armAsm->Orr(a64::w14, a64::w14, a64::w13); - armAsm->Sxtw(a64::x14, a64::w14); - armAsm->Str(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -void armEmitLWR(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x03); - armEmitVtlbRead(32, false, RXRET, RWARG1); - if (rt == 0) - return; - - emitUnalignedShift(rs, imm, 3); - armAsm->Lsr(a64::w13, RWRET, a64::w10); - armAsm->Mvn(a64::w11, a64::wzr); - armAsm->Lsr(a64::w11, a64::w11, a64::w10); - armAsm->Mvn(a64::w11, a64::w11); - armAsm->Ldr(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(a64::w15, a64::w14, a64::w11); - armAsm->Orr(a64::w15, a64::w15, a64::w13); - - a64::Label partial, done; - armAsm->Cbnz(a64::w10, &partial); - armAsm->Sxtw(a64::x15, a64::w15); - armAsm->Str(a64::x15, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->B(&done); - armAsm->Bind(&partial); - armAsm->Bfi(a64::x14, a64::x15, 0, 32); - armAsm->Str(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Bind(&done); -} - -void armEmitSWL(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x03); - armEmitVtlbRead(32, false, RXRET, RWARG1); - - emitUnalignedShift(rs, imm, 3); - armAsm->Ldr(a64::w13, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Mov(a64::w12, 24); - armAsm->Sub(a64::w12, a64::w12, a64::w10); - armAsm->Lsr(a64::w13, a64::w13, a64::w12); - armAsm->Mvn(a64::w11, a64::wzr); - armAsm->Add(a64::w12, a64::w10, 8); - armAsm->Lsl(a64::x11, a64::x11, a64::x12); - armAsm->And(a64::w11, a64::w11, RWRET); - armAsm->Orr(a64::w13, a64::w13, a64::w11); - - armAsm->And(RWARG1, a64::w9, ~0x03); - armEmitVtlbWrite(32, RWARG1, a64::w13); -} - -void armEmitSWR(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x03); - armEmitVtlbRead(32, false, RXRET, RWARG1); - - emitUnalignedShift(rs, imm, 3); - armAsm->Ldr(a64::w13, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsl(a64::w13, a64::w13, a64::w10); - armAsm->Mvn(a64::w11, a64::wzr); - armAsm->Lsl(a64::w11, a64::w11, a64::w10); - armAsm->Bic(a64::w11, RWRET, a64::w11); - armAsm->Orr(a64::w13, a64::w13, a64::w11); - - armAsm->And(RWARG1, a64::w9, ~0x03); - armEmitVtlbWrite(32, RWARG1, a64::w13); -} - -void armEmitLDL(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x07); - armEmitVtlbRead(64, false, RXRET, RWARG1); - if (rt == 0) - return; - - emitUnalignedShift(rs, imm, 7); - armAsm->Mov(a64::x11, 0x00ffffffffffffffULL); - armAsm->Lsr(a64::x11, a64::x11, a64::x10); - armAsm->Mov(a64::w12, 56); - armAsm->Sub(a64::w12, a64::w12, a64::w10); - armAsm->Lsl(a64::x13, RXRET, a64::x12); - armAsm->Ldr(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(a64::x14, a64::x14, a64::x11); - armAsm->Orr(a64::x14, a64::x14, a64::x13); - armAsm->Str(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -void armEmitLDR(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x07); - armEmitVtlbRead(64, false, RXRET, RWARG1); - if (rt == 0) - return; - - emitUnalignedShift(rs, imm, 7); - armAsm->Lsr(a64::x13, RXRET, a64::x10); - armAsm->Mvn(a64::x11, a64::xzr); - armAsm->Lsr(a64::x11, a64::x11, a64::x10); - armAsm->Mvn(a64::x11, a64::x11); - armAsm->Ldr(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->And(a64::x14, a64::x14, a64::x11); - armAsm->Orr(a64::x14, a64::x14, a64::x13); - armAsm->Str(a64::x14, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); -} - -void armEmitSDL(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x07); - armEmitVtlbRead(64, false, RXRET, RWARG1); - - emitUnalignedShift(rs, imm, 7); - armAsm->Ldr(a64::x13, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Mov(a64::w12, 56); - armAsm->Sub(a64::w12, a64::w12, a64::w10); - armAsm->Lsr(a64::x13, a64::x13, a64::x12); - armAsm->Mov(a64::x11, 0xffffffffffffff00ULL); - armAsm->Lsl(a64::x11, a64::x11, a64::x10); - armAsm->And(a64::x11, a64::x11, RXRET); - armAsm->Orr(a64::x13, a64::x13, a64::x11); - - armAsm->And(RWARG1, a64::w9, ~0x07); - armEmitVtlbWrite(64, RWARG1, a64::x13); -} - -void armEmitSDR(u32 rt, u32 rs, s32 imm) -{ - armEmitEffectiveAddr(RWARG1, rs, imm); - armAsm->And(RWARG1, RWARG1, ~0x07); - armEmitVtlbRead(64, false, RXRET, RWARG1); - - emitUnalignedShift(rs, imm, 7); - armAsm->Ldr(a64::x13, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Lsl(a64::x13, a64::x13, a64::x10); - armAsm->Mvn(a64::x11, a64::xzr); - armAsm->Lsl(a64::x11, a64::x11, a64::x10); - armAsm->Bic(a64::x11, RXRET, a64::x11); - armAsm->Orr(a64::x13, a64::x13, a64::x11); - - armAsm->And(RWARG1, a64::w9, ~0x07); - armEmitVtlbWrite(64, RWARG1, a64::x13); -} - - diff --git a/pcsx2/arm64/aR5900MMI.android.cpp b/pcsx2/arm64/aR5900MMI.android.cpp deleted file mode 100644 index 1ffacd067c..0000000000 --- a/pcsx2/arm64/aR5900MMI.android.cpp +++ /dev/null @@ -1,1337 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — MMI 128-bit SIMD codegen (Phase 5.4). -// -// The R5900's MMI group operates on the full 128-bit GPRs as packed vectors of -// bytes / halfwords / words / doublewords. These map almost one-for-one onto -// ARM64 NEON, so each generator loads GPR[rs]/GPR[rt] into scratch q-registers, -// runs a single NEON instruction, and stores the q-register back to GPR[rd]. -// -// Guest GPRs are stored little-endian in cpuRegs, so a NEON 128-bit load places -// guest element 0 (UL[0]/US[0]/UC[0]/UD[0]) into NEON lane 0 — the lane ordering -// matches the interpreter's index ordering exactly (no shuffling needed). -// -// Ground truth is pcsx2/MMI.cpp (the interpreter). Each mapping below is chosen -// to reproduce that behaviour bit-for-bit: -// PADD*/PSUB* -> Add / Sub (wrapping element add/subtract) -// PADDS*/PSUBS* -> Sqadd / Sqsub (signed saturating) -// PADDU*/PSUBU* -> Uqadd / Uqsub (unsigned saturating) -// PCGT* -> Cmgt (signed) (Rs > Rt -> all-ones mask) -// PCEQ* -> Cmeq (Rs == Rt -> all-ones mask) -// PMAX*/PMIN* -> Smax / Smin (signed) -// PABSW/PABSH -> Sqabs (saturating abs: 0x8000.. -> 0x7FFF..) -// PAND/POR/PXOR -> And / Orr / Eor -// PNOR -> Orr then Not -// PEXTL*/PEXTU* -> Zip1 / Zip2 (rt,rs) (interleave low/high halves) -// PPAC* -> Uzp1 (rt,rs) (pack: keep even-indexed elements) -// PCPYLD -> Zip1 .2D (rt,rs) (Rd = Rs.lo : Rt.lo) -// PCPYUD -> Zip2 .2D (rs,rt) (Rd = Rt.hi : Rs.hi) -// PCPYH -> broadcast US[0]/US[4] into the low/high doublewords -// -// Operand order matters for the non-commutative ops: the pack/interleave/PCPYLD -// generators feed (rt, rs) into the NEON op because the interpreter takes Rt as -// the low/even source. $zero destination writes are discarded. - -#include "aR5900.h" - -#include "R5900.h" - - - -namespace a64 = vixl::aarch64; - -// Scratch q-registers (caller-saved NEON, not held across any external call): -// VS = GPR[rs], VT = GPR[rt], VD = result. Same physical regs as the shared -// RQSCRATCH/RQSCRATCH2/RQSCRATCH3 (q30/q31/q29). -static const a64::VRegister VS = a64::VRegister(30, 128); -static const a64::VRegister VT = a64::VRegister(31, 128); -static const a64::VRegister VD = a64::VRegister(29, 128); - -static void loadQ(const a64::VRegister& v, u32 n) -{ - armAsm->Ldr(v.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(n))); -} - -static void storeQ(const a64::VRegister& v, u32 n) -{ - armAsm->Str(v.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(n))); -} - -// Binary op with the interpreter's natural (rs, rt) operand order: -// GPR[rd] = OP(GPR[rs], GPR[rt]) over the lanes given by VIEW. -#define MMI_3OP(NAME, OP, VIEW) \ - void armEmit##NAME(u32 rd, u32 rs, u32 rt) \ - { \ - if (rd == 0) \ - return; \ - loadQ(VS, rs); \ - loadQ(VT, rt); \ - armAsm->OP(VD.VIEW(), VS.VIEW(), VT.VIEW()); \ - storeQ(VD, rd); \ - } - -// Binary op with swapped (rt, rs) operand order — for the pack/interleave ops -// where the interpreter uses Rt as the low/even source operand. -#define MMI_3OP_TS(NAME, OP, VIEW) \ - void armEmit##NAME(u32 rd, u32 rs, u32 rt) \ - { \ - if (rd == 0) \ - return; \ - loadQ(VS, rs); \ - loadQ(VT, rt); \ - armAsm->OP(VD.VIEW(), VT.VIEW(), VS.VIEW()); \ - storeQ(VD, rd); \ - } - -// --- Parallel add / subtract (wrapping) ------------------------------------- -MMI_3OP(PADDW, Add, V4S) -MMI_3OP(PADDH, Add, V8H) -MMI_3OP(PADDB, Add, V16B) -MMI_3OP(PSUBW, Sub, V4S) -MMI_3OP(PSUBH, Sub, V8H) -MMI_3OP(PSUBB, Sub, V16B) - -// --- Parallel add / subtract with signed saturation ------------------------- -MMI_3OP(PADDSW, Sqadd, V4S) -MMI_3OP(PADDSH, Sqadd, V8H) -MMI_3OP(PADDSB, Sqadd, V16B) -MMI_3OP(PSUBSW, Sqsub, V4S) -MMI_3OP(PSUBSH, Sqsub, V8H) -MMI_3OP(PSUBSB, Sqsub, V16B) - -// --- Parallel add / subtract with unsigned saturation ----------------------- -MMI_3OP(PADDUW, Uqadd, V4S) -MMI_3OP(PADDUH, Uqadd, V8H) -MMI_3OP(PADDUB, Uqadd, V16B) -MMI_3OP(PSUBUW, Uqsub, V4S) -MMI_3OP(PSUBUH, Uqsub, V8H) -MMI_3OP(PSUBUB, Uqsub, V16B) - -// --- Parallel compares (produce an all-ones / all-zeros mask per lane) ------- -MMI_3OP(PCGTW, Cmgt, V4S) -MMI_3OP(PCGTH, Cmgt, V8H) -MMI_3OP(PCGTB, Cmgt, V16B) -MMI_3OP(PCEQW, Cmeq, V4S) -MMI_3OP(PCEQH, Cmeq, V8H) -MMI_3OP(PCEQB, Cmeq, V16B) - -// --- Parallel signed min / max ---------------------------------------------- -MMI_3OP(PMAXW, Smax, V4S) -MMI_3OP(PMAXH, Smax, V8H) -MMI_3OP(PMINW, Smin, V4S) -MMI_3OP(PMINH, Smin, V8H) - -// --- Parallel bitwise logic ------------------------------------------------- -MMI_3OP(PAND, And, V16B) -MMI_3OP(POR, Orr, V16B) -MMI_3OP(PXOR, Eor, V16B) - -void armEmitPNOR(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - loadQ(VS, rs); - loadQ(VT, rt); - armAsm->Orr(VD.V16B(), VS.V16B(), VT.V16B()); - armAsm->Not(VD.V16B(), VD.V16B()); - storeQ(VD, rd); -} - -// --- Interleave (extend) low / high halves ---------------------------------- -// PEXTL* interleaves the low halves of Rt (even output lanes) and Rs (odd); -// PEXTU* does the same for the high halves. Rt is the first NEON operand. -MMI_3OP_TS(PEXTLW, Zip1, V4S) -MMI_3OP_TS(PEXTLH, Zip1, V8H) -MMI_3OP_TS(PEXTLB, Zip1, V16B) -MMI_3OP_TS(PEXTUW, Zip2, V4S) -MMI_3OP_TS(PEXTUH, Zip2, V8H) -MMI_3OP_TS(PEXTUB, Zip2, V16B) - -// --- Pack (keep even-indexed elements of Rt then Rs) ------------------------ -MMI_3OP_TS(PPACW, Uzp1, V4S) -MMI_3OP_TS(PPACH, Uzp1, V8H) -MMI_3OP_TS(PPACB, Uzp1, V16B) - -// --- Doubleword copy combines ------------------------------------------------ -// PCPYLD: Rd = { Rt.UD[0], Rs.UD[0] } -> Zip1.2D(Rt, Rs) -// PCPYUD: Rd = { Rs.UD[1], Rt.UD[1] } -> Zip2.2D(Rs, Rt) -MMI_3OP_TS(PCPYLD, Zip1, V2D) -MMI_3OP(PCPYUD, Zip2, V2D) - -// --- Parallel saturating absolute value (Rt only) --------------------------- -void armEmitPABSW(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Sqabs(VD.V4S(), VT.V4S()); // 0x80000000 -> 0x7FFFFFFF, matching the clamp - storeQ(VD, rd); -} - -void armEmitPABSH(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Sqabs(VD.V8H(), VT.V8H()); // 0x8000 -> 0x7FFF - storeQ(VD, rd); -} - -// --- PCPYH: broadcast Rt.US[0] into the low doubleword, Rt.US[4] into the high. -void armEmitPCPYH(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Dup(VS.V8H(), VT.V8H(), 0); // all 8 halfwords = US[0] - armAsm->Dup(VD.V8H(), VT.V8H(), 4); // all 8 halfwords = US[4] - armAsm->Ins(VD.V2D(), 0, VS.V2D(), 0); // low doubleword <- US[0]x4; high stays US[4]x4 - storeQ(VD, rd); -} - -// ============================================================================= -// Parallel shifts by immediate (Phase 5.4 continuation) -// ============================================================================= -// Each lane is shifted independently by the same immediate amount `sa`. -// ARM64 NEON provides single-instruction forms for all three shift types: -// Shl — shift left (zero-extend out bits) -// Ushr — unsigned/logical shift right (zero-fill from the left) -// Sshr — signed/arithmetic shift right (sign-extend from the left) -// -// The guest GPRs are little-endian, so NEON lane 0 holds guest element 0 — -// the lane indexing matches the interpreter's element indexing exactly. - -// --- PSLLH/PSLLW: parallel logical shift left by `sa` ------------------------ -void armEmitPSLLH(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x0F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Shl(VD.V8H(), VT.V8H(), shift); // 16-bit lanes - } - storeQ(VD, rd); -} - -void armEmitPSLLW(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x1F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Shl(VD.V4S(), VT.V4S(), shift); // 32-bit lanes - } - storeQ(VD, rd); -} - -// --- PSRLH/PSRLW: parallel logical (unsigned) shift right by `sa` ------------ -void armEmitPSRLH(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x0F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Ushr(VD.V8H(), VT.V8H(), shift); // zero-fill from left - } - storeQ(VD, rd); -} - -void armEmitPSRLW(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x1F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Ushr(VD.V4S(), VT.V4S(), shift); // zero-fill from left - } - storeQ(VD, rd); -} - -// --- PSRAH/PSRAW: parallel arithmetic (signed) shift right by `sa` ----------- -void armEmitPSRAH(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x0F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Sshr(VD.V8H(), VT.V8H(), shift); // sign-extend from left - } - storeQ(VD, rd); -} - -void armEmitPSRAW(u32 rd, u32 rt, u32 sa) -{ - if (rd == 0) - return; - loadQ(VT, rt); - u32 shift = sa & 0x1F; - if (shift == 0) { - armAsm->Mov(VD.V16B(), VT.V16B()); // no-op shift, just copy - } else { - armAsm->Sshr(VD.V4S(), VT.V4S(), shift); // sign-extend from left - } - storeQ(VD, rd); -} - - -// ============================================================================= -// Parallel lane permutes (Phase 5.4 continuation) -// ============================================================================= -// These rearrange the halfword/word lanes within the 128-bit GPR. They don't -// map to single NEON instructions, so we use lane-by-lane insertion (Ins). - -// --- PINTH: interleave halfwords --------------------------------------------- -// Output: [Rt[0], Rs[4], Rt[1], Rs[5], Rt[2], Rs[6], Rt[3], Rs[7]] -void armEmitPINTH(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - loadQ(VS, rs); - // Build result lane-by-lane using Ins. - armAsm->Ins(VD.V8H(), 0, VT.V8H(), 0); // VD[0] = Rt[0] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V8H(), 1, VS.V8H(), 4); // VD[1] = Rs[4] - armAsm->Ins(VD.V8H(), 2, VT.V8H(), 1); // VD[2] = Rt[1] - armAsm->Ins(VD.V8H(), 3, VS.V8H(), 5); // VD[3] = Rs[5] - armAsm->Ins(VD.V8H(), 4, VT.V8H(), 2); // VD[4] = Rt[2] - armAsm->Ins(VD.V8H(), 5, VS.V8H(), 6); // VD[5] = Rs[6] - armAsm->Ins(VD.V8H(), 6, VT.V8H(), 3); // VD[6] = Rt[3] - armAsm->Ins(VD.V8H(), 7, VS.V8H(), 7); // VD[7] = Rs[7] - storeQ(VD, rd); -} - -// --- PINTEH: interleave even halfwords --------------------------------------- -// Output: [Rt[0], Rs[0], Rt[2], Rs[2], Rt[4], Rs[4], Rt[6], Rs[6]] -void armEmitPINTEH(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - loadQ(VS, rs); - armAsm->Ins(VD.V8H(), 0, VT.V8H(), 0); // VD[0] = Rt[0] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V8H(), 1, VS.V8H(), 0); // VD[1] = Rs[0] - armAsm->Ins(VD.V8H(), 2, VT.V8H(), 2); // VD[2] = Rt[2] - armAsm->Ins(VD.V8H(), 3, VS.V8H(), 2); // VD[3] = Rs[2] - armAsm->Ins(VD.V8H(), 4, VT.V8H(), 4); // VD[4] = Rt[4] - armAsm->Ins(VD.V8H(), 5, VS.V8H(), 4); // VD[5] = Rs[4] - armAsm->Ins(VD.V8H(), 6, VT.V8H(), 6); // VD[6] = Rt[6] - armAsm->Ins(VD.V8H(), 7, VS.V8H(), 6); // VD[7] = Rs[6] - storeQ(VD, rd); -} - -// --- PEXEH: extract even halfwords (swap 0<->2 in each 64-bit half) ---------- -// Output: [Rt[2], Rt[1], Rt[0], Rt[3], Rt[6], Rt[5], Rt[4], Rt[7]] -void armEmitPEXEH(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Ins(VD.V8H(), 0, VT.V8H(), 2); // VD[0] = Rt[2] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V8H(), 1, VT.V8H(), 1); // VD[1] = Rt[1] - armAsm->Ins(VD.V8H(), 2, VT.V8H(), 0); // VD[2] = Rt[0] - armAsm->Ins(VD.V8H(), 3, VT.V8H(), 3); // VD[3] = Rt[3] - armAsm->Ins(VD.V8H(), 4, VT.V8H(), 6); // VD[4] = Rt[6] - armAsm->Ins(VD.V8H(), 5, VT.V8H(), 5); // VD[5] = Rt[5] - armAsm->Ins(VD.V8H(), 6, VT.V8H(), 4); // VD[6] = Rt[4] - armAsm->Ins(VD.V8H(), 7, VT.V8H(), 7); // VD[7] = Rt[7] - storeQ(VD, rd); -} - -// --- PEXEW: extract even words (swap 32-bit lanes 0<->2) --------------------- -// Output: [Rt[2], Rt[1], Rt[0], Rt[3]] (32-bit lanes) -void armEmitPEXEW(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Ins(VD.V4S(), 0, VT.V4S(), 2); // VD[0] = Rt[2] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V4S(), 1, VT.V4S(), 1); // VD[1] = Rt[1] - armAsm->Ins(VD.V4S(), 2, VT.V4S(), 0); // VD[2] = Rt[0] - armAsm->Ins(VD.V4S(), 3, VT.V4S(), 3); // VD[3] = Rt[3] - storeQ(VD, rd); -} - -// --- PREVH: reverse halfwords within each 64-bit half ------------------------ -// Output: [Rt[3], Rt[2], Rt[1], Rt[0], Rt[7], Rt[6], Rt[5], Rt[4]] -void armEmitPREVH(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Rev64(VD.V8H(), VT.V8H()); // Single instruction! - storeQ(VD, rd); -} - -// ============================================================================= -// Remaining lane permutes (Phase 5.4 continuation) -// ============================================================================= - -// --- PROT3W: rotate 3 words (Rt-only, Rt = {UL[0],UL[1],UL[2],UL[3]} ) -------- -// Output: [Rt[1], Rt[2], Rt[0], Rt[3]] (32-bit lanes; lane 3 is unchanged) -void armEmitPROT3W(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Ins(VD.V4S(), 0, VT.V4S(), 1); // VD[0] = Rt[1] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V4S(), 1, VT.V4S(), 2); // VD[1] = Rt[2] - armAsm->Ins(VD.V4S(), 2, VT.V4S(), 0); // VD[2] = Rt[0] - armAsm->Ins(VD.V4S(), 3, VT.V4S(), 3); // VD[3] = Rt[3] (unchanged) - storeQ(VD, rd); -} - -// --- PEXCH: extract even halfwords within each 64-bit half ------------------- -// Swaps halfword pairs (1<->2) within each 64-bit half. -// Output: [Rt[0], Rt[2], Rt[1], Rt[3], Rt[4], Rt[6], Rt[5], Rt[7]] -void armEmitPEXCH(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Ins(VD.V8H(), 0, VT.V8H(), 0); // VD[0] = Rt[0] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V8H(), 1, VT.V8H(), 2); // VD[1] = Rt[2] - armAsm->Ins(VD.V8H(), 2, VT.V8H(), 1); // VD[2] = Rt[1] - armAsm->Ins(VD.V8H(), 3, VT.V8H(), 3); // VD[3] = Rt[3] - armAsm->Ins(VD.V8H(), 4, VT.V8H(), 4); // VD[4] = Rt[4] - armAsm->Ins(VD.V8H(), 5, VT.V8H(), 6); // VD[5] = Rt[6] - armAsm->Ins(VD.V8H(), 6, VT.V8H(), 5); // VD[6] = Rt[5] - armAsm->Ins(VD.V8H(), 7, VT.V8H(), 7); // VD[7] = Rt[7] - storeQ(VD, rd); -} - -// --- PEXCW: extract even words (swap word pairs 1<->2) ----------------------- -// Output: [Rt[0], Rt[2], Rt[1], Rt[3]] (32-bit lanes) -void armEmitPEXCW(u32 rd, u32 rt) -{ - if (rd == 0) - return; - loadQ(VT, rt); - armAsm->Ins(VD.V4S(), 0, VT.V4S(), 0); // VD[0] = Rt[0] (INS element; Mov-with-index needs a scalar dest) - armAsm->Ins(VD.V4S(), 1, VT.V4S(), 2); // VD[1] = Rt[2] - armAsm->Ins(VD.V4S(), 2, VT.V4S(), 1); // VD[2] = Rt[1] - armAsm->Ins(VD.V4S(), 3, VT.V4S(), 3); // VD[3] = Rt[3] - storeQ(VD, rd); -} - -// ============================================================================= -// Parallel variable shifts (Phase 5.4 continuation) -// ============================================================================= -// IMPORTANT: despite the "VW" name, the interpreter (MMI.cpp PSLLVW/PSRLVW/ -// PSRAVW) does NOT shift four independent 32-bit lanes. It shifts only lanes 0 -// and 2 of Rt (each by the matching lane of Rs, masked to 5 bits) and writes the -// 32-bit result *sign-extended to a full 64-bit doubleword*: -// -// Rd.SD[0] = (s64)(s32)(Rt.UL[0] <> (Rs.UL[0] & 0x1F)); // fills Rd.UD[0] -// Rd.SD[1] = (s64)(s32)(Rt.UL[2] <> (Rs.UL[2] & 0x1F)); // fills Rd.UD[1] -// -// So each doubleword's high word is the sign fill of its low word, NOT a shift of -// Rt.UL[1]/Rt.UL[3]. We compute each lane in a w-register (the variable shift form -// already masks the amount mod 32 == & 0x1F), Sxtw it to 64 bits, and store the -// whole doubleword. -// -// Caller-saved GPRs used as scratch: x9-x10 (avoiding x16 which is VIXL scratch). - -// --- PSLLVW: parallel logical shift left by GPR[rs] ------------------------- -void armEmitPSLLVW(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // Rt.UL[0] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // Rs.UL[0] - armAsm->Lsl(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[0] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + 8)); // Rt.UL[2] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); // Rs.UL[2] - armAsm->Lsl(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[1] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); -} - -// --- PSRLVW: parallel logical (unsigned) shift right by GPR[rs] ------------- -void armEmitPSRLVW(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // Rt.UL[0] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // Rs.UL[0] - armAsm->Lsr(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[0] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + 8)); // Rt.UL[2] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); // Rs.UL[2] - armAsm->Lsr(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[1] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); -} - -// --- PSRAVW: parallel arithmetic (signed) shift right by GPR[rs] ------------ -void armEmitPSRAVW(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // Rt.SL[0] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // Rs.UL[0] - armAsm->Asr(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[0] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + 8)); // Rt.SL[2] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); // Rs.UL[2] - armAsm->Asr(a64::w9, a64::w9, a64::w10); - armAsm->Sxtw(a64::x9, a64::w9); // sign-extend to UD[1] - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); -} - -// ============================================================================= -// Multiply-accumulate family (Phase 5.4 continuation) -// ============================================================================= -// These ops multiply pairs of elements and accumulate into the HI/LO special -// registers. The result is also written to GPR[rd] for some ops. -// -// HI/LO offsets (matching aR5900MultDiv.cpp): -// HI = 32 * 16 = 512 (HI.UD[0]) -// LO = 33 * 16 = 528 (LO.UD[0]) -// HI1 = HI + 8 = 520 (HI.UD[1]) -// LO1 = LO + 8 = 536 (LO.UD[1]) -static constexpr u32 EE_HI_OFFSET = 32u * 16u; -static constexpr u32 EE_LO_OFFSET = 33u * 16u; -static constexpr u32 EE_HI1_OFFSET = EE_HI_OFFSET + 8u; -static constexpr u32 EE_LO1_OFFSET = EE_LO_OFFSET + 8u; - -// Scratch registers for multiply-accumulate (caller-saved GPRs): -// w9-w12: 32-bit lane data and intermediates -// x13: 64-bit accumulate result -// Use the VIXL register objects directly (w9, w10, etc. are already WRegister objects). -#define WTEMP1 a64::w9 -#define WTEMP2 a64::w10 -#define WTEMP3 a64::w11 -#define WTEMP4 a64::w12 -#define XTEMP a64::x13 - -// ----------------------------------------------------------------------------- -// PMULTW: Word multiply (lanes 0 and 2) -// ----------------------------------------------------------------------------- -// LO.SD[0] = (s32)(Rs[0] * Rt[0]) -// HI.SD[0] = (s32)((Rs[0] * Rt[0]) >> 32) -// if (Rd) GPR[rd].SD[0] = Rs[0] * Rt[0] (full 64-bit) -// LO.SD[1] = (s32)(Rs[2] * Rt[2]) -// HI.SD[1] = (s32)((Rs[2] * Rt[2]) >> 32) -// if (Rd) GPR[rd].SD[1] = Rs[2] * Rt[2] -// Store a 32x32->64 product (held in XTEMP) to one lane: -// LO.SD[dd] = (s32)low32 sign-extended, HI.SD[dd] = high32 sign-extended, -// and the full 64-bit product to GPR[rd].UD[dd] when rd != 0. -static void emitWordMulStore(u32 rd, u32 rdOff, u32 loOff, u32 hiOff) -{ - if (rd != 0) - armAsm->Str(XTEMP, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + rdOff)); - armAsm->Sxtw(a64::x9, XTEMP.W()); // LO = sign-extend low 32 into 64 - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Asr(a64::x9, XTEMP, 32); // HI = arithmetic high 32 (already 64-bit) - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, hiOff)); -} - -void armEmitPMULTW(u32 rd, u32 rs, u32 rt) -{ - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Smull(XTEMP, WTEMP1, WTEMP2); - emitWordMulStore(rd, /*rdOff*/ 0, EE_LO_OFFSET, EE_HI_OFFSET); - - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); - armAsm->Ldr(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + 8)); - armAsm->Smull(XTEMP, WTEMP1, WTEMP2); - emitWordMulStore(rd, /*rdOff*/ 8, EE_LO1_OFFSET, EE_HI1_OFFSET); -} - -// ----------------------------------------------------------------------------- -// PMULTUW: Unsigned word multiply (lanes 0 and 2) -// ----------------------------------------------------------------------------- -// Same as PMULTW but unsigned multiply. -void armEmitPMULTUW(u32 rd, u32 rs, u32 rt) -{ - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Umull(XTEMP, WTEMP1, WTEMP2); - emitWordMulStore(rd, /*rdOff*/ 0, EE_LO_OFFSET, EE_HI_OFFSET); - - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); - armAsm->Ldr(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + 8)); - armAsm->Umull(XTEMP, WTEMP1, WTEMP2); - emitWordMulStore(rd, /*rdOff*/ 8, EE_LO1_OFFSET, EE_HI1_OFFSET); -} - -// ----------------------------------------------------------------------------- -// PMADDUW: Unsigned word multiply-add -// ----------------------------------------------------------------------------- -// For each lane (dd=0/ss=0 and dd=1/ss=2): -// temp = Rs[ss] * Rt[ss] (unsigned 32x32->64) -// temp2 = temp + (HI[ss] << 32) -// LO.SD[dd] = (s32)(temp & 0xffffffff) + LO[ss] -// HI.SD[dd] = (s32)(temp2 >> 32) (no division voodoo for unsigned) -// if (Rd) { GPR[rd].UL[dd*2] = LO.UL[dd*2]; GPR[rd].UL[dd*2+1] = HI.UL[dd*2]; } -// One PMADDUW lane. Interpreter: -// tempu = (LO.UL[ss] | (HI.UL[ss] << 32)) + (u64)Rs.UL[ss] * (u64)Rt.UL[ss]; -// LO.SD[dd] = (s32)(tempu & 0xffffffff); HI.SD[dd] = (s32)(tempu >> 32); -// if (Rd) GPR[rd].UD[dd] = tempu; -// The whole 64-bit accumulator is formed first so a carry out of the low word -// propagates into the high word. -static void emitPMADDUWLane(u32 rd, u32 rs, u32 rt, u32 srcOff, u32 loOff, u32 hiOff, u32 rdOff) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + srcOff)); // Rs.UL[ss] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + srcOff)); // Rt.UL[ss] - armAsm->Umull(a64::x11, a64::w9, a64::w10); // product - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, loOff)); // LO.UL[ss] (zero-extended) - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, hiOff)); // HI.UL[ss] (zero-extended) - armAsm->Add(a64::x11, a64::x11, a64::x9); - armAsm->Add(a64::x11, a64::x11, a64::Operand(a64::x10, a64::LSL, 32)); // tempu - - if (rd != 0) - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + rdOff)); // full 64-bit - - armAsm->Sxtw(a64::x9, a64::w11); // (s32)(tempu & 0xffffffff) - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Asr(a64::x10, a64::x11, 32); - armAsm->Sxtw(a64::x10, a64::w10); // (s32)(tempu >> 32) - armAsm->Str(a64::x10, a64::MemOperand(RESTATEPTR, hiOff)); -} - -void armEmitPMADDUW(u32 rd, u32 rs, u32 rt) -{ - emitPMADDUWLane(rd, rs, rt, /*srcOff*/ 0, EE_LO_OFFSET, EE_HI_OFFSET, /*rdOff*/ 0); - emitPMADDUWLane(rd, rs, rt, /*srcOff*/ 8, EE_LO1_OFFSET, EE_HI1_OFFSET, /*rdOff*/ 8); -} - -// ----------------------------------------------------------------------------- -// PMADDW: Word multiply-add with EE division voodoo -// ----------------------------------------------------------------------------- -// For each lane (dd=0/ss=0 and dd=1/ss=2): -// temp = Rs[ss] * Rt[ss] (64-bit) -// temp2 = temp + (HI[ss] << 32) -// // EE division voodoo for lane 0 only: -// if (ss==0 && ((Rt[0]&0x7FFFFFFF)==0 || (Rt[0]&0x7FFFFFFF)==0x7FFFFFFF) && Rs[0]!=Rt[0]) -// temp2 += 0x70000000 -// temp2 = (s32)(temp2 / 4294967295) // off-by-1 multiplication error -// LO.SD[dd] = (s32)(temp & 0xffffffff) + LO[ss] -// HI.SD[dd] = (s32)temp2 -// if (Rd) { GPR[rd].UL[dd*2] = LO.UL[dd*2]; GPR[rd].UL[dd*2+1] = HI.UL[dd*2]; } -// Emit one PMADDW lane: dd selects LO/HI.UD[dd], srcOff is the GPR byte offset of -// lane `ss` (Rs/Rt low word) and loOff/hiOff the LO/HI lane offsets. `voodoo` adds -// the PS2 lane-0 division quirk. -static void emitPMADDWLane(u32 rd, u32 rs, u32 rt, u32 srcOff, u32 loOff, u32 hiOff, - u32 rdOff, bool voodoo) -{ - a64::Label voodoo_done; - - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + srcOff)); // Rs[ss] - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + srcOff)); // Rt[ss] - armAsm->Smull(a64::x11, a64::w9, a64::w10); // temp = (s64)Rs[ss] * (s64)Rt[ss] - - // LO.SD[dd] = (s32)(temp & 0xffffffff) + LO[ss] — uses the *pure* product. - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Add(a64::w12, a64::w11, a64::w12); - armAsm->Sxtw(a64::x12, a64::w12); - armAsm->Str(a64::x12, a64::MemOperand(RESTATEPTR, loOff)); - - // temp2 = temp + (HI.SL[ss] << 32), with the lane-0 division voodoo. - if (voodoo) - { - // Condition: ((Rt&0x7FFFFFFF)==0 || ==0x7FFFFFFF) && Rs != Rt - // Both ==0 and ==0x7FFFFFFF are triggers, so a zero result must fall - // through to the Rs!=Rt check, not skip the add. - a64::Label voodoo_check_rs; - armAsm->And(a64::w12, a64::w10, 0x7FFFFFFF); - armAsm->Cbz(a64::w12, &voodoo_check_rs); // ==0 -> still a trigger - armAsm->Cmp(a64::w12, 0x7FFFFFFF); - armAsm->B(&voodoo_done, a64::ne); // neither 0 nor 0x7FFFFFFF -> no voodoo - armAsm->Bind(&voodoo_check_rs); - armAsm->Cmp(a64::w9, a64::w10); - armAsm->B(&voodoo_done, a64::eq); // Rs == Rt -> no voodoo - armAsm->Mov(a64::w12, 0x70000000); - armAsm->Add(a64::x11, a64::x11, a64::x12); // temp2 += 0x70000000 - armAsm->Bind(&voodoo_done); - } - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, hiOff)); // HI.SL[ss] - armAsm->Sxtw(a64::x12, a64::w12); - armAsm->Add(a64::x11, a64::x11, a64::Operand(a64::x12, a64::LSL, 32)); - - // temp2 = (s32)(temp2 / 4294967295) — positive 64-bit divisor. - armAsm->Mov(a64::x12, 0xFFFFFFFF); - armAsm->Sdiv(a64::x11, a64::x11, a64::x12); - armAsm->Sxtw(a64::x11, a64::w11); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, hiOff)); - - if (rd != 0) - { - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, hiOff)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + rdOff)); - } -} - -void armEmitPMADDW(u32 rd, u32 rs, u32 rt) -{ - emitPMADDWLane(rd, rs, rt, /*srcOff*/ 0, EE_LO_OFFSET, EE_HI_OFFSET, /*rdOff*/ 0, /*voodoo*/ true); - emitPMADDWLane(rd, rs, rt, /*srcOff*/ 8, EE_LO1_OFFSET, EE_HI1_OFFSET, /*rdOff*/ 8, /*voodoo*/ false); -} - -// ----------------------------------------------------------------------------- -// PMSUBW: Word multiply-subtract -// ----------------------------------------------------------------------------- -// For each lane (dd=0/ss=0 and dd=1/ss=2): -// temp = Rs[ss] * Rt[ss] -// temp2 = (HI[ss] << 32) - temp -// temp2 = (s32)(temp2 / 4294967295) -// LO.SD[dd] = LO[ss] - (s32)(temp & 0xffffffff) -// HI.SD[dd] = (s32)temp2 -// if (Rd) { GPR[rd].UL[dd*2] = LO.UL[dd*2]; GPR[rd].UL[dd*2+1] = HI.UL[dd*2]; } -// One PMSUBW lane. Interpreter: -// temp = Rs[ss]*Rt[ss]; temp2 = (HI.SL[ss] << 32) - temp; -// temp2 = (s32)(temp2 / 4294967295); -// LO.SD[dd] = LO.SL[ss] - (s32)(temp & 0xffffffff); HI.SD[dd] = (s32)temp2; -static void emitPMSUBWLane(u32 rd, u32 rs, u32 rt, u32 srcOff, u32 loOff, u32 hiOff, u32 rdOff) -{ - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + srcOff)); - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt) + srcOff)); - armAsm->Smull(a64::x11, a64::w9, a64::w10); // temp (pure product) - - // LO.SD[dd] = LO[ss] - (s32)(temp & 0xffffffff) — from the pure product. - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Sxtw(a64::x12, a64::w12); - armAsm->Str(a64::x12, a64::MemOperand(RESTATEPTR, loOff)); - - // temp2 = (HI.SL[ss] << 32) - temp - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, hiOff)); - armAsm->Sxtw(a64::x12, a64::w12); - armAsm->Lsl(a64::x12, a64::x12, 32); - armAsm->Sub(a64::x11, a64::x12, a64::x11); - - armAsm->Mov(a64::x12, 0xFFFFFFFF); - armAsm->Sdiv(a64::x11, a64::x11, a64::x12); - armAsm->Sxtw(a64::x11, a64::w11); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, hiOff)); - - if (rd != 0) - { - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, loOff)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, hiOff)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + rdOff)); - } -} - -void armEmitPMSUBW(u32 rd, u32 rs, u32 rt) -{ - emitPMSUBWLane(rd, rs, rt, /*srcOff*/ 0, EE_LO_OFFSET, EE_HI_OFFSET, /*rdOff*/ 0); - emitPMSUBWLane(rd, rs, rt, /*srcOff*/ 8, EE_LO1_OFFSET, EE_HI1_OFFSET, /*rdOff*/ 8); -} - -// ----------------------------------------------------------------------------- -// PMULTH: Halfword multiply (8 lanes, alternating LO/HI) -// ----------------------------------------------------------------------------- -// For n = 0,2,4,6: -// LO.SD[n/2] = (s32)(Rs.SS[n] * Rt.SS[n]) -// HI.SD[n/2] = (s32)((Rs.SS[n] * Rt.SS[n]) >> 32) -// if (Rd) GPR[rd].SD[n/2] = Rs.SS[n] * Rt.SS[n] -// Byte offsets of the 8 halfword-product destinations LO/HI.UL[n] for n=0..7, -// matching the interpreter: LO0,LO1,HI0,HI1,LO2,LO3,HI2,HI3. -static const u32 kHalfwordMacOff[8] = { - EE_LO_OFFSET + 0, EE_LO_OFFSET + 4, EE_HI_OFFSET + 0, EE_HI_OFFSET + 4, - EE_LO_OFFSET + 8, EE_LO_OFFSET + 12, EE_HI_OFFSET + 8, EE_HI_OFFSET + 12}; - -// Pack GPR[rd] = {LO.UL[0], HI.UL[0], LO.UL[2], HI.UL[2]} (shared by the -// halfword multiply-accumulate family). -static void emitHalfwordMacStoreRd(u32 rd) -{ - if (rd == 0) - return; - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Ldr(a64::x11, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - armAsm->Ldr(a64::x12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - armAsm->Bfi(a64::x11, a64::x12, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); -} - -void armEmitPMULTH(u32 rd, u32 rs, u32 rt) -{ - loadQ(VS, rs); - loadQ(VT, rt); - - // 8 independent halfword products: LO/HI.UL[n] = (s32)(Rs.SS[n] * Rt.SS[n]). - for (int n = 0; n < 8; n++) - { - armAsm->Smov(a64::w9, VT.V8H(), n); - armAsm->Smov(a64::w10, VS.V8H(), n); - armAsm->Mul(a64::w11, a64::w9, a64::w10); - armAsm->Str(a64::w11, a64::MemOperand(RESTATEPTR, kHalfwordMacOff[n])); - } - - emitHalfwordMacStoreRd(rd); -} - -// ----------------------------------------------------------------------------- -// PMADDH: Halfword multiply-add (8 lanes) -// ----------------------------------------------------------------------------- -// For n = 0,1,2,3,4,5,6,7: -// temp = LO/HI.UL[n] + Rs.SS[n] * Rt.SS[n] -// LO/HI.UL[n] = temp (alternating: n even -> LO, n odd -> HI) -// if (Rd) { GPR[rd].UL[0]=LO.UL[0], GPR[rd].UL[1]=HI.UL[0], -// GPR[rd].UL[2]=LO.UL[2], GPR[rd].UL[3]=HI.UL[2] } -void armEmitPMADDH(u32 rd, u32 rs, u32 rt) -{ - loadQ(VS, rs); - loadQ(VT, rt); - - a64::Label skip_rd; - - // Process 8 halfword lanes, accumulating into LO/HI - // n=0 -> LO.UL[0], n=1 -> LO.UL[1], n=2 -> HI.UL[0], n=3 -> HI.UL[1] - // n=4 -> LO.UL[2], n=5 -> LO.UL[3], n=6 -> HI.UL[2], n=7 -> HI.UL[3] - - // We need to load each halfword, multiply, accumulate, and store - // For simplicity, do scalar operations lane by lane - - // n=0: LO.UL[0] += Rs.SS[0] * Rt.SS[0] - armAsm->Smov(a64::w9, VT.V8H(), 0); - armAsm->Smov(a64::w10, VS.V8H(), 0); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - - // n=1: LO.UL[1] += Rs.SS[1] * Rt.SS[1] - armAsm->Smov(a64::w9, VT.V8H(), 1); - armAsm->Smov(a64::w10, VS.V8H(), 1); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 4)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 4)); - - // n=2: HI.UL[0] += Rs.SS[2] * Rt.SS[2] - armAsm->Smov(a64::w9, VT.V8H(), 2); - armAsm->Smov(a64::w10, VS.V8H(), 2); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - - // n=3: HI.UL[1] += Rs.SS[3] * Rt.SS[3] - armAsm->Smov(a64::w9, VT.V8H(), 3); - armAsm->Smov(a64::w10, VS.V8H(), 3); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 4)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 4)); - - // n=4: LO.UL[2] += Rs.SS[4] * Rt.SS[4] - armAsm->Smov(a64::w9, VT.V8H(), 4); - armAsm->Smov(a64::w10, VS.V8H(), 4); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - - // n=5: LO.UL[3] += Rs.SS[5] * Rt.SS[5] - armAsm->Smov(a64::w9, VT.V8H(), 5); - armAsm->Smov(a64::w10, VS.V8H(), 5); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 12)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 12)); - - // n=6: HI.UL[2] += Rs.SS[6] * Rt.SS[6] - armAsm->Smov(a64::w9, VT.V8H(), 6); - armAsm->Smov(a64::w10, VS.V8H(), 6); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - - // n=7: HI.UL[3] += Rs.SS[7] * Rt.SS[7] - armAsm->Smov(a64::w9, VT.V8H(), 7); - armAsm->Smov(a64::w10, VS.V8H(), 7); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 12)); - armAsm->Add(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 12)); - - // GPR[rd] if rd != 0: {LO.UL[2], HI.UL[2], LO.UL[0], HI.UL[0]} - // Actually per interpreter: UL[0]=LO.UL[0], UL[1]=HI.UL[0], UL[2]=LO.UL[2], UL[3]=HI.UL[2] - if (rd != 0) { - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Ldr(a64::x11, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - armAsm->Ldr(a64::x12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - armAsm->Bfi(a64::x11, a64::x12, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); - } -} - -// ----------------------------------------------------------------------------- -// PMSUBH: Halfword multiply-subtract (8 lanes) -// ----------------------------------------------------------------------------- -// For n = 0,1,2,3,4,5,6,7: -// temp = LO/HI.UL[n] - Rs.SS[n] * Rt.SS[n] -// LO/HI.UL[n] = temp (alternating: n even -> LO, n odd -> HI) -// if (Rd) { GPR[rd].UL[0]=LO.UL[0], GPR[rd].UL[1]=HI.UL[0], -// GPR[rd].UL[2]=LO.UL[2], GPR[rd].UL[3]=HI.UL[2] } -void armEmitPMSUBH(u32 rd, u32 rs, u32 rt) -{ - loadQ(VS, rs); - loadQ(VT, rt); - - // n=0: LO.UL[0] -= Rs.SS[0] * Rt.SS[0] - armAsm->Smov(a64::w9, VT.V8H(), 0); - armAsm->Smov(a64::w10, VS.V8H(), 0); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - - // n=1: LO.UL[1] -= Rs.SS[1] * Rt.SS[1] - armAsm->Smov(a64::w9, VT.V8H(), 1); - armAsm->Smov(a64::w10, VS.V8H(), 1); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 4)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 4)); - - // n=2: HI.UL[0] -= Rs.SS[2] * Rt.SS[2] - armAsm->Smov(a64::w9, VT.V8H(), 2); - armAsm->Smov(a64::w10, VS.V8H(), 2); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - - // n=3: HI.UL[1] -= Rs.SS[3] * Rt.SS[3] - armAsm->Smov(a64::w9, VT.V8H(), 3); - armAsm->Smov(a64::w10, VS.V8H(), 3); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 4)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 4)); - - // n=4: LO.UL[2] -= Rs.SS[4] * Rt.SS[4] - armAsm->Smov(a64::w9, VT.V8H(), 4); - armAsm->Smov(a64::w10, VS.V8H(), 4); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - - // n=5: LO.UL[3] -= Rs.SS[5] * Rt.SS[5] - armAsm->Smov(a64::w9, VT.V8H(), 5); - armAsm->Smov(a64::w10, VS.V8H(), 5); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 12)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 12)); - - // n=6: HI.UL[2] -= Rs.SS[6] * Rt.SS[6] - armAsm->Smov(a64::w9, VT.V8H(), 6); - armAsm->Smov(a64::w10, VS.V8H(), 6); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - - // n=7: HI.UL[3] -= Rs.SS[7] * Rt.SS[7] - armAsm->Smov(a64::w9, VT.V8H(), 7); - armAsm->Smov(a64::w10, VS.V8H(), 7); - armAsm->Smull(a64::x11, a64::w9, a64::w10); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 12)); - armAsm->Sub(a64::w12, a64::w12, a64::w11); - armAsm->Str(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 12)); - - // GPR[rd] if rd != 0 - if (rd != 0) { - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Ldr(a64::x11, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 8)); - armAsm->Ldr(a64::x12, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 8)); - armAsm->Bfi(a64::x11, a64::x12, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); - } -} - -// ----------------------------------------------------------------------------- -// PHMADH: Packed halfword multiply-add (8 lanes, paired) -// ----------------------------------------------------------------------------- -// For n = 0,2,4,6: -// temp = Rs.SS[n]*Rt.SS[n] + Rs.SS[n+1]*Rt.SS[n+1] -// if (n%4==0) LO.UL[n/2] += temp; else HI.UL[n/2] += temp -// if (Rd) { GPR[rd].UL[0]=LO.UL[0], GPR[rd].UL[1]=HI.UL[0], ... } -// One PHMADH/PHMSBH pair (Rs/Rt already loaded into VS/VT): -// firsttemp = Rs.SS[n+1] * Rt.SS[n+1] -// add: temp = firsttemp + Rs.SS[n]*Rt.SS[n]; odd lane = firsttemp -// sub: temp = firsttemp - Rs.SS[n]*Rt.SS[n]; odd lane = ~firsttemp (undocumented) -// `offTemp`/`offFirst` are the even/odd destination lane offsets. No accumulation -// with the previous LO/HI contents (matches the interpreter). -static void emitPHMPair(int n, u32 offTemp, u32 offFirst, bool sub) -{ - armAsm->Smov(a64::w9, VT.V8H(), n + 1); - armAsm->Smov(a64::w10, VS.V8H(), n + 1); - armAsm->Mul(a64::w11, a64::w9, a64::w10); // firsttemp - armAsm->Smov(a64::w9, VT.V8H(), n); - armAsm->Smov(a64::w10, VS.V8H(), n); - armAsm->Mul(a64::w12, a64::w9, a64::w10); // Rs.SS[n] * Rt.SS[n] - if (sub) - { - armAsm->Sub(a64::w9, a64::w11, a64::w12); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, offTemp)); - armAsm->Mvn(a64::w11, a64::w11); // ~firsttemp - } - else - { - armAsm->Add(a64::w9, a64::w11, a64::w12); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, offTemp)); - } - armAsm->Str(a64::w11, a64::MemOperand(RESTATEPTR, offFirst)); -} - -void armEmitPHMADH(u32 rd, u32 rs, u32 rt) -{ - loadQ(VS, rs); - loadQ(VT, rt); - emitPHMPair(/*n*/ 0, EE_LO_OFFSET + 0, EE_LO_OFFSET + 4, /*sub*/ false); - emitPHMPair(/*n*/ 2, EE_HI_OFFSET + 0, EE_HI_OFFSET + 4, /*sub*/ false); - emitPHMPair(/*n*/ 4, EE_LO_OFFSET + 8, EE_LO_OFFSET + 12, /*sub*/ false); - emitPHMPair(/*n*/ 6, EE_HI_OFFSET + 8, EE_HI_OFFSET + 12, /*sub*/ false); - emitHalfwordMacStoreRd(rd); -} - -// ----------------------------------------------------------------------------- -// PHMSBH: Packed halfword multiply-subtract (8 lanes, paired) -// ----------------------------------------------------------------------------- -// For n = 0,2,4,6: -// temp = Rs.SS[n]*Rt.SS[n] - Rs.SS[n+1]*Rt.SS[n+1] -// if (n%4==0) LO.UL[n/2] += temp; else HI.UL[n/2] += temp -void armEmitPHMSBH(u32 rd, u32 rs, u32 rt) -{ - loadQ(VS, rs); - loadQ(VT, rt); - emitPHMPair(/*n*/ 0, EE_LO_OFFSET + 0, EE_LO_OFFSET + 4, /*sub*/ true); - emitPHMPair(/*n*/ 2, EE_HI_OFFSET + 0, EE_HI_OFFSET + 4, /*sub*/ true); - emitPHMPair(/*n*/ 4, EE_LO_OFFSET + 8, EE_LO_OFFSET + 12, /*sub*/ true); - emitPHMPair(/*n*/ 6, EE_HI_OFFSET + 8, EE_HI_OFFSET + 12, /*sub*/ true); - emitHalfwordMacStoreRd(rd); -} - -// ----------------------------------------------------------------------------- -// PMFHI/PMFLO/PMTHI/PMTLO: MMI HI/LO moves (full 128-bit) -// ----------------------------------------------------------------------------- -// PMFHI: Rd = HI (full 128-bit) -// PMFLO: Rd = LO (full 128-bit) -// PMTHI: HI = Rs (full 128-bit) -// PMTLO: LO = Rs (full 128-bit) -void armEmitPMFHI(u32 rd) -{ - if (rd == 0) - return; - // Load full 128-bit HI into q-register - armAsm->Ldr(VD.Q(), a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Str(VD.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitPMFLO(u32 rd) -{ - if (rd == 0) - return; - armAsm->Ldr(VD.Q(), a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Str(VD.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitPMTHI(u32 rs) -{ - armAsm->Ldr(VD.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(VD.Q(), a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); -} - -void armEmitPMTLO(u32 rs) -{ - armAsm->Ldr(VD.Q(), a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(VD.Q(), a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); -} - -// ============================================================================= -// Remaining MMI misc ops (Phase 5.4 completion) -// ============================================================================= - -// ----------------------------------------------------------------------------- -// PLZCW: Count leading sign bits (excluding the sign bit itself) -// ----------------------------------------------------------------------------- -// GPR[rd].UL[0] = CountLeadingSignBits(Rs.SL[0]) - 1 -// GPR[rd].UL[1] = CountLeadingSignBits(Rs.SL[1]) - 1 -void armEmitPLZCW(u32 rd, u32 rs) -{ - if (rd == 0) - return; - - // Interpreter: GPR[rd].UL[n] = CountLeadingSignBits(Rs.SL[n]) - 1. - // ARM64 CLS counts leading bits equal to the sign bit *excluding* the sign - // bit, which is exactly CountLeadingSignBits(x) - 1 — so no adjustment. - // Lane 0 (low 32 bits) - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Cls(WTEMP2, WTEMP1); - armAsm->Str(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - // Lane 1 (high 32 bits of low 64 bits) - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 4)); - armAsm->Cls(WTEMP2, WTEMP1); - armAsm->Str(WTEMP2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 4)); -} - -// ----------------------------------------------------------------------------- -// PADSBH: Packed add/subtract halfwords (subtract low 4, add high 4) -// ----------------------------------------------------------------------------- -// Rd.US[0..3] = Rs.US[0..3] - Rt.US[0..3] (no saturation, just truncate) -// Rd.US[4..7] = Rs.US[4..7] + Rt.US[4..7] (no saturation, just truncate) -void armEmitPADSBH(u32 rd, u32 rs, u32 rt) -{ - if (rd == 0) - return; - - loadQ(VS, rs); - loadQ(VT, rt); - - // Lanes 0-3: subtract (low 4 halfwords) - for (int i = 0; i < 4; i++) { - armAsm->Smov(a64::w9, VS.V8H(), i); - armAsm->Smov(a64::w10, VT.V8H(), i); - armAsm->Sub(a64::w11, a64::w9, a64::w10); - armAsm->Ins(VD.V8H(), i, a64::w11); // insert low 16 bits (w11 -> halfword lane i) - } - // Lanes 4-7: add (high 4 halfwords) - for (int i = 4; i < 8; i++) { - armAsm->Smov(a64::w9, VS.V8H(), i); - armAsm->Smov(a64::w10, VT.V8H(), i); - armAsm->Add(a64::w11, a64::w9, a64::w10); - armAsm->Ins(VD.V8H(), i, a64::w11); - } - - storeQ(VD, rd); -} - -// QFSRV is handled by the interpreter (its shift amount is the runtime SA -// register cpuRegs.sa, not an instruction immediate), so there is no generator. - -// ----------------------------------------------------------------------------- -// PEXT5: Pack 5-bit fields (extract and expand 5-bit fields to 8-bit) -// ----------------------------------------------------------------------------- -// For each 32-bit lane: -// Rd.UL[n] = ((Rt.UL[n] & 0x1F) << 3) | ((Rt.UL[n] & 0x3E0) << 6) | -// ((Rt.UL[n] & 0x7C00) << 9) | ((Rt.UL[n] & 0x8000) << 16) -// This expands four 5-bit fields (at bits 0-4, 5-9, 10-14, 15) to four 8-bit fields -void armEmitPEXT5(u32 rd, u32 rt) -{ - if (rd == 0) - return; - - // Load Rt and process each 32-bit lane - for (int lane = 0; lane < 4; lane++) { - u32 offset = EE_GPR_OFFSET(rt) + lane * 4; - u32 outOffset = EE_GPR_OFFSET(rd) + lane * 4; - - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, offset)); - // Extract and expand each 5-bit field - // Field 0: bits 0-4 -> bits 0-7 (<< 3) - armAsm->And(WTEMP2, WTEMP1, 0x1F); - armAsm->Lsl(WTEMP2, WTEMP2, 3); - // Field 1: bits 5-9 -> bits 8-15 (<< 6) - armAsm->And(WTEMP3, WTEMP1, 0x3E0); - armAsm->Lsl(WTEMP3, WTEMP3, 6); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - // Field 2: bits 10-14 -> bits 16-23 (<< 9) - armAsm->And(WTEMP3, WTEMP1, 0x7C00); - armAsm->Lsl(WTEMP3, WTEMP3, 9); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - // Field 3: bit 15 -> bits 24-31 (<< 16) - armAsm->And(WTEMP3, WTEMP1, 0x8000); - armAsm->Lsl(WTEMP3, WTEMP3, 16); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - - armAsm->Str(WTEMP2, a64::MemOperand(RESTATEPTR, outOffset)); - } -} - -// ----------------------------------------------------------------------------- -// PPAC5: Unpack 5-bit fields (compress 8-bit fields to 5-bit) -// ----------------------------------------------------------------------------- -// For each 32-bit lane: -// Rd.UL[n] = ((Rt.UL[n] >> 3) & 0x1F) | ((Rt.UL[n] >> 6) & 0x3E0) | -// ((Rt.UL[n] >> 9) & 0x7C00) | ((Rt.UL[n] >> 16) & 0x8000) -void armEmitPPAC5(u32 rd, u32 rt) -{ - if (rd == 0) - return; - - // Load Rt and process each 32-bit lane - for (int lane = 0; lane < 4; lane++) { - u32 offset = EE_GPR_OFFSET(rt) + lane * 4; - u32 outOffset = EE_GPR_OFFSET(rd) + lane * 4; - - armAsm->Ldr(WTEMP1, a64::MemOperand(RESTATEPTR, offset)); - // Compress each 8-bit field to 5 bits - // Field 0: bits 0-7 -> bits 0-4 (>> 3) - armAsm->Lsr(WTEMP2, WTEMP1, 3); - armAsm->And(WTEMP2, WTEMP2, 0x1F); - // Field 1: bits 8-15 -> bits 5-9 (>> 6) - armAsm->Lsr(WTEMP3, WTEMP1, 6); - armAsm->And(WTEMP3, WTEMP3, 0x3E0); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - // Field 2: bits 16-23 -> bits 10-14 (>> 9) - armAsm->Lsr(WTEMP3, WTEMP1, 9); - armAsm->And(WTEMP3, WTEMP3, 0x7C00); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - // Field 3: bits 24-31 -> bit 15 (>> 16) - armAsm->Lsr(WTEMP3, WTEMP1, 16); - armAsm->And(WTEMP3, WTEMP3, 0x8000); - armAsm->Orr(WTEMP2, WTEMP2, WTEMP3); - - armAsm->Str(WTEMP2, a64::MemOperand(RESTATEPTR, outOffset)); - } -} - -// ----------------------------------------------------------------------------- -// PMFHL: Move from HI/LO (multiple variants based on sa field) -// ----------------------------------------------------------------------------- -// sa=0x00 (LW): Rd = {LO.UL[0], HI.UL[0], LO.UL[2], HI.UL[2]} -// sa=0x01 (UW): Rd = {LO.UL[1], HI.UL[1], LO.UL[3], HI.UL[3]} -// sa=0x02 (SLW): Rd = sign-clamp 64-bit from HI/LO pairs -// sa=0x03 (LH): Rd = {LO.US[0], LO.US[2], HI.US[0], HI.US[2], LO.US[4], LO.US[6], HI.US[4], HI.US[6]} -// sa=0x04 (SH): Rd = signed-saturate 16-bit from HI/LO -bool armEmitPMFHL(u32 rd, u32 sa) -{ - if (rd == 0) - return true; // interpreter also no-ops when rd==0 - - switch (sa) - { - case 0x00: // LW: Rd = {LO.UL[0], HI.UL[0], LO.UL[2], HI.UL[2]} - armAsm->Ldr(a64::x9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Ldr(a64::x10, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - armAsm->Ldr(a64::x11, a64::MemOperand(RESTATEPTR, EE_LO1_OFFSET)); - armAsm->Ldr(a64::x12, a64::MemOperand(RESTATEPTR, EE_HI1_OFFSET)); - armAsm->Bfi(a64::x11, a64::x12, 32, 32); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); - return true; - - case 0x01: // UW: Rd = {LO.UL[1], HI.UL[1], LO.UL[3], HI.UL[3]} - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET + 4)); - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET + 4)); - armAsm->Bfi(a64::x9, a64::x10, 32, 32); - armAsm->Str(a64::x9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - armAsm->Ldr(a64::w11, a64::MemOperand(RESTATEPTR, EE_LO1_OFFSET + 4)); - armAsm->Ldr(a64::w12, a64::MemOperand(RESTATEPTR, EE_HI1_OFFSET + 4)); - armAsm->Bfi(a64::x11, a64::x12, 32, 32); - armAsm->Str(a64::x11, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + 8)); - return true; - - case 0x02: // SLW: clamp each 64-bit {HI.UL[2k]:LO.UL[2k]} to signed 32-bit range - { - static const u32 loOff[2] = {EE_LO_OFFSET, EE_LO1_OFFSET}; - static const u32 hiOff[2] = {EE_HI_OFFSET, EE_HI1_OFFSET}; - for (int k = 0; k < 2; k++) - { - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, loOff[k])); // LO.UL - armAsm->Ldr(a64::w10, a64::MemOperand(RESTATEPTR, hiOff[k])); // HI.UL - armAsm->Mov(a64::w11, a64::w9); // zero-extend LO - armAsm->Bfi(a64::x11, a64::x10, 32, 32); // TempS64 = LO | (HI<<32) - armAsm->Sxtw(a64::x13, a64::w9); // default: (s64)LO.SL - armAsm->Mov(a64::x12, 0x7fffffff); - armAsm->Cmp(a64::x11, a64::x12); - armAsm->Csel(a64::x13, a64::x12, a64::x13, a64::ge); // >= 0x7fffffff - armAsm->Mov(a64::x12, 0xffffffff80000000); - armAsm->Cmp(a64::x11, a64::x12); - armAsm->Csel(a64::x13, a64::x12, a64::x13, a64::le); // <= -0x80000000 - armAsm->Str(a64::x13, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + k * 8)); - } - return true; - } - - case 0x03: // LH: pack the even halfwords of LO/HI (kHalfwordMacOff order) - for (int i = 0; i < 8; i++) - { - armAsm->Ldrh(WTEMP1, a64::MemOperand(RESTATEPTR, kHalfwordMacOff[i])); - armAsm->Strh(WTEMP1, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + i * 2)); - } - return true; - - case 0x04: // SH: signed-saturate each LO/HI word to 16 bits (kHalfwordMacOff order) - for (int i = 0; i < 8; i++) - { - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, kHalfwordMacOff[i])); - armAsm->Mov(a64::w10, 0x7fff); - armAsm->Cmp(a64::w9, a64::w10); - armAsm->Csel(a64::w9, a64::w10, a64::w9, a64::gt); // > 0x7fff -> 0x7fff - armAsm->Mov(a64::w10, 0xffff8000); - armAsm->Cmp(a64::w9, a64::w10); - armAsm->Csel(a64::w9, a64::w10, a64::w9, a64::lt); // < -0x8000 -> 0x8000 - armAsm->Strh(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd) + i * 2)); - } - return true; - - default: - return false; // unknown variant -> interpreter - } -} - -// ----------------------------------------------------------------------------- -// PMTHL: Move to HI/LO (sa=0 only) -// ----------------------------------------------------------------------------- -// sa=0: LO = {Rs.UL[0], Rs.UL[1], Rs.UL[2], Rs.UL[3]} -// HI = {Rs.UL[1], Rs.UL[0], Rs.UL[3], Rs.UL[2]} -// Actually per interpreter: LO.UL[0]=Rs.UL[0], HI.UL[0]=Rs.UL[1], LO.UL[2]=Rs.UL[2], HI.UL[2]=Rs.UL[3] -void armEmitPMTHL(u32 rs, u32 sa) -{ - if (sa != 0) - return; // only PMTHL.LW (sa=0) is defined; interpreter no-ops otherwise - - // The interpreter writes only the even words, leaving LO/HI.UL[1] and [3] - // untouched — so use 32-bit stores, not 64-bit (which would clobber them). - // LO.UL[0]=Rs.UL[0] HI.UL[0]=Rs.UL[1] LO.UL[2]=Rs.UL[2] HI.UL[2]=Rs.UL[3] - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_LO_OFFSET)); - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 4)); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_HI_OFFSET)); - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 8)); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_LO1_OFFSET)); - armAsm->Ldr(a64::w9, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs) + 12)); - armAsm->Str(a64::w9, a64::MemOperand(RESTATEPTR, EE_HI1_OFFSET)); -} - - diff --git a/pcsx2/arm64/aR5900MultDiv.android.cpp b/pcsx2/arm64/aR5900MultDiv.android.cpp deleted file mode 100644 index 8c65ab8f48..0000000000 --- a/pcsx2/arm64/aR5900MultDiv.android.cpp +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-FileCopyrightText: 2026 isztld -// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -// ARM64 EE (R5900) recompiler — multiply/divide codegen (Phase 3.5). -// -// Generates ARM64 for the R5900 multiply and divide opcodes: -// MULT/MULTU — 32×32→64-bit multiply (HI/LO; also Rd=LO when rd!=0) -// DIV/DIVU — 32-bit divide (quotient in LO, remainder in HI) -// MULT1/MULTU1 — second-pipeline multiply (HI1/LO1; MMI group) -// DIV1/DIVU1 — second-pipeline divide (HI1/LO1; MMI group) -// -// Semantics are matched 1:1 against the interpreter (R5900OpcodeImpl.cpp / -// MMI.cpp). Note the R5900 has NO DMULT/DMULTU/DDIV/DDIVU — those are not EE -// instructions (they trap as reserved), so they are intentionally absent. -// -// No register allocator yet — every source GPR is read from cpuRegs in memory -// (via RESTATEPTR = &cpuRegs) and the HI/LO results are written straight back. -// Because the source GPRs are never modified, we freely reload them instead of -// keeping more than two values live. -// -// Register discipline (see arm64-port/CONVENTIONS.md + AsmHelpers): only x17 -// (RSCRATCHADDR) is removed from VIXL's scratch list in armStartBlock, so it is -// the safe manual scratch. x16 (RXVIXLSCRATCH) doubles as VIXL's macro temp — -// it must never hold a live value across a macro that materialises an immediate. -// This code avoids that entirely: the only immediates used are encodable -// (cmp #0, mov #1, mov #-1), so no temp is ever allocated. - -#include "aR5900.h" - -#include "R5900.h" - -#include - - - -namespace a64 = vixl::aarch64; - -// Two scratch registers. RSCRATCH (x17) is the safe manual scratch; RSCRATCH2 -// (x16) is used only as a plain operand register for reg-reg ALU ops here. -static const a64::Register RSCRATCH = RSCRATCHADDR; -static const a64::Register RSCRATCHW = RSCRATCHADDR.W(); -static const a64::Register RSCRATCH2 = RXVIXLSCRATCH; -static const a64::Register RSCRATCH2W = RXVIXLSCRATCH.W(); - -// HI/LO live at GPR indices 32/33 (each GPR_reg is 128 bits). The "pipeline 1" -// results (MULT1/DIV1 family) target the upper doubleword, i.e. +8 bytes. -static constexpr u32 EE_HI_OFFSET = 32u * 16u; // HI.UD[0] (512) -static constexpr u32 EE_LO_OFFSET = 33u * 16u; // LO.UD[0] (528) -static constexpr u32 EE_HI1_OFFSET = EE_HI_OFFSET + 8u; // HI.UD[1] (520) -static constexpr u32 EE_LO1_OFFSET = EE_LO_OFFSET + 8u; // LO.UD[1] (536) - -// ------------------------------------------------------------------------ -// Shared 32×32→64 multiply. -// LO = (s32)(product & 0xffffffff) (sign-extended to 64, even for MULTU) -// HI = (s32)(product >> 32) (sign-extended to 64) -// if rd != 0: GPR[rd].UD[0] = LO (R5900 3-operand form) -// ------------------------------------------------------------------------ -static void emitMult(bool sign, u32 rd, u32 rs, u32 rt, u32 lo_off, u32 hi_off) -{ - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - - // Full 64-bit product in RSCRATCH (signed or unsigned widening multiply). - if (sign) - armAsm->Smull(RSCRATCH, RSCRATCHW, RSCRATCH2W); - else - armAsm->Umull(RSCRATCH, RSCRATCHW, RSCRATCH2W); - - // LO = sign-extended low 32 bits of the product. - armAsm->Sxtw(RSCRATCH2, RSCRATCHW); - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, lo_off)); - if (rd != 0) - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - // HI = sign-extended high 32 bits. asr #32 leaves bits 63:32 in 31:0 and - // sign-extends from bit 63 (== bit 31 of the high word), matching (s32). - armAsm->Asr(RSCRATCH, RSCRATCH, 32); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, hi_off)); -} - -// ------------------------------------------------------------------------ -// Shared signed 32-bit divide. -// LO = rs / rt, HI = rs % rt (both sign-extended to 64). -// ARM SDIV reproduces the EE's overflow quirk for free: 0x80000000 / -1 yields -// 0x80000000, and the remainder works out to 0. Only the divide-by-zero case -// needs a fixup: LO = (rs < 0) ? 1 : -1, HI = rs (HI already equals rs there, -// since SDIV yields 0 so remainder = rs - 0 = rs). -// ------------------------------------------------------------------------ -static void emitDivS(u32 rs, u32 rt, u32 lo_off, u32 hi_off) -{ - a64::Label done; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // dividend - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // divisor - - armAsm->Sdiv(RSCRATCHW, RSCRATCHW, RSCRATCH2W); // RSCRATCHW = quotient - armAsm->Mul(RSCRATCH2W, RSCRATCHW, RSCRATCH2W); // RSCRATCH2W = quotient * divisor - - // LO = sign-extended quotient (free up RSCRATCH afterwards). - armAsm->Sxtw(RSCRATCH, RSCRATCHW); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, lo_off)); - - // HI = sign-extended remainder = dividend - quotient*divisor. - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Sub(RSCRATCH2W, RSCRATCHW, RSCRATCH2W); - armAsm->Sxtw(RSCRATCH2, RSCRATCH2W); - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, hi_off)); - - // Divide-by-zero fixup for LO (HI is already correct: == dividend). - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH2W, 0); - armAsm->B(a64::ne, &done); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // dividend - armAsm->Cmp(RSCRATCHW, 0); - armAsm->Mov(RSCRATCH2W, 1); - armAsm->Csneg(RSCRATCH2W, RSCRATCH2W, RSCRATCH2W, a64::lt); // (dividend<0) ? 1 : -1 - armAsm->Sxtw(RSCRATCH2, RSCRATCH2W); - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, lo_off)); - armAsm->Bind(&done); -} - -// ------------------------------------------------------------------------ -// Shared unsigned 32-bit divide. -// LO = (s32)(rs / rt), HI = (s32)(rs % rt) (note: sign-extended to 64). -// Divide-by-zero: LO = -1 (full 64-bit), HI = rs (sign-extended). -// ------------------------------------------------------------------------ -static void emitDivU(u32 rs, u32 rt, u32 lo_off, u32 hi_off) -{ - a64::Label done; - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); // dividend - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); // divisor - - armAsm->Udiv(RSCRATCHW, RSCRATCHW, RSCRATCH2W); // RSCRATCHW = quotient (÷0 -> 0) - armAsm->Mul(RSCRATCH2W, RSCRATCHW, RSCRATCH2W); // RSCRATCH2W = quotient * divisor - - armAsm->Sxtw(RSCRATCH, RSCRATCHW); // LO = (s32)quotient - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, lo_off)); - - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Sub(RSCRATCH2W, RSCRATCHW, RSCRATCH2W); // remainder - armAsm->Sxtw(RSCRATCH2, RSCRATCH2W); // HI = (s32)remainder - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, hi_off)); - - // Divide-by-zero fixup for LO (HI already correct: == sign-extended dividend). - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - armAsm->Cmp(RSCRATCH2W, 0); - armAsm->B(a64::ne, &done); - armAsm->Mov(RSCRATCH, 0xFFFFFFFFFFFFFFFFull); // LO = -1 (encodable: all ones) - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, lo_off)); - armAsm->Bind(&done); -} - -// ------------------------------------------------------------------------ -// SPECIAL group: MULT/MULTU (funct 0x18/0x19), DIV/DIVU (0x1A/0x1B). -// ------------------------------------------------------------------------ -void armEmitMULT(u32 rd, u32 rs, u32 rt) { emitMult(true, rd, rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } -void armEmitMULTU(u32 rd, u32 rs, u32 rt) { emitMult(false, rd, rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } -void armEmitDIV(u32 rs, u32 rt) { emitDivS(rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } -void armEmitDIVU(u32 rs, u32 rt) { emitDivU(rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } - -// ------------------------------------------------------------------------ -// MMI group: MULT1/MULTU1 (funct 0x18/0x19), DIV1/DIVU1 (0x1A/0x1B). -// Identical arithmetic, but results target the upper doubleword HI1/LO1, and -// the optional Rd write reads LO.UD[1] (== the value we store to LO1). -// ------------------------------------------------------------------------ -void armEmitMULT1(u32 rd, u32 rs, u32 rt) { emitMult(true, rd, rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } -void armEmitMULTU1(u32 rd, u32 rs, u32 rt) { emitMult(false, rd, rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } -void armEmitDIV1(u32 rs, u32 rt) { emitDivS(rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } -void armEmitDIVU1(u32 rs, u32 rt) { emitDivU(rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } - -// ------------------------------------------------------------------------ -// Multiply-accumulate (MMI funct 0x00/0x01 MADD/MADDU, 0x20/0x21 MADD1/MADDU1). -// acc = (u64)LO.UL[0] | ((u64)HI.UL[0] << 32) (low 32 of each accumulator word) -// temp = acc + (rs * rt) (signed for MADD/MADD1, unsigned for the U forms) -// LO = (s32)(temp & 0xffffffff) (sign-extended to 64) -// HI = (s32)(temp >> 32) (sign-extended to 64) -// if rd != 0: GPR[rd].UD[0] = LO (R5900 3-operand form) -// The two 32-bit accumulator words are added straight onto the 64-bit product -// (HI word << 32, then LO word), so `acc` never needs its own register and the op -// fits in the two manual scratch registers. Result sign-extension is identical for -// the unsigned forms (the interpreter sign-extends LO/HI regardless). The pipeline-1 -// forms select the upper doubleword via lo_off/hi_off, exactly like MULT1. -// ------------------------------------------------------------------------ -static void emitMadd(bool sign, u32 rd, u32 rs, u32 rt, u32 lo_off, u32 hi_off) -{ - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Ldr(RSCRATCHW, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rt))); - - // The live 64-bit accumulator is kept in RSCRATCH (x17), which AsmHelpers removes - // from VIXL's scratch-register list (AsmHelpers.cpp), so it is safe to hold across - // the macro ops below — even if one of them allocated a VIXL temp it could not pick - // x17. RSCRATCH2 (x16 == RXVIXLSCRATCH) is VIXL's macro scratch, so it is only ever - // loaded and immediately consumed as a plain operand here, never held across a macro. - if (sign) - armAsm->Smull(RSCRATCH, RSCRATCH2W, RSCRATCHW); // x17 = (s64)rs * (s32)rt - else - armAsm->Umull(RSCRATCH, RSCRATCH2W, RSCRATCHW); // x17 = (u64)rs * (u32)rt - - // temp = product + (HI.UL[0] << 32) + LO.UL[0]. The w-loads zero-extend, so each - // accumulator word contributes exactly its 32 bits with no stray high bits. - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, hi_off)); // HI accumulator word - armAsm->Add(RSCRATCH, RSCRATCH, a64::Operand(RSCRATCH2, a64::LSL, 32)); - armAsm->Ldr(RSCRATCH2W, a64::MemOperand(RESTATEPTR, lo_off)); // LO accumulator word - armAsm->Add(RSCRATCH, RSCRATCH, RSCRATCH2); // RSCRATCH = temp - - // LO = sign-extended low 32 bits of temp; also Rd in the R5900 3-operand form. - armAsm->Sxtw(RSCRATCH2, RSCRATCHW); - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, lo_off)); - if (rd != 0) - armAsm->Str(RSCRATCH2, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); - - // HI = sign-extended high 32 bits (asr #32 sign-extends from bit 63 == bit 31 hi). - armAsm->Asr(RSCRATCH, RSCRATCH, 32); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, hi_off)); -} - -void armEmitMADD(u32 rd, u32 rs, u32 rt) { emitMadd(true, rd, rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } -void armEmitMADDU(u32 rd, u32 rs, u32 rt) { emitMadd(false, rd, rs, rt, EE_LO_OFFSET, EE_HI_OFFSET); } -void armEmitMADD1(u32 rd, u32 rs, u32 rt) { emitMadd(true, rd, rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } -void armEmitMADDU1(u32 rd, u32 rs, u32 rt) { emitMadd(false, rd, rs, rt, EE_LO1_OFFSET, EE_HI1_OFFSET); } - -// ------------------------------------------------------------------------ -// Pipeline-1 HI/LO moves (MMI funct 0x10-0x13: MFHI1/MTHI1/MFLO1/MTLO1). -// Full 64-bit copies to/from the upper doubleword HI1/LO1 — mirror MFHI/MFLO/ -// MTHI/MTLO (aR5900Arith.cpp) but with the +8 offsets. -// ------------------------------------------------------------------------ -void armEmitMFHI1(u32 rd) -{ - if (rd == 0) - return; - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_HI1_OFFSET)); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitMFLO1(u32 rd) -{ - if (rd == 0) - return; - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO1_OFFSET)); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rd))); -} - -void armEmitMTHI1(u32 rs) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_HI1_OFFSET)); -} - -void armEmitMTLO1(u32 rs) -{ - armAsm->Ldr(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_GPR_OFFSET(rs))); - armAsm->Str(RSCRATCH, a64::MemOperand(RESTATEPTR, EE_LO1_OFFSET)); -} - - diff --git a/platforms/android/app/build.gradle.kts b/platforms/android/app/build.gradle.kts index a775f37461..ca67f12eb5 100644 --- a/platforms/android/app/build.gradle.kts +++ b/platforms/android/app/build.gradle.kts @@ -149,6 +149,12 @@ android { externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") + // Pin a current CMake. AGP otherwise defaults to 3.22.1, which is the + // exact cmake_minimum_required floor the (2026-dated) shaderc deps + // declare; configuring spirv-tools at that floor fails to create the + // SPIRV-Tools target ("SPIRV-Tools was not found"). A newer CMake + // configures them cleanly. Must match a cmake installed in CI. + version = "3.31.6" } } buildFeatures { diff --git a/platforms/android/app/src/main/cpp/3rdparty/libjpeg-turbo/cmakescripts/BuildPackages.cmake b/platforms/android/app/src/main/cpp/3rdparty/libjpeg-turbo/cmakescripts/BuildPackages.cmake index 4f55f1461d..eaef01c560 100644 --- a/platforms/android/app/src/main/cpp/3rdparty/libjpeg-turbo/cmakescripts/BuildPackages.cmake +++ b/platforms/android/app/src/main/cpp/3rdparty/libjpeg-turbo/cmakescripts/BuildPackages.cmake @@ -139,7 +139,7 @@ endif() # WIN32 # Mac DMG ############################################################################### -if(APPLE) +if(APPLE AND NOT IOS) set(SECONDARY_BUILD "" CACHE PATH "Directory containing cross-compiled x86-64 or Armv8 (64-bit) iOS or macOS build to include in universal binaries") diff --git a/platforms/android/app/src/main/cpp/3rdparty/rapidyaml/include/c4/yml/common.hpp b/platforms/android/app/src/main/cpp/3rdparty/rapidyaml/include/c4/yml/common.hpp index c4afa6361e..3b054d70e2 100644 --- a/platforms/android/app/src/main/cpp/3rdparty/rapidyaml/include/c4/yml/common.hpp +++ b/platforms/android/app/src/main/cpp/3rdparty/rapidyaml/include/c4/yml/common.hpp @@ -415,6 +415,13 @@ struct RYML_EXPORT Callbacks m_free == that.m_free && m_error == that.m_error); } + + /** ARMSX2 shim: back-port of the set_user_data() setter that newer rapidyaml + * exposes. common/YAML.cpp (from the pcsx2master merge) calls this; the PC/mac + * build resolves a newer system ryml, but the Android NDK build still vendors + * ryml 0.10.0, which only has the raw m_user_data member. Remove once the + * vendored 3rdparty rapidyaml is de-duplicated onto canonical (REFACTOR_STATUS #4). */ + void set_user_data(void *user_data) noexcept { m_user_data = user_data; } }; diff --git a/platforms/android/app/src/main/cpp/cmake/SearchForStuff.cmake b/platforms/android/app/src/main/cpp/cmake/SearchForStuff.cmake index 61e6719fda..390596c32f 100644 --- a/platforms/android/app/src/main/cpp/cmake/SearchForStuff.cmake +++ b/platforms/android/app/src/main/cpp/cmake/SearchForStuff.cmake @@ -110,8 +110,13 @@ if(ANDROID) set(SHADERC_SKIP_COPYRIGHT_CHECK ON CACHE BOOL "" FORCE) set(SHADERC_ENABLE_WERROR_COMPILE OFF CACHE BOOL "" FORCE) set(SHADERC_THIRD_PARTY_ROOT_DIR "${CMAKE_SOURCE_DIR}/3rdparty/shaderc/third_party" CACHE STRING "" FORCE) - set(SHADERC_SPIRV_TOOLS_DIR "${SHADERC_THIRD_PARTY_ROOT_DIR}/SPIRV-Tools" CACHE STRING "" FORCE) - set(SHADERC_SPIRV_HEADERS_DIR "${SHADERC_THIRD_PARTY_ROOT_DIR}/SPIRV-Headers" CACHE STRING "" FORCE) + # Lowercase to match the directories git-sync-deps actually creates + # (DEPS uses third_party/spirv-tools, third_party/spirv-headers). The + # PascalCase repo names only resolve on case-insensitive filesystems + # (macOS APFS); on case-sensitive Linux CI they miss and shaderc fails + # with "SPIRV-Tools was not found". + set(SHADERC_SPIRV_TOOLS_DIR "${SHADERC_THIRD_PARTY_ROOT_DIR}/spirv-tools" CACHE STRING "" FORCE) + set(SHADERC_SPIRV_HEADERS_DIR "${SHADERC_THIRD_PARTY_ROOT_DIR}/spirv-headers" CACHE STRING "" FORCE) set(SHADERC_GLSLANG_DIR "${SHADERC_THIRD_PARTY_ROOT_DIR}/glslang" CACHE STRING "" FORCE) set(SPIRV_SKIP_TESTS ON CACHE BOOL "" FORCE) set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "" FORCE) diff --git a/platforms/ios/app/src/main/cpp/CMakeLists.txt b/platforms/ios/app/src/main/cpp/CMakeLists.txt index 73f0049e8c..0c6a10ba22 100644 --- a/platforms/ios/app/src/main/cpp/CMakeLists.txt +++ b/platforms/ios/app/src/main/cpp/CMakeLists.txt @@ -49,6 +49,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS") set(USE_VULKAN OFF CACHE BOOL "Enable Vulkan renderer" FORCE) set(SDL_OPENGL OFF CACHE BOOL "Enable SDL OpenGL support" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "Enable SDL OpenGL ES support" FORCE) + # iOS uses its own SwiftUI frontend, not the desktop Qt GUI or test harnesses. + set(ENABLE_QT_UI OFF CACHE BOOL "Enables building the PCSX2 Qt interface." FORCE) + set(ENABLE_TESTS OFF CACHE BOOL "Enables building the unit tests" FORCE) + set(ENABLE_GSRUNNER OFF CACHE BOOL "Enables building the GSRunner" FORCE) endif() # Project Name @@ -57,10 +61,10 @@ project(ARMSX2iOS VERSION ${ARMSX2_VERSION} LANGUAGES C CXX) add_compile_definitions(ARMSX2_VERSION_STR="${ARMSX2_VERSION}") if(CMAKE_SYSTEM_NAME STREQUAL "iOS") - add_compile_definitions(_M_ARM64=1) set(_M_ARM64 TRUE) set(_M_X86 FALSE) set(CMAKE_HOST_SYSTEM_PROCESSOR "arm64") + set(CMAKE_SYSTEM_PROCESSOR "arm64") if(CMAKE_GENERATOR STREQUAL "Xcode") if(ARMSX2_MAC_CATALYST OR NOT ARMSX2_REAL_DEVICE) @@ -356,6 +360,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS") target_include_directories(ARMSX2iOS PRIVATE ${CMAKE_SOURCE_DIR} + ${ARMSX2_ROOT} ${ARMSX2_ROOT}/common ${ARMSX2_ROOT}/pcsx2 ${ARMSX2_ROOT}/pcsx2/GS/Renderers/Metal diff --git a/platforms/ios/app/src/main/cpp/cmake/BuildParameters.cmake b/platforms/ios/app/src/main/cpp/cmake/BuildParameters.cmake index 5cf848798c..ab12a9e792 100644 --- a/platforms/ios/app/src/main/cpp/cmake/BuildParameters.cmake +++ b/platforms/ios/app/src/main/cpp/cmake/BuildParameters.cmake @@ -300,7 +300,7 @@ if(POSITION_INDEPENDENT_CODE) "The POSITION_INDEPENDENT_CODE option is enabled but is not " "supported at link time:\n${PIE_SUPPORTED_OUTPUT}") endif() - endif + endif() set(CMAKE_POSITION_INDEPENDENT_CODE ON) else()