From b884f93dd77e909d87a888e21d786e7ecb9d8daf Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 001/292] arm64: build infrastructure, perf counters, and JIT perf-dump plumbing CMake/preset wiring for the ARM64 target, the vixl C++20 enum-conversion warning suppression, PmuCounters (cycle/instret PMU reads), and the Perf jitdump dir/enable controls (EmuConfig.Profiler.EnablePerfDump, redirected out of /tmp into the cache dir). Plus small platform/build fixes (ALSA thread naming, SmallString, X11 guards, ARM MIDR CPU-name fallback, gcc lambda decay). The ARCH_ARM64 status banner now reflects that EE/IOP/VU recompilers are all implemented. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- .gitignore | 15 ++ 3rdparty/cubeb/src/cubeb_alsa.c | 6 + 3rdparty/vixl/CMakeLists.txt | 7 + CMakeLists.txt | 9 +- CMakePresets.json | 31 ++- cmake/BuildParameters.cmake | 41 +++- common/CMakeLists.txt | 12 +- common/HostSys.cpp | 86 ++++++++ common/Linux/LnxMisc.cpp | 4 + common/Perf.cpp | 59 +++++- common/Perf.h | 13 ++ common/PmuCounters.cpp | 244 ++++++++++++++++++++++ common/PmuCounters.h | 115 ++++++++++ common/SmallString.h | 1 + pcsx2/Config.h | 3 +- pcsx2/Pcsx2Config.cpp | 4 + pcsx2/VMManager.cpp | 10 + tests/ctest/common/CMakeLists.txt | 1 + tests/ctest/common/pmu_counters_tests.cpp | 102 +++++++++ 19 files changed, 745 insertions(+), 18 deletions(-) create mode 100644 common/PmuCounters.cpp create mode 100644 common/PmuCounters.h create mode 100644 tests/ctest/common/pmu_counters_tests.cpp diff --git a/.gitignore b/.gitignore index 9305218258..0ffae48662 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,21 @@ install_log.txt bad_shader_* crash-*.txt +# Ad-hoc dev logs (produced by instrumentation / testing) +crash.txt +emulog.txt +vu0_ops_*.log +vu1_ops_*.log +vu0_rec.log +vu1_rec.log +vu1_interp.log +vu1_xgkick_*.log +vkinfo_*.txt + +# Python bytecode +__pycache__/ +*.pyc + Debug Release Devel diff --git a/3rdparty/cubeb/src/cubeb_alsa.c b/3rdparty/cubeb/src/cubeb_alsa.c index be9faa490c..51e1093593 100644 --- a/3rdparty/cubeb/src/cubeb_alsa.c +++ b/3rdparty/cubeb/src/cubeb_alsa.c @@ -11,6 +11,9 @@ #define _NETBSD_SOURCE /* timersub() */ #endif #define _XOPEN_SOURCE 500 +#if defined(__linux__) +#define _GNU_SOURCE +#endif #include "cubeb-internal.h" #include "cubeb/cubeb.h" #include "cubeb_tracing.h" @@ -587,6 +590,9 @@ alsa_run_thread(void * context) int r; CUBEB_REGISTER_THREAD("cubeb rendering thread"); +#if defined(__linux__) + pthread_setname_np(pthread_self(), "Audio"); +#endif do { r = alsa_run(ctx); diff --git a/3rdparty/vixl/CMakeLists.txt b/3rdparty/vixl/CMakeLists.txt index 0304d91bf9..66f402416e 100644 --- a/3rdparty/vixl/CMakeLists.txt +++ b/3rdparty/vixl/CMakeLists.txt @@ -61,6 +61,13 @@ target_compile_definitions(vixl PUBLIC target_compile_definitions(vixl PRIVATE VIXL_CODE_BUFFER_MALLOC) +# vixl's constants-aarch64.h does bitwise ops between distinct enum types, which +# is deprecated in C++20. The deprecation propagates to every TU that includes +# vixl headers, so suppress it on the public interface as well as the library. +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(vixl PUBLIC -Wno-deprecated-enum-enum-conversion) +endif() + if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") message("Enabling vixl debug assertions") target_compile_definitions(vixl PUBLIC VIXL_DEBUG) diff --git a/CMakeLists.txt b/CMakeLists.txt index b0c394591c..197a1e54fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,12 +82,5 @@ No support will be provided, continue at your own risk. endif() if(ARCH_ARM64) - message(WARNING " -*************** UNSUPPORTED CONFIGURATION *************** -Apple Silicon support in PCSX2 is INCOMPLETE. There are -currently no EE/VU/IOP recompilers, and games will run -VERY slow. There is no date for completion yet, you -should set -DCMAKE_OSX_ARCHITECTURES=x86_64 for now, -unless you want to work on the recompilers. -*********************************************************") + message(STATUS "ARM64 build: EE, IOP, and VU0/VU1 recompilers are all available.") endif() diff --git a/CMakePresets.json b/CMakePresets.json index b6a6267427..170f6758bb 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -53,7 +53,36 @@ "inherits": "clang-base", "cacheVariables": { "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON", - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "ENABLE_RECOMPILER_TEST_HOOKS": "OFF" + } + }, + { + "name": "clang-perf", + "displayName": "Clang Devel + perf jitdump", + "description": "Devel build with USE_PERF_JITDUMP=ON; emits /tmp/pcsx2-perf-/jit-.dump for `perf inject --jit`.", + "inherits": "clang-devel", + "binaryDir": "${sourceDir}/build-perf", + "cacheVariables": { + "USE_PERF_JITDUMP": "ON" + } + }, + { + "name": "clang-handheld", + "displayName": "Clang Handheld (SDL3 / kmsdrm)", + "description": "RelWithDebInfo build of the SDL3 frontend for kmsdrm-only handhelds. No Qt. GSRunner builds with a VulkanDirect platform backend (no X11/Wayland).", + "inherits": "clang-base", + "binaryDir": "${sourceDir}/build-handheld", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "ENABLE_QT_UI": "OFF", + "ENABLE_GSRUNNER": "ON", + "ENABLE_SDL_FRONTEND": "ON", + "ENABLE_TESTS": "OFF", + "ENABLE_RECOMPILER_TEST_HOOKS": "OFF", + "USE_BACKTRACE": "OFF", + "X11_API": "OFF", + "WAYLAND_API": "OFF" } } ] diff --git a/cmake/BuildParameters.cmake b/cmake/BuildParameters.cmake index 80c44694b8..1c4b3a8277 100644 --- a/cmake/BuildParameters.cmake +++ b/cmake/BuildParameters.cmake @@ -7,10 +7,18 @@ include(GNUInstallDirs) # Misc option #------------------------------------------------------------------------------- option(ENABLE_TESTS "Enables building the unit tests" ON) +option(ENABLE_RECOMPILER_TEST_HOOKS + "Compile harness hooks (recEeExecuteBlock, recEeIsBlockLinked, etc.) into the EE recompiler. Required by tests/ctest/core/recompilers; release builds should turn this off." + ${ENABLE_TESTS}) option(ENABLE_QT_UI "Enables building the PCSX2 Qt interface." ON) option(ENABLE_GSRUNNER "Enables building the GSRunner by default. It can still be built with `make pcsx2-gsrunner` otherwise." OFF) +option(ENABLE_VURUNNER "Enables building pcsx2-vurunner (headless VU microprogram replayer for codegen iteration). Requires ENABLE_RECOMPILER_TEST_HOOKS=ON." OFF) +option(ENABLE_EERUNNER "Enables building pcsx2-eerunner (headless EE JIT-vs-interpreter divergence localizer) by default. It can still be built with `make pcsx2-eerunner` otherwise." OFF) +option(ENABLE_SDL_FRONTEND "Enables building the SDL3 / kmsdrm frontend (pcsx2-sdl) by default. It can still be built with `make pcsx2-sdl` otherwise." OFF) option(LTO_PCSX2_CORE "Enable LTO/IPO/LTCG on the subset of pcsx2 that benefits most from it but not anything else") option(USE_VTUNE "Plug VTUNE to profile GS JIT.") +option(USE_PERF_JITDUMP "Emit Linux perf jitdump (jit-.dump) for recompiled JIT blocks; use with perf record/inject." OFF) +option(USE_PERF_MAP "Emit simple /tmp/perf-.map symbol table for recompiled JIT blocks." OFF) option(PACKAGE_MODE "Use this option to ease packaging of PCSX2 (developer/distribution option)") option(BUNDLE_EMOJI_FONT "Bundles Noto Color Emoji for systems whose system emoji font isn't usable by freetype" ON) option(POSITION_INDEPENDENT_CODE "Generate position-independent code. It is recommended that you leave this on." ON) @@ -118,7 +126,10 @@ elseif("${CMAKE_HOST_SYSTEM_PROCESSOR}" STREQUAL "arm64" OR "${CMAKE_HOST_SYSTEM # Min spec is an M1 add_compile_options("-march=armv8.4-a" "-mcpu=apple-m1") else() - # Require atomic rmw instructions + # Require atomic rmw instructions (LSE, ARMv8.1+). This is the upstream + # default and targets the broad arm64 ecosystem. In-order ARMv8.0 cores + # without LSE (e.g. Cortex-A53 handhelds) must override -march to armv8-a + # in their own toolchain/preset — LSE atomics fault on them. add_compile_options("-march=armv8.1-a") endif() @@ -164,6 +175,14 @@ else() add_compile_options( "$<$:-fno-exceptions>" ) + # GCC/Clang warn on every __fi (always_inline) function definition that lacks + # an explicit `inline` keyword. PCSX2 deliberately defines __fi without + # `inline` so that .cpp-defined __fi functions still emit a strong external + # symbol; adding `inline` to the macro globally breaks linkage for those. The + # attribute itself works correctly either way, so suppress the noise. + # Unconditional (not the GNU-only DEFAULT_WARNINGS entry below) because the + # primary toolchain here is Clang, which the GCC-gated list does not cover. + add_compile_options(-Wno-attributes) endif() set(CONFIG_REL_NO_DEB $,$>) @@ -222,6 +241,22 @@ if(USE_VTUNE) list(APPEND PCSX2_DEFS ENABLE_VTUNE) endif() +if(USE_PERF_JITDUMP AND USE_PERF_MAP) + message(FATAL_ERROR "USE_PERF_JITDUMP and USE_PERF_MAP are mutually exclusive; pick one.") +endif() +if(USE_PERF_JITDUMP) + if(NOT UNIX OR APPLE) + message(FATAL_ERROR "USE_PERF_JITDUMP is Linux-only.") + endif() + list(APPEND PCSX2_DEFS ENABLE_PERF_JITDUMP) +endif() +if(USE_PERF_MAP) + if(NOT UNIX OR APPLE) + message(FATAL_ERROR "USE_PERF_MAP is Linux-only.") + endif() + list(APPEND PCSX2_DEFS ENABLE_PERF_MAP) +endif() + if(USE_OPENGL) list(APPEND PCSX2_DEFS ENABLE_OPENGL) endif() @@ -238,6 +273,10 @@ if(WAYLAND_API) list(APPEND PCSX2_DEFS WAYLAND_API) endif() +if(ENABLE_SDL_FRONTEND) + list(APPEND PCSX2_DEFS ENABLE_SDL_FRONTEND) +endif() + # -Wno-attributes: "always_inline function might not be inlinable" <= real spam (thousand of warnings!!!) # -Wno-missing-field-initializers: standard allow to init only the begin of struct/array in static init. Just a silly warning. # -Wno-unused-function: warn for function not used in release build diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index d1bad4494f..bb22d7ec52 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -24,6 +24,7 @@ target_sources(common PRIVATE MD5Digest.cpp PrecompiledHeader.cpp Perf.cpp + PmuCounters.cpp ProgressCallback.cpp ReadbackSpinManager.cpp Semaphore.cpp @@ -64,6 +65,7 @@ target_sources(common PRIVATE MD5Digest.h MRCHelpers.h Path.h + PmuCounters.h PrecompiledHeader.h ProgressCallback.h ReadbackSpinManager.h @@ -168,10 +170,14 @@ else() ) target_link_libraries(common PRIVATE ${DBUS_LINK_LIBRARIES} - X11::X11 - X11::Xrandr - X11::Xi ) + if(X11_API) + target_link_libraries(common PRIVATE + X11::X11 + X11::Xrandr + X11::Xi + ) + endif() if(USE_BACKTRACE) target_compile_definitions(common PRIVATE "HAS_LIBBACKTRACE=1") target_link_libraries(common PRIVATE libbacktrace::libbacktrace) diff --git a/common/HostSys.cpp b/common/HostSys.cpp index d1ce25ba9d..d5101e2c23 100644 --- a/common/HostSys.cpp +++ b/common/HostSys.cpp @@ -4,6 +4,7 @@ #include "HostSys.h" #include "Console.h" #include "VectorIntrin.h" +#include "fmt/format.h" #ifndef __APPLE__ #include "cpuinfo.h" @@ -142,10 +143,95 @@ void AbortWithMessage(const char* msg) #ifndef __APPLE__ // MacOS version is in DarwinMisc + +#ifdef __aarch64__ +// cpuinfo library often returns empty/unknown names on ARM Linux. +// Fall back to reading MIDR fields from /proc/cpuinfo. +static std::string DetectArmCPUName() +{ + FILE* f = fopen("/proc/cpuinfo", "r"); + if (!f) + return {}; + + u32 implementer = 0, part = 0; + char line[256]; + while (fgets(line, sizeof(line), f)) + { + if (sscanf(line, "CPU implementer : %x", &implementer) == 1) + continue; + if (sscanf(line, "CPU part : %x", &part) == 1) + break; // got both from first core + } + fclose(f); + + // Map common implementer+part to names + if (implementer == 0x41) // ARM Ltd + { + switch (part) + { + case 0xd03: return "ARM Cortex-A53"; + case 0xd04: return "ARM Cortex-A35"; + case 0xd05: return "ARM Cortex-A55"; + case 0xd07: return "ARM Cortex-A57"; + case 0xd08: return "ARM Cortex-A72"; + case 0xd09: return "ARM Cortex-A73"; + case 0xd0a: return "ARM Cortex-A75"; + case 0xd0b: return "ARM Cortex-A76"; + case 0xd0c: return "ARM Neoverse N1"; + case 0xd0d: return "ARM Cortex-A77"; + case 0xd40: return "ARM Neoverse V1"; + case 0xd41: return "ARM Cortex-A78"; + case 0xd44: return "ARM Cortex-X1"; + case 0xd46: return "ARM Cortex-A510"; + case 0xd47: return "ARM Cortex-A710"; + case 0xd48: return "ARM Cortex-X2"; + case 0xd4d: return "ARM Cortex-A715"; + case 0xd4e: return "ARM Cortex-X3"; + case 0xd80: return "ARM Cortex-A520"; + case 0xd81: return "ARM Cortex-A720"; + case 0xd82: return "ARM Cortex-X4"; + } + } + else if (implementer == 0x51) // Qualcomm + { + switch (part) + { + case 0x802: return "Qualcomm Kryo 385 Gold"; + case 0x803: return "Qualcomm Kryo 385 Silver"; + case 0xc00: return "Qualcomm Falkor"; + case 0x001: return "Qualcomm Oryon"; + } + } + else if (implementer == 0x61) // Apple + { + switch (part) + { + case 0x022: return "Apple M1 Icestorm"; + case 0x023: return "Apple M1 Firestorm"; + case 0x032: return "Apple M2 Blizzard"; + case 0x033: return "Apple M2 Avalanche"; + } + } + + if (implementer != 0 && part != 0) + return fmt::format("ARM (impl 0x{:02X} part 0x{:03X})", implementer, part); + return {}; +} +#endif + static CPUInfo CalcCPUInfo() { CPUInfo out; out.name = cpuinfo_get_package(0)->name; +#ifdef __aarch64__ + // cpuinfo often returns empty/unknown on ARM Linux — use MIDR fallback + if (out.name.empty() || out.name == "unknown" || out.name == "Unknown") + { + std::string arm_name = DetectArmCPUName(); + if (!arm_name.empty()) + out.name = std::move(arm_name); + } +#endif out.num_threads = cpuinfo_get_processors_count(); out.num_clusters = cpuinfo_get_clusters_count(); out.num_big_cores = 0; diff --git a/common/Linux/LnxMisc.cpp b/common/Linux/LnxMisc.cpp index 84901fe230..2c5c323ec0 100644 --- a/common/Linux/LnxMisc.cpp +++ b/common/Linux/LnxMisc.cpp @@ -19,8 +19,10 @@ #include #include #include +#if defined(X11_API) #include #include +#endif #include #include @@ -224,6 +226,7 @@ bool Common::InhibitScreensaver(bool inhibit) return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running."); } +#if defined(X11_API) void Common::SetMousePosition(int x, int y) { Display* display = XOpenDisplay(nullptr); @@ -328,6 +331,7 @@ void Common::DetachMousePositionCb() mouseThread.join(); } } +#endif // X11_API bool Common::PlaySoundAsync(const char* path) { diff --git a/common/Perf.cpp b/common/Perf.cpp index dd15011021..1116929b57 100644 --- a/common/Perf.cpp +++ b/common/Perf.cpp @@ -11,6 +11,7 @@ #endif #include +#include #include #ifdef __linux__ @@ -20,11 +21,18 @@ #include #include #include +#include #include #endif -//#define ProfileWithPerf -//#define ProfileWithPerfJitDump +// Set by CMake options USE_PERF_MAP / USE_PERF_JITDUMP (see cmake/BuildParameters.cmake). +// Or define manually below for ad-hoc builds. +#if defined(ENABLE_PERF_MAP) && !defined(ProfileWithPerf) +#define ProfileWithPerf +#endif +#if defined(ENABLE_PERF_JITDUMP) && !defined(ProfileWithPerfJitDump) +#define ProfileWithPerfJitDump +#endif #if defined(ENABLE_VTUNE) && defined(_WIN32) #pragma comment(lib, "jitprofiling.lib") @@ -116,9 +124,22 @@ namespace Perf static bool s_jitdump_file_opened = false; static std::mutex s_jitdump_mutex; static u32 s_jitdump_record_id; + // Default to /tmp for ad-hoc launches; pcsx2 startup overrides this with + // EmuFolders::Cache so the dump (which can be hundreds of MB) doesn't fill + // up small /tmp mounts on memory-constrained systems. + static std::string s_jitdump_dir = "/tmp"; + // Default OFF — VMManager flips this from EmuConfig.Profiler.EnablePerfDump. + // USE_PERF_JITDUMP build with the flag disabled does no file I/O at all. + static std::atomic s_jitdump_enabled{false}; static void RegisterMethod(const void* ptr, size_t size, const char* symbol) { + // Gate every JIT registration on the runtime flag — when off we + // neither open the dump file nor write any records, so a USE_PERF_JITDUMP + // build with EnablePerfDump=false costs nothing per dispatch. + if (!s_jitdump_enabled.load(std::memory_order_relaxed)) + return; + const u32 namelen = std::strlen(symbol) + 1; std::unique_lock lock(s_jitdump_mutex); @@ -126,8 +147,16 @@ namespace Perf { if (!s_jitdump_file_opened) { - char file[256]; - snprintf(file, std::size(file), "jit-%d.dump", getpid()); + // Write the JIT dump (and the synthesized jitted--*.so + // files that perf inject -j later places alongside it) into + // a per-pid subdir under s_jitdump_dir (default /tmp; pcsx2 + // overrides to EmuFolders::Cache). Per-pid dir keeps multiple + // concurrent runs separated and avoids polluting cwd. + char dir[512]; + snprintf(dir, std::size(dir), "%s/pcsx2-perf-%d", s_jitdump_dir.c_str(), getpid()); + mkdir(dir, 0700); // ignore EEXIST + char file[576]; + snprintf(file, std::size(file), "%s/jit-%d.dump", dir, getpid()); s_jitdump_file = fopen(file, "w+b"); s_jitdump_file_opened = true; if (!s_jitdump_file) @@ -212,4 +241,26 @@ namespace Perf void Group::RegisterPC(const void* ptr, size_t size, u32 pc) {} void Group::RegisterKey(const void* ptr, size_t size, const char* prefix, u64 key) {} #endif + + void SetJitDumpDir(std::string dir) + { +#if defined(__linux__) && defined(ProfileWithPerfJitDump) + std::unique_lock lock(s_jitdump_mutex); + // Caller must invoke before the first JIT block compiles; once the + // file is opened we don't redirect mid-run. + if (!s_jitdump_file_opened && !dir.empty()) + s_jitdump_dir = std::move(dir); +#else + (void)dir; +#endif + } + + void SetJitDumpEnabled(bool enabled) + { +#if defined(__linux__) && defined(ProfileWithPerfJitDump) + s_jitdump_enabled.store(enabled, std::memory_order_relaxed); +#else + (void)enabled; +#endif + } } // namespace Perf diff --git a/common/Perf.h b/common/Perf.h index e0598b7b03..bce97822a5 100644 --- a/common/Perf.h +++ b/common/Perf.h @@ -5,6 +5,7 @@ #include #include +#include #include "common/Pcsx2Types.h" namespace Perf @@ -22,6 +23,18 @@ namespace Perf void RegisterKey(const void* ptr, size_t size, const char* prefix, u64 key); }; + // Override the directory where the jitdump file (and its per-PID subdir) + // gets written. Defaults to /tmp; call before the first JIT block compiles + // to redirect (e.g. EmuFolders::Cache so /tmp doesn't fill up on tmpfs + // systems). No-op on non-jitdump builds. + void SetJitDumpDir(std::string dir); + + // Enable/disable the jitdump writer at runtime. Default false; call from + // settings load with EmuConfig.Profiler.EnablePerfDump. Once disabled the + // RegisterMethod calls early-return; nothing is written. Has no effect + // on a file that's already been opened in this process. + void SetJitDumpEnabled(bool enabled); + extern Group any; extern Group ee; extern Group iop; diff --git a/common/PmuCounters.cpp b/common/PmuCounters.cpp new file mode 100644 index 0000000000..be952e9755 --- /dev/null +++ b/common/PmuCounters.cpp @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "common/PmuCounters.h" + +#ifdef __linux__ + +#include +#include +#include +#include +#include + +#include +#include + +namespace PmuCounters +{ + namespace + { + long PerfEventOpen(struct perf_event_attr* attr, pid_t pid, int cpu, int group_fd, unsigned long flags) + { + return ::syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags); + } + + void FillAttrFor(Counter c, struct perf_event_attr& attr) + { + std::memset(&attr, 0, sizeof(attr)); + attr.size = sizeof(attr); + attr.disabled = 1; + attr.exclude_kernel = 1; + attr.exclude_hv = 1; + // PERF_FORMAT_GROUP returns all counters in the group from a + // single read on the leader fd. PERF_FORMAT_ID is not requested + // because counters are read in fixed order — the index is the + // identity. + attr.read_format = PERF_FORMAT_GROUP; + + switch (c) + { + case CpuCycles: + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_CPU_CYCLES; + break; + case InstructionsRetired: + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_INSTRUCTIONS; + break; + case BranchMisses: + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_BRANCH_MISSES; + break; + case BranchInstructions: + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_BRANCH_INSTRUCTIONS; + break; + case L1dCacheRefills: + attr.type = PERF_TYPE_HW_CACHE; + attr.config = (PERF_COUNT_HW_CACHE_L1D) + | (PERF_COUNT_HW_CACHE_OP_READ << 8) + | (PERF_COUNT_HW_CACHE_RESULT_MISS << 16); + break; + default: + break; + } + } + } // namespace + + Group::Group() + { + // Initialize every slot to the "not installed" sentinel (-1), matching + // the documented invariant. Without this the arrays zero-initialize, so + // a Group destroyed without a successful Open() would (a) report + // IsAvailable()==true for slot 0 (0 >= 0), and (b) have Close() call + // ::close(0) on the still-zero follower fds — closing stdin. Open() + // repeats this reset before installing counters. + for (int& fd : m_follower_fds) + fd = -1; + for (int& slot : m_read_slot) + slot = -1; + } + + Group::~Group() + { + Close(); + } + + bool Group::Open() + { + Close(); + + for (int& fd : m_follower_fds) + fd = -1; + for (int& slot : m_read_slot) + slot = -1; + m_installed_count = 0; + + struct perf_event_attr leader_attr; + FillAttrFor(CpuCycles, leader_attr); + // Leader: pid=0 (calling thread), cpu=-1 (any), group_fd=-1. + m_leader_fd = static_cast(PerfEventOpen(&leader_attr, 0, -1, -1, 0)); + if (m_leader_fd < 0) + { + m_leader_fd = -1; + return false; + } + m_read_slot[CpuCycles] = m_installed_count++; + + // Followers — failures are tolerated (e.g. Apple PMU under Asahi + // returns ENOENT for the L1D cache event). Read() returns 0 for + // any counter whose m_read_slot is -1. + for (int i = 1; i < Counter::Count; ++i) + { + struct perf_event_attr attr; + FillAttrFor(static_cast(i), attr); + const int fd = static_cast(PerfEventOpen(&attr, 0, -1, m_leader_fd, 0)); + if (fd < 0) + continue; + m_follower_fds[i - 1] = fd; + m_read_slot[i] = m_installed_count++; + } + + Reset(); + return true; + } + + bool Group::IsAvailable(Counter c) const + { + if (c < 0 || c >= Counter::Count) + return false; + return m_read_slot[c] >= 0; + } + + void Group::Close() + { + for (int& fd : m_follower_fds) + { + if (fd >= 0) + { + ::close(fd); + fd = -1; + } + } + if (m_leader_fd >= 0) + { + ::close(m_leader_fd); + m_leader_fd = -1; + } + } + + void Group::Reset() + { + if (m_leader_fd < 0) + return; + ::ioctl(m_leader_fd, PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP); + } + + void Group::Enable() + { + if (m_leader_fd < 0) + return; + ::ioctl(m_leader_fd, PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP); + } + + void Group::Disable() + { + if (m_leader_fd < 0) + return; + ::ioctl(m_leader_fd, PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP); + } + + Values Group::Read() const + { + Values out{}; + if (m_leader_fd < 0) + return out; + + // PERF_FORMAT_GROUP read layout. Assumes read_format is GROUP-ONLY — no + // PERF_FORMAT_TOTAL_TIME_ENABLED/RUNNING/ID/LOST. Those flags insert extra + // u64s around values[] and would shift every counter; if attr.read_format + // ever gains one, this struct and the offset math below must change too. + // u64 nr; // number of counters actually installed + // u64 values[nr]; // each installed counter's accumulated count + struct ReadBuf + { + u64 nr; + u64 values[Counter::Count]; + } buf{}; + const size_t expected_bytes = sizeof(u64) * (1 + m_installed_count); + const ssize_t n = ::read(m_leader_fd, &buf, expected_bytes); + if (n < static_cast(expected_bytes)) + return out; + if (static_cast(buf.nr) != m_installed_count) + return out; + for (int i = 0; i < Counter::Count; ++i) + { + if (m_read_slot[i] >= 0) + out[i] = buf.values[m_read_slot[i]]; + } + return out; + } + + const char* Name(Counter c) + { + switch (c) + { + case CpuCycles: return "cycles"; + case InstructionsRetired: return "instructions"; + case BranchMisses: return "branch-misses"; + case BranchInstructions: return "branches"; + case L1dCacheRefills: return "L1-dcache-load-misses"; + default: return "?"; + } + } +} // namespace PmuCounters + +#else // !__linux__ + +namespace PmuCounters +{ + Group::Group() = default; + Group::~Group() = default; + bool Group::Open() { return false; } + bool Group::IsAvailable(Counter) const { return false; } + void Group::Close() {} + void Group::Reset() {} + void Group::Enable() {} + void Group::Disable() {} + Values Group::Read() const { return {}; } + const char* Name(Counter c) + { + switch (c) + { + case CpuCycles: return "cycles"; + case InstructionsRetired: return "instructions"; + case BranchMisses: return "branch-misses"; + case BranchInstructions: return "branches"; + case L1dCacheRefills: return "L1-dcache-load-misses"; + default: return "?"; + } + } +} // namespace PmuCounters + +#endif diff --git a/common/PmuCounters.h b/common/PmuCounters.h new file mode 100644 index 0000000000..ba56ce3cb0 --- /dev/null +++ b/common/PmuCounters.h @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Types.h" + +#include + +// Thin wrapper around perf_event_open(2) for measuring hardware perf counters +// on the calling thread, user-mode only. Designed for codegen-iteration loops +// that need cycles/instructions/branch-misses/L1D-refills per iteration with +// sub-microsecond overhead. +// +// Linux-only — on other platforms Open() returns false and Read() returns +// zeros, so callers should treat "open failed" as "skip the bench" rather +// than as an error. +// +// Counter access can be restricted by /proc/sys/kernel/perf_event_paranoid: +// 2 (default on many distros) → user-only counters require CAP_PERFMON, +// Open() will fail without it. +// 1 → user-only counters work for unprivileged +// users. +// 0 → all counters available. +// To enable on a development box: `sysctl kernel.perf_event_paranoid=1`. +// +// Per-counter availability also varies by host. Apple Silicon under Asahi +// exposes cycles / instructions / branch-misses / branch-instructions but +// not the generic L1D cache events. Open() tolerates per-counter failures: +// the leader (CPU_CYCLES) is required, followers that fail are simply not +// installed and will read as 0. Use IsAvailable(c) to distinguish "0 events" +// from "this PMU doesn't have that counter." + +namespace PmuCounters +{ + // Counters we read in a single perf_event_open group. Order matches the + // order of values returned by Read() and Measure(). + enum Counter : int + { + CpuCycles = 0, // PERF_COUNT_HW_CPU_CYCLES — required (group leader) + InstructionsRetired, // PERF_COUNT_HW_INSTRUCTIONS + BranchMisses, // PERF_COUNT_HW_BRANCH_MISSES + BranchInstructions, // PERF_COUNT_HW_BRANCH_INSTRUCTIONS + L1dCacheRefills, // L1D read miss — unavailable on Apple PMU + Count + }; + + using Values = std::array; + + class Group + { + public: + Group(); + ~Group(); + + Group(const Group&) = delete; + Group& operator=(const Group&) = delete; + + // Opens the hardware perf counters as one group on the calling thread, + // user-mode-only, initially disabled. Returns false on syscall + // failure (most commonly EACCES from perf_event_paranoid). + bool Open(); + + // Reset accumulated counts to zero. + void Reset(); + + // Toggle counting. Group leader uses PERF_FORMAT_GROUP; a single + // PERF_EVENT_IOC_ENABLE/DISABLE ioctl with PERF_IOC_FLAG_GROUP flips + // every counter in the group atomically. + void Enable(); + void Disable(); + + // Read current counter values. Safe to call any time after Open(); + // returns zeros if the group isn't open. Read is allowed while + // counters are running — the values are an instantaneous snapshot. + Values Read() const; + + // Convenience: enable, run callable, disable, read. The Reset() + // before the run means the returned values are deltas, not absolute. + template + Values Measure(F&& fn) + { + Reset(); + Enable(); + fn(); + Disable(); + return Read(); + } + + // True iff Open() succeeded and the group hasn't been closed. + bool IsOpen() const { return m_leader_fd >= 0; } + + // True iff this counter was actually installed by Open(). Followers + // that returned ENOENT (PMU doesn't support the event) read as 0 + // from Read(); IsAvailable lets callers distinguish "really 0" from + // "not measured." + bool IsAvailable(Counter c) const; + + private: + void Close(); + + // fds[0] is the group leader; fds[1..] are followers attached via + // the leader's group_fd. -1 marks slots that failed to open. + int m_leader_fd = -1; + int m_follower_fds[Counter::Count - 1] = {}; + + // Slot i in the PERF_FORMAT_GROUP read buffer that corresponds to + // Counter i. -1 means the counter wasn't installed. + int m_read_slot[Counter::Count] = {}; + int m_installed_count = 0; + }; + + // Static text label for a counter — useful for printing. + const char* Name(Counter c); +} // namespace PmuCounters diff --git a/common/SmallString.h b/common/SmallString.h index 1b4d74dbe2..1e87caaca0 100644 --- a/common/SmallString.h +++ b/common/SmallString.h @@ -261,6 +261,7 @@ public: } __fi SmallStackString(const SmallStackString& copy) + : SmallStringBase() { init(); assign(copy); diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 615c7ce26b..db240e9284 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -612,7 +612,8 @@ struct Pcsx2Config RecBlocks_EE : 1, // Enables per-block profiling for the EE recompiler [unimplemented] RecBlocks_IOP : 1, // Enables per-block profiling for the IOP recompiler [unimplemented] RecBlocks_VU0 : 1, // Enables per-block profiling for the VU0 recompiler [unimplemented] - RecBlocks_VU1 : 1; // Enables per-block profiling for the VU1 recompiler [unimplemented] + RecBlocks_VU1 : 1, // Enables per-block profiling for the VU1 recompiler [unimplemented] + EnablePerfDump : 1; // Linux: write JIT blocks to perf jitdump (USE_PERF_JITDUMP build only). BITFIELD_END // Default is Disabled, with all recs enabled underneath. diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index df57ca9341..d8b77316f9 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -415,6 +415,9 @@ void Pcsx2Config::SpeedhackOptions::LoadSave(SettingsWrapper& wrap) Pcsx2Config::ProfilerOptions::ProfilerOptions() : bitset(0xfffffffe) { + // Default OFF: perf jitdump is opt-in to avoid GB-scale dumps every play + // session on USE_PERF_JITDUMP builds. + EnablePerfDump = false; } void Pcsx2Config::ProfilerOptions::LoadSave(SettingsWrapper& wrap) @@ -426,6 +429,7 @@ void Pcsx2Config::ProfilerOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBool(RecBlocks_IOP); SettingsWrapBitBool(RecBlocks_VU0); SettingsWrapBitBool(RecBlocks_VU1); + SettingsWrapBitBool(EnablePerfDump); } bool Pcsx2Config::ProfilerOptions::operator!=(const ProfilerOptions& right) const diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 4771b0eaf8..404a342dab 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -47,6 +47,7 @@ #include "common/Error.h" #include "common/FileSystem.h" #include "common/FPControl.h" +#include "common/Perf.h" #include "common/ScopedGuard.h" #include "common/SettingsWrapper.h" #include "common/SmallString.h" @@ -559,6 +560,11 @@ void VMManager::Internal::LoadStartupSettings() EmuFolders::LoadConfig(*bsi); EmuFolders::EnsureFoldersExist(); + // Redirect perf jitdump (Linux ProfileWithPerfJitDump builds) out of /tmp + // into the cache dir; the dump can be hundreds of MB and tmpfs /tmp on + // embedded targets fills up. No-op on non-jitdump builds. + Perf::SetJitDumpDir(EmuFolders::Cache); + // We need to create the console window early, otherwise it appears behind the main window. UpdateLoggingSettings(*bsi); @@ -618,6 +624,10 @@ void VMManager::LoadSettings() LoadInputBindings(*si, lock); UpdateLoggingSettings(*si); + // Apply runtime perf-dump gate from Profiler config (no-op on + // non-USE_PERF_JITDUMP builds). + Perf::SetJitDumpEnabled(EmuConfig.Profiler.EnablePerfDump); + if (HasValidOrInitializingVM()) { WarnAboutUnsafeSettings(); diff --git a/tests/ctest/common/CMakeLists.txt b/tests/ctest/common/CMakeLists.txt index 00a0ce0711..57648177b8 100644 --- a/tests/ctest/common/CMakeLists.txt +++ b/tests/ctest/common/CMakeLists.txt @@ -2,6 +2,7 @@ add_pcsx2_test(common_test byteswap_tests.cpp filesystem_tests.cpp path_tests.cpp + pmu_counters_tests.cpp small_string_tests.cpp string_util_tests.cpp ) diff --git a/tests/ctest/common/pmu_counters_tests.cpp b/tests/ctest/common/pmu_counters_tests.cpp new file mode 100644 index 0000000000..6d1b0ee199 --- /dev/null +++ b/tests/ctest/common/pmu_counters_tests.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Smoke tests for PmuCounters. The tests SUCCEED-and-skip-assertions when +// perf_event_open is restricted (perf_event_paranoid >= 2 without +// CAP_PERFMON), to avoid CI failures on locked-down hosts. + +#include "common/PmuCounters.h" + +#include + +#include +#include + +TEST(PmuCounters, OpenSucceedsOrSkipsCleanly) +{ + PmuCounters::Group g; + const bool opened = g.Open(); + if (!opened) + { + GTEST_SKIP() << "perf_event_open denied — set kernel.perf_event_paranoid=1 " + "to enable this test on a development box."; + } + EXPECT_TRUE(g.IsOpen()); + // Leader is always installed if Open() returned true. + EXPECT_TRUE(g.IsAvailable(PmuCounters::CpuCycles)); +} + +TEST(PmuCounters, MeasureRecordsNonZeroCyclesAndInstructions) +{ + PmuCounters::Group g; + if (!g.Open()) + GTEST_SKIP() << "perf_event_open denied"; + + // Busy work the optimizer can't prove dead — accumulator escapes via the + // gtest assertion below. + volatile u64 sink = 0; + const auto values = g.Measure([&]() { + for (u64 i = 0; i < 1'000'000; ++i) + sink += i * 7u; + }); + + EXPECT_GT(values[PmuCounters::CpuCycles], 0u); + EXPECT_GT(values[PmuCounters::InstructionsRetired], 0u); + // Instructions ≈ same order as cycles for this loop. Loose bound so the + // test does not need to reason about IPC on whichever host is running. + EXPECT_GT(values[PmuCounters::InstructionsRetired], + values[PmuCounters::CpuCycles] / 100u); + + (void)sink; // placate the optimizer; the volatile load is what matters +} + +TEST(PmuCounters, ResetClearsCounts) +{ + PmuCounters::Group g; + if (!g.Open()) + GTEST_SKIP() << "perf_event_open denied"; + + volatile u64 sink = 0; + g.Enable(); + for (u64 i = 0; i < 10'000; ++i) + sink += i; + g.Disable(); + const auto first = g.Read(); + EXPECT_GT(first[PmuCounters::CpuCycles], 0u); + + // Reset() reads back exactly zero only because the group is Disabled here: + // PERF_EVENT_IOC_RESET on a still-enabled leader can race in-flight counting + // and return a small non-zero value. Don't copy this == 0 assertion into a + // context where the counters are still running (Read() itself is allowed + // while running, but a reset-then-read-zero is not). + g.Reset(); + const auto after_reset = g.Read(); + EXPECT_EQ(after_reset[PmuCounters::CpuCycles], 0u); + EXPECT_EQ(after_reset[PmuCounters::InstructionsRetired], 0u); + + (void)sink; +} + +TEST(PmuCounters, NameReturnsStableLabels) +{ + EXPECT_STREQ("cycles", PmuCounters::Name(PmuCounters::CpuCycles)); + EXPECT_STREQ("instructions", PmuCounters::Name(PmuCounters::InstructionsRetired)); + EXPECT_STREQ("branch-misses", PmuCounters::Name(PmuCounters::BranchMisses)); + EXPECT_STREQ("branches", PmuCounters::Name(PmuCounters::BranchInstructions)); + EXPECT_STREQ("L1-dcache-load-misses", PmuCounters::Name(PmuCounters::L1dCacheRefills)); +} + +TEST(PmuCounters, ReadReturnsZeroForUnavailableCounter) +{ + PmuCounters::Group g; + if (!g.Open()) + GTEST_SKIP() << "perf_event_open denied"; + + const auto values = g.Read(); + for (int i = 0; i < PmuCounters::Count; ++i) + { + const auto c = static_cast(i); + if (!g.IsAvailable(c)) + EXPECT_EQ(values[i], 0u) << PmuCounters::Name(c); + } +} From e0b85251aad00b55a5c4dcb2753b3a172782c2b8 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 002/292] build: make the Qt debugger UI optional (ENABLE_QT_DEBUGGER) The Qt debugger and its KDDockWidgets dependency are gated behind a new ENABLE_QT_DEBUGGER option (default ON, OFF on ARM64 handheld targets). When off, the Debugger/* sources, the KDDockWidgets find_package/link/DLL-copy, and the debugger entry points in MainWindow / QtHost / DebugSettingsWidget all compile out, dropping the dependency entirely on builds that don't ship the debugger. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- cmake/BuildParameters.cmake | 10 ++ cmake/SearchForStuff.cmake | 4 +- pcsx2-qt/CMakeLists.txt | 180 +++++++++++----------- pcsx2-qt/MainWindow.cpp | 19 ++- pcsx2-qt/MainWindow.h | 4 + pcsx2-qt/QtHost.cpp | 11 +- pcsx2-qt/Settings/DebugSettingsWidget.cpp | 4 + pcsx2/CMakeLists.txt | 10 +- 8 files changed, 149 insertions(+), 93 deletions(-) diff --git a/cmake/BuildParameters.cmake b/cmake/BuildParameters.cmake index 1c4b3a8277..85632711d0 100644 --- a/cmake/BuildParameters.cmake +++ b/cmake/BuildParameters.cmake @@ -152,6 +152,16 @@ else() message(FATAL_ERROR "Unsupported architecture: ${CMAKE_HOST_SYSTEM_PROCESSOR}") endif() +# The Qt debugger UI depends on KDDockWidgets. Handheld/ARM64 targets don't ship +# the debugger, so default it off there to drop the dependency; on elsewhere to +# match upstream. Only meaningful when ENABLE_QT_UI is on. +if(ARCH_ARM64) + set(_ENABLE_QT_DEBUGGER_DEFAULT OFF) +else() + set(_ENABLE_QT_DEBUGGER_DEFAULT ON) +endif() +option(ENABLE_QT_DEBUGGER "Build the Qt debugger UI (requires KDDockWidgets)." ${_ENABLE_QT_DEBUGGER_DEFAULT}) + # Require C++20. set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/cmake/SearchForStuff.cmake b/cmake/SearchForStuff.cmake index f3b71b7dd2..963a6f88f3 100644 --- a/cmake/SearchForStuff.cmake +++ b/cmake/SearchForStuff.cmake @@ -121,7 +121,9 @@ if(ENABLE_QT_UI) endif() # The docking system for the debugger. - find_package(KDDockWidgets-qt6 2.3.0 REQUIRED) + if(ENABLE_QT_DEBUGGER) + find_package(KDDockWidgets-qt6 2.3.0 REQUIRED) + endif() endif() if(WIN32) diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index ba11786f0a..0cb16ae0c1 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -177,91 +177,6 @@ target_sources(pcsx2-qt PRIVATE Settings/USBBindingWidget_GunCon2.ui Settings/USBBindingWidget_RyojouhenCon.ui Settings/USBBindingWidget_ShinkansenCon.ui - Debugger/AnalysisOptionsDialog.cpp - Debugger/AnalysisOptionsDialog.h - Debugger/AnalysisOptionsDialog.ui - Debugger/DebuggerSettingsManager.cpp - Debugger/DebuggerSettingsManager.h - Debugger/DebuggerEvents.h - Debugger/DebuggerView.cpp - Debugger/DebuggerView.h - Debugger/DebuggerWindow.cpp - Debugger/DebuggerWindow.h - Debugger/DebuggerWindow.ui - Debugger/DisassemblyView.cpp - Debugger/DisassemblyView.h - Debugger/DisassemblyView.ui - Debugger/JsonValueWrapper.h - Debugger/ModuleModel.cpp - Debugger/ModuleModel.h - Debugger/ModuleView.cpp - Debugger/ModuleView.h - Debugger/RegisterView.cpp - Debugger/RegisterView.h - Debugger/RegisterView.ui - Debugger/StackModel.cpp - Debugger/StackModel.h - Debugger/StackView.cpp - Debugger/StackView.h - Debugger/ThreadModel.cpp - Debugger/ThreadModel.h - Debugger/ThreadView.cpp - Debugger/ThreadView.h - Debugger/Breakpoints/BreakpointDialog.cpp - Debugger/Breakpoints/BreakpointDialog.h - Debugger/Breakpoints/BreakpointDialog.ui - Debugger/Breakpoints/BreakpointModel.cpp - Debugger/Breakpoints/BreakpointModel.h - Debugger/Breakpoints/BreakpointView.cpp - Debugger/Breakpoints/BreakpointView.h - Debugger/Breakpoints/BreakpointView.ui - Debugger/Docking/DockLayout.cpp - Debugger/Docking/DockLayout.h - Debugger/Docking/DockManager.cpp - Debugger/Docking/DockManager.h - Debugger/Docking/DockMenuBar.cpp - Debugger/Docking/DockMenuBar.h - Debugger/Docking/DockTables.cpp - Debugger/Docking/DockTables.h - Debugger/Docking/DockUtils.cpp - Debugger/Docking/DockUtils.h - Debugger/Docking/DockViews.cpp - Debugger/Docking/DockViews.h - Debugger/Docking/DropIndicators.cpp - Debugger/Docking/DropIndicators.h - Debugger/Docking/LayoutEditorDialog.cpp - Debugger/Docking/LayoutEditorDialog.h - Debugger/Docking/LayoutEditorDialog.ui - Debugger/Docking/NoLayoutsWidget.cpp - Debugger/Docking/NoLayoutsWidget.h - Debugger/Docking/NoLayoutsWidget.ui - Debugger/Memory/MemorySearchView.cpp - Debugger/Memory/MemorySearchView.h - Debugger/Memory/MemorySearchView.ui - Debugger/Memory/MemoryView.cpp - Debugger/Memory/MemoryView.h - Debugger/Memory/MemoryView.ui - Debugger/Memory/SavedAddressesModel.cpp - Debugger/Memory/SavedAddressesModel.h - Debugger/Memory/SavedAddressesView.cpp - Debugger/Memory/SavedAddressesView.h - Debugger/Memory/SavedAddressesView.ui - Debugger/SymbolTree/NewSymbolDialogs.cpp - Debugger/SymbolTree/NewSymbolDialogs.h - Debugger/SymbolTree/NewSymbolDialog.ui - Debugger/SymbolTree/SymbolTreeLocation.cpp - Debugger/SymbolTree/SymbolTreeLocation.h - Debugger/SymbolTree/SymbolTreeModel.cpp - Debugger/SymbolTree/SymbolTreeModel.h - Debugger/SymbolTree/SymbolTreeNode.cpp - Debugger/SymbolTree/SymbolTreeNode.h - Debugger/SymbolTree/SymbolTreeDelegates.cpp - Debugger/SymbolTree/SymbolTreeDelegates.h - Debugger/SymbolTree/SymbolTreeViews.cpp - Debugger/SymbolTree/SymbolTreeViews.h - Debugger/SymbolTree/SymbolTreeView.ui - Debugger/SymbolTree/TypeString.cpp - Debugger/SymbolTree/TypeString.h Tools/InputRecording/NewInputRecordingDlg.cpp Tools/InputRecording/NewInputRecordingDlg.h Tools/InputRecording/NewInputRecordingDlg.ui @@ -279,6 +194,100 @@ if (NOT APPLE) ) endif() +# The debugger UI (and its KDDockWidgets dependency) is optional. Default off on +# ARM64 handheld targets; see ENABLE_QT_DEBUGGER in cmake/BuildParameters.cmake. +if(ENABLE_QT_DEBUGGER) + target_compile_definitions(pcsx2-qt PRIVATE ENABLE_QT_DEBUGGER) + target_link_libraries(pcsx2-qt PRIVATE KDAB::kddockwidgets) + target_sources(pcsx2-qt PRIVATE + Debugger/AnalysisOptionsDialog.cpp + Debugger/AnalysisOptionsDialog.h + Debugger/AnalysisOptionsDialog.ui + Debugger/DebuggerSettingsManager.cpp + Debugger/DebuggerSettingsManager.h + Debugger/DebuggerEvents.h + Debugger/DebuggerView.cpp + Debugger/DebuggerView.h + Debugger/DebuggerWindow.cpp + Debugger/DebuggerWindow.h + Debugger/DebuggerWindow.ui + Debugger/DisassemblyView.cpp + Debugger/DisassemblyView.h + Debugger/DisassemblyView.ui + Debugger/JsonValueWrapper.h + Debugger/ModuleModel.cpp + Debugger/ModuleModel.h + Debugger/ModuleView.cpp + Debugger/ModuleView.h + Debugger/RegisterView.cpp + Debugger/RegisterView.h + Debugger/RegisterView.ui + Debugger/StackModel.cpp + Debugger/StackModel.h + Debugger/StackView.cpp + Debugger/StackView.h + Debugger/ThreadModel.cpp + Debugger/ThreadModel.h + Debugger/ThreadView.cpp + Debugger/ThreadView.h + Debugger/Breakpoints/BreakpointDialog.cpp + Debugger/Breakpoints/BreakpointDialog.h + Debugger/Breakpoints/BreakpointDialog.ui + Debugger/Breakpoints/BreakpointModel.cpp + Debugger/Breakpoints/BreakpointModel.h + Debugger/Breakpoints/BreakpointView.cpp + Debugger/Breakpoints/BreakpointView.h + Debugger/Breakpoints/BreakpointView.ui + Debugger/Docking/DockLayout.cpp + Debugger/Docking/DockLayout.h + Debugger/Docking/DockManager.cpp + Debugger/Docking/DockManager.h + Debugger/Docking/DockMenuBar.cpp + Debugger/Docking/DockMenuBar.h + Debugger/Docking/DockTables.cpp + Debugger/Docking/DockTables.h + Debugger/Docking/DockUtils.cpp + Debugger/Docking/DockUtils.h + Debugger/Docking/DockViews.cpp + Debugger/Docking/DockViews.h + Debugger/Docking/DropIndicators.cpp + Debugger/Docking/DropIndicators.h + Debugger/Docking/LayoutEditorDialog.cpp + Debugger/Docking/LayoutEditorDialog.h + Debugger/Docking/LayoutEditorDialog.ui + Debugger/Docking/NoLayoutsWidget.cpp + Debugger/Docking/NoLayoutsWidget.h + Debugger/Docking/NoLayoutsWidget.ui + Debugger/Memory/MemorySearchView.cpp + Debugger/Memory/MemorySearchView.h + Debugger/Memory/MemorySearchView.ui + Debugger/Memory/MemoryView.cpp + Debugger/Memory/MemoryView.h + Debugger/Memory/MemoryView.ui + Debugger/Memory/SavedAddressesModel.cpp + Debugger/Memory/SavedAddressesModel.h + Debugger/Memory/SavedAddressesView.cpp + Debugger/Memory/SavedAddressesView.h + Debugger/Memory/SavedAddressesView.ui + Debugger/SymbolTree/NewSymbolDialogs.cpp + Debugger/SymbolTree/NewSymbolDialogs.h + Debugger/SymbolTree/NewSymbolDialog.ui + Debugger/SymbolTree/SymbolTreeLocation.cpp + Debugger/SymbolTree/SymbolTreeLocation.h + Debugger/SymbolTree/SymbolTreeModel.cpp + Debugger/SymbolTree/SymbolTreeModel.h + Debugger/SymbolTree/SymbolTreeNode.cpp + Debugger/SymbolTree/SymbolTreeNode.h + Debugger/SymbolTree/SymbolTreeDelegates.cpp + Debugger/SymbolTree/SymbolTreeDelegates.h + Debugger/SymbolTree/SymbolTreeViews.cpp + Debugger/SymbolTree/SymbolTreeViews.h + Debugger/SymbolTree/SymbolTreeView.ui + Debugger/SymbolTree/TypeString.cpp + Debugger/SymbolTree/TypeString.h + ) +endif() + file(GLOB TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Translations/pcsx2-qt_*-*.ts) target_precompile_headers(pcsx2-qt PRIVATE PrecompiledHeader.h) @@ -297,7 +306,6 @@ target_link_libraries(pcsx2-qt PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets - KDAB::kddockwidgets ) if(NOT WIN32 AND NOT APPLE) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 4ccb95ed04..fa1becdf5f 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -12,7 +12,9 @@ #include "QtHost.h" #include "QtUtils.h" #include "SettingWidgetBinder.h" +#ifdef ENABLE_QT_DEBUGGER #include "Debugger/Docking/DockManager.h" +#endif #include "Settings/AchievementLoginDialog.h" #include "Settings/ControllerSettingsWindow.h" #include "Settings/GameListSettingsWidget.h" @@ -643,7 +645,11 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread) connect(m_ui.actionToolbarPause, &QAction::toggled, thread, &EmuThread::setVMPaused); connect(m_ui.actionToolbarFullscreen, &QAction::triggered, thread, &EmuThread::toggleFullscreen); connect(m_ui.actionToggleSoftwareRendering, &QAction::triggered, thread, &EmuThread::toggleSoftwareRendering); +#ifdef ENABLE_QT_DEBUGGER connect(m_ui.actionDebugger, &QAction::triggered, this, &MainWindow::openDebugger); +#else + m_ui.actionDebugger->setVisible(false); +#endif connect(m_ui.actionReloadPatches, &QAction::triggered, thread, &EmuThread::reloadPatches); } @@ -791,7 +797,9 @@ void MainWindow::quit() void MainWindow::destroySubWindows() { +#ifdef ENABLE_QT_DEBUGGER DebuggerWindow::destroyInstance(); +#endif if (m_controller_settings_window) { @@ -1010,10 +1018,12 @@ void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) if (enabled) { +#ifdef ENABLE_QT_DEBUGGER // If PauseOnEntry is enabled, we prompt the user to disable Hardcore Mode // or cancel the action later, so we should keep the debugger around if (g_debugger_window && !DebugInterface::getPauseOnEntry()) DebuggerWindow::destroyInstance(); +#endif } } @@ -1404,7 +1414,10 @@ bool MainWindow::shouldMouseLock() const if (m_display_created == false || m_display_surface == nullptr) return false; - const bool windowsHidden = (!g_debugger_window || g_debugger_window->isHidden()) && + const bool windowsHidden = +#ifdef ENABLE_QT_DEBUGGER + (!g_debugger_window || g_debugger_window->isHidden()) && +#endif (!m_controller_settings_window || m_controller_settings_window->isHidden()) && (!m_settings_window || m_settings_window->isHidden()); @@ -1870,6 +1883,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) action = menu.addAction(tr("Full Boot")); connect(action, &QAction::triggered, [this, entry]() { startGameListEntry(*entry, std::nullopt, false); }); +#ifdef ENABLE_QT_DEBUGGER if (m_ui.menuDebug->menuAction()->isVisible()) { action = menu.addAction(tr("Boot and Debug")); @@ -1879,6 +1893,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) DebuggerWindow::getInstance()->show(); }); } +#endif menu.addSeparator(); populateLoadStateMenu(&menu, QString::fromStdString(entry->path), QString::fromStdString(entry->serial), entry->crc); @@ -3224,11 +3239,13 @@ void MainWindow::doGameSettings(const char* category) } } +#ifdef ENABLE_QT_DEBUGGER void MainWindow::openDebugger() { DebuggerWindow* dwnd = DebuggerWindow::getInstance(); dwnd->isVisible() ? dwnd->activateWindow() : dwnd->show(); } +#endif void MainWindow::doControllerSettings(ControllerSettingsWindow::Category category) { diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index b6ec363716..a45e5a2869 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -16,7 +16,9 @@ #include "Tools/InputRecording/InputRecordingViewer.h" #include "Settings/ControllerSettingsWindow.h" #include "Settings/SettingsWindow.h" +#ifdef ENABLE_QT_DEBUGGER #include "Debugger/DebuggerWindow.h" +#endif #include "ui_MainWindow.h" class QProgressBar; @@ -115,7 +117,9 @@ public: void doSettings(const char* category = nullptr); void doGameSettings(const char* category = nullptr); +#ifdef ENABLE_QT_DEBUGGER void openDebugger(); +#endif void checkMousePosition(int x, int y); public Q_SLOTS: void checkForUpdates(bool display_message, bool force_check); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index b64c3e3e75..fbe74c9aa9 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -2,7 +2,10 @@ // SPDX-License-Identifier: GPL-3.0+ #include "AutoUpdaterDialog.h" +#ifdef ENABLE_QT_DEBUGGER #include "Debugger/DebuggerWindow.h" +#endif +#include "DebugTools/DebugInterface.h" #include "DisplayWidget.h" #include "GameList/GameListWidget.h" #include "LogWindow.h" @@ -2547,10 +2550,16 @@ int main(int argc, char* argv[]) if (s_start_big_picture_mode || Host::GetBaseBoolSettingValue("UI", "StartBigPictureMode", false)) g_emu_thread->startFullscreenUI(s_start_fullscreen || Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false)); - if (s_boot_and_debug || DebuggerWindow::shouldShowOnStartup()) + if (s_boot_and_debug +#ifdef ENABLE_QT_DEBUGGER + || DebuggerWindow::shouldShowOnStartup() +#endif + ) { DebugInterface::setPauseOnEntry(s_boot_and_debug); +#ifdef ENABLE_QT_DEBUGGER g_main_window->openDebugger(); +#endif } // Skip the update check if we're booting a game directly. diff --git a/pcsx2-qt/Settings/DebugSettingsWidget.cpp b/pcsx2-qt/Settings/DebugSettingsWidget.cpp index e0de9baf9c..afa3c9b02e 100644 --- a/pcsx2-qt/Settings/DebugSettingsWidget.cpp +++ b/pcsx2-qt/Settings/DebugSettingsWidget.cpp @@ -5,7 +5,9 @@ #include "QtUtils.h" #include "SettingWidgetBinder.h" +#ifdef ENABLE_QT_DEBUGGER #include "Debugger/DebuggerWindow.h" +#endif #include "Settings/DebugAnalysisSettingsWidget.h" #include "Settings/SettingsWindow.h" @@ -38,8 +40,10 @@ DebugSettingsWidget::DebugSettingsWidget(SettingsWindow* settings_dialog, QWidge SettingWidgetBinder::BindWidgetToIntSetting( sif, m_user_interface.refreshInterval, "Debugger/UserInterface", "RefreshInterval", 1000); connect(m_user_interface.refreshInterval, &QSpinBox::valueChanged, this, []() { +#ifdef ENABLE_QT_DEBUGGER if (g_debugger_window) g_debugger_window->updateFromSettings(); +#endif }); dialog()->registerWidgetHelp( m_user_interface.refreshInterval, tr("Refresh Interval"), tr("1000ms"), diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 4652ff99f5..5d4ec9837c 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1312,10 +1312,12 @@ function(setup_main_executable target) # Copy dependency libraries. set(DEPS_BINDIR "${CMAKE_SOURCE_DIR}/deps/bin") set(DEPS_TO_COPY freetype.dll harfbuzz.dll jpeg62.dll libpng16.dll libsharpyuv.dll libwebp.dll libwebpdemux.dll libwebpmux.dll lz4.dll SDL3.dll shaderc_shared.dll z.dll zstd.dll plutovg.dll plutosvg.dll ryml.dll) - set(DEPS_TO_COPY - $,kddockwidgets-qt6d.dll,kddockwidgets-qt6.dll> - ${DEPS_TO_COPY} - ) + if(ENABLE_QT_DEBUGGER) + set(DEPS_TO_COPY + $,kddockwidgets-qt6d.dll,kddockwidgets-qt6.dll> + ${DEPS_TO_COPY} + ) + endif() foreach(DEP_TO_COPY ${DEPS_TO_COPY}) install(FILES "${DEPS_BINDIR}/${DEP_TO_COPY}" DESTINATION "${CMAKE_SOURCE_DIR}/bin") endforeach() From 6799ab0d2de7b01b9c303ea369cad39a8fd6b6b8 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 003/292] arm64: memory placement, VTLB codegen, and TLB-miss handling SharedMemoryMappingArea::Create() gains a fixed_base_hint so AllocateMemoryMap can pin the JIT arena at a constant VA (kArenaBase=4GB, 256MB-stride fallback) on arm64; the VTLB fastmem backpatch thunk (RecStubs), ArmAddressRecorder relocation hooks (AsmHelpers), and the cpuTlbMiss rec-vs-interp PC split (R5900) round out the arm64 memory layer. Windows arm64 is a no-op stub. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- common/HostSys.h | 7 +- common/Linux/LnxHostSys.cpp | 31 ++- common/Windows/WinHostSys.cpp | 7 +- pcsx2/Memory.cpp | 37 +++- pcsx2/Memory.h | 6 + pcsx2/R5900.cpp | 45 ++++- pcsx2/arm64/AsmHelpers.cpp | 73 ++++++- pcsx2/arm64/AsmHelpers.h | 81 +++++++- pcsx2/arm64/RecStubs.cpp | 351 ++++++++++++++++++++++++++++++++-- pcsx2/vtlb.cpp | 18 +- pcsx2/vtlb.h | 7 +- 11 files changed, 621 insertions(+), 42 deletions(-) diff --git a/common/HostSys.h b/common/HostSys.h index 3d6ce3b542..a7b58cb213 100644 --- a/common/HostSys.h +++ b/common/HostSys.h @@ -139,7 +139,12 @@ namespace PageFaultHandler class SharedMemoryMappingArea { public: - static std::unique_ptr Create(size_t size, bool jit = false); + // fixed_base_hint: when non-zero, the area is placed at that VA (256MB-stride + // fallback slots, then kernel placement) so the reserved region — and any JIT + // code mapped inside it — lands at the same address every run, which a caller + // can rely on for address-stable code caching. Zero = kernel-chosen placement + // (the default). + static std::unique_ptr Create(size_t size, bool jit = false, uptr fixed_base_hint = 0); ~SharedMemoryMappingArea(); diff --git a/common/Linux/LnxHostSys.cpp b/common/Linux/LnxHostSys.cpp index 523e6eaaaf..360f385a81 100644 --- a/common/Linux/LnxHostSys.cpp +++ b/common/Linux/LnxHostSys.cpp @@ -153,7 +153,7 @@ SharedMemoryMappingArea::~SharedMemoryMappingArea() } -std::unique_ptr SharedMemoryMappingArea::Create(size_t size, bool jit) +std::unique_ptr SharedMemoryMappingArea::Create(size_t size, bool jit, uptr fixed_base_hint) { pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned"); @@ -162,7 +162,34 @@ std::unique_ptr SharedMemoryMappingArea::Create(size_t if (jit) flags |= MAP_JIT; #endif - void* alloc = mmap(nullptr, size, PROT_NONE, flags, -1, 0); + + void* alloc = MAP_FAILED; + + // Deterministic VA placement for the on-disk VU program cache: when a + // fixed base hint is given, try it (plus 256MB-stride fallback slots) with a + // hint-and-verify mmap (no MAP_FIXED, so we never clobber an existing + // mapping). If none of the slots land exactly, fall through to kernel + // placement — the program cache records the arena base and simply misses on + // a mismatch, so this trades determinism for booting, never correctness. + if (fixed_base_hint != 0) + { + for (int slot = 0; slot <= 10; slot++) + { + void* const want = reinterpret_cast(fixed_base_hint + (static_cast(slot) << 28)); + void* const got = mmap(want, size, PROT_NONE, flags, -1, 0); + if (got == MAP_FAILED) + continue; + if (got == want) + { + alloc = got; + break; + } + munmap(got, size); + } + } + + if (alloc == MAP_FAILED) + alloc = mmap(nullptr, size, PROT_NONE, flags, -1, 0); if (alloc == MAP_FAILED) return nullptr; diff --git a/common/Windows/WinHostSys.cpp b/common/Windows/WinHostSys.cpp index 90c23051b2..bf312d04db 100644 --- a/common/Windows/WinHostSys.cpp +++ b/common/Windows/WinHostSys.cpp @@ -143,10 +143,15 @@ SharedMemoryMappingArea::PlaceholderMap::iterator SharedMemoryMappingArea::FindP return m_placeholder_ranges.end(); } -std::unique_ptr SharedMemoryMappingArea::Create(size_t size, bool jit) +std::unique_ptr SharedMemoryMappingArea::Create(size_t size, bool jit, uptr fixed_base_hint) { pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned"); + // Deterministic fixed-base placement is not implemented on Windows; kernel-chosen + // placement is used and the program cache simply incurs a miss if the arena base + // differs across runs. + (void)fixed_base_hint; + void* alloc = VirtualAlloc2(GetCurrentProcess(), nullptr, size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); if (!alloc) return nullptr; diff --git a/pcsx2/Memory.cpp b/pcsx2/Memory.cpp index a772ed4689..c1bacd816b 100644 --- a/pcsx2/Memory.cpp +++ b/pcsx2/Memory.cpp @@ -95,7 +95,22 @@ bool SysMemory::AllocateMemoryMap() return false; } - if (!(s_memory_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::MainSize + HostMemoryMap::CodeSize, true))) + // Constant-VA arena for the on-disk VU program cache: on arm64 the + // whole data+code reservation must sit at the same VA every run so cached + // JIT code (which lives in the code half, at BasePointer() + MainSize) + // reloads without repatching its baked addresses. 4GB clears the ASLR brk + // window (non-PIE image at 0x400000 + brk randomization < 2GB) and sits far + // below the mmap_base / PIE-load regions, so the slot-0 candidate succeeds + // deterministically; Create() walks 256MB-stride fallback slots and finally + // kernel placement (program-cache misses, never corruption). Other arches + // pass 0 and take kernel-chosen placement. +#if defined(__aarch64__) || defined(_M_ARM64) + constexpr uptr kArenaBase = 0x100000000ull; // 4GB +#else + constexpr uptr kArenaBase = 0; +#endif + + if (!(s_memory_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::MainSize + HostMemoryMap::CodeSize, true, kArenaBase))) { Host::ReportErrorAsync("Error", "Failed to map main memory."); ReleaseMemoryMap(); @@ -172,11 +187,19 @@ void SysMemory::ReleaseMemoryMap() } } +void SysMemory::ReserveMemory() +{ + // Claim the host memory map (and the arm64 constant-VA arena) up front, so + // the fixed-base placement isn't lost to an intervening heap/mmap. Idempotent. + if (!s_data_memory_file_handle) + AllocateMemoryMap(); +} + bool SysMemory::Allocate() { DevCon.WriteLn(Color_StrongBlue, "Allocating host memory for virtual systems..."); - if (!AllocateMemoryMap()) + if (!s_data_memory_file_handle && !AllocateMemoryMap()) return false; memAllocate(); @@ -411,6 +434,16 @@ void memMapPhy() // High memory, uninstalled on the configuration we emulate vtlb_MapHandler(null_handler, Ps2MemSize::ExposedRam, 0x10000000 - Ps2MemSize::ExposedRam); + // Physical RAM mirrors used by BIOS InitRDRAM for RDRAM device configuration. + // On real PS2 hardware: + // 0x20000000-0x21FFFFFF = uncached mirror of main RAM + // 0x30000000-0x31FFFFFF = uncached & accelerated mirror of main RAM + // These mirrors must be present in the physical map; without them, BIOS writes + // to RDRAM device registers hit UnmappedPhyHandler (bus error). + // Requires VTLB_PMAP_SZ >= 1GB to cover these addresses. + vtlb_MapBlock(eeMem->Main, 0x20000000, Ps2MemSize::ExposedRam); + vtlb_MapBlock(eeMem->Main, 0x30000000, Ps2MemSize::ExposedRam); + // Various ROMs (all read-only) vtlb_MapBlock(eeMem->ROM, 0x1fc00000, Ps2MemSize::Rom); vtlb_MapBlock(eeMem->ROM1, 0x1e000000, Ps2MemSize::Rom1); diff --git a/pcsx2/Memory.h b/pcsx2/Memory.h index 4efb4db72a..d9ea3d6264 100644 --- a/pcsx2/Memory.h +++ b/pcsx2/Memory.h @@ -98,6 +98,12 @@ namespace HostMemoryMap namespace SysMemory { + /// Reserve the host memory map (and, on arm64, the constant-VA arena) early, + /// before other allocations could squat on the fixed base. Idempotent — a + /// later Allocate() reuses the same reservation. Used by headless runners and + /// SDL frontends that need the deterministic arena claimed before any + /// heap/mmap could squat on the fixed base. + void ReserveMemory(); bool Allocate(); void Reset(); void Release(); diff --git a/pcsx2/R5900.cpp b/pcsx2/R5900.cpp index d97df15440..1368a8f966 100644 --- a/pcsx2/R5900.cpp +++ b/pcsx2/R5900.cpp @@ -166,8 +166,17 @@ __ri void cpuException(u32 code, u32 bd) void cpuTlbMiss(u32 addr, u32 bd, u32 excode) { - // Avoid too much spamming on the interpreter - if (Cpu != &intCpu || IsDebugBuild) { + // Avoid too much spamming. On x86 the recompiler uses CancelInstruction and + // seldom reaches here, so logging the rec path is cheap; on arm64 every rec + // TLB miss is funneled through this function (see s_recTlbMissOccurred below), + // so logging the rec path would spam Release on every miss. Gate the arm64 + // rec path on debug builds, matching the original "don't spam on interp" intent. +#ifdef __aarch64__ + const bool log_tlb_miss = IsDebugBuild; +#else + const bool log_tlb_miss = (Cpu != &intCpu) || IsDebugBuild; +#endif + if (log_tlb_miss) { Console.Error("cpuTlbMiss pc:%x, cycl:%llx, addr: %x, status=%x, code=%x", cpuRegs.pc, cpuRegs.cycle, addr, cpuRegs.CP0.n.Status.val, excode); } @@ -177,8 +186,30 @@ void cpuTlbMiss(u32 addr, u32 bd, u32 excode) cpuRegs.CP0.n.Context |= (addr >> 9) & 0x007FFFF0; cpuRegs.CP0.n.EntryHi = (addr & 0xFFFFE000) | (cpuRegs.CP0.n.EntryHi & 0x1FFF); - cpuRegs.pc -= 4; + // The interpreter advances cpuRegs.pc past the current instruction + // before executing it, so pc -= 4 gets back to the faulting instruction. + // The recompiler's FLUSH_PC writes the current instruction's PC + // (not advanced), so we must NOT subtract 4 in that case. + const bool isRec = (Cpu != &intCpu); + if (!isRec) + cpuRegs.pc -= 4; + cpuException(excode, bd); + + // For the ARM64 recompiler: set a flag so the JIT block can detect the + // exception after the interpreter call returns and dispatch to the + // exception vector. We don't use CancelInstruction (longjmp) because + // that disrupts cycle counting and timing. The x86 recompiler uses + // CancelInstruction instead. +#ifdef __aarch64__ + if (isRec) + { + // Defined in the arm64 EE recompiler TU; the dispatcher reads it after + // this call to route the block to the exception vector. + extern u32 s_recTlbMissOccurred; + s_recTlbMissOccurred = 1; + } +#endif } void cpuTlbMissR(u32 addr, u32 bd) { @@ -394,7 +425,6 @@ __fi void _cpuEventTest_Shared() // Console.WriteLn( " IOP ahead by: %d cycles", -EEsCycle ); EEsCycle = psxCpu->ExecuteBlock(EEsCycle); - iopEventAction = false; } @@ -435,7 +465,10 @@ __fi void _cpuEventTest_Shared() // ---- Schedule Next Event Test -------------- const float mutiplier = static_cast(PS2CLK) / static_cast(PSXCLK); - const int nextIopEventDeta = ((psxRegs.iopNextEventCycle - psxRegs.cycle) * mutiplier); + // See R3000A.cpp:PSX_INT for the host-divergence rationale: cast the u32 + // cycle delta to s32 *before* the float multiply. + const s32 iopCyclesUntilEvent = static_cast(psxRegs.iopNextEventCycle - psxRegs.cycle); + const int nextIopEventDeta = static_cast(iopCyclesUntilEvent * mutiplier); // 8 or more cycles behind and there's an event scheduled if (EEsCycle >= nextIopEventDeta) { @@ -448,7 +481,7 @@ __fi void _cpuEventTest_Shared() else { // Otherwise IOP is caught up/not doing anything so we can wait for the next event. - cpuSetNextEventDelta(((psxRegs.iopNextEventCycle - psxRegs.cycle) * mutiplier) - EEsCycle); + cpuSetNextEventDelta(nextIopEventDeta - EEsCycle); } // Apply vsync and other counter nextCycles diff --git a/pcsx2/arm64/AsmHelpers.cpp b/pcsx2/arm64/AsmHelpers.cpp index a8dc18ce69..1815738677 100644 --- a/pcsx2/arm64/AsmHelpers.cpp +++ b/pcsx2/arm64/AsmHelpers.cpp @@ -59,6 +59,9 @@ const vixl::aarch64::VRegister& armQRegister(int n) } +// Opt-in only (matches origin/master): uncomment to compile vixl's +// PrintDisassembler/Decoder for armDisassembleAndDumpCode. Off by default so +// the disassembler TUs and statics don't ship in normal builds. //#define INCLUDE_DISASSEMBLER #ifdef INCLUDE_DISASSEMBLER @@ -71,6 +74,7 @@ thread_local a64::MacroAssembler* armAsm; thread_local u8* armAsmPtr; thread_local size_t armAsmCapacity; thread_local ArmConstantPool* armConstantPool; +thread_local ArmAddressRecorder* armAddressRecorder; #ifdef INCLUDE_DISASSEMBLER static std::mutex armDisasmMutex; @@ -136,12 +140,15 @@ void armDisassembleAndDumpCode(const void* ptr, size_t size) std::unique_lock lock(armDisasmMutex); if (!armDisasm) { - armDisasm = std::make_unique(stderr); + std::FILE* logFile = Log::GetFileLogHandle(); + armDisasm = std::make_unique(logFile ? logFile : stderr); armDisasmDecoder = std::make_unique(); armDisasmDecoder->AppendVisitor(armDisasm.get()); } - armDisasmDecoder->Decode(static_cast(ptr), static_cast(ptr) + size); + const auto* start = reinterpret_cast(ptr); + const auto* end = reinterpret_cast(static_cast(ptr) + size); + armDisasmDecoder->Decode(start, end); #else Console.Error("Not compiled with INCLUDE_DISASSEMBLER"); #endif @@ -162,13 +169,21 @@ void armEmitJmp(const void* ptr, bool force_inline) if (use_blr) { + if (armAddressRecorder) + armAddressRecorder->OnAbsoluteTarget(ptr); armAsm->Mov(RXVIXLSCRATCH, reinterpret_cast(ptr)); armAsm->Br(RXVIXLSCRATCH); } else { - a64::SingleEmissionCheckScope guard(armAsm); - armAsm->b(displacement); + { + a64::SingleEmissionCheckScope guard(armAsm); + armAsm->b(displacement); + } + // Record after emission: the scope entry may flush a pending vixl + // literal pool, so the insn address is only known once it's out. + if (armAddressRecorder) + armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, false); } } @@ -187,13 +202,19 @@ void armEmitCall(const void* ptr, bool force_inline) if (use_blr) { + if (armAddressRecorder) + armAddressRecorder->OnAbsoluteTarget(ptr); armAsm->Mov(RXVIXLSCRATCH, reinterpret_cast(ptr)); armAsm->Blr(RXVIXLSCRATCH); } else { - a64::SingleEmissionCheckScope guard(armAsm); - armAsm->bl(displacement); + { + a64::SingleEmissionCheckScope guard(armAsm); + armAsm->bl(displacement); + } + if (armAddressRecorder) + armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, true); } } @@ -226,6 +247,23 @@ void armEmitCondBranch(a64::Condition cond, const void* ptr) static_cast(reinterpret_cast(ptr) - reinterpret_cast(armGetCurrentCodePointer())); //pxAssert(Common::IsAligned(jump_distance, 4)); + // A recorder patching this branch on relocation needs the imm26 reach of a + // plain B — B.cond's ±1MB imm19 may not survive the move. Force the long + // form for targets the recorder marks relocatable and record the B. + if (armAddressRecorder && armAddressRecorder->WantsLongCondBranch(ptr)) + { + a64::MacroEmissionCheckScope guard(armAsm); + a64::Label branch_not_taken; + armAsm->b(&branch_not_taken, a64::InvertCondition(cond)); + + const s64 new_jump_distance = + static_cast(reinterpret_cast(ptr) - reinterpret_cast(armGetCurrentCodePointer())); + armAsm->b(new_jump_distance >> 2); + armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, false); + armAsm->bind(&branch_not_taken); + return; + } + if (a64::Instruction::IsValidImmPCOffset(a64::CondBranchType, jump_distance >> 2)) { a64::SingleEmissionCheckScope guard(armAsm); @@ -249,6 +287,23 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr) // psxAsm->Mov(reg, static_cast(reinterpret_cast(addr))); pxAssert(reg.IsX()); + if (armAddressRecorder && + armAddressRecorder->ClassifyMove(addr) == ArmAddressRecorder::MoveForm::CanonicalAbs) + { + // Fixed-width 16-byte form: every operand bit lives in a movz/movk + // imm16 field a relocation patcher can rewrite in place. + const u64 v = reinterpret_cast(addr); + { + vixl::ExactAssemblyScope guard(armAsm, 16); + armAsm->movz(reg, v & 0xFFFF, 0); + armAsm->movk(reg, (v >> 16) & 0xFFFF, 16); + armAsm->movk(reg, (v >> 32) & 0xFFFF, 32); + armAsm->movk(reg, (v >> 48) & 0xFFFF, 48); + } + armAddressRecorder->OnCanonicalAbsMove(armGetCurrentCodePointer() - 16, addr); + return; + } + const void* current_code_ptr_page = reinterpret_cast( reinterpret_cast(armGetCurrentCodePointer()) & ~static_cast(0xFFF)); const void* ptr_page = @@ -261,6 +316,8 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr) a64::SingleEmissionCheckScope guard(armAsm); armAsm->adrp(reg, page_displacement); } + if (armAddressRecorder) + armAddressRecorder->OnAdrp(armGetCurrentCodePointer() - 4, addr); armAsm->Add(reg, reg, page_offset); } else if (vixl::IsInt21(page_displacement) && a64::Assembler::IsImmLogical(page_offset, 64)) @@ -269,10 +326,14 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr) a64::SingleEmissionCheckScope guard(armAsm); armAsm->adrp(reg, page_displacement); } + if (armAddressRecorder) + armAddressRecorder->OnAdrp(armGetCurrentCodePointer() - 4, addr); armAsm->Orr(reg, reg, page_offset); } else { + if (armAddressRecorder) + armAddressRecorder->OnAbsoluteTarget(addr); armAsm->Mov(reg, reinterpret_cast(addr)); } } diff --git a/pcsx2/arm64/AsmHelpers.h b/pcsx2/arm64/AsmHelpers.h index 022ef3ca85..3857319e29 100644 --- a/pcsx2/arm64/AsmHelpers.h +++ b/pcsx2/arm64/AsmHelpers.h @@ -24,9 +24,14 @@ #define RXARG3 vixl::aarch64::x2 #define RXARG4 vixl::aarch64::x3 -#define RXVIXLSCRATCH vixl::aarch64::x16 -#define RWVIXLSCRATCH vixl::aarch64::w16 -#define RSCRATCHADDR vixl::aarch64::x17 +#define RXVIXLSCRATCH vixl::aarch64::x16 // Reserved for VIXL internal use — do NOT use in rec code +#define RWVIXLSCRATCH vixl::aarch64::w16 // Reserved for VIXL internal use — do NOT use in rec code +#define RSCRATCHADDR vixl::aarch64::x17 // Address scratch — removed from VIXL pool in armStartBlock + +// General-purpose value scratch registers for recompiler use. +// These are caller-saved and NOT in VIXL's scratch pool. +#define RXSCRATCH vixl::aarch64::x8 +#define RWSCRATCH vixl::aarch64::w8 #define RQSCRATCH vixl::aarch64::q30 #define RDSCRATCH vixl::aarch64::d30 @@ -59,12 +64,54 @@ const vixl::aarch64::VRegister& armQRegister(int n); class ArmConstantPool; +// Address-emission observer for the on-disk VU program cache. While a +// recorder is attached (mVU code-cache episodes only — see mVUopenCodeCache), +// the emit helpers below report every host-address-bearing emission so the +// recorder can build a relocation fixup table, and let it force canonical +// fixed-width forms where the default encoding couldn't be patched after the +// code block moves: +// - armMoveAddressToReg of a volatile (heap) target → movz+movk×3 (16 bytes, +// patchable) instead of the shortest mov/adrp form. +// - armEmitCondBranch to a relocatable target → inverted-cond skip + B imm26 +// (B.cond's ±1MB imm19 can't survive arbitrary replacement). +// `at` arguments are the address of the first emitted instruction of the +// reported shape. All hooks are no-ops when no recorder is attached. +class ArmAddressRecorder +{ +public: + enum class MoveForm + { + Default, // emit in shortest form; recorder may still log it + CanonicalAbs, // emit fixed-width movz+movk×3 so the operand is patchable + }; + + virtual ~ArmAddressRecorder() = default; + + // armMoveAddressToReg: pick the emission form for `addr`. + virtual MoveForm ClassifyMove(const void* addr) = 0; + // armMoveAddressToReg emitted the canonical 16-byte movz+movk×3 at `at`. + virtual void OnCanonicalAbsMove(u8* at, const void* addr) = 0; + // armMoveAddressToReg emitted ADRP (+Add/Orr) at `at`; the page offset is + // PC-relative and must be re-paged if this code moves. + virtual void OnAdrp(u8* at, const void* addr) = 0; + // armEmitJmp/armEmitCall/armEmitCondBranch emitted a direct B/BL imm26 at + // `at` targeting `target`. + virtual void OnDirectBranch(u8* at, const void* target, bool is_call) = 0; + // armEmitCondBranch: return true to force the long (cond-skip + B) form. + virtual bool WantsLongCondBranch(const void* target) = 0; + // An absolute (movz/movk-materialized) target with no patch site — emitted + // by the out-of-range paths of armEmitJmp/armEmitCall/armMoveAddressToReg. + // Recorder uses this to verify the target is run-invariant. + virtual void OnAbsoluteTarget(const void* target) = 0; +}; + static const u32 SP_SCRATCH_OFFSET = 0; extern thread_local vixl::aarch64::MacroAssembler* armAsm; extern thread_local u8* armAsmPtr; extern thread_local size_t armAsmCapacity; extern thread_local ArmConstantPool* armConstantPool; +extern thread_local ArmAddressRecorder* armAddressRecorder; static __fi bool armHasBlock() { @@ -104,6 +151,34 @@ void armGetMemOperandInRegister(const vixl::aarch64::Register& addr_reg, void armLoadConstant128(const vixl::aarch64::VRegister& reg, const void* ptr); +// Pack 4 per-lane bool lanes (each lane is all-1s or 0 — the natural output of +// a NEON CMxx / FCMxx against zero) into a 4-bit GPR using the canonical +// AArch64 movemask idiom: AND with a per-lane weight vector, ADDV-sum across +// lanes, then UMOV to GPR. +// +// `data` is clobbered (AND in-place, ADDV writes the low S lane in-place). +// `tmp` is loaded with the weight vector via the vixl literal pool; must +// differ from `data`. Both must be Q-form (128-bit). +// +// PS2 MAC flag bit order is bit0=W, bit3=X (reverse of NEON lane order). Pass +// reverse=true to get that mapping; reverse=false yields lane[i]→bit[i]. +// +// Emits 4 insns: ldr q (literal pool) + and.16b + addv s + umov w. +__fi static void armEmitPackLaneBits(const vixl::aarch64::Register& dst, + const vixl::aarch64::VRegister& data, const vixl::aarch64::VRegister& tmp, + bool reverse) +{ + // Weight vector as u32 lanes [0..3]. low64 packs lanes 0+1, high64 packs 2+3. + // forward {1,2,4,8}: low = (2<<32)|1, high = (8<<32)|4 + // reverse {8,4,2,1}: low = (4<<32)|8, high = (1<<32)|2 + const u64 low64 = reverse ? 0x0000000400000008ULL : 0x0000000200000001ULL; + const u64 high64 = reverse ? 0x0000000100000002ULL : 0x0000000800000004ULL; + armAsm->Ldr(tmp, high64, low64); + armAsm->And(data.V16B(), data.V16B(), tmp.V16B()); + armAsm->Addv(vixl::aarch64::VRegister(data.GetCode(), 32), data.V4S()); + armAsm->Umov(dst, data.V4S(), 0); +} + // may clobber RSCRATCH/RSCRATCH2. they shouldn't be inputs. void armEmitVTBL(const vixl::aarch64::VRegister& dst, const vixl::aarch64::VRegister& src1, const vixl::aarch64::VRegister& src2, const vixl::aarch64::VRegister& tbl); diff --git a/pcsx2/arm64/RecStubs.cpp b/pcsx2/arm64/RecStubs.cpp index 4b77a7db1c..4accf0254d 100644 --- a/pcsx2/arm64/RecStubs.cpp +++ b/pcsx2/arm64/RecStubs.cpp @@ -1,30 +1,347 @@ // SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0 +#include "arm64/AsmHelpers.h" +#include "arm64/iR5900-arm64.h" #include "common/Console.h" +#include "common/HostSys.h" +#include "Memory.h" #include "MTVU.h" #include "SaveState.h" #include "vtlb.h" #include "common/Assertions.h" -void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc, u32 guest_addr, u32 gpr_bitmask, u32 fpr_bitmask, u8 address_register, u8 data_register, u8 size_in_bits, bool is_signed, bool is_load, bool is_fpr) +namespace a64 = vixl::aarch64; + +using namespace vtlb_private; + +void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc, u32 guest_addr, + u32 gpr_bitmask, u32 fpr_bitmask, u8 address_register, u8 data_register, + u8 size_in_bits, bool is_signed, bool is_load, bool is_fpr) { - pxFailRel("Not implemented."); + DevCon.WriteLn("Backpatching %s at %p[%u] (pc %08X vaddr %08X): GPR %08X FPR %08X Addr %u Data %u Size %u Flags %02X %02X", + is_load ? "load" : "store", (void*)code_address, code_size, guest_pc, guest_addr, + gpr_bitmask, fpr_bitmask, address_register, data_register, size_in_bits, is_signed, is_load); + + u8* thunk = recBeginThunk(); + + // Collect caller-saved GPRs that need saving. + // Callee-saved (x19+) are preserved by the C call and don't need saving. + // For loads into a GPR, skip the data register (result goes there). + static constexpr u32 MAX_SAVE_GPRS = 16; + u8 gprs_to_save[MAX_SAVE_GPRS]; + u32 num_gprs = 0; + + for (u32 i = 0; i < 19; i++) + { + if (!(gpr_bitmask & (1u << i))) + continue; + // Skip scratch/reserved: x8 (RWSCRATCH), x16 (VIXL), x17 (RSCRATCHADDR), x18 (platform) + if (i == 8 || i >= 16) + continue; + // For loads into GPR, skip the data register + if (is_load && !is_fpr && i == data_register) + continue; + pxAssert(num_gprs < MAX_SAVE_GPRS); + gprs_to_save[num_gprs++] = static_cast(i); + } + + // Collect NEON regs that need saving. + // q8-q15 lower 64 bits are callee-saved, but the JIT uses full 128-bit, so save all live ones. + static constexpr u32 MAX_SAVE_FPRS = 32; + u8 fprs_to_save[MAX_SAVE_FPRS]; + u32 num_fprs = 0; + + for (u32 i = 0; i < 32; i++) + { + if (!(fpr_bitmask & (1u << i))) + continue; + // For loads into FPR, skip the data register + if (is_load && is_fpr && i == data_register) + continue; + pxAssert(num_fprs < MAX_SAVE_FPRS); + fprs_to_save[num_fprs++] = static_cast(i); + } + + // Calculate stack size (must be 16-byte aligned) + const u32 gpr_save_bytes = num_gprs * 8; + const u32 fpr_save_bytes = num_fprs * 16; + const u32 stack_size = (gpr_save_bytes + fpr_save_bytes + 15u) & ~15u; + + if (stack_size > 0) + armAsm->Sub(a64::sp, a64::sp, stack_size); + + // Save GPRs to stack + u32 offset = 0; + for (u32 i = 0; i < num_gprs; i++) + { + armAsm->Str(a64::XRegister(gprs_to_save[i]), a64::MemOperand(a64::sp, offset)); + offset += 8; + } + + // Save NEON regs to stack + for (u32 i = 0; i < num_fprs; i++) + { + armAsm->Str(a64::QRegister(fprs_to_save[i]), a64::MemOperand(a64::sp, offset)); + offset += 16; + } + + // At this point, all host registers still have their original JIT values + // (STR only reads, doesn't modify the source register). + + // Flush cpuRegs.pc and cpuRegs.code for exception handling. + // The fastmem path skips iFlushCall, so these may be stale. + // If the vtlb handler triggers a TLB miss or other exception, + // cpuTlbMiss reads cpuRegs.pc to set EPC. + armAsm->Mov(RWSCRATCH, guest_pc); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + + armAsm->Mov(RWSCRATCH, *(u32*)PSM(guest_pc)); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.code)); + + // Set up arguments for the vtlb handler call. + + // 128-bit fastmem path. Always uses q0 (data_register == 0); the JIT + // callers in recVTLB-arm64.cpp materialize the load result / store + // value into q0 and never allocate q0 to a guest register at the + // fastmem emit point. vtlb_memRead128 returns r128 in q0 per AAPCS64; + // vtlb_memWrite128 takes value in q0. + if (size_in_bits == 128) + { + pxAssertRel(is_fpr && data_register == 0, + "128-bit fastmem backpatch must target q0"); + + if (address_register != 9) + armAsm->Mov(a64::w9, armWRegister(address_register)); + + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->Tbnz(a64::x0, 63, &slow_path); + + if (is_load) + armAsm->Ldr(a64::q0, a64::MemOperand(a64::x0)); + else + armAsm->Str(a64::q0, a64::MemOperand(a64::x0)); + armAsm->B(&done); + + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + // Spill/reload RECCYCLE around the vtlb handler call — the slow path + // dispatches to MMIO handlers (hwRead*/hwWrite*) which read/write + // cpuRegs.cycle (timer regs, IntCHackCheck, etc.). Without this, + // the handler sees a stale cycle value, which can mis-schedule + // events and cause cascading mid-block timing bugs. Matches the + // pattern at recVTLB-arm64.cpp:112+120. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + if (is_load) + armEmitCall((void*)vtlb_memRead128); + else + armEmitCall((void*)vtlb_memWrite128); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&done); + } + else if (is_load) + { + // Load backpatch: emit inline VTLB read code (same as vtlbSoftmemRead). + if (address_register != 9) + armAsm->Mov(a64::w9, armWRegister(address_register)); + + // Inline VTLB lookup + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->Tbnz(a64::x0, 63, &slow_path); + + // Fast path: direct memory read via resolved host pointer + switch (size_in_bits) + { + case 8: + if (is_signed) + armAsm->Ldrsb(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldrb(a64::w0, a64::MemOperand(a64::x0)); + break; + case 16: + if (is_signed) + armAsm->Ldrsh(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldrh(a64::w0, a64::MemOperand(a64::x0)); + break; + case 32: + if (is_signed) + armAsm->Ldrsw(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldr(a64::w0, a64::MemOperand(a64::x0)); + break; + case 64: + armAsm->Ldr(a64::x0, a64::MemOperand(a64::x0)); + break; + default: pxFailRel("Unsupported load size in backpatch"); break; + } + armAsm->B(&done); + + // Slow path: call vtlb_memRead handler + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + // Spill/reload RECCYCLE — see 128-bit slow_path above for rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + switch (size_in_bits) + { + case 8: armEmitCall((void*)vtlb_memRead); break; + case 16: armEmitCall((void*)vtlb_memRead); break; + case 32: armEmitCall((void*)vtlb_memRead); break; + case 64: armEmitCall((void*)vtlb_memRead); break; + default: break; + } + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + // Extend the handler return into x0 for the 64-bit cpuRegs.GPR store. + // AAPCS64 leaves the upper bits of x0 unspecified for sub-word returns, + // so UNSIGNED sub-64-bit loads must Uxtw too — otherwise the garbage + // upper 32 bits leak into the 64-bit EE GPR (LWU/LBU/LHU faulting to an + // MMIO/handler page). The fast inline path zero-extends via Ldrb/Ldrh/ + // Ldr w0; this mirrors that, and the const-paddr shortcut in + // recVTLB-arm64.cpp which handles the identical hazard. + if (size_in_bits < 64) + { + if (is_signed) + { + if (size_in_bits == 8) + armAsm->Sxtb(a64::x0, a64::w0); + else if (size_in_bits == 16) + armAsm->Sxth(a64::x0, a64::w0); + else if (size_in_bits == 32) + armAsm->Sxtw(a64::x0, a64::w0); + } + else + { + armAsm->Uxtw(a64::x0, a64::w0); + } + } + + armAsm->Bind(&done); + + // Move result to data register + if (!is_fpr) + { + if (data_register != 0) + armAsm->Mov(armXRegister(data_register), a64::x0); + } + else + { + armAsm->Fmov(a64::SRegister(data_register), a64::w0); + } + } + else + { + // Store backpatch: emit inline VTLB write code (same as vtlbSoftmemWrite), + // emitting the inline VTLB lookup + store rather than calling vtlb_memWrite. + // Move address to w9, value to w10 (standard scratch for inline VTLB). + if (address_register != 9) + armAsm->Mov(a64::w9, armWRegister(address_register)); + if (data_register != 10) + { + if (size_in_bits <= 32) + armAsm->Mov(a64::w10, armWRegister(data_register)); + else + armAsm->Mov(a64::x10, armXRegister(data_register)); + } + + // Inline VTLB lookup: vmap[addr >> PAGE_BITS] → ppf + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->Tbnz(a64::x0, 63, &slow_path); + + // Fast path: direct memory write via resolved host pointer + switch (size_in_bits) + { + case 8: armAsm->Strb(a64::w10, a64::MemOperand(a64::x0)); break; + case 16: armAsm->Strh(a64::w10, a64::MemOperand(a64::x0)); break; + case 32: armAsm->Str(a64::w10, a64::MemOperand(a64::x0)); break; + case 64: armAsm->Str(a64::x10, a64::MemOperand(a64::x0)); break; + default: pxFailRel("Unsupported store size in backpatch"); break; + } + armAsm->B(&done); + + // Slow path: call vtlb_memWrite handler + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + if (size_in_bits <= 32) + armAsm->Mov(a64::w1, a64::w10); + else + armAsm->Mov(a64::x1, a64::x10); + + // Spill/reload RECCYCLE — see 128-bit slow_path above for rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + switch (size_in_bits) + { + case 8: armEmitCall((void*)vtlb_memWrite); break; + case 16: armEmitCall((void*)vtlb_memWrite); break; + case 32: armEmitCall((void*)vtlb_memWrite); break; + case 64: armEmitCall((void*)vtlb_memWrite); break; + default: pxFailRel("Unsupported store size in backpatch"); break; + } + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&done); + } + + // Restore GPRs from stack + offset = 0; + for (u32 i = 0; i < num_gprs; i++) + { + armAsm->Ldr(a64::XRegister(gprs_to_save[i]), a64::MemOperand(a64::sp, offset)); + offset += 8; + } + + // Restore NEON regs from stack + for (u32 i = 0; i < num_fprs; i++) + { + armAsm->Ldr(a64::QRegister(fprs_to_save[i]), a64::MemOperand(a64::sp, offset)); + offset += 16; + } + + if (stack_size > 0) + armAsm->Add(a64::sp, a64::sp, stack_size); + + // Branch back to the instruction after the faulting load/store + armEmitJmp((void*)(code_address + code_size)); + + u8* thunk_end = recEndThunk(); + + // Flush instruction cache for the ENTIRE thunk. + // ARM64 icache is not coherent with dcache — without this, the CPU may + // execute stale instructions from previously compiled code at the thunk's + // address, causing SIGILL or corruption. + HostSys::FlushInstructionCache(thunk, static_cast(thunk_end - thunk)); + + // Patch the faulting instruction with a B (branch) to the thunk. + // ARM64 B instruction: 0x14000000 | imm26, where imm26 = byte_offset / 4 + const s64 branch_offset = static_cast(thunk - reinterpret_cast(code_address)); + pxAssert((branch_offset & 3) == 0); + const s64 branch_imm26 = branch_offset >> 2; + pxAssertRel(branch_imm26 >= -0x2000000 && branch_imm26 <= 0x1FFFFFF, + "Backpatch thunk too far from faulting instruction for B instruction"); + + HostSys::BeginCodeWrite(); + u32* patch_ptr = reinterpret_cast(code_address); + *patch_ptr = 0x14000000u | (static_cast(branch_imm26) & 0x03FFFFFFu); + HostSys::EndCodeWrite(); + + // Flush icache at the patch point too. + HostSys::FlushInstructionCache(reinterpret_cast(code_address), 4); } -bool SaveStateBase::vuJITFreeze() -{ - if(IsSaving()) - vu1Thread.WaitVU(); - - Console.Warning("recompiler state is stubbed in arm64!"); - - // HACK!! - - // size of microRegInfo structure - std::array empty_data{}; - Freeze(empty_data); - Freeze(empty_data); - return true; -} +// vuJITFreeze() is defined in microVU-arm64.cpp diff --git a/pcsx2/vtlb.cpp b/pcsx2/vtlb.cpp index 36e7d4747a..91424117ff 100644 --- a/pcsx2/vtlb.cpp +++ b/pcsx2/vtlb.cpp @@ -516,7 +516,8 @@ static __ri void vtlb_Miss(u32 addr, u32 mode) if (EmuConfig.Gamefixes.GoemonTlbHack) GoemonTlbMissDebug(); - // Hack to handle expected tlb miss by some games. + // Interpreter: raise the exception, then CancelInstruction stops the current + // instruction so the exception vector is dispatched immediately. if (Cpu == &intCpu) { if (mode) @@ -524,7 +525,6 @@ static __ri void vtlb_Miss(u32 addr, u32 mode) else cpuTlbMissR(addr, cpuRegs.branch); - // Exception handled. Current instruction need to be stopped Cpu->CancelInstruction(); return; } @@ -539,9 +539,23 @@ static __ri void vtlb_Miss(u32 addr, u32 mode) return; } +#ifdef __aarch64__ + // arm64 recompiler: raise the TLB-miss exception here. cpuTlbMissR/W sets + // cpuRegs.pc to the exception vector, which the rec picks up at the next + // dispatch — no CancelInstruction longjmp (which is interpreter-only; the + // arm64 rec returns and lets the exception state take effect at block end). + if (mode) + cpuTlbMissW(addr, cpuRegs.branch); + else + cpuTlbMissR(addr, cpuRegs.branch); +#else + // x86 recompiler: upstream behavior — log and continue without raising + // (x86 recCancelInstruction is a stub, so the arm64 exception path above + // must not run here). static int spamStop = 0; if (spamStop++ < 50 || IsDevBuild) Console.Error(message); +#endif } // BusError exception: more serious than a TLB miss. If properly emulated the PS2 kernel diff --git a/pcsx2/vtlb.h b/pcsx2/vtlb.h index 8f321703f1..6125dbb144 100644 --- a/pcsx2/vtlb.h +++ b/pcsx2/vtlb.h @@ -121,7 +121,10 @@ namespace vtlb_private static const uint VTLB_PAGE_MASK = 4095; static const uint VTLB_PAGE_SIZE = 4096; - static const uint VTLB_PMAP_SZ = _1mb * 512; + // Physical map covers 1GB to include RAM mirrors at 0x20000000 (uncached) + // and 0x30000000 (uncached & accelerated) used by BIOS InitRDRAM. + // 1GB is sufficient for all known PS2 mappings. + static const uint VTLB_PMAP_SZ = _1mb * 1024; static const uint VTLB_PMAP_ITEMS = VTLB_PMAP_SZ / VTLB_PAGE_SIZE; static const uint VTLB_VMAP_ITEMS = _4gb / VTLB_PAGE_SIZE; @@ -189,7 +192,7 @@ namespace vtlb_private // third indexer -- 128 possible handlers! void* RWFT[5][2][VTLB_HANDLER_ITEMS]; - VTLBPhysical pmap[VTLB_PMAP_ITEMS]; //512KB // PS2 physical to x86 physical + VTLBPhysical pmap[VTLB_PMAP_ITEMS]; //2MB (VTLB_PMAP_ITEMS * sizeof(VTLBPhysical)) // PS2 physical to host physical VTLBVirtual* vmap; //4MB (allocated by vtlb_init) // PS2 virtual to x86 physical From ac8258a9504190b320a6f045c26968e5c9ffe20f Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 004/292] arm64: EE (R5900) recompiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full ARM64 EE dynarec — dispatcher, iFlushCall, block emitter and opcode subgroups (Arit/Branch/Jump/LoadStore/Misc/Move/MultDiv/Shift/Templates), the COP0/COP2/FPU/MMI coprocessor codegen, the register allocator core (iCore), VTLB codegen, and the EE block-analysis pass. EE GPRs are allocated in NEON registers, per the approach used in a reference ARM64 PS2 implementation. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/R5900OpcodeTables.cpp | 2 +- pcsx2/arm64/BaseblockEx-arm64.h | 212 +++ pcsx2/arm64/iCOP0-arm64.cpp | 325 ++++ pcsx2/arm64/iCOP2-arm64.cpp | 1952 ++++++++++++++++++++++++ pcsx2/arm64/iCore-arm64.cpp | 1197 +++++++++++++++ pcsx2/arm64/iCore-arm64.h | 327 ++++ pcsx2/arm64/iFPU-arm64.cpp | 924 ++++++++++++ pcsx2/arm64/iFPUd-arm64.cpp | 412 +++++ pcsx2/arm64/iMMI-arm64.cpp | 1433 ++++++++++++++++++ pcsx2/arm64/iR5900-arm64.cpp | 2013 +++++++++++++++++++++++++ pcsx2/arm64/iR5900-arm64.h | 297 ++++ pcsx2/arm64/iR5900Analysis.h | 65 + pcsx2/arm64/iR5900Arit-arm64.cpp | 456 ++++++ pcsx2/arm64/iR5900AritImm-arm64.cpp | 163 ++ pcsx2/arm64/iR5900Branch-arm64.cpp | 530 +++++++ pcsx2/arm64/iR5900Jump-arm64.cpp | 126 ++ pcsx2/arm64/iR5900LoadStore-arm64.cpp | 5 + pcsx2/arm64/iR5900Misc-arm64.cpp | 550 +++++++ pcsx2/arm64/iR5900Move-arm64.cpp | 264 ++++ pcsx2/arm64/iR5900MultDiv-arm64.cpp | 594 ++++++++ pcsx2/arm64/iR5900Shift-arm64.cpp | 427 ++++++ pcsx2/arm64/iR5900Templates-arm64.cpp | 303 ++++ pcsx2/arm64/recVTLB-arm64.cpp | 1214 +++++++++++++++ pcsx2/x86/iR5900Analysis.cpp | 4 + 24 files changed, 13794 insertions(+), 1 deletion(-) create mode 100644 pcsx2/arm64/BaseblockEx-arm64.h create mode 100644 pcsx2/arm64/iCOP0-arm64.cpp create mode 100644 pcsx2/arm64/iCOP2-arm64.cpp create mode 100644 pcsx2/arm64/iCore-arm64.cpp create mode 100644 pcsx2/arm64/iCore-arm64.h create mode 100644 pcsx2/arm64/iFPU-arm64.cpp create mode 100644 pcsx2/arm64/iFPUd-arm64.cpp create mode 100644 pcsx2/arm64/iMMI-arm64.cpp create mode 100644 pcsx2/arm64/iR5900-arm64.cpp create mode 100644 pcsx2/arm64/iR5900-arm64.h create mode 100644 pcsx2/arm64/iR5900Analysis.h create mode 100644 pcsx2/arm64/iR5900Arit-arm64.cpp create mode 100644 pcsx2/arm64/iR5900AritImm-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Branch-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Jump-arm64.cpp create mode 100644 pcsx2/arm64/iR5900LoadStore-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Misc-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Move-arm64.cpp create mode 100644 pcsx2/arm64/iR5900MultDiv-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Shift-arm64.cpp create mode 100644 pcsx2/arm64/iR5900Templates-arm64.cpp create mode 100644 pcsx2/arm64/recVTLB-arm64.cpp diff --git a/pcsx2/R5900OpcodeTables.cpp b/pcsx2/R5900OpcodeTables.cpp index 3644483b5b..b5395f6f14 100644 --- a/pcsx2/R5900OpcodeTables.cpp +++ b/pcsx2/R5900OpcodeTables.cpp @@ -25,7 +25,7 @@ namespace R5900 { // Generates an entry for the given opcode name. // Assumes the default function naming schemes for interpreter and recompiler functions. -#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added. +#if defined(_M_X86) || defined(ARCH_ARM64) // ARM64 EE recompiler is supported # define MakeOpcode( name, cycles, flags ) \ static const OPCODE name = { \ #name, \ diff --git a/pcsx2/arm64/BaseblockEx-arm64.h b/pcsx2/arm64/BaseblockEx-arm64.h new file mode 100644 index 0000000000..bca1f2f1cb --- /dev/null +++ b/pcsx2/arm64/BaseblockEx-arm64.h @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64-specific BaseBlocks with block linking that stays signal-safe. +// +// x86 BaseBlocks (pcsx2/x86/BaseblockEx.h) uses a std::multimap to track +// pending link sites and rewrites them whenever a block is added or +// removed. The multimap is not used here because Remove() runs from the +// SIGSEGV fastmem handler, where mutating an STL container is unsafe. +// +// This implementation supports block linking while keeping Remove() signal-safe: +// - Link()/New() touch the multimap, but they only run from the +// compile path (single-threaded, never from a signal). +// - Remove() does NOT walk the link map. Instead it overwrites the +// first 4 bytes of each removed block with `B JITCompile`, so any +// stale link still resolves correctly via the dispatcher (which +// can re-patch the link to the freshly compiled target on its next +// dispatch). Block memory isn't reclaimed until a full reset, so +// this 4-byte rewrite always lands on memory the recompiler still +// owns. +// +// The patch site for each link is the address of a single B instruction +// emitted by SetBranchImm (see iR5900-arm64.cpp). Aligned 32-bit stores +// are atomic on AArch64, and `cacheflush(2)` on the patch site is +// async-signal-safe, so the redirect-stub Remove() write is signal-safe. +// +// Range: B imm26 covers ±128 MB. The EE recompiler region is 64 MB +// (HostMemoryMap::EErecSize), and JITCompile lives in the same region, +// so all link sites are reachable with a single B. + +#pragma once + +#include + +#include "x86/BaseblockEx.h" // BASEBLOCK, BASEBLOCKEX, BaseBlockArray, recLUT_SetPage + +class Arm64BaseBlocks +{ +protected: + using linkmap_t = std::multimap; + + BaseBlockArray blocks; + linkmap_t links; + uptr jitcompile = 0; + + // Encode a B imm26 from `site` to `target`. ARM64 B encoding: + // bits[31:26] = 000101 + // bits[25:0] = sign-extended ((target - site) >> 2) + static u32 EncodeB(uptr site, uptr target) + { + const intptr_t off = static_cast(target) - static_cast(site); + pxAssertRel((off & 3) == 0, "Branch offset not 4-byte aligned"); + const intptr_t imm26 = off >> 2; + pxAssertRel(imm26 >= -(1 << 25) && imm26 < (1 << 25), "Branch offset out of B imm26 range"); + return 0x14000000u | (static_cast(imm26) & 0x03FFFFFFu); + } + + static void PatchAtomic(uptr site, u32 instr) + { + // 4-byte aligned word stores are atomic on AArch64. + *reinterpret_cast(site) = instr; + // Then make sure cores fetching instructions see the new word. + __builtin___clear_cache(reinterpret_cast(site), + reinterpret_cast(site) + 4); + } + +public: + Arm64BaseBlocks() + : blocks(0x4000) + { + } + + void SetJITCompile(const void* recompiler_) + { + jitcompile = reinterpret_cast(recompiler_); + } + + // Register a link site that wants to branch directly to the block at + // `pc`. Patches immediately if a block already exists; otherwise + // records the site so New(pc, ...) can patch it later. + void Link(u32 pc, void* patch_site) + { + pxAssertRel(jitcompile, "SetJITCompile() must be called before Link()"); + + BASEBLOCKEX* target = Get(pc); + const uptr target_addr = (target && target->startpc == pc) + ? target->fnptr : jitcompile; + PatchAtomic(reinterpret_cast(patch_site), + EncodeB(reinterpret_cast(patch_site), target_addr)); + + links.insert({pc, reinterpret_cast(patch_site)}); + } + + BASEBLOCKEX* New(u32 startpc, uptr fnptr) + { + // Patch any pending links waiting for a block at this PC. After + // patching they go directly to fnptr instead of routing through + // JITCompile. + const auto range = links.equal_range(startpc); + for (auto it = range.first; it != range.second; ++it) + PatchAtomic(it->second, EncodeB(it->second, fnptr)); + + return blocks.insert(startpc, fnptr); + } + + int LastIndex(u32 startpc) const + { + if (blocks.size() == 0) + return -1; + + int imin = 0, imax = (int)blocks.size() - 1, imid; + + while (imin != imax) + { + imid = (imin + imax + 1) >> 1; + + if (blocks[imid].startpc > startpc) + imax = imid - 1; + else + imin = imid; + } + + if (IsDevBuild) + { + if (imin != 0) + pxAssert(blocks[imin].startpc <= startpc); + if (imin < (int)blocks.size() - 1) + pxAssert(blocks[imin + 1].startpc > startpc); + } + + return imin; + } + + __fi int Index(u32 startpc) const + { + int idx = LastIndex(startpc); + + if ((idx == -1) || (startpc < blocks[idx].startpc) || + ((blocks[idx].size) && (startpc >= blocks[idx].startpc + blocks[idx].size * 4))) + return -1; + else + return idx; + } + + __fi BASEBLOCKEX* operator[](int idx) + { + if (idx < 0 || idx >= (int)blocks.size()) + return 0; + + return &blocks[idx]; + } + + __fi BASEBLOCKEX* Get(u32 startpc) + { + return (*this)[Index(startpc)]; + } + + // Signal-safe: writes a redirect stub at each removed block's entry + // point so any stale link still resolves through JITCompile, then + // erases from the flat sorted array. Does NOT touch the link map — + // stale entries there are harmless (they just trigger a re-patch on + // the next compile cycle for the same PC). + __fi void Remove(int first, int last) + { + pxAssert(first <= last); + + if (jitcompile) + { + for (int i = first; i <= last; ++i) + { + const uptr site = blocks[i].fnptr; + PatchAtomic(site, EncodeB(site, jitcompile)); + } + } + + blocks.erase(first, last + 1); + } + + __fi void Reset() + { + blocks.clear(); + links.clear(); + } + +#ifdef PCSX2_RECOMPILER_TESTS + // Test-only introspection. Returns true iff a link patch site within the + // block containing src_pc targets a block at dst_pc. The link multimap + // is keyed by destination PC, so the entries for dst_pc are walked to check + // whether the patch site lies inside [block.fnptr, block.fnptr + x86size), + // where x86size is BASEBLOCKEX's (legacy-named) host machine-code byte size. + // O(L_d + log B) where L_d is the number of links to dst_pc and B is the + // block count. + bool IsLinked(u32 src_pc, u32 dst_pc) const + { + const int idx = LastIndex(src_pc); + if (idx < 0) + return false; + const BASEBLOCKEX& b = blocks[idx]; + if (src_pc < b.startpc || src_pc >= b.startpc + b.size * 4) + return false; + const uptr lo = b.fnptr; + const uptr hi = b.fnptr + b.x86size; + const auto range = links.equal_range(dst_pc); + for (auto it = range.first; it != range.second; ++it) + { + if (it->second >= lo && it->second < hi) + return true; + } + return false; + } +#endif +}; diff --git a/pcsx2/arm64/iCOP0-arm64.cpp b/pcsx2/arm64/iCOP0-arm64.cpp new file mode 100644 index 0000000000..6acb488f55 --- /dev/null +++ b/pcsx2/arm64/iCOP0-arm64.cpp @@ -0,0 +1,325 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE COP0 Instruction Codegen — NEON-based + +#include "arm64/iR5900-arm64.h" +#include "arm64/AsmHelpers.h" + +#include "Hw.h" +#include "Memory.h" + +namespace a64 = vixl::aarch64; + +namespace Interp = R5900::Interpreter::OpcodeImpl::COP0; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { +namespace COP0 { + +// COP0 branch (BC0F/T/FL/TL) — native DMAC-condition test. The branch +// condition is (((DMAC_STAT | ~DMAC_PCR) & 0x3ff) == 0x3ff): BC0T branches +// when true, BC0F when false. Emitting the test inline avoids a costly +// iFlushCall + interpreter dispatch per iteration in DMAC-poll spin loops. +// BC0 uses the same SaveBranchState/recompileNextInstruction/SetBranchImm +// branch-emission pattern as recBC1F/recBC1T on this target. + +// Emit the DMAC condition test, leaving the result in the flags (CMP vs 0x3ff). +// 32-bit loads are fine: the result is masked to the low 10 bits, matching x86. +static void _setupBranchTestBC0() +{ + _eeFlushAllDirty(); + armLoadPtr(RWARG1, &psHu32(DMAC_PCR)); + armAsm->Mvn(RWARG1, RWARG1); // ~PCR + armLoadPtr(RWSCRATCH, &psHu32(DMAC_STAT)); + armAsm->Orr(RWARG1, RWARG1, RWSCRATCH); // STAT | ~PCR + armAsm->And(RWARG1, RWARG1, 0x3ff); + armAsm->Cmp(RWARG1, 0x3ff); // EQ ⇔ condition true +} + +static a64::Label* s_pBC0Label = nullptr; + +static void recSetBranchBC0(bool branchOnTrue) +{ + _setupBranchTestBC0(); + s_pBC0Label = new a64::Label(); + // Emit the "skip the taken-branch" jump: the negation of the branch cond. + if (branchOnTrue) + armAsm->B(s_pBC0Label, a64::ne); // BC0T: skip (fall through) when condition false + else + armAsm->B(s_pBC0Label, a64::eq); // BC0F: skip when condition true +} + +static void recBindBC0Label() +{ + armAsm->Bind(s_pBC0Label); + delete s_pBC0Label; + s_pBC0Label = nullptr; +} + +void recBC0F() +{ + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + const bool swap = TrySwapDelaySlot(0, 0, 0, false); + recSetBranchBC0(false); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC0Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBC0T() +{ + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + const bool swap = TrySwapDelaySlot(0, 0, 0, false); + recSetBranchBC0(true); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC0Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBC0FL() +{ + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + recSetBranchBC0(false); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC0Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +void recBC0TL() +{ + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + recSetBranchBC0(true); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC0Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +REC_FUNC(TLBR); +REC_FUNC(TLBP); +REC_FUNC(TLBWI); +REC_FUNC(TLBWR); + +REC_SYS(ERET); +REC_SYS(EI); + +// DI — inline, non-branching. Unlike EI (which must branch so newly-enabled +// interrupts fire), disabling interrupts needs no block exit, so DI is emitted +// inline and the block stays open. Mirrors x86 iCOP0.cpp. The next instruction +// is recompiled BEFORE applying DI so the interrupt-mask change takes effect one +// instruction late (fixes booting in Jak X, Namco 50th, Spongebob, The +// Incredibles, etc.). +void recDI() +{ + if (!g_recompilingDelaySlot) + recompileNextInstruction(false, false); // DI delayed by one instruction + + // Clear Status.EIE (bit 16) unless in user mode with no exception level: + // clear iff (EXL|ERL|EDI) set OR KSU == 0 (kernel). Matches Interp::DI + // (COP0.cpp:708-717) and the x86 TEST 0x20006 / TEST 0x18 guard. + a64::Label doClear; + a64::Label done; + armLoadEERegPtr(RWSCRATCH, &cpuRegs.CP0.r[12]); // Status + armAsm->Tst(RWSCRATCH, 0x20006); // EXL | ERL | EDI + armAsm->B(&doClear, a64::ne); + armAsm->Tst(RWSCRATCH, 0x18); // KSU (non-zero => user mode) + armAsm->B(&done, a64::ne); + armAsm->Bind(&doClear); + armAsm->And(RWSCRATCH, RWSCRATCH, ~static_cast(0x10000)); // clear EIE + armStoreEERegPtr(RWSCRATCH, &cpuRegs.CP0.r[12]); + armAsm->Bind(&done); +} + +#ifdef FORCE_INTERP_COP0 +REC_FUNC(MFC0); +REC_FUNC(MTC0); +#else + +// Apply pending block cycles to RECCYCLE and flush to cpuRegs.cycle so the +// interpreter helper (which reads cpuRegs.cycle directly) sees the right +// value. The helper is called inline mid-block (not via DispatcherEvent), +// so the caller must follow up with emitReloadCycle() once it returns to +// keep RECCYCLE in sync with anything the helper wrote. +static void emitFlushBlockCycles() +{ + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); +} + +static void emitReloadCycle() +{ + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); +} + +// MFC0: rt = sign_extend(COP0[rd]) +void recMFC0() +{ + // Count (rd=9) must still tick even when writing to $zero — interpreter + // MFC0 increments CP0.n.Count and updates lastCOP0Cycle before checking + // _Rt_. Match that gate exactly. + if (!_Rt_ && _Rd_ != 9) + return; + + switch (_Rd_) + { + case 9: // Count — inline cycle update; no iFlushCall/interp call + { // (matches interp COP0.cpp:564-575). + // Bring cpuRegs.cycle / RECCYCLE current first (Count reads it). + emitFlushBlockCycles(); // RECCYCLE (x25) == updated cycle, also in memory + // incr = cycle - lastCOP0Cycle; if (incr == 0) incr++; (interp :566-568) + armAsm->Ldr(RXSCRATCH, armCpuRegMem(&cpuRegs.lastCOP0Cycle)); + armAsm->Sub(RXSCRATCH, RECCYCLE, RXSCRATCH); + armAsm->Cmp(RXSCRATCH, 0); + armAsm->Csinc(RXSCRATCH, RXSCRATCH, a64::xzr, a64::ne); // 0 -> 1 + // CP0.n.Count += incr (32-bit register; low 32 of incr) + armAsm->Ldr(RWARG1, armCpuRegMem(&cpuRegs.CP0.r[9])); + armAsm->Add(RWARG1, RWARG1, RWSCRATCH); + armAsm->Str(RWARG1, armCpuRegMem(&cpuRegs.CP0.r[9])); + // lastCOP0Cycle = cycle (interp :569) + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.lastCOP0Cycle)); + if (!_Rt_) + return; + // rt = sign_extend(CP0.r[9]) (interp :571-577) + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.CP0.r[9]); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + return; + } + + case 25: // Performance counters — cycle-dependent + iFlushCall(FLUSH_INTERPRETER); + emitFlushBlockCycles(); + armEmitCall((void*)Interp::MFC0); + emitReloadCycle(); + return; + + case 24: // Debug breakpoint register — ignore + return; + + case 12: // Status — mask reserved bits, matching interp MFC0 case 12 (COP0.cpp) + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.CP0.r[_Rd_]); + // 0xf0c79c1f is not a valid AArch64 logical immediate, so materialize it + // in a scratch register. Use RWARG1 (caller-saved, dead here), not the + // reserved address scratch x17 (RSCRATCHADDR), which armLoad*Ptr clobbers. + armAsm->Mov(RWARG1, 0xf0c79c1fu); + armAsm->And(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + return; + + default: + // Simple case: rt = sign_extend(cpuRegs.CP0.r[rd]) + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.CP0.r[_Rd_]); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + return; + } +} + +// MTC0: COP0[rd] = rt +void recMTC0() +{ + switch (_Rd_) + { + case 9: // Count — inline; no iFlushCall/interp call + { // (matches interp COP0.cpp:583-585): lastCOP0Cycle = cycle; + // CP0.r[9] = rt[31:0]. Bring cycle current first. + emitFlushBlockCycles(); // RECCYCLE (x25) == updated cycle, also in memory + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.lastCOP0Cycle)); + if (GPR_IS_CONST1(_Rt_)) + { + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.CP0.r[9])); + } + else + { + _deleteEEreg(_Rt_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.CP0.r[9])); + } + return; + } + + case 12: // Status — has side effects (interrupt check) + case 16: // Config — has side effects + case 25: // Performance counters + iFlushCall(FLUSH_INTERPRETER); + emitFlushBlockCycles(); + armEmitCall((void*)Interp::MTC0); + emitReloadCycle(); + return; + + case 24: // Debug breakpoint register — log-only in interp (COP0.cpp:599-601) + return; + + default: + // Simple case: cpuRegs.CP0.r[rd] = rt[31:0] + if (GPR_IS_CONST1(_Rt_)) + { + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.CP0.r[_Rd_])); + } + else + { + _deleteEEreg(_Rt_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.CP0.r[_Rd_])); + } + return; + } +} + +#endif // !FORCE_INTERP_COP0 + +} // namespace COP0 +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp new file mode 100644 index 0000000000..422d6abe09 --- /dev/null +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -0,0 +1,1952 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 native COP2 (VU0 macro mode) codegen using NEON. +// Memory-based: loads VF regs from VU0.VF[], computes with NEON, stores back. +// MAC/status flags are updated via C helper calls for correctness. +// No VU register allocator — each instruction is self-contained. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +// ======================================================================== +// COP2 instruction field decoding (VU encoding within EE instruction) +// ======================================================================== +// VU fields reuse EE instruction bit positions: +// _Ft_ = bits 20-16 (same as _Rt_) +// _Fs_ = bits 15-11 (same as _Rd_) +// _Fd_ = bits 10-6 (same as _Sa_) +// dest = bits 24-21 (XYZW write mask) + +#define _Ft_cop2 _Rt_ +#define _Fs_cop2 _Rd_ +#define _Fd_cop2 _Sa_ + +#define _X_cop2 ((cpuRegs.code >> 24) & 0x1) +#define _Y_cop2 ((cpuRegs.code >> 23) & 0x1) +#define _Z_cop2 ((cpuRegs.code >> 22) & 0x1) +#define _W_cop2 ((cpuRegs.code >> 21) & 0x1) +#define _XYZW_cop2 ((cpuRegs.code >> 21) & 0xF) + +// Broadcast field for bc variants (bits 1-0 of function code) +#define _bc_cop2 (cpuRegs.code & 0x3) + +// Fsf/Ftf fields for scalar source selection +#define _Fsf_cop2 ((cpuRegs.code >> 21) & 0x3) +#define _Ftf_cop2 ((cpuRegs.code >> 23) & 0x3) + +// ======================================================================== +// NEON scratch register assignments for COP2 +// ======================================================================== +// q30 (RQSCRATCH) = fs operand / result +// q31 (RQSCRATCH2) = ft operand +// q29 (RQSCRATCH3) = dest mask / ACC / temp + +// ======================================================================== +// Dest field mask table — 16 entries for each XYZW combination +// ======================================================================== +// Each entry is a 128-bit mask: lane = 0xFFFFFFFF if written, 0 if not. +// XYZW is 4 bits: X=bit3, Y=bit2, Z=bit1, W=bit0 +// Lane order in NEON: [0]=x, [1]=y, [2]=z, [3]=w +alignas(16) static const u32 s_cop2DestMasks[16][4] = { + {0x00000000, 0x00000000, 0x00000000, 0x00000000}, // 0000 + {0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF}, // 000W + {0x00000000, 0x00000000, 0xFFFFFFFF, 0x00000000}, // 00Z0 + {0x00000000, 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF}, // 00ZW + {0x00000000, 0xFFFFFFFF, 0x00000000, 0x00000000}, // 0Y00 + {0x00000000, 0xFFFFFFFF, 0x00000000, 0xFFFFFFFF}, // 0Y0W + {0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, 0x00000000}, // 0YZ0 + {0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF}, // 0YZW + {0xFFFFFFFF, 0x00000000, 0x00000000, 0x00000000}, // X000 + {0xFFFFFFFF, 0x00000000, 0x00000000, 0xFFFFFFFF}, // X00W + {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}, // X0Z0 + {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF}, // X0ZW + {0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0x00000000}, // XY00 + {0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0xFFFFFFFF}, // XY0W + {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x00000000}, // XYZ0 + {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF}, // XYZW +}; + +// ======================================================================== +// VF register load/store helpers +// ======================================================================== + +// Load VU0.VF[reg] into NEON Q register +static void cop2LoadVF(const a64::VRegister& qreg, int vfReg) +{ + armAsm->Ldr(qreg, armVU0Mem(&VU0.VF[vfReg])); +} + +// Store NEON Q register to VU0.VF[reg] +static void cop2StoreVF(const a64::VRegister& qreg, int vfReg) +{ + armAsm->Str(qreg, armVU0Mem(&VU0.VF[vfReg])); +} + +// Load VU0.ACC into NEON Q register +static void cop2LoadACC(const a64::VRegister& qreg) +{ + armAsm->Ldr(qreg, armVU0Mem(&VU0.ACC)); +} + +// Store NEON Q register to VU0.ACC +static void cop2StoreACC(const a64::VRegister& qreg) +{ + armAsm->Str(qreg, armVU0Mem(&VU0.ACC)); +} + +// ======================================================================== +// Dest field masking +// ======================================================================== + +// Apply dest mask: merge 'result' in RQSCRATCH into VU0.VF[fdReg], writing +// only the lanes selected by `xyzw`. The variants without an explicit `xyzw` +// read it from the instruction (_XYZW_cop2); VOPMSUB / VOPMULA force xyzw=0xE +// since PS2 hardware always writes XYZ regardless of the encoded dest field. +static void cop2ApplyDestMaskExplicit(int fdReg, int xyzw) +{ + if (xyzw == 0xF) + { + if (fdReg != 0) + cop2StoreVF(RQSCRATCH, fdReg); + return; + } + + if (xyzw == 0) + return; + + if (fdReg == 0) + return; + + cop2LoadVF(RQSCRATCH3, fdReg); + + armMoveAddressToReg(RSCRATCHADDR, &s_cop2DestMasks[xyzw]); + armAsm->Ldr(RQSCRATCH2, a64::MemOperand(RSCRATCHADDR)); + + armAsm->Bsl(RQSCRATCH2.V16B(), RQSCRATCH.V16B(), RQSCRATCH3.V16B()); + cop2StoreVF(RQSCRATCH2, fdReg); +} + +static void cop2ApplyDestMask(int fdReg) +{ + cop2ApplyDestMaskExplicit(fdReg, _XYZW_cop2); +} + +static void cop2ApplyDestMaskACCExplicit(const a64::VRegister& result, int xyzw) +{ + if (xyzw == 0xF) + { + cop2StoreACC(result); + return; + } + + if (xyzw == 0) + return; + + if (result.GetCode() != RQSCRATCH.GetCode()) + armAsm->Mov(RQSCRATCH.V16B(), result.V16B()); + + cop2LoadACC(RQSCRATCH3); + + armMoveAddressToReg(RSCRATCHADDR, &s_cop2DestMasks[xyzw]); + armAsm->Ldr(RQSCRATCH2, a64::MemOperand(RSCRATCHADDR)); + + armAsm->Bsl(RQSCRATCH2.V16B(), RQSCRATCH.V16B(), RQSCRATCH3.V16B()); + cop2StoreACC(RQSCRATCH2); +} + +static void cop2ApplyDestMaskACC(const a64::VRegister& result) +{ + cop2ApplyDestMaskACCExplicit(result, _XYZW_cop2); +} + +// NOTE: MAC/status flag updates are deferred — VU0.macflag/statusflag are not +// updated here. Most games don't read COP2 flags. When flag support is needed, +// emit a C call to update flags per-instruction. The interpreter fallback ops +// (DIV, CLIP, etc.) still update flags correctly. + +// COP2 accesses VU0 memory, not cpuRegs GPRs — no EE register flush needed. + +// ======================================================================== +// PS2 VU float clamping +// ======================================================================== +// PS2 VU has no infinities — overflow clamps to ±FLT_MAX (0x7f7fffff). +// NEON FPCR has FZ=1 (denormals flushed to zero), so only post-op clamping is needed. +// FMINNM/FMAXNM match x86 MINPS/MAXPS semantics: NaN → non-NaN operand. + +alignas(16) static const u32 s_cop2MaxFloat[4] = {0x7f7fffff, 0x7f7fffff, 0x7f7fffff, 0x7f7fffff}; + +// VCLIP positive per-lane clip-bit weights ([+x@bit0, +y@bit2, +z@bit4]; lane w +// unused). The negative weights ([-x@bit1, -y@bit3, -z@bit5]) are these << 1, so +// only one constant is needed. After Cmgt the positive/negative masks are +// weighted per lane and a horizontal Addv collapses them into the 6-bit field +// (the +/- bits per axis are mutually exclusive and the lane contributions +// occupy disjoint bit ranges, so the add never carries between bits). +alignas(16) static const u32 s_cop2ClipWeightPos[4] = {0x01, 0x04, 0x10, 0x00}; + +// Clamp RQSCRATCH to [-FLT_MAX, +FLT_MAX] (removes infinities and NaNs) +// FMINNM/FMAXNM match x86 MINPS/MAXPS semantics: NaN → non-NaN operand. +static void cop2ClampResult() +{ + armMoveAddressToReg(RSCRATCHADDR, &s_cop2MaxFloat); + armAsm->Ldr(RQSCRATCH2, a64::MemOperand(RSCRATCHADDR)); + armAsm->Fneg(RQSCRATCH3.V4S(), RQSCRATCH2.V4S()); // -FLT_MAX + + armAsm->Fminnm(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); // clamp to +FLT_MAX + armAsm->Fmaxnm(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); // clamp to -FLT_MAX +} + +// Clamp an arbitrary operand register `qreg` to [-FLT_MAX, +FLT_MAX], using +// tmpHi/tmpLo to hold the ±FLT_MAX bounds. Input-operand variant of +// cop2ClampResult — used to pre-clamp an FMAC operand before the arithmetic +// (matching x86 mVU's cFs/cFt operand clamps) rather than clamping the result. +static void cop2ClampReg(const a64::VRegister& qreg, + const a64::VRegister& tmpHi, const a64::VRegister& tmpLo) +{ + armMoveAddressToReg(RSCRATCHADDR, &s_cop2MaxFloat); + armAsm->Ldr(tmpHi, a64::MemOperand(RSCRATCHADDR)); + armAsm->Fneg(tmpLo.V4S(), tmpHi.V4S()); + armAsm->Fminnm(qreg.V4S(), qreg.V4S(), tmpHi.V4S()); + armAsm->Fmaxnm(qreg.V4S(), qreg.V4S(), tmpLo.V4S()); +} + +// Single-temp variant of cop2ClampReg: clamps `qreg` to [-FLT_MAX, +FLT_MAX] +// using just one scratch register (it negates the +FLT_MAX bound in place +// between the two clamps). Needed when pre-clamping a broadcast FMAC operand, +// where Fs/Ft already occupy two of the three q-scratch regs and only one is +// free. +static void cop2ClampRegOneTmp(const a64::VRegister& qreg, const a64::VRegister& tmp) +{ + armMoveAddressToReg(RSCRATCHADDR, &s_cop2MaxFloat); + armAsm->Ldr(tmp, a64::MemOperand(RSCRATCHADDR)); + armAsm->Fminnm(qreg.V4S(), qreg.V4S(), tmp.V4S()); // clamp to +FLT_MAX + armAsm->Fneg(tmp.V4S(), tmp.V4S()); // -FLT_MAX + armAsm->Fmaxnm(qreg.V4S(), qreg.V4S(), tmp.V4S()); // clamp to -FLT_MAX +} + +// Pre-clamp the broadcast-MUL operands per mVU_FMACa: (_XYZW_PS)?(cFs|cFt):cFs. +// cFs (clamp Fs) is applied on every mask; cFt (clamp the broadcast Ft) only +// when all four lanes are active. Fs is in RQSCRATCH, the already-broadcast Ft +// in RQSCRATCH2, RQSCRATCH3 is the scratch. This catches operand overflow before +// it propagates through the multiply (TOTA, Disgaea, Ice Age on VU0), instead of +// only clamping the product afterward. +static void cop2EmitMulInputClamp() +{ + cop2ClampRegOneTmp(RQSCRATCH, RQSCRATCH3); // cFs (every mask) + if (_XYZW_cop2 == 0xf) + cop2ClampRegOneTmp(RQSCRATCH2, RQSCRATCH3); // cFt (full mask only) +} + +// ======================================================================== +// PS2 VU integer-comparison MAX/MINI +// ======================================================================== +// PS2 VMAX/VMINI use signed integer comparison on float bit patterns, +// NOT IEEE FMAX/FMIN. This handles NaN and negative values correctly: +// fp_max(a,b) = both_neg ? min_s32(a,b) : max_s32(a,b) +// Implemented as: selection = CMGT(a,b) XOR both_neg_mask, then BSL. +// +// Expects: RQSCRATCH = a (from VF[fs]), RQSCRATCH2 = b (from VF[ft] or broadcast) +// Result: RQSCRATCH = fp_max(a, b) or fp_min(a, b) +// Clobbers: RQSCRATCH, RQSCRATCH2 preserved, RQSCRATCH3 used as scratch. + +static void cop2EmitIntegerMax(int fsReg) +{ + // q30=a, q31=b, q29=scratch + armAsm->And(RQSCRATCH3.V16B(), RQSCRATCH.V16B(), RQSCRATCH2.V16B()); // both_neg test + armAsm->Sshr(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), 31); // broadcast sign → mask + armAsm->Cmgt(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); // a > b (signed int) + armAsm->Eor(RQSCRATCH.V16B(), RQSCRATCH.V16B(), RQSCRATCH3.V16B()); // selection = CMGT XOR both_neg + cop2LoadVF(RQSCRATCH3, fsReg); // reload a into q29 + armAsm->Bsl(RQSCRATCH.V16B(), RQSCRATCH3.V16B(), RQSCRATCH2.V16B()); // sel ? a : b +} + +static void cop2EmitIntegerMin(int fsReg) +{ + // Same as max but BSL operands swapped: sel ? b : a + armAsm->And(RQSCRATCH3.V16B(), RQSCRATCH.V16B(), RQSCRATCH2.V16B()); + armAsm->Sshr(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), 31); + armAsm->Cmgt(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + armAsm->Eor(RQSCRATCH.V16B(), RQSCRATCH.V16B(), RQSCRATCH3.V16B()); + cop2LoadVF(RQSCRATCH3, fsReg); + armAsm->Bsl(RQSCRATCH.V16B(), RQSCRATCH2.V16B(), RQSCRATCH3.V16B()); // sel ? b : a +} + +// ======================================================================== +// MAC/Status flag update infrastructure +// ======================================================================== +// Implements mVUupdateFlags + mVUallocSFLAGc/d semantics. +// The status flag is stored in a "denormalized" format during macro mode: +// Bits 0-3: Zero sticky per lane (ZS) +// Bits 4-7: Sign sticky per lane (SS) +// Bits 8-11: Zero current per lane (Z) +// Bits 12-15: Sign current per lane (S) +// Bits 16+: D/I/O/U flags (from divide ops) +// +// The "normalized" format in VU0.VI[REG_STATUS_FLAG] has: +// Bit 0: Z (any current zero), Bit 1: S (any current sign) +// Bit 6: ZS (any sticky zero), Bit 7: SS (any sticky sign) +// Bits 2-5,8+: D/I/O/U flags + +// Runtime storage for denormalized status flag during macro op. Plain static +// (not thread_local): COP2/VU0 macro mode runs only on the EE thread (VU0 is +// lockstep with the EE; MTVU offloads VU1 only), and the JIT bakes this address +// in at emit time — a fixed global address is correct and avoids materializing a +// thread-local slot that only ever has one instance. +static u32 s_cop2DenormStatusFlag; + +// Emit code to denormalize status flag from VU0.VI[REG_STATUS_FLAG] +// into s_cop2DenormStatusFlag (mVUallocSFLAGd). +// Denormalized = ((norm >> 3) & 0x18) | ((norm << 11) & 0x1800) | ((norm << 14) & 0x3cf0000) +static void cop2EmitDenormalizeStatusFlag() +{ + // Load normalized status flag + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); + + // tmp2 = norm + const a64::Register tmp1 = a64::w1; + const a64::Register tmp2 = a64::w2; + armAsm->Mov(tmp2, RWSCRATCH); + + // reg = (norm >> 3) & 0x18 + armAsm->Lsr(RWSCRATCH, tmp2, 3); + armAsm->And(RWSCRATCH, RWSCRATCH, 0x18); + + // tmp1 = (norm << 11) & 0x1800 + armAsm->Lsl(tmp1, tmp2, 11); + armAsm->And(tmp1, tmp1, 0x1800); + armAsm->Orr(RWSCRATCH, RWSCRATCH, tmp1); + + // tmp2 = (norm << 14) & 0x3cf0000 + armAsm->Lsl(tmp2, tmp2, 14); + armAsm->Mov(a64::w3, 0x3cf0000); + armAsm->And(tmp2, tmp2, a64::w3); + armAsm->Orr(RWSCRATCH, RWSCRATCH, tmp2); + + // Store denormalized flag + armMoveAddressToReg(RSCRATCHADDR, &s_cop2DenormStatusFlag); + armAsm->Str(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); +} + +// Emit code to normalize status flag from s_cop2DenormStatusFlag +// back to VU0.VI[REG_STATUS_FLAG] (mVUallocSFLAGc). +static void cop2EmitNormalizeStatusFlag() +{ + // Load denormalized flag + armMoveAddressToReg(RSCRATCHADDR, &s_cop2DenormStatusFlag); + armAsm->Ldr(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + const a64::Register result = a64::w1; + armAsm->Mov(result, a64::wzr); // result = 0 + + // Z bit (norm bit 0): set if any of denorm bits 8-11 + armAsm->Tst(RWSCRATCH, 0x0f00); + armAsm->Cset(a64::w2, a64::ne); + armAsm->Orr(result, result, a64::w2); // bit 0 + + // S bit (norm bit 1): set if any of denorm bits 12-15 + armAsm->Tst(RWSCRATCH, 0xf000); + armAsm->Cset(a64::w2, a64::ne); + armAsm->Orr(result, result, a64::Operand(a64::w2, a64::LSL, 1)); // bit 1 + + // 'result' now holds the current Z (bit0) / S (bit1). Sticky bits are not + // derived from a separate denorm range — they accumulate from the current + // Z/S below (preserve old sticky, OR current into both current and sticky + // positions), matching the interpreter's statusflag-shift behavior. + // + // VI[STATUS] = (VI[STATUS] & 0xFC0) | (Z/S) | ((Z/S) << 6) + // + // This MUST mirror the interpreter's COP2 macro oracle SYNCMSFLAGS() + // (VUops.cpp): preserve 0xFC0 — NOT 0xFF0; the 0xFF0 mask belongs to the + // FMAC-pipeline-flush path, not the COP2 macro path — then write the low + // nibble into bits 0-3 and shifted into the sticky bits 6-9. + // + // LIMITATION: cop2EmitFlagUpdate() computes only Z (Fcmeq) and S (Cmlt) here; + // it never sets the U (underflow, exp==0) or O (overflow, exp==255) bits that + // interp's VU_STAT_UPDATE (VUflags.cpp) can produce. So 'result' only ever + // holds bits 0-1, and masking it with 0x3 is exact. DO NOT widen the 0xFC0 + // preserve to 0xFF0 (or the 0x3 result-mask to 0xF) without first teaching + // cop2EmitFlagUpdate to compute U/O — a bare widen keeps stale bits the + // interpreter clears and regresses EeVu0Cop2Macro.VaddXyzwSumsLanes. No game + // in the corpus reads U/O after a COP2 macro FMAC, so this gap is latent. + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); + armAsm->And(RWSCRATCH, RWSCRATCH, 0xFC0); // preserve existing sticky (bits 6-11) + armAsm->And(result, result, 0x3); // keep only Z/S (bits 0-1) + armAsm->Orr(RWSCRATCH, RWSCRATCH, result); // OR in current Z/S + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::Operand(result, a64::LSL, 6)); // OR shifted into sticky + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); +} + +// Emit code to update MAC and status flags from the result in RQSCRATCH. +// Implements mVUupdateFlags behavior. +// xyzw = dest field mask (which lanes were written). +// Uses RQSCRATCH2, RQSCRATCH3 as temporaries. +static void cop2EmitFlagUpdate(int xyzw) +{ + // Flags are updated unconditionally for correctness; no liveness-based + // skip is applied here. + + if (xyzw == 0) return; + + // Save result — flag extraction clobbers NEON scratch registers + // Use q28 to preserve the result while extracting flags from it + a64::VRegister savedResult = a64::VRegister(28, 128); + armAsm->Mov(savedResult.V16B(), RQSCRATCH.V16B()); + + // --- Extract sign bits from result --- + // CMLT produces all-1s per lane if negative. armEmitPackLaneBits expects + // all-1s/0 lanes (no Ushr needed) — AND-with-weights gives back the weight + // when the lane is set and 0 otherwise. + armAsm->Cmlt(RQSCRATCH2.V4S(), savedResult.V4S(), 0); + + // --- Extract zero bits --- + // FCMEQ produces all-1s per lane if == 0.0 + armAsm->Fcmeq(RQSCRATCH3.V4S(), savedResult.V4S(), 0); + + // --- Pack 4 lane bits into GPR in PS2 MAC flag order --- + // PS2 MAC flag: bit0=W, bit1=Z, bit2=Y, bit3=X (reverse of NEON lane order + // [0]=x, [1]=y, [2]=z, [3]=w). reverse=true picks weight vector {8,4,2,1}. + // RQSCRATCH (q30) is free here — savedResult lives in q28. + const a64::Register signBits = a64::w1; + const a64::Register zeroBits = a64::w2; + armEmitPackLaneBits(signBits, RQSCRATCH2, RQSCRATCH, /*reverse=*/true); + armEmitPackLaneBits(zeroBits, RQSCRATCH3, RQSCRATCH, /*reverse=*/true); + + // --- Apply XYZW dest mask --- + // _XYZW_cop2 = X(bit3) Y(bit2) Z(bit1) W(bit0) — matches PS2 MAC order + // Lanes not in dest mask should have their flag bits cleared. + armAsm->And(signBits, signBits, xyzw); + armAsm->And(zeroBits, zeroBits, xyzw); + + // --- Build MAC flag: (sign << 4) | zero --- + const a64::Register macFlag = a64::w3; + armAsm->Lsl(macFlag, signBits, 4); + armAsm->Orr(macFlag, macFlag, zeroBits); + + // --- Write MAC flag to VU0.VI[REG_MAC_FLAG] --- + armAsm->Str(macFlag, armVU0Mem(&VU0.VI[REG_MAC_FLAG])); + + // --- Update denormalized status flag --- + // Load current denorm flag + armMoveAddressToReg(RSCRATCHADDR, &s_cop2DenormStatusFlag); + armAsm->Ldr(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + // Clear current (non-sticky) bits 8-15 + armAsm->Mov(a64::w4, 0xFF00); + armAsm->Bic(RWSCRATCH, RWSCRATCH, a64::w4); + + // OR macFlag into sticky bits (0-7) — accumulates over time + armAsm->Orr(RWSCRATCH, RWSCRATCH, macFlag); + + // OR (macFlag << 8) into current bits (8-15) — this instruction's result + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::Operand(macFlag, a64::LSL, 8)); + + // Store back. RSCRATCHADDR still holds &s_cop2DenormStatusFlag from the load + // above (none of the Mov/Bic/Orr between touch it), so no reload is needed. + armAsm->Str(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + // Restore result to RQSCRATCH for subsequent cop2ApplyDestMask + armAsm->Mov(RQSCRATCH.V16B(), savedResult.V16B()); +} + +// ======================================================================== +// COP2 Macro Mode Setup/Teardown +// ======================================================================== +// ARM64 setupMacroOp/endMacroOp (see microVU_Macro.inl for the x86 version). +// Mode flags: 0x01=read Q, 0x02=write Q, 0x10=update status/MAC flags. + +// cop2EmitConditionalSync is declared in iR5900-arm64.h (callable from +// recVTLB-arm64.cpp for LQC2/SQC2); definition is later in this file. + +void setupMacroOp_arm64(int mode) +{ + // VU0 sync is gated on EEINST analysis (EEINST_COP2_SYNC_VU0 / FINISH_VU0). + // In the common case where the analysis says no sync is needed, this emits + // zero instructions (per-op recXXX gates sync via COP2_Interlock / + // mVUSyncVU0 / mVUFinishVU0). + cop2EmitConditionalSync(false, _vu0FinishMicro); + + if (mode & 0x10) // Status/MAC flags will be updated + { + // Always denormalize the status flag; no liveness-based skip is applied. + cop2EmitDenormalizeStatusFlag(); + } + + if (mode & 0x01) // Q register will be read — load into RQSCRATCH3 + { + // Q is loaded per-instruction by the Q-variant ops (ADDq etc.) + // No global load needed here — the Q-variant ops load Q inline. + } + + // microVU0 state setup so mVU-reuse wrappers (REC_COP2_mVU0_ARM64) can + // drive mVU_LQI/SQI/MFIR/MTIR/... directly from macro-mode dispatch. + // Hand-rolled arithmetic ops (recCOP2_VADDx etc.) don't read this state, + // so unconditional setup is a cheap no-op cost for them. + mVUmacroSetupCOP2State(mode, g_pCurInstInfo ? g_pCurInstInfo->info : 0u); +} + +void endMacroOp_arm64(int mode) +{ + if (mode & 0x02) // Q register was written + { + // DIV/SQRT/RSQRT write Q inline — no global store needed here. + } + + if (mode & 0x10) // Status/MAC flags were updated + { + // Always normalize status flag back to VU0.VI[REG_STATUS_FLAG]. + // Each COP2 macro instruction is self-contained, so the normalized + // flag must be written every time. The vuFlagHack optimization + // (skipping normalization when no one reads the flag) requires + // correct denormalized flag persistence across instructions, + // which is not yet supported. + cop2EmitNormalizeStatusFlag(); + } + + // microVU0 state teardown — flushPartialForCOP2 + cop2=0 + regAlloc reset. + mVUmacroEndCOP2State(); +} + +// Macro for COP2 arithmetic ops that go through the setup/teardown pipeline. +// opFunc emits the actual NEON arithmetic + flag update. +#define REC_COP2_ARM64(f, mode) \ + void recCOP2_V##f() \ + { \ + setupMacroOp_arm64(mode); \ + cop2Op_##f(); \ + endMacroOp_arm64(mode); \ + } + +// ======================================================================== +// COP2 Transfer ops: QMFC2, QMTC2, CFC2, CTC2 +// ======================================================================== +// These move data between EE GPRs and VU0 registers. +// VU0 sync is conditional on VU0 actually running (VPU_STAT bit 0). +// Sync is skipped in the common case where VU0 micro isn't executing. + +extern void vu0Sync(); +extern void _vu0FinishMicro(); +extern void _vu0WaitMicro(); + +// Emit conditional VU0 sync: uses EEINST analysis flags when available, +// falls back to runtime VPU_STAT check otherwise. +// Implements the COP2_Interlock + mVUSyncVU0/mVUFinishVU0 sync protocol. +void cop2EmitConditionalSync(bool interlock, void (*finishFunc)()) +{ + // Handle interlock (bit 0 set): COP2_Interlock pattern + if (interlock) + { + // Interlock requires sync — check if analysis says VU0 could be running + if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) + { + // Lighter flush than FLUSH_EVERYTHING: FLUSH_FREE_XMM | FLUSH_FREE_VU0 + // skips callee-saved EE-GPR writebacks (callee-saved survives the C + // call) while still evicting caller-saved GPR + all NEON. + iFlushCall(FLUSH_FREE_XMM | FLUSH_FREE_VU0); + + // Apply block cycles to RECCYCLE (the pinned cpuRegs.cycle). + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + // Runtime: skip if VU0 not running + a64::Label skipSync; + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_VPU_STAT])); + armAsm->Tbz(RWSCRATCH, 0, &skipSync); + + // Flush RECCYCLE before vu0Sync — it reads cpuRegs.cycle to + // determine how many VU0 micro cycles to run. Reload after, + // since vu0Sync may advance cpuRegs.cycle. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armEmitCall((void*)vu0Sync); + if (finishFunc) + armEmitCall((void*)finishFunc); + + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&skipSync); + } + // else: analysis says no VU0 program between COP2 ops, safe to skip + return; + } + + // Non-interlock: check analysis flags for sync/finish + const bool needsSync = (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) != 0; + const bool needsFinish = (g_pCurInstInfo->info & EEINST_COP2_FINISH_VU0) != 0; + + if (!needsSync && !needsFinish) + return; // Analysis says no sync needed + + // Lighter flush — see interlock branch above for rationale. + iFlushCall(FLUSH_FREE_XMM | FLUSH_FREE_VU0); + + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + // Runtime: skip if VU0 not running + a64::Label skipSync; + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_VPU_STAT])); + armAsm->Tbz(RWSCRATCH, 0, &skipSync); + + // Flush + reload around the C call (see comment above). + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + if (needsSync) + armEmitCall((void*)vu0Sync); + else + armEmitCall((void*)_vu0FinishMicro); + + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&skipSync); +} + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +// QMFC2: cpuRegs.GPR[rt] = VU0.VF[fs] (128-bit copy, VF → EE GPR) +void recCOP2_QMFC2() +{ + iFlushCall(FLUSH_EVERYTHING); + cop2EmitConditionalSync(cpuRegs.code & 1, _vu0FinishMicro); + + if (_Rt_ == 0) return; + GPR_DEL_CONST(_Rt_); + + // 128-bit copy: VU0.VF[fs] → cpuRegs.GPR.r[rt] + armAsm->Ldr(RQSCRATCH, armVU0Mem(&VU0.VF[_Rd_])); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_])); +} + +// QMTC2: VU0.VF[fs] = cpuRegs.GPR[rt] (128-bit copy, EE GPR → VF) +void recCOP2_QMTC2() +{ + iFlushCall(FLUSH_EVERYTHING); + cop2EmitConditionalSync(cpuRegs.code & 1, _vu0WaitMicro); + + if (_Rd_ == 0) return; // VF[0] is read-only + + // 128-bit copy: cpuRegs.GPR.r[rt] → VU0.VF[fs] + if (GPR_IS_CONST1(_Rt_)) + { + armMoveAddressToReg(RSCRATCHADDR, &g_cpuConstRegs[_Rt_]); + armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); + } + else + { + armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_])); + } + armAsm->Str(RQSCRATCH, armVU0Mem(&VU0.VF[_Rd_])); +} + +// CFC2: cpuRegs.GPR[rt] = sign_extend_32_to_64(VU0.VI[fs]) +void recCOP2_CFC2() +{ + iFlushCall(FLUSH_EVERYTHING); + cop2EmitConditionalSync(cpuRegs.code & 1, _vu0FinishMicro); + + if (_Rt_ == 0) return; + GPR_DEL_CONST(_Rt_); + + if (_Rd_ == REG_R) + { + // REG_R: mask to 23 bits, write only UL[0] + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_R])); + armAsm->And(RWSCRATCH, RWSCRATCH, 0x7FFFFF); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[0])); + } + else + { + // General VI: load 32-bit, sign-extend to UL[0]+UL[1] + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[_Rd_])); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[0])); + // Sign-extend: UL[1] = (UL[0] & 0x80000000) ? 0xFFFFFFFF : 0 + armAsm->Asr(RWSCRATCH, RWSCRATCH, 31); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[1])); + } +} + +// CTC2: cpuRegs.GPR[rt] → VU0.VI[fs] (with special-case registers) +// _Fs_ is known at compile time, so dispatch happens at compile time. +// FBRST and CMSAR1 fall back to interpreter (complex side effects). +// CTC2() is in global namespace (VU0.cpp), referenced via ::CTC2. + +void recCOP2_CTC2() +{ + const int fs = _Rd_; // _Fs_ in VU encoding = _Rd_ in EE encoding + + if (fs == 0) return; // VI[0] is read-only + + // Read-only registers — no-op + if (fs == REG_MAC_FLAG || fs == REG_TPC || fs == REG_VPU_STAT) + return; + + // FBRST and CMSAR1 have complex side effects — use interpreter + if (fs == REG_FBRST || fs == REG_CMSAR1) + { + recCall(::CTC2); + return; + } + + // For all other cases: flush + conditional sync, then inline write + iFlushCall(FLUSH_EVERYTHING); + cop2EmitConditionalSync(cpuRegs.code & 1, _vu0WaitMicro); + + // Load source value from cpuRegs.GPR[rt].UL[0] + if (GPR_IS_CONST1(_Rt_)) + { + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + } + else + { + armAsm->Ldr(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[0])); + } + + if (fs == REG_R) + { + // REG_R: (value & 0x7FFFFF) | 0x3F800000 + armAsm->And(RWSCRATCH, RWSCRATCH, 0x7FFFFF); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x3F800000); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[REG_R])); + } + else if (fs == REG_CLIP_FLAG) + { + // REG_CLIP_FLAG: write to both clipflag and VI + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.clipflag)); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[REG_CLIP_FLAG])); + } + else if (fs == REG_STATUS_FLAG) + { + // STATUS_FLAG: take only the 0xFC0 field from the GPR, preserve the + // low-6 sticky bits in VI[STATUS], then denormalize the result + // (mVUallocSFLAGd) and broadcast it into all four lanes of + // micro_statusflags — microVU reads that array for flag sync, so a raw + // 32-bit overwrite of VI[STATUS] alone leaves it stale and corrupts VU + // flag state. RWSCRATCH = GPR[_Rt_].UL[0] here (== 0 for _Rt_==0, so + // the RMW degrades to STATUS &= 0x3F). + armAsm->And(RWSCRATCH, RWSCRATCH, 0xFC0); // masked field from GPR + + armAsm->Ldr(RWARG2, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); + armAsm->And(RWARG2, RWARG2, 0x3F); // preserve sticky bits 0-5 + armAsm->Orr(RWARG2, RWARG2, RWSCRATCH); // RWARG2 = new normalized STATUS + armAsm->Str(RWARG2, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); + + // Denormalize the new STATUS (in RWARG2) into RWSCRATCH: + // denorm = ((s>>3)&0x18) | ((s<<11)&0x1800) | ((s<<14)&0x3cf0000) + armAsm->Lsr(RWSCRATCH, RWARG2, 3); + armAsm->And(RWSCRATCH, RWSCRATCH, 0x18); + armAsm->Lsl(a64::w2, RWARG2, 11); + armAsm->And(a64::w2, a64::w2, 0x1800); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::w2); + armAsm->Lsl(a64::w3, RWARG2, 14); + armAsm->Mov(a64::w4, 0x3cf0000); // not a valid logical-imm; materialize + armAsm->And(a64::w3, a64::w3, a64::w4); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::w3); + + // Broadcast the denormalized value into all 4 lanes of micro_statusflags. + armAsm->Dup(RQSCRATCH.V4S(), RWSCRATCH); + armAsm->Str(RQSCRATCH, armVU0Mem(&VU0.micro_statusflags)); + } + else + { + // Default: write 32-bit value to VI register + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[fs])); + } +} + +// ======================================================================== +// COP2 Integer ops: IADD, ISUB, IADDI, IAND, IOR +// ======================================================================== +// 16-bit VI register operations. VU field encoding: +// _Id_ = _Sa_ & 0xF (destination VI), _Is_ = _Rd_ & 0xF, _It_ = _Rt_ & 0xF + +#define _Id_cop2 (_Sa_ & 0xF) +#define _Is_cop2 (_Rd_ & 0xF) +#define _It_cop2 (_Rt_ & 0xF) + +// IADD: VI[id] = VI[is] + VI[it] +void recCOP2_VIADD() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Id_cop2 == 0) return; + + armAsm->Ldrsh(RWSCRATCH, armVU0Mem(&VU0.VI[_Is_cop2])); + armAsm->Ldrsh(RWARG2, armVU0Mem(&VU0.VI[_It_cop2])); + armAsm->Add(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[_Id_cop2])); +} + +// ISUB: VI[id] = VI[is] - VI[it] +void recCOP2_VISUB() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Id_cop2 == 0) return; + + armAsm->Ldrsh(RWSCRATCH, armVU0Mem(&VU0.VI[_Is_cop2])); + armAsm->Ldrsh(RWARG2, armVU0Mem(&VU0.VI[_It_cop2])); + armAsm->Sub(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[_Id_cop2])); +} + +// IADDI: VI[it] = VI[is] + sign_ext_5bit_imm +void recCOP2_VIADDI() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_It_cop2 == 0) return; + + // 5-bit immediate at bits 10-6, sign-extended + s16 imm = ((_Sa_ & 0x1F)); + imm = ((imm & 0x10) ? (s16)(0xFFF0 | imm) : imm); + + armAsm->Ldrsh(RWSCRATCH, armVU0Mem(&VU0.VI[_Is_cop2])); + armAsm->Add(RWSCRATCH, RWSCRATCH, imm); + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[_It_cop2])); +} + +// IAND: VI[id] = VI[is] & VI[it] +void recCOP2_VIAND() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Id_cop2 == 0) return; + + armAsm->Ldrh(RWSCRATCH, armVU0Mem(&VU0.VI[_Is_cop2])); + armAsm->Ldrh(RWARG2, armVU0Mem(&VU0.VI[_It_cop2])); + armAsm->And(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[_Id_cop2])); +} + +// IOR: VI[id] = VI[is] | VI[it] +void recCOP2_VIOR() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Id_cop2 == 0) return; + + armAsm->Ldrh(RWSCRATCH, armVU0Mem(&VU0.VI[_Is_cop2])); + armAsm->Ldrh(RWARG2, armVU0Mem(&VU0.VI[_It_cop2])); + armAsm->Orr(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[_Id_cop2])); +} + +// ======================================================================== +// SIMPLE template: VMOVE, VMR32, VNOP, VWAITQ, VABS +// ======================================================================== + +// VMOVE: VF[ft] = VF[fs] (masked by dest) +void recCOP2_VMOVE() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; // VF0 is read-only + + const int xyzw = _XYZW_cop2; + if (xyzw == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2ApplyDestMask(_Ft_cop2); +} + +// VMR32: rotate VF[fs] lanes right by one, store to VF[ft] (masked) +// x=y, y=z, z=w, w=x (rotate left in element order) +void recCOP2_VMR32() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + const int xyzw = _XYZW_cop2; + if (xyzw == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + // EXT rotates: target lane order is [y,z,w,x] from [x,y,z,w] + // That's a left rotation by 1 lane = EXT #4 (4 bytes) + armAsm->Ext(RQSCRATCH.V16B(), RQSCRATCH.V16B(), RQSCRATCH.V16B(), 4); + cop2ApplyDestMask(_Ft_cop2); +} + +// VNOP: no operation +void recCOP2_VNOP() +{ +} + +// VWAITQ: wait for Q register (no-op in macro mode) +void recCOP2_VWAITQ() +{ +} + +// VABS: VF[ft] = abs(VF[fs]) (masked) +void recCOP2_VABS() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + const int xyzw = _XYZW_cop2; + if (xyzw == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armAsm->Fabs(RQSCRATCH.V4S(), RQSCRATCH.V4S()); + cop2ApplyDestMask(_Ft_cop2); +} + +// ======================================================================== +// VEC_ARITH template: VADD, VSUB, VMUL +// Pattern: VF[fd] = VF[fs] OP VF[ft] (masked by dest) +// ======================================================================== + +void recCOP2_VADD() +{ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + armAsm->Fadd(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + cop2ClampResult(); + cop2EmitFlagUpdate(_XYZW_cop2); + cop2ApplyDestMask(_Fd_cop2); + + endMacroOp_arm64(0x110); +} + +void recCOP2_VSUB() +{ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + armAsm->Fsub(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + cop2ClampResult(); + cop2EmitFlagUpdate(_XYZW_cop2); + cop2ApplyDestMask(_Fd_cop2); + + endMacroOp_arm64(0x110); +} + +void recCOP2_VMUL() +{ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + cop2ClampResult(); + cop2EmitFlagUpdate(_XYZW_cop2); + cop2ApplyDestMask(_Fd_cop2); + + endMacroOp_arm64(0x110); +} + +void recCOP2_VMAX() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Fd_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + cop2EmitIntegerMax(_Fs_cop2); + cop2ApplyDestMask(_Fd_cop2); +} + +void recCOP2_VMINI() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Fd_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + cop2EmitIntegerMin(_Fs_cop2); + cop2ApplyDestMask(_Fd_cop2); +} + +// ======================================================================== +// Broadcast helpers for _BC variants +// ======================================================================== + +// Load VF[ft] and broadcast lane 'bc' (0=x, 1=y, 2=z, 3=w) to all lanes +static void cop2LoadBroadcast(const a64::VRegister& qreg, int vfReg, int bc) +{ + cop2LoadVF(qreg, vfReg); + armAsm->Dup(qreg.V4S(), qreg.V4S(), bc); +} + +// ======================================================================== +// ADD_BC / SUB_BC / MUL_BC template +// Pattern: VF[fd] = VF[fs] OP VF[ft].bc (broadcast one lane) +// ======================================================================== + +// Helper macro for broadcast binary ops (with input/output clamping + flags). +// mulClamp=true pre-clamps the FMAC operands per mVU_MULx cFs/cFt (MUL family); +// ADD/SUB pass false (ADD clampType=0; SUB's input clamp is a separate concern). +#define COP2_BC_OP(name, neonOp, bc, mulClamp) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + if (mulClamp) cop2EmitMulInputClamp(); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x110); \ + } + +// ADDx/y/z/w +COP2_BC_OP(ADDx, Fadd, 0, false) +COP2_BC_OP(ADDy, Fadd, 1, false) +COP2_BC_OP(ADDz, Fadd, 2, false) +COP2_BC_OP(ADDw, Fadd, 3, false) + +// SUBx/y/z/w +COP2_BC_OP(SUBx, Fsub, 0, false) +COP2_BC_OP(SUBy, Fsub, 1, false) +COP2_BC_OP(SUBz, Fsub, 2, false) +COP2_BC_OP(SUBw, Fsub, 3, false) + +// MULx/y/z/w — pre-clamp Fs (and Ft on full mask) per mVU_MULx cFs/cFt spec +COP2_BC_OP(MULx, Fmul, 0, true) +COP2_BC_OP(MULy, Fmul, 1, true) +COP2_BC_OP(MULz, Fmul, 2, true) +COP2_BC_OP(MULw, Fmul, 3, true) + +// MAXx/y/z/w — PS2 integer comparison, not IEEE FMAX +#define COP2_BC_MAX(name, bc) \ + void recCOP2_V##name() \ + { \ + cop2EmitConditionalSync(false, _vu0FinishMicro); \ + if (_Fd_cop2 == 0) return; \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + cop2EmitIntegerMax(_Fs_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + } + +// MINIx/y/z/w — PS2 integer comparison, not IEEE FMIN +#define COP2_BC_MINI(name, bc) \ + void recCOP2_V##name() \ + { \ + cop2EmitConditionalSync(false, _vu0FinishMicro); \ + if (_Fd_cop2 == 0) return; \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + cop2EmitIntegerMin(_Fs_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + } + +COP2_BC_MAX(MAXx, 0) +COP2_BC_MAX(MAXy, 1) +COP2_BC_MAX(MAXz, 2) +COP2_BC_MAX(MAXw, 3) + +COP2_BC_MINI(MINIx, 0) +COP2_BC_MINI(MINIy, 1) +COP2_BC_MINI(MINIz, 2) +COP2_BC_MINI(MINIw, 3) + +// MAXi/MINIi — broadcast I register, PS2 integer comparison +void recCOP2_VMAXi() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Fd_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); + cop2EmitIntegerMax(_Fs_cop2); + cop2ApplyDestMask(_Fd_cop2); +} + +void recCOP2_VMINIi() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Fd_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); + cop2EmitIntegerMin(_Fs_cop2); + cop2ApplyDestMask(_Fd_cop2); +} + +// ======================================================================== +// ADDq/SUBq/MULq — broadcast Q register +// ======================================================================== + +#define COP2_Q_OP(name, neonOp) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x111); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_Q]); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x111); \ + } + +COP2_Q_OP(ADDq, Fadd) +COP2_Q_OP(SUBq, Fsub) +COP2_Q_OP(MULq, Fmul) + +// ADDi/SUBi/MULi — broadcast I register +#define COP2_I_OP(name, neonOp) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x110); \ + } + +COP2_I_OP(ADDi, Fadd) +COP2_I_OP(SUBi, Fsub) +COP2_I_OP(MULi, Fmul) + +// ======================================================================== +// MADD/MSUB variants: VF[fd] = ACC ± VF[fs] * VF[ft] +// ======================================================================== + +// MADD/MSUB use separate FMUL+FADD/FSUB (not FMLA/FMLS) to match PS2 VU +// intermediate rounding. PS2 rounds the multiply result before adding to ACC. + +void recCOP2_VMADD() +{ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + cop2LoadACC(RQSCRATCH3); + armAsm->Fadd(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + cop2ClampResult(); + cop2EmitFlagUpdate(_XYZW_cop2); + cop2ApplyDestMask(_Fd_cop2); + + endMacroOp_arm64(0x110); +} + +void recCOP2_VMSUB() +{ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2LoadVF(RQSCRATCH2, _Ft_cop2); + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + cop2LoadACC(RQSCRATCH3); + armAsm->Fsub(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + cop2ClampResult(); + cop2EmitFlagUpdate(_XYZW_cop2); + cop2ApplyDestMask(_Fd_cop2); + + endMacroOp_arm64(0x110); +} + +// MADD/MSUB broadcast variants: separate FMUL + FADD/FSUB. +// +// MADDx/y/z/w pre-clamp Fs before the multiply (clampFs=true): mVU_MADDx passes +// cFs, and the interpreter routes Fs through vuDouble, so an Inf/NaN Fs against +// a zero broadcast Ft must become FLT_MAX*0 = 0 rather than Inf*0 = NaN folded +// to +/-FLT_MAX by the result clamp. MSUBx/y/z/w use mVU_FMACd (clampType=0, +// no cFs) — that Fs divergence is shared/by-design, so MSUB keeps clampFs=false. +// The MADDw extras (cACC|cFt) are a separate concern. RQSCRATCH2/RQSCRATCH3 are +// free as the ±FLT_MAX bounds here (Ft/ACC are loaded after the clamp). +#define COP2_MADD_BC(name, addOp, bc, clampFs) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + if (clampFs) \ + cop2ClampReg(RQSCRATCH, RQSCRATCH2, RQSCRATCH3); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x110); \ + } + +COP2_MADD_BC(MADDx, Fadd, 0, true) +COP2_MADD_BC(MADDy, Fadd, 1, true) +COP2_MADD_BC(MADDz, Fadd, 2, true) +COP2_MADD_BC(MADDw, Fadd, 3, true) + +COP2_MADD_BC(MSUBx, Fsub, 0, false) +COP2_MADD_BC(MSUBy, Fsub, 1, false) +COP2_MADD_BC(MSUBz, Fsub, 2, false) +COP2_MADD_BC(MSUBw, Fsub, 3, false) + +// MADDq/MSUBq — broadcast Q +#define COP2_MADD_Q(name, addOp) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x111); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_Q]); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x111); \ + } + +COP2_MADD_Q(MADDq, Fadd) +COP2_MADD_Q(MSUBq, Fsub) + +// MADDi/MSUBi — broadcast I +#define COP2_MADD_I(name, addOp) \ + void recCOP2_V##name() \ + { \ + if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMask(_Fd_cop2); \ + endMacroOp_arm64(0x110); \ + } + +COP2_MADD_I(MADDi, Fadd) +COP2_MADD_I(MSUBi, Fsub) + +// OPMSUB: VF[fd].xyz = ACC.xyz - VF[fs].yzx * VF[ft].zxy (cross product subtract) +// PS2 always writes XYZ only, ignoring the instruction's dest field. +void recCOP2_VOPMSUB() +{ + if (_Fd_cop2 == 0) return; + setupMacroOp_arm64(0x110); + + cop2LoadVF(RQSCRATCH, _Fs_cop2); // fs = [x,y,z,w] + cop2LoadVF(RQSCRATCH2, _Ft_cop2); // ft = [x,y,z,w] + cop2LoadACC(RQSCRATCH3); // ACC + + // Build fs.yzx: EXT #4 gives [y,z,w,x], fix lane 2 (w→x) + a64::VRegister fsRot = a64::VRegister(28, 128); + armAsm->Ext(fsRot.V16B(), RQSCRATCH.V16B(), RQSCRATCH.V16B(), 4); // [y,z,w,x] + armAsm->Ins(fsRot.V4S(), 2, RQSCRATCH.V4S(), 0); // [y,z,x,x] + + // Build ft.zxy: RQSCRATCH2 still holds ft from the load above (the fsRot + // construction and ACC load only touch RQSCRATCH/v28/RQSCRATCH3), so reuse it. + a64::VRegister ftRot = a64::VRegister(27, 128); + armAsm->Ext(ftRot.V16B(), RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), 8); // [z,w,x,y] + armAsm->Ins(ftRot.V4S(), 1, RQSCRATCH2.V4S(), 0); // [z,x,x,y] + armAsm->Ins(ftRot.V4S(), 2, RQSCRATCH2.V4S(), 1); // [z,x,y,y] + + // ACC - fs.yzx * ft.zxy (separate FMUL+FSUB for PS2 rounding) + armAsm->Fmul(RQSCRATCH.V4S(), fsRot.V4S(), ftRot.V4S()); + armAsm->Fsub(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + cop2ClampResult(); + // OPMSUB always updates XYZ flags only (0xE), W MAC flag cleared. + // PS2 hardware ignores the W bit of the instruction's dest field — + // only XYZ are ever written. Force the mask to XYZ regardless of encoding. + cop2EmitFlagUpdate(0xE); + cop2ApplyDestMaskExplicit(_Fd_cop2, _XYZW_cop2 & 0xE); + + endMacroOp_arm64(0x110); +} + +// ======================================================================== +// Accumulator write variants (xxxA): result goes to ACC instead of VF[fd] +// ======================================================================== + +// VADDA/VSUBA/VMULA: ACC = VF[fs] OP VF[ft] +#define COP2_ACCUM_OP(name, neonOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadVF(RQSCRATCH2, _Ft_cop2); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +COP2_ACCUM_OP(ADDA, Fadd) +COP2_ACCUM_OP(SUBA, Fsub) +COP2_ACCUM_OP(MULA, Fmul) + +// Broadcast accumulator variants: ACC = VF[fs] OP VF[ft].bc +// mulClamp=true pre-clamps the FMAC operands (cFs every mask + cFt on the full +// mask) per mVU_MULAx cFs/cFt; ADD/SUB pass false (clampType=0). +#define COP2_ACCUM_BC(name, neonOp, bc, mulClamp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + if (mulClamp) cop2EmitMulInputClamp(); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +// ADDAx/y/z/w +COP2_ACCUM_BC(ADDAx, Fadd, 0, false) +COP2_ACCUM_BC(ADDAy, Fadd, 1, false) +COP2_ACCUM_BC(ADDAz, Fadd, 2, false) +COP2_ACCUM_BC(ADDAw, Fadd, 3, false) + +// SUBAx/y/z/w +COP2_ACCUM_BC(SUBAx, Fsub, 0, false) +COP2_ACCUM_BC(SUBAy, Fsub, 1, false) +COP2_ACCUM_BC(SUBAz, Fsub, 2, false) +COP2_ACCUM_BC(SUBAw, Fsub, 3, false) + +// MULAx/y/z/w — pre-clamp Fs (and Ft on full mask) before the multiply per +// mVU_MULAx: `(_XYZW_PS)?(cFs|cFt):cFs` (TOTA, DoM). cFs catches an Inf/NaN +// Fs against a zero broadcast (Inf*0 = NaN -> result-clamped ±FLT_MAX instead +// of the interpreter's vuDouble(Fs)-clamped 0). MULAw uses the same path to +// ensure the always-on cFs is applied. +COP2_ACCUM_BC(MULAx, Fmul, 0, true) +COP2_ACCUM_BC(MULAy, Fmul, 1, true) +COP2_ACCUM_BC(MULAz, Fmul, 2, true) +COP2_ACCUM_BC(MULAw, Fmul, 3, true) + +// ACCUMq variants +#define COP2_ACCUM_Q(name, neonOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x111); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_Q]); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x111); \ + } + +COP2_ACCUM_Q(ADDAq, Fadd) +COP2_ACCUM_Q(SUBAq, Fsub) +COP2_ACCUM_Q(MULAq, Fmul) + +// ACCUMi variants +#define COP2_ACCUM_I(name, neonOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); \ + armAsm->neonOp(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +COP2_ACCUM_I(ADDAi, Fadd) +COP2_ACCUM_I(SUBAi, Fsub) +COP2_ACCUM_I(MULAi, Fmul) + +// MADDA/MSUBA variants: ACC = ACC ± VF[fs] * VF[ft] +// Separate FMUL+FADD/FSUB for PS2 intermediate rounding. +#define COP2_MADDA_OP(name, addOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadVF(RQSCRATCH2, _Ft_cop2); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +COP2_MADDA_OP(MADDA, Fadd) +COP2_MADDA_OP(MSUBA, Fsub) + +// MADDA/MSUBA broadcast variants: ACC = ACC ± VF[fs] * VF[ft].bc +#define COP2_MADDA_BC(name, addOp, bc) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +COP2_MADDA_BC(MADDAx, Fadd, 0) +COP2_MADDA_BC(MADDAy, Fadd, 1) +COP2_MADDA_BC(MADDAz, Fadd, 2) +COP2_MADDA_BC(MADDAw, Fadd, 3) + +COP2_MADDA_BC(MSUBAx, Fsub, 0) +COP2_MADDA_BC(MSUBAy, Fsub, 1) +COP2_MADDA_BC(MSUBAz, Fsub, 2) +COP2_MADDA_BC(MSUBAw, Fsub, 3) + +// MADDAq/MSUBAq +#define COP2_MADDA_Q(name, addOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x111); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_Q]); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x111); \ + } + +COP2_MADDA_Q(MADDAq, Fadd) +COP2_MADDA_Q(MSUBAq, Fsub) + +// MADDAi/MSUBAi +#define COP2_MADDA_I(name, addOp) \ + void recCOP2_V##name() \ + { \ + setupMacroOp_arm64(0x110); \ + cop2LoadVF(RQSCRATCH, _Fs_cop2); \ + armLd1rVU0(RQSCRATCH2.V4S(), &VU0.VI[REG_I]); \ + armAsm->Fmul(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); \ + cop2LoadACC(RQSCRATCH3); \ + armAsm->addOp(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); \ + cop2ClampResult(); \ + cop2EmitFlagUpdate(_XYZW_cop2); \ + cop2ApplyDestMaskACC(RQSCRATCH); \ + endMacroOp_arm64(0x110); \ + } + +COP2_MADDA_I(MADDAi, Fadd) +COP2_MADDA_I(MSUBAi, Fsub) + +// OPMULA: ACC.xyz = VF[fs].yzx * VF[ft].zxy (cross product to accumulator) +// PS2 always writes XYZ only, ignoring the instruction's dest field. +void recCOP2_VOPMULA() +{ + setupMacroOp_arm64(0x110); + cop2LoadVF(RQSCRATCH, _Fs_cop2); // fs = [x,y,z,w] + cop2LoadVF(RQSCRATCH2, _Ft_cop2); // ft = [x,y,z,w] + + // Build fs.yzx: EXT #4 gives [y,z,w,x], fix lane 2 (w→x) + a64::VRegister fsRot = a64::VRegister(28, 128); + armAsm->Ext(fsRot.V16B(), RQSCRATCH.V16B(), RQSCRATCH.V16B(), 4); // [y,z,w,x] + armAsm->Ins(fsRot.V4S(), 2, RQSCRATCH.V4S(), 0); // [y,z,x,x] + + // Build ft.zxy: EXT #8 gives [z,w,x,y], fix lanes 1,2 + a64::VRegister ftRot = a64::VRegister(27, 128); + armAsm->Ext(ftRot.V16B(), RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), 8); // [z,w,x,y] + armAsm->Ins(ftRot.V4S(), 1, RQSCRATCH2.V4S(), 0); // [z,x,x,y] + armAsm->Ins(ftRot.V4S(), 2, RQSCRATCH2.V4S(), 1); // [z,x,y,y] + + armAsm->Fmul(RQSCRATCH.V4S(), fsRot.V4S(), ftRot.V4S()); + cop2ClampResult(); + // OPMULA always updates XYZ flags only (0xE), W MAC flag cleared. + // PS2 hardware writes ACC.xyz only; ACC.w is preserved regardless of mask. + cop2EmitFlagUpdate(0xE); + + cop2ApplyDestMaskACCExplicit(RQSCRATCH, _XYZW_cop2 & 0xE); + endMacroOp_arm64(0x110); +} + +// ======================================================================== +// Conversion ops: ITOF0/4/12/15, FTOI0/4/12/15 +// ======================================================================== + +void recCOP2_VITOF0() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armAsm->Scvtf(RQSCRATCH.V4S(), RQSCRATCH.V4S()); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VITOF4() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armAsm->Scvtf(RQSCRATCH.V4S(), RQSCRATCH.V4S(), 4); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VITOF12() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armAsm->Scvtf(RQSCRATCH.V4S(), RQSCRATCH.V4S(), 12); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VITOF15() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + armAsm->Scvtf(RQSCRATCH.V4S(), RQSCRATCH.V4S(), 15); + cop2ApplyDestMask(_Ft_cop2); +} + +// Float→signed-int convert (Fcvtzs) with NaN saturation, for COP2 macro-mode +// VFTOIx. ARM64 NEON Fcvtzs returns 0 for a NaN input, but the PS2 — like +// mVU_FTOIx (microVU_Upper-arm64.inl) and the interpreter — saturates NaN to a +// sign-based INT_MAX/INT_MIN. Finite overflow and ±Inf already saturate +// correctly in Fcvtzs; only NaN lanes need the fixup. Source lanes are in +// RQSCRATCH and the converted+saturated result is left there; `fbits` is the +// fixed-point fraction (0/4/12/15). Uses RQSCRATCH2/RQSCRATCH3 as temps. +// +// Uses the same sign-based BIF pattern as mVU_FTOIx, but materializes the +// 0x7FFFFFFF constant with MVNI (NOT(0x80<<24)) instead of loading +// mVUglob.absclip, since the COP2 macro path does not set up the mVUglob base +// register. +static void cop2EmitFtoiSaturated(int fbits) +{ + // Build the saturation value and NaN mask from the source float BEFORE the + // convert clobbers RQSCRATCH. + armAsm->Sshr(RQSCRATCH2.V4S(), RQSCRATCH.V4S(), 31); // 0xffffffff if sign set + armAsm->Mvni(RQSCRATCH3.V4S(), 0x80, a64::LSL, 24); // 0x7fffffff (INT_MAX) per lane + armAsm->Eor(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH3.V16B()); // +NaN→0x7fffffff, -NaN→0x80000000 + armAsm->Fcmeq(RQSCRATCH3.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S()); // 0xffffffff where NOT NaN + + if (fbits) + armAsm->Fcvtzs(RQSCRATCH.V4S(), RQSCRATCH.V4S(), fbits); + else + armAsm->Fcvtzs(RQSCRATCH.V4S(), RQSCRATCH.V4S()); + + // NaN lanes (notNan==0): replace Fcvtzs's 0 with the saturation value. + // BIF: dst bit <- src bit where mask bit is 0. + armAsm->Bif(RQSCRATCH.V16B(), RQSCRATCH2.V16B(), RQSCRATCH3.V16B()); +} + +void recCOP2_VFTOI0() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2EmitFtoiSaturated(0); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VFTOI4() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2EmitFtoiSaturated(4); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VFTOI12() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2EmitFtoiSaturated(12); + cop2ApplyDestMask(_Ft_cop2); +} + +void recCOP2_VFTOI15() +{ + cop2EmitConditionalSync(false, _vu0FinishMicro); + if (_Ft_cop2 == 0) return; + + cop2LoadVF(RQSCRATCH, _Fs_cop2); + cop2EmitFtoiSaturated(15); + cop2ApplyDestMask(_Ft_cop2); +} + +// ======================================================================== +// Division ops: VDIV, VSQRT, VRSQRT +// ======================================================================== +// These are scalar operations on single VF lanes, writing to the Q register. +// In macro mode, the result is immediately available (no pipeline delay). +// After computing Q, sync: copy to VI[REG_Q] and update D/I status flags. +// Complex edge cases (div-by-zero, negative sqrt) are handled with branches. + +// Emit SYNCFDIV: copy VU0.q to VU0.VI[REG_Q], update D/I status flags. +// statusflag = (statusflag & 0x3CF) | (statusflag_DI & 0x30) | ((statusflag_DI & 0x30) << 6) +static void cop2EmitSyncFDiv() +{ + // Copy q to VI[REG_Q] + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.q)); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[REG_Q])); + + // Update status flag: (old & 0x3CF) | (statusflag & 0x30) | ((statusflag & 0x30) << 6) + armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); + armAsm->And(a64::w1, a64::w1, 0x3CF); // clear D/I bits + + armAsm->Ldr(a64::w2, armVU0Mem(&VU0.statusflag)); + armAsm->And(a64::w2, a64::w2, 0x30); // D/I current bits + + armAsm->Orr(a64::w1, a64::w1, a64::w2); // current D/I + armAsm->Orr(a64::w1, a64::w1, a64::Operand(a64::w2, a64::LSL, 6)); // sticky D/I + + armAsm->Str(a64::w1, armVU0Mem(&VU0.VI[REG_STATUS_FLAG])); +} + +// VDIV: Q = VF[fs].fsf / VF[ft].ftf +void recCOP2_VDIV() +{ + const int fsf = _Fsf_cop2; + const int ftf = _Ftf_cop2; + + // Clear D/I flags + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Mov(RWARG1, 0x30); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); // clear D/I bits + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + // Load fs scalar and ft scalar + armAsm->Ldr(RSSCRATCH, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf])); // s30 = fs[fsf] + armAsm->Ldr(RSSCRATCH2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); // s31 = ft[ftf] + + // Check ft == 0 + a64::Label ftNonZero, done; + armAsm->Fcmp(RSSCRATCH2, 0.0); + armAsm->B(a64::ne, &ftNonZero); + + // ft == 0: set D/I flags, Q = ±FLT_MAX based on sign XOR + { + // Check if fs == 0 too → invalid (D flag = 0x10), else divide-by-zero (I flag = 0x20) + armAsm->Fcmp(RSSCRATCH, 0.0); + armAsm->Mov(a64::w1, 0x10); // invalid (0/0) + armAsm->Mov(a64::w2, 0x20); // div-by-zero + armAsm->Csel(a64::w1, a64::w1, a64::w2, a64::eq); + + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::w1); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + // Q = sign(fs) XOR sign(ft) ? -FLT_MAX : +FLT_MAX + armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf])); + armAsm->Ldr(a64::w2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); + armAsm->Eor(a64::w1, a64::w1, a64::w2); + armAsm->Mov(a64::w2, 0x7F7FFFFF); // +FLT_MAX + armAsm->Mov(a64::w3, 0xFF7FFFFF); // -FLT_MAX (encoded as two MOVs by vixl) + armAsm->Tst(a64::w1, 0x80000000); + armAsm->Csel(RWSCRATCH, a64::w3, a64::w2, a64::ne); + + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.q)); + } + armAsm->B(&done); + + // ft != 0: Q = fs / ft, then clamp + armAsm->Bind(&ftNonZero); + { + armAsm->Fdiv(RSSCRATCH, RSSCRATCH, RSSCRATCH2); + // Clamp result against ±FLT_MAX held in callee-saved s8/s9. + armAsm->Fminnm(RSSCRATCH, RSSCRATCH, a64::s8); + armAsm->Fmaxnm(RSSCRATCH, RSSCRATCH, a64::s9); + armAsm->Str(RSSCRATCH, armVU0Mem(&VU0.q)); + } + + armAsm->Bind(&done); + cop2EmitSyncFDiv(); +} + +// VSQRT: Q = sqrt(|VF[ft].ftf|) +void recCOP2_VSQRT() +{ + const int ftf = _Ftf_cop2; + + // Clear D/I flags + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Mov(RWARG1, 0x30); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); // clear D/I bits + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + // Load ft scalar + armAsm->Ldr(RSSCRATCH, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); + + // If ft < 0, set invalid flag (D flag = 0x10) + a64::Label notNeg; + armAsm->Fcmp(RSSCRATCH, 0.0); + armAsm->B(a64::ge, ¬Neg); + { + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + } + armAsm->Bind(¬Neg); + + // Q = sqrt(|ft|) + armAsm->Fabs(RSSCRATCH, RSSCRATCH); + armAsm->Fsqrt(RSSCRATCH, RSSCRATCH); + + // Clamp against ±FLT_MAX held in callee-saved s8/s9. + armAsm->Fminnm(RSSCRATCH, RSSCRATCH, a64::s8); + armAsm->Fmaxnm(RSSCRATCH, RSSCRATCH, a64::s9); + + armAsm->Str(RSSCRATCH, armVU0Mem(&VU0.q)); + + cop2EmitSyncFDiv(); +} + +// VRSQRT: Q = VF[fs].fsf / sqrt(|VF[ft].ftf|) +void recCOP2_VRSQRT() +{ + const int fsf = _Fsf_cop2; + const int ftf = _Ftf_cop2; + + // Clear D/I flags + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Mov(RWARG1, 0x30); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); // clear D/I bits + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + // Load ft scalar + armAsm->Ldr(RSSCRATCH2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); // s31 = ft[ftf] + + // Load fs scalar + armAsm->Ldr(RSSCRATCH, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf])); // s30 = fs[fsf] + + // Check ft == 0 → div-by-zero + a64::Label ftNonZero, done; + armAsm->Fcmp(RSSCRATCH2, 0.0); + armAsm->B(a64::ne, &ftNonZero); + + // ft == 0: set div-by-zero flag (0x20), Q based on signs + { + armAsm->Fcmp(RSSCRATCH, 0.0); + + // fs == 0: set invalid flag too (0x10), Q = ±0 + a64::Label fsNonZero; + armAsm->B(a64::ne, &fsNonZero); + { + // D/I flags: 0x30 (both invalid and div-by-zero) + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x30); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + // Q = sign(fs) XOR sign(ft) ? -0 : +0 + armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf])); + armAsm->Ldr(a64::w2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); + armAsm->Eor(a64::w1, a64::w1, a64::w2); + armAsm->And(RWSCRATCH, a64::w1, 0x80000000); // just sign bit, or 0 + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.q)); + armAsm->B(&done); + } + + // fs != 0: Q = ±FLT_MAX + armAsm->Bind(&fsNonZero); + { + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x20); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + + armAsm->Ldr(a64::w1, armVU0Mem(&VU0.VF[_Fs_cop2].UL[fsf])); + armAsm->Ldr(a64::w2, armVU0Mem(&VU0.VF[_Ft_cop2].UL[ftf])); + armAsm->Eor(a64::w1, a64::w1, a64::w2); + armAsm->Mov(a64::w2, 0x7F7FFFFF); + armAsm->Mov(a64::w3, 0xFF7FFFFF); + armAsm->Tst(a64::w1, 0x80000000); + armAsm->Csel(RWSCRATCH, a64::w3, a64::w2, a64::ne); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.q)); + armAsm->B(&done); + } + } + + // ft != 0: normal path + armAsm->Bind(&ftNonZero); + { + // If ft < 0, set invalid flag + a64::Label notNeg; + armAsm->Fcmp(RSSCRATCH2, 0.0); + armAsm->B(a64::ge, ¬Neg); + { + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x10); + armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.statusflag)); + } + armAsm->Bind(¬Neg); + + // Q = fs / sqrt(|ft|) + armAsm->Fabs(RSSCRATCH2, RSSCRATCH2); + armAsm->Fsqrt(RSSCRATCH2, RSSCRATCH2); + armAsm->Fdiv(RSSCRATCH, RSSCRATCH, RSSCRATCH2); + + // Clamp against ±FLT_MAX held in callee-saved s8/s9. + armAsm->Fminnm(RSSCRATCH, RSSCRATCH, a64::s8); + armAsm->Fmaxnm(RSSCRATCH, RSSCRATCH, a64::s9); + + armAsm->Str(RSSCRATCH, armVU0Mem(&VU0.q)); + } + + armAsm->Bind(&done); + cop2EmitSyncFDiv(); +} + +// ======================================================================== +// CLIP: 6-plane frustum clip test +// ======================================================================== +// Compares VF[fs].xyz against ±|VF[ft].w| using signed integer comparison. +// Result: 6 bits shifted into clipflag history (24-bit rolling window). +// Bit layout per test: bit0=+x, bit1=-x, bit2=+y, bit3=-y, bit4=+z, bit5=-z + +void recCOP2_VCLIP() +{ + // Load ft.w as integer, compute |ft.w| with denormal handling + // If denormal (exponent == 0), use 0x007fffff instead + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VF[_Ft_cop2].UL[3])); // w lane + + // value = (raw & 0x7f800000) ? (raw & 0x7fffffff) : 0x007fffff + armAsm->Mov(a64::w1, RWSCRATCH); + armAsm->And(a64::w2, a64::w1, 0x7F800000); // exponent field + armAsm->And(a64::w1, a64::w1, 0x7FFFFFFF); // |raw| = clear sign + armAsm->Mov(a64::w3, 0x007FFFFF); // denormal replacement + armAsm->Cmp(a64::w2, 0); + armAsm->Csel(a64::w1, a64::w1, a64::w3, a64::ne); // w1 = clip value + + // Shift clipflag left by 6 + armAsm->Ldr(a64::w4, armVU0Mem(&VU0.clipflag)); + armAsm->Lsl(a64::w4, a64::w4, 6); + + // Load fs = [x,y,z,w] as integers for the lane comparisons. + armAsm->Ldr(RQSCRATCH, armVU0Mem(&VU0.VF[_Fs_cop2])); // q30 = [x,y,z,w] + + // Vectorized signed-integer clip test (matches the interp's + // (s32)(fs.lane ^ {0,0x80000000}) > value exactly — Cmgt is SCMGT). The + // scalar 6× UMOV/CMP/CSET loop collapses to two NEON compares plus a + // weighted horizontal add. + // pos = (s32)fs > value → +x,+y,+z lanes + // neg = (s32)(fs^signbit) > value → -x,-y,-z lanes + armAsm->Dup(RQSCRATCH3.V4S(), a64::w1); // q29 = [value × 4] + armAsm->Movi(RQSCRATCH2.V4S(), 0x80, a64::LSL, 24); // q31 = [0x80000000 × 4] + armAsm->Eor(RQSCRATCH2.V16B(), RQSCRATCH.V16B(), RQSCRATCH2.V16B()); // q31 = fs ^ sign + a64::VRegister posMask = a64::VRegister(28, 128); + armAsm->Cmgt(posMask.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); // pos mask + armAsm->Cmgt(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); // neg mask + + // Weight each lane by its clip bit and fold to a 6-bit field. The negative + // weights are the positive ones << 1 ([1,4,16,0] -> [2,8,32,0]), so a single + // constant load plus a Shl covers both. +/- per axis are mutually exclusive + // and the weights are disjoint bits, so Add+Addv = OR (no carries). + a64::VRegister weight = a64::VRegister(27, 128); + armMoveAddressToReg(RSCRATCHADDR, &s_cop2ClipWeightPos); + armAsm->Ldr(weight, a64::MemOperand(RSCRATCHADDR)); // [1,4,16,0] + armAsm->And(posMask.V16B(), posMask.V16B(), weight.V16B()); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), weight.V16B()); + armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), 1); // neg weights = pos << 1 + armAsm->Add(posMask.V4S(), posMask.V4S(), RQSCRATCH2.V4S()); + armAsm->Addv(posMask.S(), posMask.V4S()); // sum lanes → scalar + armAsm->Umov(a64::w2, posMask.V4S(), 0); // 6-bit clip field + + // Merge into clipflag and mask to 24 bits + armAsm->Orr(a64::w4, a64::w4, a64::w2); + armAsm->And(a64::w4, a64::w4, 0xFFFFFF); + + // Store clipflag and sync to VI[REG_CLIP_FLAG] + armAsm->Str(a64::w4, armVU0Mem(&VU0.clipflag)); + armAsm->Str(a64::w4, armVU0Mem(&VU0.VI[REG_CLIP_FLAG])); + + // Broadcast the new clipflag into all 4 lanes of micro_clipflags. A + // subsequent VU0 microprogram loads its clip-flag instances directly from + // the VURegs::micro_clipflags field in the mVU Execute prologue — without + // this they would be stale (pre-VCLIP). RQSCRATCH is free here (its earlier + // fs load is consumed). + armAsm->Dup(RQSCRATCH.V4S(), a64::w4); + armAsm->Str(RQSCRATCH, armVU0Mem(&VU0.micro_clipflags)); +} + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 + +// ======================================================================== +// cop2flags — determines which control flags a COP2 instruction writes. +// Used by the analysis pass (iR5900Analysis.cpp) for flag optimization. +// Returns: 0=none, 1=status, 2=MAC, 3=both, 4=clip +// Architecture-independent — identical to x86 version. +// ======================================================================== + +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; + 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; +} diff --git a/pcsx2/arm64/iCore-arm64.cpp b/pcsx2/arm64/iCore-arm64.cpp new file mode 100644 index 0000000000..0bb548ecc5 --- /dev/null +++ b/pcsx2/arm64/iCore-arm64.cpp @@ -0,0 +1,1197 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "Config.h" +#include "R3000A.h" +#include "R5900.h" +#include "Vif.h" +#include "VU.h" +#include "arm64/iR5900-arm64.h" +#include "arm64/iR3000A-arm64.h" + +#include "common/Assertions.h" +#include "common/Console.h" + +namespace a64 = vixl::aarch64; + +//#define RALOG(...) fprintf(stderr, __VA_ARGS__) +#define RALOG(...) + + +//////////////////////////////////////////////////////////////////////////////// +// IOP constant propagation externs +// These are defined in the IOP recompiler, but the register allocator needs +// them to handle PSX register allocation correctly. + +extern u32 g_psxConstRegs[32]; +extern u32 g_psxHasConstReg, g_psxFlushedConstReg; + +#define PSX_IS_CONST1(reg) ((reg) < 32 && (g_psxHasConstReg & (1 << (reg)))) +#define PSX_DEL_CONST(reg) \ + { \ + if ((reg) < 32) \ + g_psxHasConstReg &= ~(1 << (reg)); \ + } + +//////////////////////////////////////////////////////////////////////////////// +// Shared state + +EEINST* g_pCurInstInfo = nullptr; + +u16 g_arm64AllocCounter = 0; +u16 g_neonAllocCounter = 0; + +// EE constant propagation state +alignas(16) GPR_reg64 g_cpuConstRegs[32] = {}; +u32 g_cpuHasConstReg = 0, g_cpuFlushedConstReg = 0; + +//////////////////////////////////////////////////////////////////////////////// +// ARM64 GPR Register Allocator + +_arm64gprregs arm64gprs[NUM_ARM_GPR_REGS], s_saveArm64GPRregs[NUM_ARM_GPR_REGS]; +static uint g_arm64checknext = 0; + +_arm64neonregs arm64neon[NUM_ARM_NEON_REGS], s_saveArm64NEONregs[NUM_ARM_NEON_REGS]; + +// ARM64 register allocation policy: +// x0-x3: argument/return registers (caller-saved, allocatable) +// x4-x15: caller-saved temporaries (allocatable) +// x16: VIXL intra-procedure scratch — NOT allocatable +// x17: RSCRATCHADDR — NOT allocatable +// x18: platform reserved — NOT allocatable +// x19: RFASTMEMBASE — NOT allocatable (reserved for fastmem base) +// x20: RSTATE — NOT allocatable (reserved for cpuRegs pointer) +// x21: RPSXSTATE — NOT allocatable (reserved for psxRegs pointer in IOP JIT) +// x22-x23: callee-saved (allocatable) +// x24: RVU0 — NOT allocatable (reserved for &VU0 pointer in EE COP2 JIT) +// x25: RECCYCLE — NOT allocatable (reserved for cpuRegs.cycle) +// x26-x28: callee-saved (allocatable) +// x29: frame pointer — NOT allocatable +// x30: link register — NOT allocatable + +// Bitmask of allocatable aarch64 GPRs. Bit `n` set ↔ x_n is in the pool. +// Cleared bits, all-pinned/scratch as documented above: +// bit 8 — x8 : RXSCRATCH/RWSCRATCH (value scratch) +// bits 9-10 — x9/x10 : load/store address + value scratch +// bits 16-18 — x16 (vixl), x17 (RSCRATCHADDR), x18 (platform reserved) +// bit 19 — x19 : RFASTMEMBASE +// bit 20 — x20 : RSTATE (cpuRegs base pointer) +// bit 21 — x21 : RPSXSTATE (psxRegs base; shared alloc table with EE) +// bit 24 — x24 : RVU0 (pinned &VU0 for iCOP2) +// bit 25 — x25 : RECCYCLE (pinned cpuRegs.cycle) +// bits 29-30 — x29/x30 : FP, LR — never allocatable +// Inner allocator loop runs 31× per cache miss and was nine sequential +// `if (armreg == N) return false` branches per probe; collapse to one +// LSR + AND + cbz against this mask. +static constexpr uint32_t ALLOCATABLE_MASK = ~((1u << 8) + | (1u << 9) | (1u << 10) + | (7u << 16) + | (1u << 19) | (1u << 20) | (1u << 21) + | (1u << 24) | (1u << 25) + | (3u << 29)); + +bool _isAllocatableArm64GPR(int armreg) +{ + return ((ALLOCATABLE_MASK >> armreg) & 1u) != 0u; +} + +void _initArm64GPRregs() +{ + std::memset(arm64gprs, 0, sizeof(arm64gprs)); + g_arm64AllocCounter = 0; + g_arm64checknext = 0; +} + +bool _hasArm64GPR(int type, int reg, int required_mode) +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == type && arm64gprs[i].reg == reg) + return ((arm64gprs[i].mode & required_mode) == required_mode); + } + return false; +} + +int _getFreeArm64GPR(int mode) +{ + int tempi = -1; + u32 bestcount = 0x10000; + + // First pass: find a completely free register + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + const int reg = (g_arm64checknext + i) % NUM_ARM_GPR_REGS; + if (arm64gprs[reg].inuse || !_isAllocatableArm64GPR(reg)) + continue; + + if ((mode & MODE_CALLEESAVED) && !armIsCalleeSavedRegister(reg)) + continue; + + if ((mode & MODE_COP2) && mVUIsReservedCOP2(reg)) + continue; + + g_arm64checknext = (reg + 1) % NUM_ARM_GPR_REGS; + return reg; + } + + // Second pass: evict by LRU, prefer temps first + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (!_isAllocatableArm64GPR(i)) + continue; + if ((mode & MODE_CALLEESAVED) && !armIsCalleeSavedRegister(i)) + continue; + if ((mode & MODE_COP2) && mVUIsReservedCOP2(i)) + continue; + + pxAssert(arm64gprs[i].inuse); + if (arm64gprs[i].needed) + continue; + + if (arm64gprs[i].type == ARM64TYPE_TEMP) + { + _freeArm64GPR(i); + return i; + } + + if (arm64gprs[i].counter < bestcount) + { + tempi = i; + bestcount = arm64gprs[i].counter; + } + } + + if (tempi != -1) + { + _freeArm64GPR(tempi); + return tempi; + } + + pxFailRel("ARM64 GPR register allocation error"); + return -1; +} + +void _writebackArm64GPR(int armreg) +{ + switch (arm64gprs[armreg].type) + { + case ARM64TYPE_GPR: + RALOG("Writing back ARM64 GPR %d for guest reg %d\n", armreg, arm64gprs[armreg].reg); + armStoreEERegPtr(armXRegister(armreg), &cpuRegs.GPR.r[arm64gprs[armreg].reg].UD[0]); + break; + + case ARM64TYPE_FPRC: + RALOG("Writing back ARM64 GPR %d for guest FPCR %d\n", armreg, arm64gprs[armreg].reg); + armStoreEERegPtr(armWRegister(armreg), &fpuRegs.fprc[arm64gprs[armreg].reg]); + break; + + case ARM64TYPE_VIREG: + RALOG("Writing back ARM64 GPR %d for guest VI %d\n", armreg, arm64gprs[armreg].reg); + armAsm->Strh(armWRegister(armreg), armVU0Mem(&VU0.VI[arm64gprs[armreg].reg].UL)); + break; + + case ARM64TYPE_PCWRITEBACK: + RALOG("Writing back PC writeback from ARM64 GPR %d\n", armreg); + armAsm->Str(armWRegister(armreg), armCpuRegMem(&cpuRegs.pcWriteback)); + break; + + case ARM64TYPE_PSX: + RALOG("Writing back ARM64 GPR %d for guest PSX reg %d\n", armreg, arm64gprs[armreg].reg); + armAsm->Str(armWRegister(armreg), armPsxRegMem(&psxRegs.GPR.r[arm64gprs[armreg].reg])); + break; + + case ARM64TYPE_PSX_PCWRITEBACK: + RALOG("Writing back PSX PC writeback from ARM64 GPR %d\n", armreg); + armAsm->Str(armWRegister(armreg), armPsxRegMem(&psxRegs.pcWriteback)); + break; + + default: + break; + } +} + +void _freeArm64GPR(int armreg) +{ + pxAssert(armreg >= 0 && armreg < NUM_ARM_GPR_REGS); + if (!arm64gprs[armreg].inuse) + return; + + if (arm64gprs[armreg].mode & MODE_WRITE) + _writebackArm64GPR(armreg); + + arm64gprs[armreg].inuse = 0; + arm64gprs[armreg].mode = 0; +} + +void _freeArm64GPRWithoutWriteback(int armreg) +{ + pxAssert(armreg >= 0 && armreg < NUM_ARM_GPR_REGS); + arm64gprs[armreg].inuse = 0; + arm64gprs[armreg].mode = 0; +} + +void _freeArm64GPRregs() +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse) + _freeArm64GPR(i); + } +} + +void _flushArm64GPRregs() +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && (arm64gprs[i].mode & MODE_WRITE)) + { + _writebackArm64GPR(i); + arm64gprs[i].mode &= ~MODE_WRITE; + arm64gprs[i].mode |= MODE_READ; + } + } +} + +int _checkArm64GPR(int type, int reg, int mode) +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == type && arm64gprs[i].reg == reg) + { + arm64gprs[i].mode |= mode; + arm64gprs[i].counter = g_arm64AllocCounter++; + arm64gprs[i].needed = 1; + return i; + } + } + return -1; +} + +int _allocArm64GPR(int type, int reg, int mode) +{ + if (type == ARM64TYPE_GPR || type == ARM64TYPE_PSX) + pxAssertMsg(reg >= 0 && reg < 34, "Register index out of bounds."); + + int hostNEONreg = (type == ARM64TYPE_GPR) ? _checkNEONreg(NEONTYPE_GPRREG, reg, 0) : -1; + + // Check if already allocated + if (type != ARM64TYPE_TEMP) + { + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (!arm64gprs[i].inuse || arm64gprs[i].type != type || arm64gprs[i].reg != reg) + continue; + + if (type == ARM64TYPE_VIREG && reg < 0) + continue; + + if (type == ARM64TYPE_GPR && (mode & MODE_WRITE)) + { + if (GPR_IS_CONST1(reg)) + GPR_DEL_CONST(reg); + if (hostNEONreg >= 0) + { + pxAssert(!(arm64neon[hostNEONreg].mode & MODE_WRITE)); + _freeNEONreg(hostNEONreg); + } + } + else if (type == ARM64TYPE_PSX && (mode & MODE_WRITE)) + { + if (PSX_IS_CONST1(reg)) + PSX_DEL_CONST(reg); + } + + arm64gprs[i].counter = g_arm64AllocCounter++; + arm64gprs[i].mode |= mode & ~MODE_CALLEESAVED; + arm64gprs[i].needed = true; + return i; + } + } + + // Need to allocate a new register + const int regnum = _getFreeArm64GPR(mode); + arm64gprs[regnum].type = type; + arm64gprs[regnum].reg = reg; + arm64gprs[regnum].mode = mode & ~MODE_CALLEESAVED; + arm64gprs[regnum].counter = g_arm64AllocCounter++; + arm64gprs[regnum].needed = true; + arm64gprs[regnum].inuse = true; + + if (mode & MODE_READ) + { + switch (type) + { + case ARM64TYPE_GPR: + { + if (reg == 0) + { + // r0 is always zero + armAsm->Mov(armWRegister(regnum), 0); + } + else if (hostNEONreg >= 0) + { + // Value is in a NEON register, extract lower 64 bits + RALOG("Copying guest reg %d from NEON %d to GPR %d\n", reg, hostNEONreg, regnum); + armAsm->Mov(armXRegister(regnum), armQRegister(hostNEONreg).V2D(), 0); + + if (arm64neon[hostNEONreg].mode & MODE_WRITE) + { + _freeNEONreg(hostNEONreg); + } + } + else if (GPR_IS_CONST1(reg)) + { + RALOG("Loading constant %lld for guest reg %d to GPR %d\n", + (long long)g_cpuConstRegs[reg].SD[0], reg, regnum); + armAsm->Mov(armXRegister(regnum), g_cpuConstRegs[reg].SD[0]); + g_cpuFlushedConstReg |= (1u << reg); + arm64gprs[regnum].mode |= MODE_WRITE; + } + else + { + RALOG("Loading guest reg %d to GPR %d\n", reg, regnum); + armLoadEERegPtr(armXRegister(regnum), &cpuRegs.GPR.r[reg].UD[0]); + } + } + break; + + case ARM64TYPE_FPRC: + RALOG("Loading guest FPCR %d to GPR %d\n", reg, regnum); + armLoadEERegPtr(armWRegister(regnum), &fpuRegs.fprc[reg]); + break; + + case ARM64TYPE_PSX: + { + if (reg == 0) + { + armAsm->Mov(armWRegister(regnum), 0); + } + else if (PSX_IS_CONST1(reg)) + { + armAsm->Mov(armWRegister(regnum), g_psxConstRegs[reg]); + g_psxFlushedConstReg |= (1u << reg); + arm64gprs[regnum].mode |= MODE_WRITE; + } + else + { + armLoadPsxRegPtr(armWRegister(regnum), &psxRegs.GPR.r[reg]); + } + } + break; + + case ARM64TYPE_VIREG: + { + RALOG("Loading guest VI reg %d to GPR %d\n", reg, regnum); + armAsm->Ldrh(armWRegister(regnum), armVU0Mem(&VU0.VI[reg].US[0])); + } + break; + + default: + break; + } + } + + if (type == ARM64TYPE_GPR && (mode & MODE_WRITE)) + { + if (reg < 32 && GPR_IS_CONST1(reg)) + GPR_DEL_CONST(reg); + if (hostNEONreg >= 0) + { + // We're about to write this guest reg into the scalar GPR, so the + // cached NEON copy is superseded — discard it WITHOUT writeback + // (mirrors _allocGPRtoNEONreg and x86 _allocGPRtoXMMreg). Writing + // it back would store a stale value the new GPR's flush overwrites. + _freeNEONregWithoutWriteback(hostNEONreg); + } + } + else if (type == ARM64TYPE_PSX && (mode & MODE_WRITE)) + { + if (reg < 32 && PSX_IS_CONST1(reg)) + PSX_DEL_CONST(reg); + } + + return regnum; +} + +void _addNeededArm64GPR(int type, int reg) +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == type && arm64gprs[i].reg == reg) + arm64gprs[i].needed = 1; + } +} + +void _clearNeededArm64GPRregs() +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].needed && arm64gprs[i].type == ARM64TYPE_TEMP) + _freeArm64GPR(i); + arm64gprs[i].needed = 0; + } +} + +void _flushConstReg(int reg) +{ + if (GPR_IS_CONST1(reg) && !(g_cpuFlushedConstReg & (1 << reg))) + { + armAsm->Mov(RXSCRATCH, static_cast(g_cpuConstRegs[reg].SD[0])); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[reg].UD[0])); + g_cpuFlushedConstReg |= (1 << reg); + if (reg == 0) + DevCon.Warning("Flushing r0!"); + } +} + +void _flushConstRegs(bool delete_const) +{ + for (u32 i = 0; i < 32; i++) + { + if (!GPR_IS_CONST1(i) || g_cpuFlushedConstReg & (1u << i)) + continue; + + armAsm->Mov(RXSCRATCH, static_cast(g_cpuConstRegs[i].UD[0])); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[i].UD[0])); + g_cpuFlushedConstReg |= 1u << i; + } + + if (delete_const) + { + // Clear ALL const state, including already-flushed registers. + // After an interpreter call, the interpreter may have modified any + // register — stale const flags would cause subsequent native code + // to use outdated values from g_cpuConstRegs instead of memory. + g_cpuHasConstReg = 1; // keep r0 (always zero) + g_cpuFlushedConstReg = 1; + } +} + +void _validateRegs() +{ +#ifdef PCSX2_DEVBUILD + for (s8 guestreg = 0; guestreg < 32; guestreg++) + { + u32 gprreg = 0, gprmode = 0; + u32 neonreg = 0, neonmode = 0; + for (int hostreg = 0; hostreg < NUM_ARM_GPR_REGS; hostreg++) + { + if (arm64gprs[hostreg].inuse && arm64gprs[hostreg].type == ARM64TYPE_GPR && arm64gprs[hostreg].reg == guestreg) + { + pxAssertMsg(gprreg == 0 && gprmode == 0, "register not already allocated in GPR"); + gprreg = hostreg; + gprmode = arm64gprs[hostreg].mode; + } + } + for (int hostreg = 0; hostreg < NUM_ARM_NEON_REGS; hostreg++) + { + if (arm64neon[hostreg].inuse && arm64neon[hostreg].type == NEONTYPE_GPRREG && arm64neon[hostreg].reg == guestreg) + { + pxAssertMsg(neonreg == 0 && neonmode == 0, "register not already allocated in NEON"); + neonreg = hostreg; + neonmode = arm64neon[hostreg].mode; + } + } + + if ((gprmode | neonmode) & MODE_WRITE) + pxAssertMsg((gprmode & MODE_WRITE) != (neonmode & MODE_WRITE), "only one of GPR/NEON is in write state"); + } +#endif +} + +// Type-specific convenience wrappers over _addNeededArm64GPR. +void _addNeededGPRtoArm64GPR(int gprreg) { _addNeededArm64GPR(ARM64TYPE_GPR, gprreg); } +void _addNeededPSXtoArm64GPR(int gprreg) { _addNeededArm64GPR(ARM64TYPE_PSX, gprreg); } + +void _deleteGPRtoArm64GPR(int reg, int flush) +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == ARM64TYPE_GPR && arm64gprs[i].reg == reg) + { + switch (flush) + { + case DELETE_REG_FREE: _freeArm64GPR(i); break; + case DELETE_REG_FLUSH: + if (arm64gprs[i].mode & MODE_WRITE) + { + _writebackArm64GPR(i); + // Drop MODE_WRITE (keep MODE_READ) so a later + // _freeArm64GPR won't store the same value again. + arm64gprs[i].mode = (arm64gprs[i].mode & ~MODE_WRITE) | MODE_READ; + } + break; + case DELETE_REG_FLUSH_AND_FREE: _freeArm64GPR(i); break; + case DELETE_REG_FREE_NO_WRITEBACK: _freeArm64GPRWithoutWriteback(i); break; + } + return; + } + } +} + +void _deletePSXtoArm64GPR(int reg, int flush) +{ + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == ARM64TYPE_PSX && arm64gprs[i].reg == reg) + { + switch (flush) + { + case DELETE_REG_FREE: _freeArm64GPR(i); break; + case DELETE_REG_FLUSH: + if (arm64gprs[i].mode & MODE_WRITE) + { + _writebackArm64GPR(i); + // Drop MODE_WRITE (keep MODE_READ) so a later + // _freeArm64GPR won't store the same value again. + arm64gprs[i].mode = (arm64gprs[i].mode & ~MODE_WRITE) | MODE_READ; + } + break; + case DELETE_REG_FLUSH_AND_FREE: _freeArm64GPR(i); break; + case DELETE_REG_FREE_NO_WRITEBACK: _freeArm64GPRWithoutWriteback(i); break; + } + return; + } + } +} + +int _allocIfUsedGPRtoArm64(int gprreg, int mode) +{ + return EEINST_USEDTEST(gprreg) ? _allocArm64GPR(ARM64TYPE_GPR, gprreg, mode) : -1; +} + +int _allocIfUsedVItoArm64(int vireg, int mode) +{ + return EEINST_VIUSEDTEST(vireg) ? _allocArm64GPR(ARM64TYPE_VIREG, vireg, mode) : -1; +} + +//////////////////////////////////////////////////////////////////////////////// +// ARM64 NEON Register Allocator + +void _initArm64NEONregs() +{ + std::memset(arm64neon, 0, sizeof(arm64neon)); + g_neonAllocCounter = 0; +} + +// Reserved NEON scalars for PS2 FPU clamp constants (held across the JIT +// session). s8 = +FLT_MAX, s9 = -FLT_MAX. Loaded in the EE dispatcher and +// mVU dispatcher prologues; used by fpuClampResult and iCOP2 scalar +// VDIV/VSQRT/VRSQRT. Lower 64 bits are callee-saved per AAPCS64, so the +// values survive every armEmitCall path without compile-time tracking. +// v8/v9 are skipped by every _getFreeArm64NEON search loop below — no +// allocator codepath can land on them. +static constexpr u32 NEON_RESERVED_FPU_MAX = 8; +static constexpr u32 NEON_RESERVED_FPU_MIN = 9; + +// Callee-saved NEON range available to the allocator: q10-q15 +// (indices 8/9 reserved above). EE GPR values allocated here survive FPU +// interpreter calls without flushing. +static constexpr u32 NEON_CALLEE_SAVED_START = 10; +static constexpr u32 NEON_CALLEE_SAVED_END = 16; // exclusive + +int _getFreeArm64NEON(u32 minreg, u32 maxreg) +{ + int tempi = -1; + u32 bestcount = 0x10000; + + // Check for free registers + for (u32 i = minreg; i < maxreg; i++) + { + if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN) + continue; + if (!arm64neon[i].inuse) + return i; + } + + // Check for dead regs + tempi = -1; + bestcount = 0xffff; + for (u32 i = minreg; i < maxreg; i++) + { + if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN) + continue; + pxAssert(arm64neon[i].inuse); + if (arm64neon[i].needed) + continue; + + pxAssert(arm64neon[i].type != NEONTYPE_TEMP); + + if (arm64neon[i].counter < bestcount) + { + switch (arm64neon[i].type) + { + case NEONTYPE_GPRREG: + if (EEINST_USEDTEST(arm64neon[i].reg)) + continue; + break; + case NEONTYPE_FPREG: + if (FPUINST_USEDTEST(arm64neon[i].reg)) + continue; + break; + case NEONTYPE_VFREG: + if (EEINST_VFUSEDTEST(arm64neon[i].reg)) + continue; + break; + } + + tempi = i; + bestcount = arm64neon[i].counter; + } + } + + if (tempi != -1) + { + _freeNEONreg(tempi); + return tempi; + } + + // Last resort: take the LRU register + bestcount = 0xffff; + for (u32 i = minreg; i < maxreg; i++) + { + if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN) + continue; + pxAssert(arm64neon[i].inuse); + if (arm64neon[i].needed) + continue; + + if (arm64neon[i].counter < bestcount) + { + tempi = i; + bestcount = arm64neon[i].counter; + } + } + + if (tempi != -1) + { + _freeNEONreg(tempi); + return tempi; + } + + pxFailRel("ARM64 NEON register allocation error"); + return -1; +} + +// Overload for backward compatibility (full range) +int _getFreeArm64NEON(u32 maxreg) +{ + return _getFreeArm64NEON(0, maxreg); +} + +int _allocTempNEONreg() +{ + const int neonreg = _getFreeArm64NEON(); + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = NEONTYPE_TEMP; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; + return neonreg; +} + +int _checkNEONreg(int type, int reg, int mode) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && (arm64neon[i].type == (type & 0xff)) && (arm64neon[i].reg == reg)) + { + if (type == NEONTYPE_GPRREG && (mode & MODE_WRITE)) + return _allocGPRtoNEONreg(reg, mode); + + arm64neon[i].mode |= mode; + arm64neon[i].counter = g_neonAllocCounter++; + arm64neon[i].needed = 1; + return i; + } + } + return -1; +} + +bool _hasNEONreg(int type, int reg, int required_mode) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == type && arm64neon[i].reg == reg) + return ((arm64neon[i].mode & required_mode) == required_mode); + } + return false; +} + +int _allocFPtoNEONreg(int fpreg, int mode) +{ + // Check if already allocated + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (!arm64neon[i].inuse || arm64neon[i].type != NEONTYPE_FPREG || arm64neon[i].reg != fpreg) + continue; + + // Slot already holds the live value (MODE_READ → loaded from memory, + // MODE_WRITE → freshly written; both are authoritative over memory). + // Reloading here would clobber a MODE_WRITE-only live value with + // stale memory, breaking chained ops where the next read consumes + // the previous write. Mirrors _allocGPRtoNEONreg's reuse path. + arm64neon[i].counter = g_neonAllocCounter++; + arm64neon[i].needed = 1; + arm64neon[i].mode |= mode; + return i; + } + + // New allocation + const int neonreg = _getFreeArm64NEON(); + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = NEONTYPE_FPREG; + arm64neon[neonreg].reg = fpreg; + arm64neon[neonreg].mode = mode; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; + + if (mode & MODE_READ) + { + armLoadEERegPtr(armSRegister(neonreg), &fpuRegs.fpr[fpreg].f); + } + + return neonreg; +} + +int _allocGPRtoNEONreg(int gprreg, int mode) +{ + const int hostGPRreg = _checkArm64GPR(ARM64TYPE_GPR, gprreg, MODE_READ); + + // Check if already in NEON + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (!arm64neon[i].inuse || arm64neon[i].type != NEONTYPE_GPRREG || arm64neon[i].reg != gprreg) + continue; + + if (mode & MODE_WRITE && hostGPRreg >= 0) + { + // Dual-dirty (NEON MODE_WRITE + arm64gpr MODE_WRITE for the same + // guest reg) means a scalar op left a pending lower-64 write. + // Flush it before freeing so the value isn't lost. This case is + // legitimate, not an error: eeRecompileCodeXMM can reuse a + // MMI-written slot for a subsequent MMI Rd while the scalar GPR + // allocator still holds an unrelated MODE_WRITE entry for the + // same guest reg. + if (arm64gprs[hostGPRreg].mode & MODE_WRITE) + _writebackArm64GPR(hostGPRreg); + _freeArm64GPRWithoutWriteback(hostGPRreg); + } + + if (mode & MODE_WRITE && GPR_IS_CONST1(gprreg)) + GPR_DEL_CONST(gprreg); + + arm64neon[i].counter = g_neonAllocCounter++; + arm64neon[i].needed = true; + arm64neon[i].mode |= mode; + return i; + } + + // Allocate EE GPRs to callee-saved NEON range so they survive C + // function calls (FPU interpreter, etc.) without flushing. + const int neonreg = _getFreeArm64NEON(NEON_CALLEE_SAVED_START, NEON_CALLEE_SAVED_END); + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = NEONTYPE_GPRREG; + arm64neon[neonreg].reg = gprreg; + arm64neon[neonreg].mode = mode; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; + + if (mode & MODE_READ) + { + if (gprreg == 0) + { + armAsm->Movi(armQRegister(neonreg).V2D(), 0); + } + else if (GPR_IS_CONST1(gprreg)) + { + // Load full 128 bits from memory, replace lower 64 with constant + armLoadEERegPtr(armQRegister(neonreg), &cpuRegs.GPR.r[gprreg].UQ); + armAsm->Mov(RXSCRATCH, static_cast(g_cpuConstRegs[gprreg].SD[0])); + armAsm->Ins(armQRegister(neonreg).V2D(), 0, RXSCRATCH); + arm64neon[neonreg].mode |= MODE_WRITE; + g_cpuFlushedConstReg |= (1u << gprreg); + + if (hostGPRreg >= 0) + _freeArm64GPRWithoutWriteback(hostGPRreg); + } + else if (hostGPRreg >= 0) + { + // Load full 128, replace lower if dirty + armLoadEERegPtr(armQRegister(neonreg), &cpuRegs.GPR.r[gprreg].UQ); + if (arm64gprs[hostGPRreg].mode & MODE_WRITE) + { + armAsm->Ins(armQRegister(neonreg).V2D(), 0, armXRegister(hostGPRreg)); + _freeArm64GPRWithoutWriteback(hostGPRreg); + arm64neon[neonreg].mode |= MODE_WRITE; + } + } + else + { + armLoadEERegPtr(armQRegister(neonreg), &cpuRegs.GPR.r[gprreg].UQ); + } + } + + if (mode & MODE_WRITE && gprreg < 32 && GPR_IS_CONST1(gprreg)) + GPR_DEL_CONST(gprreg); + if (mode & MODE_WRITE && hostGPRreg >= 0) + _freeArm64GPRWithoutWriteback(hostGPRreg); + + return neonreg; +} + +int _allocFPACCtoNEONreg(int mode) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (!arm64neon[i].inuse || arm64neon[i].type != NEONTYPE_FPACC) + continue; + + // Same invariant as _allocFPtoNEONreg: the slot already holds the + // authoritative value (loaded or freshly written). Reloading would + // clobber a MODE_WRITE-only ACC with stale memory, so a later read of + // ACC must consume the value emitted earlier in the same block rather + // than the pre-block memory image. + arm64neon[i].counter = g_neonAllocCounter++; + arm64neon[i].needed = 1; + arm64neon[i].mode |= mode; + return i; + } + + const int neonreg = _getFreeArm64NEON(); + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = NEONTYPE_FPACC; + arm64neon[neonreg].reg = 0; + arm64neon[neonreg].mode = mode; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; + + if (mode & MODE_READ) + { + armLoadEERegPtr(armSRegister(neonreg), &fpuRegs.ACC.f); + } + + return neonreg; +} + +int _allocVFtoNEONreg(int vfreg, int mode) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (!arm64neon[i].inuse || arm64neon[i].type != NEONTYPE_VFREG || arm64neon[i].reg != vfreg) + continue; + + if (!(arm64neon[i].mode & MODE_READ) && (mode & MODE_READ)) + { + armLoadPtr(armQRegister(i), &VU0.VF[vfreg]); + arm64neon[i].mode |= MODE_READ; + } + + arm64neon[i].counter = g_neonAllocCounter++; + arm64neon[i].needed = 1; + arm64neon[i].mode |= mode; + return i; + } + + const int neonreg = _getFreeArm64NEON(); + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = NEONTYPE_VFREG; + arm64neon[neonreg].reg = vfreg; + arm64neon[neonreg].mode = mode; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; + + if (mode & MODE_READ) + armLoadPtr(armQRegister(neonreg), &VU0.VF[vfreg]); + + return neonreg; +} + +void _writebackNEONreg(int neonreg) +{ + switch (arm64neon[neonreg].type) + { + case NEONTYPE_GPRREG: + { + // EE GPRs are 128-bit. Store the full Q register so MMI ops (which + // write all 128 bits via eeRecompileCodeXMM) preserve their upper + // 64-bit lanes through the writeback. _allocGPRtoNEONreg always + // loads 128 bits on MODE_READ, so writeback symmetry is required. + const int reg = arm64neon[neonreg].reg; + if (reg == NEONGPR_LO) + armStorePtr(armQRegister(neonreg), &cpuRegs.LO.UQ); + else if (reg == NEONGPR_HI) + armStorePtr(armQRegister(neonreg), &cpuRegs.HI.UQ); + else + armStorePtr(armQRegister(neonreg), &cpuRegs.GPR.r[reg].UQ); + } + break; + + case NEONTYPE_FPREG: + { + armStoreEERegPtr(armSRegister(neonreg), &fpuRegs.fpr[arm64neon[neonreg].reg].f); + } + break; + + case NEONTYPE_FPACC: + { + armStoreEERegPtr(armSRegister(neonreg), &fpuRegs.ACC.f); + } + break; + + case NEONTYPE_VFREG: + armStorePtr(armQRegister(neonreg), &VU0.VF[arm64neon[neonreg].reg]); + break; + + default: + break; + } +} + +void _freeNEONreg(int neonreg) +{ + pxAssert(neonreg >= 0 && neonreg < NUM_ARM_NEON_REGS); + if (!arm64neon[neonreg].inuse) + return; + + if (arm64neon[neonreg].mode & MODE_WRITE) + _writebackNEONreg(neonreg); + + arm64neon[neonreg].inuse = 0; + arm64neon[neonreg].mode = 0; +} + +void _freeNEONregWithoutWriteback(int neonreg) +{ + pxAssert(neonreg >= 0 && neonreg < NUM_ARM_NEON_REGS); + arm64neon[neonreg].inuse = 0; + arm64neon[neonreg].mode = 0; +} + +void _freeNEONregs() +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse) + _freeNEONreg(i); + } +} + +void _flushNEONreg(int neonreg) +{ + if (arm64neon[neonreg].inuse && (arm64neon[neonreg].mode & MODE_WRITE)) + { + _writebackNEONreg(neonreg); + arm64neon[neonreg].mode &= ~MODE_WRITE; + arm64neon[neonreg].mode |= MODE_READ; + } +} + +void _flushNEONregs() +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + _flushNEONreg(i); +} + +void _addNeededFPtoNEONreg(int fpreg) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_FPREG && arm64neon[i].reg == fpreg) + arm64neon[i].needed = 1; + } +} + +void _addNeededFPACCtoNEONreg() +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_FPACC) + arm64neon[i].needed = 1; + } +} + +void _addNeededGPRtoNEONreg(int gprreg) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_GPRREG && arm64neon[i].reg == gprreg) + arm64neon[i].needed = 1; + } +} + +void _clearNeededNEONregs() +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].needed && arm64neon[i].type == NEONTYPE_TEMP) + _freeNEONreg(i); + arm64neon[i].needed = 0; + } +} + +void _deleteGPRtoNEONreg(int reg, int flush) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_GPRREG && arm64neon[i].reg == reg) + { + switch (flush) + { + case DELETE_REG_FREE: _freeNEONreg(i); break; + case DELETE_REG_FLUSH: + if (arm64neon[i].mode & MODE_WRITE) + { + _writebackNEONreg(i); + // Drop MODE_WRITE (keep MODE_READ) so a later + // _freeNEONreg won't store the same value again. + arm64neon[i].mode = (arm64neon[i].mode & ~MODE_WRITE) | MODE_READ; + } + break; + case DELETE_REG_FLUSH_AND_FREE: _freeNEONreg(i); break; + case DELETE_REG_FREE_NO_WRITEBACK: _freeNEONregWithoutWriteback(i); break; + } + return; + } + } +} + +void _deleteFPtoNEONreg(int reg, int flush) +{ + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_FPREG && arm64neon[i].reg == reg) + { + switch (flush) + { + case DELETE_REG_FREE: _freeNEONreg(i); break; + case DELETE_REG_FLUSH: + if (arm64neon[i].mode & MODE_WRITE) + { + _writebackNEONreg(i); + // Drop MODE_WRITE (keep MODE_READ) so a later + // _freeNEONreg won't store the same value again. + arm64neon[i].mode = (arm64neon[i].mode & ~MODE_WRITE) | MODE_READ; + } + break; + case DELETE_REG_FLUSH_AND_FREE: _freeNEONreg(i); break; + case DELETE_REG_FREE_NO_WRITEBACK: _freeNEONregWithoutWriteback(i); break; + } + return; + } + } +} + +void _reallocateNEONreg(int neonreg, int newtype, int newreg, int newmode, bool writeback) +{ + if (arm64neon[neonreg].inuse && writeback) + _writebackNEONreg(neonreg); + + arm64neon[neonreg].inuse = 1; + arm64neon[neonreg].type = newtype; + arm64neon[neonreg].reg = newreg; + arm64neon[neonreg].mode = newmode; + arm64neon[neonreg].needed = 1; + arm64neon[neonreg].counter = g_neonAllocCounter++; +} + +int _allocIfUsedGPRtoNEON(int gprreg, int mode) +{ + return EEINST_XMMUSEDTEST(gprreg) ? _allocGPRtoNEONreg(gprreg, mode) : -1; +} + +int _allocIfUsedFPUtoNEON(int fpureg, int mode) +{ + return FPUINST_USEDTEST(fpureg) ? _allocFPtoNEONreg(fpureg, mode) : -1; +} + +void _flushCOP2regs() +{ + // Flush any VU registers cached in host regs + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_VFREG) + _freeNEONreg(i); + } + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == ARM64TYPE_VIREG) + _freeArm64GPR(i); + } +} + +// Stubs for COP2 reserved register management +void mVUFreeCOP2GPR(int hostreg) +{ +} + +bool mVUIsReservedCOP2(int hostreg) +{ + return false; +} + +void mVUFreeCOP2NEONreg(int hostreg) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// Architecture-independent utility functions + +void _recClearInst(EEINST* pinst) +{ + std::memset(pinst, 0, sizeof(EEINST)); + std::memset(pinst->regs, EEINST_LIVE, sizeof(pinst->regs)); + std::memset(pinst->fpuregs, EEINST_LIVE, sizeof(pinst->fpuregs)); + std::memset(pinst->vfregs, EEINST_LIVE, sizeof(pinst->vfregs)); + std::memset(pinst->viregs, EEINST_LIVE, sizeof(pinst->viregs)); +} + +u32 _recIsRegReadOrWritten(EEINST* pinst, int size, u8 xmmtype, u8 reg) +{ + u32 inst = 1; + while (size-- > 0) + { + for (u32 i = 0; i < std::size(pinst->writeType); ++i) + { + if ((pinst->writeType[i] == xmmtype) && (pinst->writeReg[i] == reg)) + return inst; + } + for (u32 i = 0; i < std::size(pinst->readType); ++i) + { + if ((pinst->readType[i] == xmmtype) && (pinst->readReg[i] == reg)) + return inst; + } + ++inst; + pinst++; + } + return 0; +} + +void _recFillRegister(EEINST& pinst, int type, int reg, int write) +{ + if (write) + { + for (u32 i = 0; i < std::size(pinst.writeType); ++i) + { + if (pinst.writeType[i] == NEONTYPE_TEMP) + { + pinst.writeType[i] = type; + pinst.writeReg[i] = reg; + return; + } + } + pxAssume(false); + } + else + { + for (u32 i = 0; i < std::size(pinst.readType); ++i) + { + if (pinst.readType[i] == NEONTYPE_TEMP) + { + pinst.readType[i] = type; + pinst.readReg[i] = reg; + return; + } + } + pxAssume(false); + } +} diff --git a/pcsx2/arm64/iCore-arm64.h b/pcsx2/arm64/iCore-arm64.h new file mode 100644 index 0000000000..4332713de6 --- /dev/null +++ b/pcsx2/arm64/iCore-arm64.h @@ -0,0 +1,327 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "arm64/AsmHelpers.h" +#include "VUmicro.h" + +// ARM64 Register Allocator +// Mirrors the x86 allocator in x86/iCore.h but adapted for ARM64 register conventions. + +//#define RALOG(...) fprintf(stderr, __VA_ARGS__) +#define RALOG(...) + +//////////////////////////////////////////////////////////////////////////////// +// Shared Register Allocation Flags (same as x86 — shared with instruction codegen) + +#define MODE_READ 1 +#define MODE_WRITE 2 +#define MODE_CALLEESAVED 0x20 +#define MODE_COP2 0x40 + +#define PROCESS_EE_XMM 0x02 + +#define PROCESS_EE_S 0x04 +#define PROCESS_EE_T 0x08 +#define PROCESS_EE_D 0x10 + +#define PROCESS_EE_LO 0x40 +#define PROCESS_EE_HI 0x80 +#define PROCESS_EE_ACC 0x40 + +// Extract host register index from info bitmask. +// ARM64 needs 5 bits per field (registers 0-28), unlike x86 which uses 4 bits (0-15). +// Fields are packed into a 32-bit info word with 5-bit register indices. +// +// NOTE: EEREC_LO, EEREC_HI and EEREC_ACC intentionally decode the SAME field +// (bits 23..27). Five distinct 5-bit fields plus the presence flags overflow the +// 32-bit word, so LO/HI/ACC share one slot. This is only sound because no op needs +// two of them live simultaneously through the allocator: integer MULT/DIV/MADD and +// PMFHL load LO/HI directly from memory (bypassing the allocator), and ACC is used +// exclusively by FPU ops (never alongside LO/HI). eeRecompileCodeXMM asserts the one +// dangerous combination (LO+HI); an op needing both must bypass the allocator too. +#define EEREC_S (((info) >> 8) & 0x1f) +#define EEREC_T (((info) >> 13) & 0x1f) +#define EEREC_D (((info) >> 18) & 0x1f) +#define EEREC_LO (((info) >> 23) & 0x1f) +#define EEREC_HI (((info) >> 23) & 0x1f) +#define EEREC_ACC (((info) >> 23) & 0x1f) + +#define PROCESS_EE_SET_S(reg) (((reg) << 8) | PROCESS_EE_S) +#define PROCESS_EE_SET_T(reg) (((reg) << 13) | PROCESS_EE_T) +#define PROCESS_EE_SET_D(reg) (((reg) << 18) | PROCESS_EE_D) +#define PROCESS_EE_SET_LO(reg) (((reg) << 23) | PROCESS_EE_LO) +#define PROCESS_EE_SET_HI(reg) (((reg) << 23) | PROCESS_EE_HI) +#define PROCESS_EE_SET_ACC(reg) (((reg) << 23) | PROCESS_EE_ACC) + +#define PROCESS_CONSTS 1 +#define PROCESS_CONSTT 2 + +//////////////////////////////////////////////////////////////////////////////// +// NEON (128-bit) Register Allocation — equivalent to XMM on x86 + +enum xmminfo : u16 +{ + XMMINFO_READLO = 0x001, + XMMINFO_READHI = 0x002, + XMMINFO_WRITELO = 0x004, + XMMINFO_WRITEHI = 0x008, + XMMINFO_WRITED = 0x010, + XMMINFO_READD = 0x020, + XMMINFO_READS = 0x040, + XMMINFO_READT = 0x080, + XMMINFO_READACC = 0x200, + XMMINFO_WRITEACC = 0x400, + XMMINFO_WRITET = 0x800, + + XMMINFO_64BITOP = 0x1000, + XMMINFO_FORCEREGS = 0x2000, + XMMINFO_FORCEREGT = 0x4000, + XMMINFO_NORENAME = 0x8000 +}; + +//////////////////////////////////////////////////////////////////////////////// +// ARM64 GPR Register Allocation + +// Total number of ARM64 GPR registers we track (x0-x30 = 31) +static constexpr int NUM_ARM_GPR_REGS = 31; + +// Total number of ARM64 NEON registers we track (q0-q28, excluding q29-q31 scratch) +static constexpr int NUM_ARM_NEON_REGS = 29; + +enum arm64gprtype : u8 +{ + ARM64TYPE_TEMP = 0, + ARM64TYPE_GPR = 1, // EE GPR (lower 64 bits) + ARM64TYPE_FPRC = 2, // FPU control register + ARM64TYPE_VIREG = 3, // VU integer register + ARM64TYPE_PCWRITEBACK = 4, + ARM64TYPE_PSX = 5, // IOP GPR + ARM64TYPE_PSX_PCWRITEBACK = 6 +}; + +struct _arm64gprregs +{ + u8 inuse; + s8 reg; // guest register index + u8 mode; // MODE_READ / MODE_WRITE + u8 needed; // pinned for current instruction + u8 type; // ARM64TYPE_* + u16 counter; // LRU counter + u32 extra; // extra info (e.g., IOP constant regs) +}; + +// NEON register types — same values as XMM types for compatibility +#define NEONTYPE_TEMP 0 +#define NEONTYPE_GPRREG 1 // EE GPR (full 128 bits) +#define NEONTYPE_FPREG 6 // FPU register +#define NEONTYPE_FPACC 7 // FPU accumulator +#define NEONTYPE_VFREG 8 // VU VF register + +// x86 type aliases — used by shared analysis code (iR5900Analysis.cpp) +#define XMMTYPE_TEMP NEONTYPE_TEMP +#define XMMTYPE_GPRREG NEONTYPE_GPRREG +#define XMMTYPE_FPREG NEONTYPE_FPREG +#define XMMTYPE_VFREG NEONTYPE_VFREG +#define X86TYPE_VIREG ARM64TYPE_VIREG +#define X86TYPE_GPR ARM64TYPE_GPR + +// Register index aliases for analysis (same values as x86) +#define XMMGPR_LO NEONGPR_LO // 33 +#define XMMGPR_HI NEONGPR_HI // 32 +#define XMMFPU_ACC NEONFPU_ACC // 32 + +#define NEONGPR_LO 33 +#define NEONGPR_HI 32 +#define NEONFPU_ACC 32 + +enum : int +{ + DELETE_REG_FREE = 0, + DELETE_REG_FLUSH = 1, + DELETE_REG_FLUSH_AND_FREE = 2, + DELETE_REG_FREE_NO_WRITEBACK = 3 +}; + +struct _arm64neonregs +{ + u8 inuse; + s8 reg; // guest register index + u8 type; // NEONTYPE_* + u8 mode; // MODE_READ / MODE_WRITE + u8 needed; // pinned for current instruction + u16 counter; // LRU counter +}; + +//////////////////////////////////////////////////////////////////////////////// +// ARM64 GPR allocator functions + +extern _arm64gprregs arm64gprs[NUM_ARM_GPR_REGS], s_saveArm64GPRregs[NUM_ARM_GPR_REGS]; + +bool _isAllocatableArm64GPR(int armreg); +void _initArm64GPRregs(); +int _getFreeArm64GPR(int mode); +int _allocArm64GPR(int type, int reg, int mode); +int _checkArm64GPR(int type, int reg, int mode); +bool _hasArm64GPR(int type, int reg, int required_mode = 0); +void _addNeededArm64GPR(int type, int reg); +void _clearNeededArm64GPRregs(); +void _freeArm64GPR(int armreg); +void _freeArm64GPRWithoutWriteback(int armreg); +void _freeArm64GPRregs(); +void _flushArm64GPRregs(); +void _flushConstRegs(bool delete_const); +void _flushConstReg(int reg); +void _validateRegs(); +void _writebackArm64GPR(int armreg); + +void mVUFreeCOP2GPR(int hostreg); +bool mVUIsReservedCOP2(int hostreg); + +//////////////////////////////////////////////////////////////////////////////// +// ARM64 NEON allocator functions + +extern _arm64neonregs arm64neon[NUM_ARM_NEON_REGS], s_saveArm64NEONregs[NUM_ARM_NEON_REGS]; + +void _initArm64NEONregs(); +int _getFreeArm64NEON(u32 maxreg = NUM_ARM_NEON_REGS); +int _allocTempNEONreg(); +int _allocFPtoNEONreg(int fpreg, int mode); +int _allocGPRtoNEONreg(int gprreg, int mode); +int _allocFPACCtoNEONreg(int mode); +void _reallocateNEONreg(int neonreg, int newtype, int newreg, int newmode, bool writeback = true); +int _checkNEONreg(int type, int reg, int mode); +bool _hasNEONreg(int type, int reg, int required_mode = 0); +void _addNeededFPtoNEONreg(int fpreg); +void _addNeededFPACCtoNEONreg(); +void _addNeededGPRtoArm64GPR(int gprreg); +void _addNeededPSXtoArm64GPR(int gprreg); +void _addNeededGPRtoNEONreg(int gprreg); +void _clearNeededNEONregs(); +void _deleteGPRtoArm64GPR(int reg, int flush); +void _deletePSXtoArm64GPR(int reg, int flush); +void _deleteGPRtoNEONreg(int reg, int flush); +void _deleteFPtoNEONreg(int reg, int flush); +void _freeNEONreg(int neonreg); +void _freeNEONregWithoutWriteback(int neonreg); +void _freeNEONregs(); +void _writebackNEONreg(int neonreg); +int _allocVFtoNEONreg(int vfreg, int mode); +void mVUFreeCOP2NEONreg(int hostreg); +void _flushCOP2regs(); +void _flushNEONreg(int neonreg); +void _flushNEONregs(); + +//////////////////////////////////////////////////////////////////////////////// +// Instruction Info — architecture-independent, shared with x86 +// (EEINST, liveness analysis, etc.) + +#define EEINST_LIVE 1 +#define EEINST_LASTUSE 8 +#define EEINST_XMM 0x20 // keep name for compat — means "will use NEON/128-bit" +#define EEINST_USED 0x40 + +#define EEINST_COP2_DENORMALIZE_STATUS_FLAG 0x100 +#define EEINST_COP2_NORMALIZE_STATUS_FLAG 0x200 +#define EEINST_COP2_STATUS_FLAG 0x400 +#define EEINST_COP2_MAC_FLAG 0x800 +#define EEINST_COP2_CLIP_FLAG 0x1000 +#define EEINST_COP2_SYNC_VU0 0x2000 +#define EEINST_COP2_FINISH_VU0 0x4000 +#define EEINST_COP2_FLUSH_VU0_REGISTERS 0x8000 + +struct EEINST +{ + u16 info; + u8 regs[34]; // HI=32, LO=33 + u8 fpuregs[33]; // ACC=32 + u8 vfregs[34]; // ACC=32, I=33 + u8 viregs[16]; + + u8 writeType[3], writeReg[3]; + u8 readType[4], readReg[4]; +}; + +extern EEINST* g_pCurInstInfo; +extern void _recClearInst(EEINST* pinst); +extern u32 _recIsRegReadOrWritten(EEINST* pinst, int size, u8 xmmtype, u8 reg); +extern void _recFillRegister(EEINST& pinst, int type, int reg, int write); + +#define EE_WRITE_DEAD_VALUES 1 + +static __fi bool EEINST_USEDTEST(u32 reg) +{ + return (g_pCurInstInfo->regs[reg] & (EEINST_USED | EEINST_LASTUSE)) == EEINST_USED; +} + +static __fi bool EEINST_XMMUSEDTEST(u32 reg) +{ + return (g_pCurInstInfo->regs[reg] & (EEINST_USED | EEINST_XMM | EEINST_LASTUSE)) == (EEINST_USED | EEINST_XMM); +} + +static __fi bool EEINST_VFUSEDTEST(u32 reg) +{ + return (g_pCurInstInfo->vfregs[reg] & (EEINST_USED | EEINST_LASTUSE)) == EEINST_USED; +} + +static __fi bool EEINST_VIUSEDTEST(u32 reg) +{ + return (g_pCurInstInfo->viregs[reg] & (EEINST_USED | EEINST_LASTUSE)) == EEINST_USED; +} + +static __fi bool EEINST_LIVETEST(u32 reg) +{ + return EE_WRITE_DEAD_VALUES || ((g_pCurInstInfo->regs[reg] & EEINST_LIVE) != 0); +} + +static __fi bool EEINST_RENAMETEST(u32 reg) +{ + return (reg == 0 || !EEINST_USEDTEST(reg) || !EEINST_LIVETEST(reg)); +} + +static __fi bool FPUINST_ISLIVE(u32 reg) { return !!(g_pCurInstInfo->fpuregs[reg] & EEINST_LIVE); } +static __fi bool FPUINST_LASTUSE(u32 reg) { return !!(g_pCurInstInfo->fpuregs[reg] & EEINST_LASTUSE); } + +static __fi bool FPUINST_USEDTEST(u32 reg) +{ + return (g_pCurInstInfo->fpuregs[reg] & (EEINST_USED | EEINST_LASTUSE)) == EEINST_USED; +} + +static __fi bool FPUINST_LIVETEST(u32 reg) +{ + return EE_WRITE_DEAD_VALUES || FPUINST_ISLIVE(reg); +} + +static __fi bool FPUINST_RENAMETEST(u32 reg) +{ + return (!EEINST_USEDTEST(reg) || !EEINST_LIVETEST(reg)); +} + +extern u16 g_arm64AllocCounter; +extern u16 g_neonAllocCounter; + +// Allocates only if later instructions use this register +int _allocIfUsedGPRtoArm64(int gprreg, int mode); +int _allocIfUsedVItoArm64(int vireg, int mode); +int _allocIfUsedGPRtoNEON(int gprreg, int mode); +int _allocIfUsedFPUtoNEON(int fpureg, int mode); + +//////////////////////////////////////////////////////////////////////////////// +// Flush call parameters — same values as x86 for compatibility + +#define FLUSH_NONE 0x000 +#define FLUSH_CONSTANT_REGS 0x001 +#define FLUSH_FLUSH_XMM 0x002 // flush NEON regs (keep name for compat) +#define FLUSH_FREE_XMM 0x004 // flush + free NEON regs +#define FLUSH_ALL_X86 0x020 // flush ARM64 GPRs (keep name for compat) +#define FLUSH_FREE_TEMP_X86 0x040 // flush + free temp ARM64 GPRs +#define FLUSH_FREE_NONTEMP_X86 0x080 // free non-temp ARM64 GPRs +#define FLUSH_FREE_VU0 0x100 +#define FLUSH_PC 0x200 +#define FLUSH_CODE 0x800 + +#define FLUSH_EVERYTHING 0x1ff +#define FLUSH_INTERPRETER 0xfff +#define FLUSH_FULLVTLB 0x000 +#define FLUSH_NODESTROY (FLUSH_CONSTANT_REGS | FLUSH_FLUSH_XMM | FLUSH_ALL_X86) diff --git a/pcsx2/arm64/iFPU-arm64.cpp b/pcsx2/arm64/iFPU-arm64.cpp new file mode 100644 index 0000000000..c1a08c6d96 --- /dev/null +++ b/pcsx2/arm64/iFPU-arm64.cpp @@ -0,0 +1,924 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE FPU (COP1) Instruction Codegen — NEON-based +// Transfer ops (MFC1/MTC1/CFC1/CTC1): native with NEON allocation. +// Branch ops (BC1F/BC1T): native, read fprc[31] directly. +// Arithmetic ops: interpreter fallback (PS2 float clamping needed). + +#include "arm64/iR5900-arm64.h" + +#include + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { +namespace COP1 { + +namespace Interp = R5900::Interpreter::OpcodeImpl::COP1; + +#ifdef FORCE_INTERP_FPU +REC_FUNC(CFC1); +REC_FUNC(CTC1); +REC_FUNC(MFC1); +REC_FUNC(MTC1); +REC_SYS(BC1F); +REC_SYS(BC1T); +REC_SYS(BC1FL); +REC_SYS(BC1TL); +#else + +#define _Ft_ _Rt_ +#define _Fs_ _Rd_ +#define _Fd_ _Sa_ + +#define FPUflagC 0x00800000 +#define FPUflagI 0x00020000 +#define FPUflagD 0x00010000 +#define FPUflagSI 0x00000040 +#define FPUflagSD 0x00000020 + +//------------------------------------------------------------------ +// CFC1 — rt = fprc[fs] (read FPU control register) +//------------------------------------------------------------------ +void recCFC1() +{ + if (!_Rt_) return; + + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + + if (_Fs_ >= 16) + { + // FCR31: mask out always-zero bits, set always-one bits + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->And(RWSCRATCH, RWSCRATCH, 0x0083c078); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x01000001); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + } + else + { + // FCR0: read-only revision register + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[0]); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + } + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); +} + +//------------------------------------------------------------------ +// CTC1 — fprc[fs] = rt (write FPU control register) +//------------------------------------------------------------------ +void recCTC1() +{ + if (_Fs_ != 31) return; + + if (GPR_IS_CONST1(_Rt_)) + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + else + { + _deleteEEreg(_Rt_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); + } + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[_Fs_]); +} + +//------------------------------------------------------------------ +// MFC1 — rt = sign_extend(fpr[fs]) (move 32-bit float to GPR) +//------------------------------------------------------------------ +void recMFC1() +{ + if (!_Rt_) return; + + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + + // FPR-side allocator coherence: fpr[fs] may be live in NEON (e.g. a + // preceding ADD_S wrote it, possibly MODE_WRITE-only). If it is already + // resident, read it straight from the host reg instead of flushing it to + // memory and reloading (store→load-forward stall on A53). + // MFC1 doesn't modify fpr[fs], so leave the allocator slot intact. Only + // the not-resident case falls back to the memory load. + const int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, MODE_READ); + if (fsreg >= 0) + { + armAsm->Fmov(RWSCRATCH, armSRegister(fsreg)); + } + else + { + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fpr[_Fs_].UL); + } + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); +} + +//------------------------------------------------------------------ +// MTC1 — fpr[fs] = rt[31:0] (move GPR lower 32 bits to FPR) +//------------------------------------------------------------------ +void recMTC1() +{ + if (GPR_IS_CONST1(_Rt_)) + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + else + { + _deleteEEreg(_Rt_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); + } + + // If fpr[fs] is already resident in NEON, write the new bits straight into + // the host reg and mark it dirty (MODE_WRITE), keeping it hot for a + // following FPU op; the block epilogue flushes the S-reg to fpr[fs].f. + // MTC1 overwrites fpr[fs] wholesale, so any prior MODE_WRITE-only value + // in the slot is dead and correctly discarded by overwriting lane 0. + // Not-resident → store to memory. + const int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, MODE_WRITE); + if (fsreg >= 0) + { + armAsm->Fmov(armSRegister(fsreg), RWSCRATCH); + } + else + { + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fpr[_Fs_].UL); + } +} + +//------------------------------------------------------------------ +// BC1F / BC1T — branch on FPU condition flag +//------------------------------------------------------------------ + +// FPU branch setup: flush state and test fprc[31] condition flag. +// Emits conditional forward branch (skip label), matching EE branch pattern. +// bne=false: BC1F (skip if C set), bne=true: BC1T (skip if C clear) +static a64::Label* s_pBC1Label = nullptr; + +static void recSetBranchBC1(bool branchOnTrue) +{ + _eeFlushAllDirty(); + + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + + // FPUflagC (0x00800000) is a single fixed bit (23), so the Tst+B.cond pair + // collapses to one test-bit-and-branch (Tbnz/Tbz). The forward branch + // skips the delay slot on the not-taken edge. + static_assert(FPUflagC == (1u << 23), "FPUflagC must be a single bit for Tbz/Tbnz"); + s_pBC1Label = new a64::Label(); + if (branchOnTrue) + armAsm->Tbz(RWSCRATCH, 23, s_pBC1Label); // BC1T: skip taken if C clear + else + armAsm->Tbnz(RWSCRATCH, 23, s_pBC1Label); // BC1F: skip taken if C set +} + +static void recBindBC1Label() +{ + armAsm->Bind(s_pBC1Label); + delete s_pBC1Label; + s_pBC1Label = nullptr; +} + +void recBC1F() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + const bool swap = TrySwapDelaySlot(0, 0, 0, true); + recSetBranchBC1(false); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC1Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBC1T() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + const bool swap = TrySwapDelaySlot(0, 0, 0, true); + recSetBranchBC1(true); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC1Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBC1FL() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + recSetBranchBC1(false); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC1Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +void recBC1TL() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + recSetBranchBC1(true); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC1Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +#undef _Ft_ +#undef _Fs_ +#undef _Fd_ + +#endif // !FORCE_INTERP_FPU + +//------------------------------------------------------------------ +// FPU Arithmetic — lightweight interpreter call +// FPU ops only touch fpuRegs memory, not cpuRegs.GPR. EE GPRs are +// in callee-saved NEON registers (q8-q15) that survive C calls. +// Only flush PC/code for exception handling — skip NEON flush. +//------------------------------------------------------------------ + +static void recFPUCall(void (*func)()) +{ + // Flush PC and code (needed if FPU op triggers an exception) + armAsm->Mov(RWSCRATCH, pc); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + + armAsm->Mov(RWSCRATCH, cpuRegs.code); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.code)); + + // FPU allocator coherence: the interpreter reads fpuRegs.fpr[] and + // fpuRegs.ACC directly, and those values can live in NEON slots + // (MODE_WRITE-only) until block-end flush — so writeback every + // FPREG/FPACC slot before the call. EE GPRs in callee-saved q8-q15 + // survive (FPU interpreter doesn't touch cpuRegs.GPR), so iFlushCall's + // full eviction is not needed here. + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && + (arm64neon[i].type == NEONTYPE_FPREG || arm64neon[i].type == NEONTYPE_FPACC)) + { + _freeNEONreg(i); + } + } + + armEmitCall((void*)func); +} + + +#define _Ft_ _Rt_ +#define _Fs_ _Rd_ +#define _Fd_ _Sa_ + +// PS2 FPU max representable float (no infinity) — exactly ±FLT_MAX +// under IEEE-754 single precision: 0x7F7FFFFF / 0xFF7FFFFF. +// Load-bearing for the EE/mVU dispatcher prologues that park these bit +// patterns in s8/s9 — keep this assert if the type of FLT_MAX ever changes. +static_assert(FLT_MAX == 3.40282346638528859811704183484516925e+38f, + "FLT_MAX must be the IEEE-754 0x7F7FFFFF bit pattern (PS2 FPU clamp upper bound)."); + +// Clamp a result in `fpr` to PS2 float range (no inf/nan). +// +// Branchless Fminnm/Fmaxnm: the Number variants are NaN-eating (matching +// x86 MINSS/MAXSS), so NaN routes through Fminnm to +max and Fmaxnm +// passes it through. Both ±Inf get clamped to ±max. +// +// The ±FLT_MAX bounds live in callee-saved s8/s9 — loaded once at JIT +// session entry in `_DynGen_EnterRecompiledCode` and held across every +// armEmitCall via AAPCS64. v8/v9 are excluded from the NEON allocator +// pool so no codegen path can clobber them. 2 host insns per clamp. +// +// NaN sign is not preserved (matches x86 fpuFloat / ClampValues; the +// PS2 FPU has no NaN concept so this is design-correct). +static void fpuClampResult(const a64::VRegister& fpr) +{ + armAsm->Fminnm(fpr, fpr, a64::s8); + armAsm->Fmaxnm(fpr, fpr, a64::s9); +} + +// One-sided positive clamp for results that are statically non-negative +// (ABS.S). Fabs clears the sign bit, so the value is always >= 0 (NaN -> +// +0x7FFFFFFF), which makes the lower Fmaxnm(-FLT_MAX) of fpuClampResult dead. +// Like x86 recABS_S_xmm, only the positive clamp is needed (the ABS result is +// never negative), so only one Fminnm vs 0x7f7fffff is emitted. +// Saves one NEON insn per ABS.S. +static void fpuClampResultPositive(const a64::VRegister& fpr) +{ + armAsm->Fminnm(fpr, fpr, a64::s8); +} + +// Sign-preserving operand clamp for FPU comparisons (C.cond.S). +// +// Mirrors the x86 JIT's fpuFloat3 (PMIN.SD vs 0x7f7fffff then PMIN.UD vs +// 0xff7fffff): +NaN->+fMax, -NaN->-fMax, +Inf->+fMax, -Inf->-fMax. The PS2 +// FPU has no Inf/NaN concept, so both compare operands must be clamped first; +// a raw Fcmp on an unclamped NaN would go unordered (all-false) where the PS2 +// wants an ordered compare against ±FLT_MAX. +// +// Integer SMIN/UMIN preserve the sign bit — unlike fpuClampResult's +// Fminnm/Fmaxnm, which fold every NaN to +fMax and would mis-order -NaN. +// s8/s9 already hold the 0x7f7fffff / 0xff7fffff bit patterns. Only lane 0 +// (the scalar S reg) is consumed by the following Fcmp, so the upper V4S +// lanes are don't-care. (Denormal flush-to-zero is intentionally omitted to +// match fpuFloat3; that is a pre-existing shared JIT-vs-interp behavior.) +static void fpuClampCompareOperand(const a64::VRegister& s) +{ + const a64::VRegister v(s.GetCode(), a64::kQRegSize); + armAsm->Smin(v.V4S(), v.V4S(), a64::VRegister(8, a64::kQRegSize).V4S()); + armAsm->Umin(v.V4S(), v.V4S(), a64::VRegister(9, a64::kQRegSize).V4S()); +} + +// Source-operand clamp for the FPU arithmetic family, gated on +// CHECK_FPU_EXTRA_OVERFLOW (per-game GameDB clampMode>=2). When enabled, the +// PS2 FPU recs clamp each fpr source to ±fMax *before* the op — matching the +// interpreter (which routes every operand through fpuDouble) and x86 +// recCommutativeOp/recMADDtemp (fpuFloat2 under the same gate). This catches +// Inf*0 -> NaN / (+Inf)+(-Inf) -> NaN poison where an fpr was filled with raw +// Inf/NaN bits via MOV.S/LWC1/MTC1; without it the op produces a NaN that +// the result clamp folds to +fMax, diverging from the interpreter's +// clamp-then-compute (e.g. fMax*0 = 0). +// +// Copies into `scratch` rather than mutating the allocator-resident source +// (vs x86's in-place fpuFloat2) so a later read of the same fpr in this block +// still sees the unclamped value. Sign-preserving (fpuClampCompareOperand), +// so -Inf -> -fMax. Only fpr-sourced operands (S/T) need this; ACC is written +// only by always-clamping acc-ops and can never be poisoned. In the default +// config (flag off) this emits nothing and returns the source reg. +static a64::VRegister fpuClampInput(const a64::VRegister& src, const a64::VRegister& scratch) +{ + if (!CHECK_FPU_EXTRA_OVERFLOW) + return src; + armAsm->Fmov(scratch, src); + fpuClampCompareOperand(scratch); + return scratch; +} + +// FpuMulHack (Tales of Destiny Remake gamefix, EmuConfig.Gamefixes.FpuMulHack). +// x86 routes every FPU multiply (MUL/MULA/MADD/MSUB) through FPU_MUL, which — +// when the gamefix is on — patches the single specific product 0.25 * (π/2) +// (0x3e800000 * 0x40490fdb) to 0x3f490fda so the game stops hanging in one +// late-game room. Emit `dst = (hit) ? 0x3f490fda : s*t`; callers clamp/accumulate +// dst as they normally would (the magic value is an ordinary small float, so a +// following fpuClampResult is a no-op). In the default config (gamefix off) this +// is a bare Fmul — zero added cost. +static void emitFpuMul(const a64::VRegister& dst, const a64::VRegister& s, const a64::VRegister& t) +{ + if (!CHECK_FPUMULHACK) + { + armAsm->Fmul(dst, s, t); + return; + } + + a64::Label noHack, done; + armAsm->Fmov(RWARG1, s); + armAsm->Fmov(RWARG2, t); + armAsm->Mov(RWSCRATCH, 0x3e800000); + armAsm->Cmp(RWARG1, RWSCRATCH); + armAsm->B(&noHack, a64::ne); + armAsm->Mov(RWSCRATCH, 0x40490fdb); + armAsm->Cmp(RWARG2, RWSCRATCH); + armAsm->B(&noHack, a64::ne); + armAsm->Mov(RWSCRATCH, 0x3f490fda); + armAsm->Fmov(dst, RWSCRATCH); + armAsm->B(&done); + armAsm->Bind(&noHack); + armAsm->Fmul(dst, s, t); + armAsm->Bind(&done); +} + +//------------------------------------------------------------------ +// Simple FPU ops — no clamping needed +//------------------------------------------------------------------ + +static void recMOV_S_xmm(int info) +{ + // MOV.S is a raw bit-copy (PS2 FPR[fd] = FPR[fs]); no clamp/NaN logic. + // Skip the emit entirely when fd and fs alias the same host reg (guest + // fs==fd): the allocator hands back EEREC_D==EEREC_S and the Fmov would be + // an identity self-move. + if (EEREC_D != EEREC_S) + armAsm->Fmov(armSRegister(EEREC_D), armSRegister(EEREC_S)); +} + +void recMOV_S() +{ + eeFPURecompileCode(recMOV_S_xmm, Interp::MOV_S, + XMMINFO_WRITED | XMMINFO_READS); +} + +static void recABS_S_xmm(int info) +{ + armAsm->Fabs(armSRegister(EEREC_D), armSRegister(EEREC_S)); + // ABS output is always non-negative -> one-sided positive clamp. + fpuClampResultPositive(armSRegister(EEREC_D)); +} + +void recABS_S() +{ + eeFPURecompileCode(recABS_S_xmm, Interp::ABS_S, + XMMINFO_WRITED | XMMINFO_READS); +} + +static void recNEG_S_xmm(int info) +{ + armAsm->Fneg(armSRegister(EEREC_D), armSRegister(EEREC_S)); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recNEG_S() +{ + eeFPURecompileCode(recNEG_S_xmm, Interp::NEG_S, + XMMINFO_WRITED | XMMINFO_READS); +} + +//------------------------------------------------------------------ +// FPU Comparisons — set/clear fprc[31] condition bit +//------------------------------------------------------------------ + +void recC_F() +{ + // Always false — clear condition bit + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Mov(RWARG1, FPUflagC); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG1); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); +} + +void recC_EQ() +{ + _deleteFPtoNEONreg(_Fs_, DELETE_REG_FLUSH_AND_FREE); + _deleteFPtoNEONreg(_Ft_, DELETE_REG_FLUSH_AND_FREE); + armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_].f); + armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_].f); + fpuClampCompareOperand(RSSCRATCH); + fpuClampCompareOperand(RSSCRATCH2); + armAsm->Fcmp(RSSCRATCH, RSSCRATCH2); + armAsm->Mov(a64::w0, 0); + armAsm->Cset(a64::w0, a64::eq); + // Set or clear FPUflagC based on result (w0 holds cset result, don't clobber) + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Mov(RWARG2, FPUflagC); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::Operand(a64::w0, a64::LSL, 23)); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); +} + +void recC_LT() +{ + _deleteFPtoNEONreg(_Fs_, DELETE_REG_FLUSH_AND_FREE); + _deleteFPtoNEONreg(_Ft_, DELETE_REG_FLUSH_AND_FREE); + armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_].f); + armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_].f); + fpuClampCompareOperand(RSSCRATCH); + fpuClampCompareOperand(RSSCRATCH2); + armAsm->Fcmp(RSSCRATCH, RSSCRATCH2); + armAsm->Mov(a64::w0, 0); + armAsm->Cset(a64::w0, a64::lt); + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Mov(RWARG2, FPUflagC); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::Operand(a64::w0, a64::LSL, 23)); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); +} + +void recC_LE() +{ + _deleteFPtoNEONreg(_Fs_, DELETE_REG_FLUSH_AND_FREE); + _deleteFPtoNEONreg(_Ft_, DELETE_REG_FLUSH_AND_FREE); + armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_].f); + armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_].f); + fpuClampCompareOperand(RSSCRATCH); + fpuClampCompareOperand(RSSCRATCH2); + armAsm->Fcmp(RSSCRATCH, RSSCRATCH2); + armAsm->Mov(a64::w0, 0); + armAsm->Cset(a64::w0, a64::le); + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Mov(RWARG2, FPUflagC); armAsm->Bic(RWSCRATCH, RWSCRATCH, RWARG2); + armAsm->Orr(RWSCRATCH, RWSCRATCH, a64::Operand(a64::w0, a64::LSL, 23)); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); +} + +//------------------------------------------------------------------ +// FPU Arithmetic — native with PS2 clamping (no inf/nan) +//------------------------------------------------------------------ + +// "Full" / DOUBLE-precision emitters (iFPUd-arm64.cpp), selected per-op when +// CHECK_FPU_FULL (GameDB eeClampMode:3). Default config uses the fast paths below. +namespace DOUBLE { +void recADD_S_xmm(int info); +void recSUB_S_xmm(int info); +void recADDA_S_xmm(int info); +void recSUBA_S_xmm(int info); +void recMUL_S_xmm(int info); +void recMULA_S_xmm(int info); +void recMADD_S_xmm(int info); +void recMSUB_S_xmm(int info); +void recMADDA_S_xmm(int info); +void recMSUBA_S_xmm(int info); +} // namespace DOUBLE + +static void recADD_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + armAsm->Fadd(armSRegister(EEREC_D), s, t); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recADD_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recADD_S_xmm : recADD_S_xmm, Interp::ADD_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +static void recSUB_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + armAsm->Fsub(armSRegister(EEREC_D), s, t); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recSUB_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recSUB_S_xmm : recSUB_S_xmm, Interp::SUB_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +static void recMUL_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(armSRegister(EEREC_D), s, t); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recMUL_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMUL_S_xmm : recMUL_S_xmm, Interp::MUL_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +// Emit: ldr x9, [addr]; msr FPCR, x9 — load a 64-bit FPCR bitmask from `addr`. +// The PS2 FPU divides/sqrts in round-to-nearest while ADD/MUL round toward zero, +// so DIV must briefly swap FPCR to FPUDivFPCR and restore FPUFPCR after (mirrors +// x86 recDIV_S_xmm's xLDMXCSR pair). Uses x8/x9 as scratch. +static void emitLoadFPCR(const void* addr) +{ + armMoveAddressToReg(a64::x8, const_cast(addr)); + armAsm->Ldr(a64::x9, a64::MemOperand(a64::x8)); + armAsm->Msr(a64::FPCR, a64::x9); +} + +// Native DIV.S — port of x86 recDIVhelper1 (CHECK_FPU_EXTRA_FLAGS is always on) +// + the recDIV_S_xmm FPCR round-mode swap. Matches interp DIV_S / +// checkDivideByZero: +// - divisor == 0 (exp field 0; FZ in FPCR flushes denormals so the float +// compare catches them too): result = sign(Fs^Ft) | 0x7f7fffff (±fMax), +// and set I|SI for 0/0, D|SD for x/0; +// - otherwise native Fdiv (round-to-nearest) then ±fMax result clamp. +// I|D are cleared first to match the divide-by-zero result-shape and sticky +// flag semantics of the interpreter. +static void recDIV_S_xmm(int info) +{ + const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; + if (swapFpcr) + emitLoadFPCR(&EmuConfig.Cpu.FPUDivFPCR.bitmask); + + // Copy both operands into temps: EEREC_D may alias EEREC_S/EEREC_T, and the + // div-by-zero path needs the raw (pre-clamp) dividend/divisor sign bits. + const int dreg = _allocTempNEONreg(); + const int treg = _allocTempNEONreg(); + armAsm->Fmov(armSRegister(dreg), armSRegister(EEREC_S)); + armAsm->Fmov(armSRegister(treg), armSRegister(EEREC_T)); + + // Clear I|D. + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagD); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + + a64::Label normal, setMax, xDiv, end; + + armAsm->Fcmp(armSRegister(treg), 0.0); + armAsm->B(&normal, a64::ne); // divisor != 0 → normal divide (unordered too) + + // Divisor is zero: distinguish 0/0 (I|SI) from x/0 (D|SD). + armAsm->Fcmp(armSRegister(dreg), 0.0); + armAsm->B(&xDiv, a64::ne); + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagSI); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->B(&setMax); + armAsm->Bind(&xDiv); + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagD | FPUflagSD); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + + armAsm->Bind(&setMax); + // result = sign(Fs ^ Ft) | 0x7f7fffff + armAsm->Fmov(RWARG1, armSRegister(dreg)); + armAsm->Fmov(RWARG2, armSRegister(treg)); + armAsm->Eor(RWARG1, RWARG1, RWARG2); + armAsm->And(RWARG1, RWARG1, 0x80000000); + armAsm->Orr(RWARG1, RWARG1, 0x7f7fffff); + armAsm->Fmov(armSRegister(EEREC_D), RWARG1); + armAsm->B(&end); + + armAsm->Bind(&normal); + if (CHECK_FPU_EXTRA_OVERFLOW) + { + fpuClampCompareOperand(armSRegister(dreg)); + fpuClampCompareOperand(armSRegister(treg)); + } + armAsm->Fdiv(armSRegister(EEREC_D), armSRegister(dreg), armSRegister(treg)); + fpuClampResult(armSRegister(EEREC_D)); + + armAsm->Bind(&end); + + _freeNEONreg(dreg); + _freeNEONreg(treg); + + if (swapFpcr) + emitLoadFPCR(&EmuConfig.Cpu.FPUFPCR.bitmask); +} + +void recDIV_S() +{ + eeFPURecompileCode(recDIV_S_xmm, Interp::DIV_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +static void recSQRT_S_xmm(int info) +{ + const a64::VRegister ft = armSRegister(EEREC_T); + + // PS2 SQRT.S rounds to nearest regardless of the configured FCR31 rounding + // mode (same hardware quirk as DIV.S — see recDIV_S_xmm + the emitLoadFPCR + // comment). The EE rec runs under host FPCR = FPUFPCR (ChopZero by default), + // so swap to the nearest-rounding FPUDivFPCR around the Fsqrt and restore + // FPUFPCR after. Mirrors x86 recSQRT_S_xmm (iFPU.cpp:1745-1782). + const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; + if (swapFpcr) + emitLoadFPCR(&EmuConfig.Cpu.FPUDivFPCR.bitmask); + + // PS2 SQRT.S flag handling (interp SQRT_S, FPU.cpp; CHECK_FPU_EXTRA_FLAGS + // is always on): clear I|D unconditionally, then set I|SI when Ft is a + // negative *non-zero* value (exp field nonzero AND sign bit set). The + // ±0 / denormal-as-zero case (exp field == 0) sets no flag. Read the Ft + // bits before Fabs clobbers EEREC_D, which may alias EEREC_T. + armAsm->Fmov(RWARG1, ft); + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagD); + a64::Label skipFlag; + armAsm->Tst(RWARG1, 0x7F800000); // exp field + armAsm->B(&skipFlag, a64::eq); // ±0/denorm → no flag + armAsm->Tbz(RWARG1, 31, &skipFlag); // positive → no flag + armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagI | FPUflagSI); + armAsm->Bind(&skipFlag); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + + // PS2 takes sqrt of |ft| → Fabs first. + armAsm->Fabs(armSRegister(EEREC_D), ft); + armAsm->Fsqrt(armSRegister(EEREC_D), armSRegister(EEREC_D)); + fpuClampResult(armSRegister(EEREC_D)); + + if (swapFpcr) + emitLoadFPCR(&EmuConfig.Cpu.FPUFPCR.bitmask); +} + +void recSQRT_S() +{ + eeFPURecompileCode(recSQRT_S_xmm, Interp::SQRT_S, + XMMINFO_WRITED | XMMINFO_READT); +} + +void recRSQRT_S() +{ + // Defer to the interpreter: interp RSQRT_S (FPU.cpp) sets D|SD when Ft + // (divisor) is zero and I|SI when Ft is negative, and its Ft==0 branch + // returns ±posFmax keyed off the Ft sign (not Fs) — neither the sticky + // flags nor that result shape are reproducible by a raw Fdiv. RSQRT is + // rare, so the interpreter call is the lowest-risk match and keeps emitted + // code small. Same shape as recDIV_S. + recFPUCall(Interp::RSQRT_S); +} + +// PS2 FPU has no NaN concept — match x86 MAXSS/MINSS NaN-eating semantics +// with Fmaxnm/Fminnm (Fmax/Fmin IEEE-propagate NaN, same trap as mVUclamp1). +// No clamp needed: MAX/MIN cannot widen finite inputs. +static void recMAX_S_xmm(int info) +{ + armAsm->Fmaxnm(armSRegister(EEREC_D), armSRegister(EEREC_S), armSRegister(EEREC_T)); +} + +void recMAX_S() +{ + eeFPURecompileCode(recMAX_S_xmm, Interp::MAX_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +static void recMIN_S_xmm(int info) +{ + armAsm->Fminnm(armSRegister(EEREC_D), armSRegister(EEREC_S), armSRegister(EEREC_T)); +} + +void recMIN_S() +{ + eeFPURecompileCode(recMIN_S_xmm, Interp::MIN_S, + XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); +} + +//------------------------------------------------------------------ +// FPU Accumulator ops — ACC = fs OP ft, then fd = ACC OP fs2 +//------------------------------------------------------------------ + +static void recADDA_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + armAsm->Fadd(armSRegister(EEREC_ACC), s, t); + fpuClampResult(armSRegister(EEREC_ACC)); +} + +void recADDA_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recADDA_S_xmm : recADDA_S_xmm, Interp::ADDA_S, + XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT); +} + +static void recSUBA_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + armAsm->Fsub(armSRegister(EEREC_ACC), s, t); + fpuClampResult(armSRegister(EEREC_ACC)); +} + +void recSUBA_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recSUBA_S_xmm : recSUBA_S_xmm, Interp::SUBA_S, + XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT); +} + +static void recMULA_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(armSRegister(EEREC_ACC), s, t); + fpuClampResult(armSRegister(EEREC_ACC)); +} + +void recMULA_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMULA_S_xmm : recMULA_S_xmm, Interp::MULA_S, + XMMINFO_WRITEACC | XMMINFO_READS | XMMINFO_READT); +} + +// fd = ACC + fs * ft. PS2 ISA mandates two separate roundings (mul then +// add), so don't fuse into FMA. RSSCRATCH (s30) is the non-pool scratch +// for the intermediate product — leaves EEREC_S/T allocator-resident. +static void recMADD_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(RSSCRATCH, s, t); + fpuClampResult(RSSCRATCH); + armAsm->Fadd(armSRegister(EEREC_D), armSRegister(EEREC_ACC), RSSCRATCH); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recMADD_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMADD_S_xmm : recMADD_S_xmm, Interp::MADD_S, + XMMINFO_WRITED | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT); +} + +// fd = ACC - fs * ft +static void recMSUB_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(RSSCRATCH, s, t); + fpuClampResult(RSSCRATCH); + armAsm->Fsub(armSRegister(EEREC_D), armSRegister(EEREC_ACC), RSSCRATCH); + fpuClampResult(armSRegister(EEREC_D)); +} + +void recMSUB_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMSUB_S_xmm : recMSUB_S_xmm, Interp::MSUB_S, + XMMINFO_WRITED | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT); +} + +// ACC = ACC + fs * ft. Unlike MADD_S, interp MADDA_S (FPU.cpp) adds the raw +// fs*ft product without routing it through fpuDouble — only the final ACC is +// overflow-clamped. So do NOT clamp the intermediate product here, else an +// overflowing product clamped to +-fMax cancels an opposite-signed ACC instead +// of overflowing the accumulate. +static void recMADDA_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(RSSCRATCH, s, t); + armAsm->Fadd(armSRegister(EEREC_ACC), armSRegister(EEREC_ACC), RSSCRATCH); + fpuClampResult(armSRegister(EEREC_ACC)); +} + +void recMADDA_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMADDA_S_xmm : recMADDA_S_xmm, Interp::MADDA_S, + XMMINFO_WRITEACC | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT); +} + +// ACC = ACC - fs * ft. Same as MADDA_S: interp MSUBA_S does not clamp the +// intermediate product, only the final ACC. +static void recMSUBA_S_xmm(int info) +{ + const a64::VRegister s = fpuClampInput(armSRegister(EEREC_S), RSSCRATCH); + const a64::VRegister t = fpuClampInput(armSRegister(EEREC_T), RSSCRATCH2); + emitFpuMul(RSSCRATCH, s, t); + armAsm->Fsub(armSRegister(EEREC_ACC), armSRegister(EEREC_ACC), RSSCRATCH); + fpuClampResult(armSRegister(EEREC_ACC)); +} + +void recMSUBA_S() +{ + eeFPURecompileCode(CHECK_FPU_FULL ? DOUBLE::recMSUBA_S_xmm : recMSUBA_S_xmm, Interp::MSUBA_S, + XMMINFO_WRITEACC | XMMINFO_READACC | XMMINFO_READS | XMMINFO_READT); +} + +// CVT.S: fd = (float)int_bits_of(fpr[fs]) +static void recCVT_S_xmm(int info) +{ + armAsm->Fmov(RWSCRATCH, armSRegister(EEREC_S)); + armAsm->Scvtf(armSRegister(EEREC_D), RWSCRATCH); +} + +void recCVT_S() +{ + eeFPURecompileCode(recCVT_S_xmm, Interp::CVT_S, + XMMINFO_WRITED | XMMINFO_READS); +} + +// CVT.W: fd_bits = (int32_t)fpr[fs] truncating toward zero. +// PS2 clamps overflow to INT32_MAX/MIN — ARM64 Fcvtzs saturates by default, +// matching interp for the finite-overflow and ±Inf cases. The one divergence +// is NaN: ARM Fcvtzs yields 0, but the PS2 (interp CVT_W, FPU.cpp) saturates +// NaN by sign — positive NaN → 0x7fffffff, negative NaN → 0x80000000. Fix up +// the NaN case only (cold branch over the source-sign select). +static void recCVT_W_xmm(int info) +{ + const a64::VRegister fs = armSRegister(EEREC_S); + armAsm->Fcvtzs(RWSCRATCH, fs); + a64::Label done; + armAsm->Fcmp(fs, fs); // NaN → unordered (V set) + armAsm->B(&done, a64::vc); // ordered → keep Fcvtzs result + armAsm->Fmov(RWARG1, fs); // NaN: broadcast source sign + armAsm->Asr(RWARG1, RWARG1, 31); // 0 if +, 0xFFFFFFFF if - + armAsm->Eor(RWSCRATCH, RWARG1, 0x7fffffff); // + → 0x7fffffff, − → 0x80000000 + armAsm->Bind(&done); + armAsm->Fmov(armSRegister(EEREC_D), RWSCRATCH); +} + +void recCVT_W() +{ + eeFPURecompileCode(recCVT_W_xmm, Interp::CVT_W, + XMMINFO_WRITED | XMMINFO_READS); +} + +#undef _Ft_ +#undef _Fs_ +#undef _Fd_ + +} // namespace COP1 +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iFPUd-arm64.cpp b/pcsx2/arm64/iFPUd-arm64.cpp new file mode 100644 index 0000000000..8bb723f19e --- /dev/null +++ b/pcsx2/arm64/iFPUd-arm64.cpp @@ -0,0 +1,412 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE FPU (COP1) — "Full" / DOUBLE-precision codegen. +// +// This is the arm64 port of pcsx2/x86/iFPUd.cpp: the PS2-accurate FPU that +// widens each single to IEEE double, performs the op in double, then narrows +// back to a PS2 single with the hardware's overflow/underflow/clamp semantics. +// It is selected only when CHECK_FPU_FULL (EmuConfig.Cpu.Recompiler.fpuFullMode, +// the GameDB `eeClampMode:3` path — FFX, Max Payne, Dark Cloud 2, Klonoa 2 …). +// Default config runs the single-precision fast path in iFPU-arm64.cpp. +// +// The algorithm is translated from the x86 semantics; the codegen follows the +// iFPU-arm64.cpp idioms (scalar Fcvt, GPR bit-twiddle via Fmov, the +// armLoadEERegPtr fprc[31]/ACCflag accessors). The shared interpreter +// (FPU.cpp fpuDouble) has no double path, so this codegen has no interpreter +// counterpart. + +#include "arm64/iR5900-arm64.h" + +#include + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { +namespace COP1 { +namespace DOUBLE { + +#define _Ft_ _Rt_ +#define _Fs_ _Rd_ +#define _Fd_ _Sa_ + +#define FPUflagO 0x00008000 +#define FPUflagU 0x00004000 +#define FPUflagSO 0x00000010 +#define FPUflagSU 0x00000008 + +// ---- PS2 single -> IEEE double -------------------------------------------- +// +// A PS2 single with exponent field 0xff is a *normal* large number (1.m * 2^128), +// but IEEE reads exp 0xff as Inf/NaN — so a plain cvtss2sd would corrupt it. +// For those (and only those) lower the exponent by one in the single domain, +// widen exactly, then raise the exponent by one in the double domain. Mirrors +// x86 ToDouble (xPSUB.D one_exp / xCVTSS2SD / xPADD.Q dbl_one_exp). +// +// Operates in place on temp NEON reg `idx`: reads the S lane, writes the D lane. +static void ToDouble(int idx) +{ + const a64::VRegister s = armSRegister(idx); + const a64::VRegister d = armDRegister(idx); + + a64::Label simple, done; + armAsm->Fmov(RWSCRATCH, s); + armAsm->And(RWARG1, RWSCRATCH, 0x7f800000); + armAsm->Cmp(RWARG1, 0x7f800000); + armAsm->B(&simple, a64::ne); + + // Complex: exp field == 0xff (Inf/NaN to IEEE, finite to PS2). + armAsm->Sub(RWSCRATCH, RWSCRATCH, 0x00800000); // lower exponent by one (single) + armAsm->Fmov(s, RWSCRATCH); + armAsm->Fcvt(d, s); // cvtss2sd (now finite) + armAsm->Fmov(RXSCRATCH, d); + armAsm->Mov(RXARG1, static_cast(1) << 52); // dbl_one_exp + armAsm->Add(RXSCRATCH, RXSCRATCH, RXARG1); // raise exponent by one (double) + armAsm->Fmov(d, RXSCRATCH); + armAsm->B(&done); + + armAsm->Bind(&simple); + armAsm->Fcvt(d, s); + + armAsm->Bind(&done); +} + +// ---- IEEE double -> PS2 single (full overflow/underflow/flag handling) ----- +// +// Port of x86 ToPS2FPU_Full. `idx` holds the double result (D lane); `absidx` +// is a scratch NEON reg. On return the PS2 single is in `idx`'s S lane. +// Comparisons are done on the integer bit pattern of |x| — valid because every +// operand here is a finite double, so unsigned-integer order == magnitude order +// (sidesteps NaN/unordered, which never reach this point for ADD/SUB/MUL). +static void ToPS2FPU_Full(int idx, bool flags, int /*absidx*/, bool acc, bool addsub) +{ + const a64::VRegister s = armSRegister(idx); + const a64::VRegister d = armDRegister(idx); + + if (flags) + { + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagO | FPUflagU); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + if (acc) + { + armLoadEERegPtr(RWSCRATCH, &fpuRegs.ACCflag); + armAsm->Bic(RWSCRATCH, RWSCRATCH, 1); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.ACCflag); + } + } + + // abs = |reg| (integer, low 63 bits) + armAsm->Fmov(RXSCRATCH, d); + armAsm->And(RXARG1, RXSCRATCH, 0x7fffffffffffffffULL); + + a64::Label toComplex, toUnderflow, toOverflow, end; + + armAsm->Mov(RXARG2, static_cast(1151) << 52); // dbl_cvt_overflow (2^128) + armAsm->Cmp(RXARG1, RXARG2); + armAsm->B(&toComplex, a64::hs); + + armAsm->Mov(RXARG2, static_cast(897) << 52); // dbl_underflow (2^-126) + armAsm->Cmp(RXARG1, RXARG2); + armAsm->B(&toUnderflow, a64::lo); + + // In-range: plain narrow. + armAsm->Fcvt(s, d); + armAsm->B(&end); + + armAsm->Bind(&toComplex); + armAsm->Mov(RXARG2, static_cast(1152) << 52); // dbl_ps2_overflow (2^129) + armAsm->Cmp(RXARG1, RXARG2); + armAsm->B(&toOverflow, a64::hs); + + // Large but PS2-representable (exp-0xff range): lower double exp, narrow, + // raise single exp — the inverse of ToDouble's complex path. + armAsm->Mov(RXARG2, static_cast(1) << 52); + armAsm->Sub(RXSCRATCH, RXSCRATCH, RXARG2); + armAsm->Fmov(d, RXSCRATCH); + armAsm->Fcvt(s, d); + armAsm->Fmov(RWSCRATCH, s); + armAsm->Add(RWSCRATCH, RWSCRATCH, 0x00800000); + armAsm->Fmov(s, RWSCRATCH); + armAsm->B(&end); + + armAsm->Bind(&toOverflow); + // Beyond PS2 range: narrow then clamp to +/-max (keep sign, set all other bits). + armAsm->Fcvt(s, d); + armAsm->Fmov(RWSCRATCH, s); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x7fffffff); + armAsm->Fmov(s, RWSCRATCH); + if (flags) + { + armLoadEERegPtr(RWARG1, &fpuRegs.fprc[31]); + armAsm->Orr(RWARG1, RWARG1, FPUflagO | FPUflagSO); + armStoreEERegPtr(RWARG1, &fpuRegs.fprc[31]); + if (acc) + { + armLoadEERegPtr(RWARG1, &fpuRegs.ACCflag); + armAsm->Orr(RWARG1, RWARG1, 1); + armStoreEERegPtr(RWARG1, &fpuRegs.ACCflag); + } + } + armAsm->B(&end); + + armAsm->Bind(&toUnderflow); + a64::Label uDone; + if (flags) + { + // Set U|SU unless the result is exactly +/-0. + armAsm->Fmov(RXSCRATCH, d); + armAsm->And(RXARG1, RXSCRATCH, 0x7fffffffffffffffULL); + a64::Label isZero; + armAsm->Cbz(RXARG1, &isZero); + armLoadEERegPtr(RWARG2, &fpuRegs.fprc[31]); + armAsm->Orr(RWARG2, RWARG2, FPUflagU | FPUflagSU); + armStoreEERegPtr(RWARG2, &fpuRegs.fprc[31]); + if (addsub) + { + // ADD/SUB leave the (post-normalization) mantissa bits in place; + // reconstruct a PS2 denormal single: bits[22:0] = dbl_mant[51:29], + // bit31 = sign, exp = 0. (x86 PSLL.Q 12 / PSRL.Q 41 / sign<<31 / POR.) + armAsm->Fmov(RXSCRATCH, d); + armAsm->Lsl(RXARG1, RXSCRATCH, 12); + armAsm->Lsr(RXARG1, RXARG1, 41); + armAsm->Lsr(RXARG2, RXSCRATCH, 63); + armAsm->Lsl(RXARG2, RXARG2, 31); + armAsm->Orr(RWSCRATCH, RWARG1, RWARG2); + armAsm->Fmov(s, RWSCRATCH); + armAsm->B(&uDone); + } + armAsm->Bind(&isZero); + } + // Flush to +/-0 (keep sign). + armAsm->Fcvt(s, d); + armAsm->Fmov(RWSCRATCH, s); + armAsm->And(RWSCRATCH, RWSCRATCH, 0x80000000); + armAsm->Fmov(s, RWSCRATCH); + + armAsm->Bind(&uDone); + armAsm->Bind(&end); +} + +// ---- PS2 add/sub guard-bit emulation -------------------------------------- +// +// The EE FPU has no guard bits to the right of the mantissa; subtraction (and +// add of mixed signs) can shift the mantissa left and expose what would have +// been guard bits. This masks the low mantissa bits of the smaller operand by +// the exponent difference so they read as zero. Port of x86 FPU_ADD_SUB; both +// operands (single, in temp NEON regs `idxd`/`idxt`) are mutated in place. +static void FPU_ADD_SUB(int idxd, int idxt) +{ + const a64::VRegister sd = armSRegister(idxd); + const a64::VRegister st = armSRegister(idxt); + + armAsm->Fmov(RWARG1, sd); // d bits + armAsm->Fmov(RWARG2, st); // t bits + armAsm->Ubfx(RWARG3, RWARG1, 23, 8); // expd + armAsm->Ubfx(RWSCRATCH, RWARG2, 23, 8); // expt + armAsm->Sub(RWARG3, RWARG3, RWSCRATCH); // diff = expd - expt (signed) + + a64::Label caseD25, casePos, caseEq, caseDn25, done; + armAsm->Cmp(RWARG3, 25); + armAsm->B(&caseD25, a64::ge); + armAsm->Cmp(RWARG3, 0); + armAsm->B(&casePos, a64::gt); + armAsm->B(&caseEq, a64::eq); + armAsm->Cmn(RWARG3, 25); // cmp diff, -25 + armAsm->B(&caseDn25, a64::le); + + // diff in -24..-1 (expd < expt): mask tempd's low (-diff-1) bits. + armAsm->Neg(RWSCRATCH, RWARG3); + armAsm->Sub(RWSCRATCH, RWSCRATCH, 1); + armAsm->Mov(RWARG4, 0xffffffff); + armAsm->Lsl(RWARG4, RWARG4, RWSCRATCH); + armAsm->And(RWARG1, RWARG1, RWARG4); + armAsm->Fmov(sd, RWARG1); + armAsm->B(&done); + + armAsm->Bind(&caseD25); + // diff >= 25 (expt much smaller): tempt keeps only its sign. + armAsm->And(RWARG2, RWARG2, 0x80000000); + armAsm->Fmov(st, RWARG2); + armAsm->B(&done); + + armAsm->Bind(&casePos); + // diff in 1..24 (expt smaller): mask tempt's low (diff-1) bits. + armAsm->Sub(RWSCRATCH, RWARG3, 1); + armAsm->Mov(RWARG4, 0xffffffff); + armAsm->Lsl(RWARG4, RWARG4, RWSCRATCH); + armAsm->And(RWARG2, RWARG2, RWARG4); + armAsm->Fmov(st, RWARG2); + armAsm->B(&done); + + armAsm->Bind(&caseDn25); + // diff <= -25 (expd much smaller): tempd keeps only its sign. + armAsm->And(RWARG1, RWARG1, 0x80000000); + armAsm->Fmov(sd, RWARG1); + + armAsm->Bind(&caseEq); // diff == 0: nothing + armAsm->Bind(&done); +} + +// ---- Op cores -------------------------------------------------------------- + +// Copy an allocator-resident FP source (EEREC_S/EEREC_T) into a fresh temp so +// ToDouble can mutate it without corrupting the guest fpr slot. +static int copySrc(int eerec) +{ + const int idx = _allocTempNEONreg(); + armAsm->Fmov(armSRegister(idx), armSRegister(eerec)); + return idx; +} + +// ADD/SUB/ADDA/SUBA: FPU_ADD_SUB guard mask -> widen -> op in double -> narrow. +static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) +{ + const int sreg = copySrc(EEREC_S); + const int treg = copySrc(EEREC_T); + + FPU_ADD_SUB(sreg, treg); + ToDouble(sreg); + ToDouble(treg); + + if (op == 0) + armAsm->Fadd(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + else + armAsm->Fsub(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + + ToPS2FPU_Full(sreg, true, treg, acc, true); + armAsm->Fmov(armSRegister(eeRecDst), armSRegister(sreg)); + + _freeNEONreg(sreg); + _freeNEONreg(treg); +} + +// MUL/MULA: widen -> multiply in double -> narrow. (FPUMULHACK — the Tales of +// Destiny gamefix — is intentionally not folded in here; default off.) +static void recMULop(int info, int eeRecDst, bool acc) +{ + const int sreg = copySrc(EEREC_S); + const int treg = copySrc(EEREC_T); + + ToDouble(sreg); + ToDouble(treg); + armAsm->Fmul(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + + ToPS2FPU_Full(sreg, true, treg, acc, false); + armAsm->Fmov(armSRegister(eeRecDst), armSRegister(sreg)); + + _freeNEONreg(sreg); + _freeNEONreg(treg); +} + +// MADD/MSUB/MADDA/MSUBA: (Fd or ACC) = ACC +/- Fs*Ft, with two PS2-accurate +// roundings (the multiply, then the accumulate) and overflow propagation from +// BOTH the product and the prior ACC. Port of x86 recMaddsub. +// +// The control flow mirrors x86: do the full-mode multiply (which may raise O), +// guard-mask ACC against the product, then branch on whether the product +// overflowed (FPUflagO) or the incoming ACC was already saturated (ACCflag&1). +// If either did, the accumulate is dominated by a 2^128-class term and the +// result is just +/-max with the dominant sign — skip the double add entirely. +// Only when both are finite is the accumulation performed in double. +static void recMaddsub(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) +{ + const int sreg = copySrc(EEREC_S); + const int treg = copySrc(EEREC_T); + + // --- multiply stage: sreg = ToPS2FPU(ToDouble(s) * ToDouble(t)). Sets O on + // product overflow; acc=false so it never touches ACCflag here. --- + ToDouble(sreg); + ToDouble(treg); + armAsm->Fmul(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + ToPS2FPU_Full(sreg, true, treg, false, false); + + // --- reload ACC (allocator-resident) into treg, then guard-mask it against + // the single-precision product. --- + armAsm->Fmov(armSRegister(treg), armSRegister(EEREC_ACC)); + FPU_ADD_SUB(treg, sreg); + + a64::Label mulovf, accovf, operation, skipall; + + // product overflowed? -> mulovf + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Tst(RWSCRATCH, FPUflagO); + armAsm->B(&mulovf, a64::ne); + ToDouble(sreg); + + // prior ACC saturated? -> accovf + armLoadEERegPtr(RWSCRATCH, &fpuRegs.ACCflag); + armAsm->Tst(RWSCRATCH, 1); + armAsm->B(&accovf, a64::ne); + ToDouble(treg); + armAsm->B(&operation); + + armAsm->Bind(&mulovf); + // Product is a saturated single; for SUB negate its sign, then it becomes + // the (single) accumulate result. Falls through into accovf. + if (op == 1) + { + armAsm->Fmov(RWSCRATCH, armSRegister(sreg)); + armAsm->Eor(RWSCRATCH, RWSCRATCH, 0x80000000); + armAsm->Fmov(armSRegister(sreg), RWSCRATCH); + } + armAsm->Fmov(armSRegister(treg), armSRegister(sreg)); + + armAsm->Bind(&accovf); + // SetMaxValue(treg): keep sign, set all lower bits -> +/-PS2 max. + armAsm->Fmov(RWSCRATCH, armSRegister(treg)); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x7fffffff); + armAsm->Fmov(armSRegister(treg), RWSCRATCH); + // Clear O|U then raise O|SO (and ACCflag for the *A variants). + armLoadEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + armAsm->Bic(RWSCRATCH, RWSCRATCH, FPUflagO | FPUflagU); + armAsm->Orr(RWSCRATCH, RWSCRATCH, FPUflagO | FPUflagSO); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.fprc[31]); + if (acc) + { + armLoadEERegPtr(RWSCRATCH, &fpuRegs.ACCflag); + armAsm->Orr(RWSCRATCH, RWSCRATCH, 1); + armStoreEERegPtr(RWSCRATCH, &fpuRegs.ACCflag); + } + armAsm->B(&skipall); + + armAsm->Bind(&operation); + // Both finite: accumulate in double, narrow with flags. + if (op == 1) + armAsm->Fsub(armDRegister(treg), armDRegister(treg), armDRegister(sreg)); + else + armAsm->Fadd(armDRegister(treg), armDRegister(treg), armDRegister(sreg)); + ToPS2FPU_Full(treg, true, sreg, acc, true); + + armAsm->Bind(&skipall); + armAsm->Fmov(armSRegister(eeRecDst), armSRegister(treg)); + + _freeNEONreg(sreg); + _freeNEONreg(treg); +} + +// ---- Per-opcode DOUBLE emitters (called by the CHECK_FPU_FULL branch in +// iFPU-arm64.cpp via eeFPURecompileCode) ------------------------------- + +void recADD_S_xmm(int info) { recFPUOp(info, EEREC_D, 0, false); } +void recSUB_S_xmm(int info) { recFPUOp(info, EEREC_D, 1, false); } +void recADDA_S_xmm(int info) { recFPUOp(info, EEREC_ACC, 0, true); } +void recSUBA_S_xmm(int info) { recFPUOp(info, EEREC_ACC, 1, true); } +void recMUL_S_xmm(int info) { recMULop(info, EEREC_D, false); } +void recMULA_S_xmm(int info) { recMULop(info, EEREC_ACC, true); } +void recMADD_S_xmm(int info) { recMaddsub(info, EEREC_D, 0, false); } +void recMSUB_S_xmm(int info) { recMaddsub(info, EEREC_D, 1, false); } +void recMADDA_S_xmm(int info) { recMaddsub(info, EEREC_ACC, 0, true); } +void recMSUBA_S_xmm(int info) { recMaddsub(info, EEREC_ACC, 1, true); } + +#undef _Ft_ +#undef _Fs_ +#undef _Fd_ + +} // namespace DOUBLE +} // namespace COP1 +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iMMI-arm64.cpp b/pcsx2/arm64/iMMI-arm64.cpp new file mode 100644 index 0000000000..a1dcde61e2 --- /dev/null +++ b/pcsx2/arm64/iMMI-arm64.cpp @@ -0,0 +1,1433 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE MMI (Multimedia Instructions) Codegen — NEON-based +// +// All MMI instructions are 128-bit SIMD operations on the EE's 128-bit GPRs. +// NEON Q registers are used throughout: load from cpuRegs.GPR, operate, store back. + +#include "arm64/iR5900-arm64.h" +#include "arm64/AsmHelpers.h" +#include "common/Assertions.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { +namespace MMI { + +namespace Interp = R5900::Interpreter::OpcodeImpl::MMI; + +// ============================================================================ +// Helpers for 128-bit GPR load/store +// ============================================================================ + +// Flush any const propagation state for a register and invalidate allocations. +// Must be called before reading a register's 128-bit value from memory, +// since const prop only tracks the lower 64 bits. +static void mmiFlushReg(int reg) +{ + if (reg == 0) return; + if (GPR_IS_CONST1(reg)) + { + // Const prop only has lower 64 bits — flush to memory so upper 64 bits + // are preserved alongside the correct lower 64 bits. + _flushEEreg(reg); + } + _deleteEEreg(reg, 1); +} + +// Prepare destination: invalidate const/alloc state (the full 128 bits will be overwritten) +static void mmiInvalidateDest(int reg) +{ + if (reg == 0) return; + _deleteEEreg(reg, 0); + GPR_DEL_CONST(reg); +} + +// Load 128-bit GPR into a NEON Q register +static void mmiLoadReg(const a64::VRegister& qreg, int gpr) +{ + if (gpr == 0) + { + // r0 is always zero + armAsm->Movi(qreg.V16B(), 0); + } + else + { + armAsm->Ldr(qreg, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ)); + } +} + +// Store 128-bit NEON Q register to GPR +static void mmiStoreReg(int gpr, const a64::VRegister& qreg) +{ + pxAssert(gpr != 0); + armAsm->Str(qreg, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ)); +} + +// Standard 3-operand MMI: rd = rs OP rt (128-bit). +// +// Routes through eeRecompileCodeXMM so consecutive MMI ops on the same guest +// register stay register-resident in the allocator-managed NEON pool instead +// of bouncing through memory. Allocator handles const tracking, GPR-side +// eviction, NaN-zero of r0, and "already in NEON" reuse. +// +// Each user gets three locals: +// qs — VRegister view of EEREC_S (Rs input, MODE_READ) +// qt — VRegister view of EEREC_T (Rt input, MODE_READ) +// qd — VRegister view of EEREC_D (Rd output, MODE_WRITE) +// +// Functions that need a fourth temp can use RQSCRATCH / RQSCRATCH2 — both are +// outside the allocator pool. Do NOT clobber qs or qt +// before the final write to qd, otherwise the allocator's MODE_READ state +// for them is invalidated. +#define MMI_3OP_SETUP() \ + if (!_Rd_) return; \ + int info = eeRecompileCodeXMM(XMMINFO_READS | XMMINFO_READT | XMMINFO_WRITED); \ + const a64::VRegister qs = armQRegister(EEREC_S); \ + const a64::VRegister qt = armQRegister(EEREC_T); \ + const a64::VRegister qd = armQRegister(EEREC_D); \ + (void)info + +// 2-operand: rd = OP(rt). +// qt — VRegister view of EEREC_T (Rt input, MODE_READ) +// qd — VRegister view of EEREC_D (Rd output, MODE_WRITE) +#define MMI_2OP_SETUP() \ + if (!_Rd_) return; \ + int info = eeRecompileCodeXMM(XMMINFO_READT | XMMINFO_WRITED); \ + const a64::VRegister qt = armQRegister(EEREC_T); \ + const a64::VRegister qd = armQRegister(EEREC_D); \ + (void)info + +// ============================================================================ +// Logical Operations (128-bit) +// ============================================================================ + +void recPAND() +{ + MMI_3OP_SETUP(); + armAsm->And(qd.V16B(), qs.V16B(), qt.V16B()); +} + +void recPOR() +{ + if (!_Rd_) + return; + + // `por rd, r0, rt` is the canonical PS2 128-bit register-move idiom and is + // common. Special-case an r0 operand to avoid allocating r0 into a NEON reg + // and materialize a zero just to OR it in (conditional XMMINFO, + // Movi when both r0, register-copy when one is r0). + const bool s_zero = (_Rs_ == 0); + const bool t_zero = (_Rt_ == 0); + int info = eeRecompileCodeXMM((s_zero ? 0 : XMMINFO_READS) | (t_zero ? 0 : XMMINFO_READT) | XMMINFO_WRITED); + const a64::VRegister qd = armQRegister(EEREC_D); + + if (s_zero && t_zero) + armAsm->Movi(qd.V2D(), 0); + else if (s_zero) + armAsm->Mov(qd.V16B(), armQRegister(EEREC_T).V16B()); + else if (t_zero) + armAsm->Mov(qd.V16B(), armQRegister(EEREC_S).V16B()); + else + armAsm->Orr(qd.V16B(), armQRegister(EEREC_S).V16B(), armQRegister(EEREC_T).V16B()); +} + +void recPXOR() +{ + MMI_3OP_SETUP(); + armAsm->Eor(qd.V16B(), qs.V16B(), qt.V16B()); +} + +void recPNOR() +{ + MMI_3OP_SETUP(); + armAsm->Orr(qd.V16B(), qs.V16B(), qt.V16B()); + armAsm->Not(qd.V16B(), qd.V16B()); +} + +// ============================================================================ +// Packed Arithmetic — Signed +// ============================================================================ + +void recPADDW() +{ + MMI_3OP_SETUP(); + armAsm->Add(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPSUBW() +{ + MMI_3OP_SETUP(); + armAsm->Sub(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPADDH() +{ + MMI_3OP_SETUP(); + armAsm->Add(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPSUBH() +{ + MMI_3OP_SETUP(); + armAsm->Sub(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPADDB() +{ + MMI_3OP_SETUP(); + armAsm->Add(qd.V16B(), qs.V16B(), qt.V16B()); +} + +void recPSUBB() +{ + MMI_3OP_SETUP(); + armAsm->Sub(qd.V16B(), qs.V16B(), qt.V16B()); +} + +// ============================================================================ +// Packed Arithmetic — Unsigned +// ============================================================================ + +void recPADDUW() +{ + MMI_3OP_SETUP(); + armAsm->Uqadd(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPSUBUW() +{ + MMI_3OP_SETUP(); + armAsm->Uqsub(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPADDUH() +{ + MMI_3OP_SETUP(); + armAsm->Uqadd(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPSUBUH() +{ + MMI_3OP_SETUP(); + armAsm->Uqsub(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPADDUB() +{ + MMI_3OP_SETUP(); + armAsm->Uqadd(qd.V16B(), qs.V16B(), qt.V16B()); +} + +void recPSUBUB() +{ + MMI_3OP_SETUP(); + armAsm->Uqsub(qd.V16B(), qs.V16B(), qt.V16B()); +} + +// ============================================================================ +// Packed Arithmetic — Saturating Signed +// ============================================================================ + +void recPADDSW() +{ + MMI_3OP_SETUP(); + armAsm->Sqadd(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPSUBSW() +{ + MMI_3OP_SETUP(); + armAsm->Sqsub(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPADDSH() +{ + MMI_3OP_SETUP(); + armAsm->Sqadd(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPSUBSH() +{ + MMI_3OP_SETUP(); + armAsm->Sqsub(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPADDSB() +{ + MMI_3OP_SETUP(); + armAsm->Sqadd(qd.V16B(), qs.V16B(), qt.V16B()); +} + +void recPSUBSB() +{ + MMI_3OP_SETUP(); + armAsm->Sqsub(qd.V16B(), qs.V16B(), qt.V16B()); +} + +// ============================================================================ +// Packed Compare — Greater Than (signed) +// ============================================================================ + +void recPCGTW() +{ + MMI_3OP_SETUP(); + armAsm->Cmgt(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPCGTH() +{ + MMI_3OP_SETUP(); + armAsm->Cmgt(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPCGTB() +{ + MMI_3OP_SETUP(); + armAsm->Cmgt(qd.V16B(), qs.V16B(), qt.V16B()); +} + +// ============================================================================ +// Packed Compare — Equal +// ============================================================================ + +void recPCEQW() +{ + MMI_3OP_SETUP(); + armAsm->Cmeq(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPCEQH() +{ + MMI_3OP_SETUP(); + armAsm->Cmeq(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPCEQB() +{ + MMI_3OP_SETUP(); + armAsm->Cmeq(qd.V16B(), qs.V16B(), qt.V16B()); +} + +// ============================================================================ +// Packed Min/Max (signed) +// ============================================================================ + +void recPMAXW() +{ + MMI_3OP_SETUP(); + armAsm->Smax(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPMINW() +{ + MMI_3OP_SETUP(); + armAsm->Smin(qd.V4S(), qs.V4S(), qt.V4S()); +} + +void recPMAXH() +{ + MMI_3OP_SETUP(); + armAsm->Smax(qd.V8H(), qs.V8H(), qt.V8H()); +} + +void recPMINH() +{ + MMI_3OP_SETUP(); + armAsm->Smin(qd.V8H(), qs.V8H(), qt.V8H()); +} + +// ============================================================================ +// Packed Absolute Value (signed) +// ============================================================================ + +void recPABSW() +{ + MMI_2OP_SETUP(); + // PS2 PABSW saturates INT_MIN → INT_MAX (per MMI.cpp _PABSW). NEON Abs + // preserves INT_MIN; Sqabs is the saturating form that matches. + armAsm->Sqabs(qd.V4S(), qt.V4S()); +} + +void recPABSH() +{ + MMI_2OP_SETUP(); + // Mirror of PABSW for halfword lanes. + armAsm->Sqabs(qd.V8H(), qt.V8H()); +} + +// ============================================================================ +// Register Copy / Move +// ============================================================================ + +// PCPYLD: rd = { rs.UD[0], rt.UD[0] } — copy (concatenate) lower doubleword of each source. +void recPCPYLD() +{ + MMI_3OP_SETUP(); + armAsm->Zip1(qd.V2D(), qt.V2D(), qs.V2D()); +} + +// PCPYUD: rd = {rt[127:64], rs[127:64]} — upper doublewords interleaved +void recPCPYUD() +{ + MMI_3OP_SETUP(); + armAsm->Zip2(qd.V2D(), qs.V2D(), qt.V2D()); +} + +// PCPYH: rd = {rt.UH[4] x4, rt.UH[0] x4} — replicate halfwords. +// Register-resident via the allocator (MMI_2OP_SETUP) instead of a +// memory-bounce (Ldr q from Rt + Str q to Rd + const flush), matching +// sibling single-source MMI ops (PABSW/PCPYLD). Saves a full-width load +// + store per execution. +// Broadcast rt.H[4] into scratch FIRST so the qd==qt aliased case stays correct +// (a qd write would otherwise clobber rt before H[4] is read). +void recPCPYH() +{ + MMI_2OP_SETUP(); + armAsm->Dup(RQSCRATCH.V8H(), qt.V8H(), 4); // rt.H[4] x8 (read qt before qd write) + armAsm->Dup(qd.V8H(), qt.V8H(), 0); // qd = rt.H[0] x8 + armAsm->Mov(qd.V2D(), 1, RQSCRATCH.V2D(), 0); // upper 64 <- rt.H[4] x4 +} + +// PMFHI: rd = HI (128-bit) +void recPMFHI() +{ + if (!_Rd_) return; + mmiInvalidateDest(_Rd_); + + armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +// PMFLO: rd = LO (128-bit) +void recPMFLO() +{ + if (!_Rd_) return; + mmiInvalidateDest(_Rd_); + + armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +// PMTHI: HI = rs (128-bit). Take Rs from its NEON slot when allocated and store +// straight to HI memory. HI is never NEON-resident in the EE rec (no opcode +// passes XMMINFO_*HI — see iR5900Templates-arm64.cpp), so no allocator +// invalidation is needed. +void recPMTHI() +{ + int info = eeRecompileCodeXMM(XMMINFO_READS); + (void)info; + armAsm->Str(armQRegister(EEREC_S), armCpuRegMem(&cpuRegs.HI.UQ)); +} + +// PMTLO: LO = rs (128-bit). LO is never NEON-resident (see recPMTHI). +void recPMTLO() +{ + int info = eeRecompileCodeXMM(XMMINFO_READS); + (void)info; + armAsm->Str(armQRegister(EEREC_S), armCpuRegMem(&cpuRegs.LO.UQ)); +} + +// ============================================================================ +// Packed Shifts (by immediate sa field) +// ============================================================================ + +void recPSLLW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + if (_Sa_ == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Shl(RQSCRATCH.V4S(), RQSCRATCH.V4S(), _Sa_); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +void recPSRLW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + if (_Sa_ == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Ushr(RQSCRATCH.V4S(), RQSCRATCH.V4S(), _Sa_); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +void recPSRAW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + if (_Sa_ == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Sshr(RQSCRATCH.V4S(), RQSCRATCH.V4S(), _Sa_); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +// Halfword shifts: interp uses (_Sa_ & 0xf) per MMI.cpp:228/240/252 — +// only 4 of the 5 sa bits are live since the lane is 16-bit. vixl +// Shl/Ushr/Sshr V8H require shift ∈ [0,15]; mask up front to match +// interp and stay inside the encoder's range. +void recPSLLH() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + const u32 sa = _Sa_ & 0xf; + if (sa == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Shl(RQSCRATCH.V8H(), RQSCRATCH.V8H(), sa); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +void recPSRLH() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + const u32 sa = _Sa_ & 0xf; + if (sa == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Ushr(RQSCRATCH.V8H(), RQSCRATCH.V8H(), sa); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +void recPSRAH() +{ + if (!_Rd_) return; + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + mmiLoadReg(RQSCRATCH, _Rt_); + const u32 sa = _Sa_ & 0xf; + if (sa == 0) + { + mmiStoreReg(_Rd_, RQSCRATCH); + return; + } + armAsm->Sshr(RQSCRATCH.V8H(), RQSCRATCH.V8H(), sa); + mmiStoreReg(_Rd_, RQSCRATCH); +} + +// ============================================================================ +// Pack / Unpack (Extend / Compress) +// ============================================================================ + +// PEXTLW: interleave lower 32-bit words of rs and rt +// rd = {rs.UL[1], rt.UL[1], rs.UL[0], rt.UL[0]} +void recPEXTLW() +{ + MMI_3OP_SETUP(); + armAsm->Zip1(qd.V4S(), qt.V4S(), qs.V4S()); +} + +// PEXTUW: interleave upper 32-bit words of rs and rt +// rd = {rs.UL[3], rt.UL[3], rs.UL[2], rt.UL[2]} +void recPEXTUW() +{ + MMI_3OP_SETUP(); + armAsm->Zip2(qd.V4S(), qt.V4S(), qs.V4S()); +} + +// PEXTLH: interleave lower 16-bit halfwords +void recPEXTLH() +{ + if (!_Rd_) + return; + + // rs==0 fast path: the odd output halfwords are all zero, so this + // is just a zero-extend of rt's lower 4 halfwords to words. Skip requesting + // XMMINFO_READS — otherwise the allocator pins a callee-saved NEON reg and + // materializes a zero vector for r0 just to Zip it in. Zip1(qt,qt) duplicates + // each halfword, then Ushr clears the high half of every word lane. + if (_Rs_ == 0) + { + int info = eeRecompileCodeXMM(XMMINFO_READT | XMMINFO_WRITED); + const a64::VRegister qt = armQRegister(EEREC_T); + const a64::VRegister qd = armQRegister(EEREC_D); + (void)info; + armAsm->Zip1(qd.V8H(), qt.V8H(), qt.V8H()); // {H0,H0,H1,H1,H2,H2,H3,H3} + armAsm->Ushr(qd.V4S(), qd.V4S(), 16); // {0:H0, 0:H1, 0:H2, 0:H3} + return; + } + + MMI_3OP_SETUP(); + armAsm->Zip1(qd.V8H(), qt.V8H(), qs.V8H()); +} + +// PEXTUH: interleave upper 16-bit halfwords +void recPEXTUH() +{ + MMI_3OP_SETUP(); + armAsm->Zip2(qd.V8H(), qt.V8H(), qs.V8H()); +} + +// PEXTLB: interleave lower bytes +void recPEXTLB() +{ + MMI_3OP_SETUP(); + armAsm->Zip1(qd.V16B(), qt.V16B(), qs.V16B()); +} + +// PEXTUB: interleave upper bytes +void recPEXTUB() +{ + MMI_3OP_SETUP(); + armAsm->Zip2(qd.V16B(), qt.V16B(), qs.V16B()); +} + +// PPACW: pack words — {rs.UL[2], rs.UL[0], rt.UL[2], rt.UL[0]} +void recPPACW() +{ + MMI_3OP_SETUP(); + armAsm->Uzp1(qd.V4S(), qt.V4S(), qs.V4S()); +} + +// PPACH: pack halfwords — even halfwords from rs and rt +void recPPACH() +{ + MMI_3OP_SETUP(); + armAsm->Uzp1(qd.V8H(), qt.V8H(), qs.V8H()); +} + +// PPACB: pack bytes — even bytes from rs and rt +void recPPACB() +{ + MMI_3OP_SETUP(); + armAsm->Uzp1(qd.V16B(), qt.V16B(), qs.V16B()); +} + +// PADSBH: rd.UH[0..3] = rs.UH[0..3] - rt.UH[0..3], rd.UH[4..7] = rs.UH[4..7] + rt.UH[4..7] +// Lower 4 halfwords: subtract. Upper 4 halfwords: add. +void recPADSBH() +{ + MMI_3OP_SETUP(); + // Compute the add into a scratch FIRST. If Rd aliases Rs or Rt, the + // allocator hands qd back as the same Q-reg as qs/qt — writing qd in + // the sub step would clobber the source still needed for the add. + armAsm->Add(RQSCRATCH.V8H(), qs.V8H(), qt.V8H()); + // qd = sub result (all 8 halfwords); safe to clobber qs/qt now. + armAsm->Sub(qd.V8H(), qs.V8H(), qt.V8H()); + // Blend: keep lower 64 bits of sub in qd, upper 64 bits from add. + armAsm->Mov(qd.V2D(), 1, RQSCRATCH.V2D(), 1); +} + +// ============================================================================ +// Interleave halfwords +// ============================================================================ + +// PINTH: rd.US[2k]=Rt.US[k], rd.US[2k+1]=Rs.US[k+4], k=0..3 — interleave low 4 +// halfwords of Rt with high 4 of Rs. +void recPINTH() +{ + MMI_3OP_SETUP(); + // Move rs upper 64 → low position of scratch (don't clobber qs). + armAsm->Dup(RQSCRATCH.V2D(), qs.V2D(), 1); // tmp = {rs.UD[1], rs.UD[1]} + // zip1.8h of rt(lower) and rs_upper(lower) gives interleaved result. + armAsm->Zip1(qd.V8H(), qt.V8H(), RQSCRATCH.V8H()); +} + +// PINTEH: rd = {rs.UH[6],rt.UH[6], rs.UH[4],rt.UH[4], rs.UH[2],rt.UH[2], rs.UH[0],rt.UH[0]} +// Interleave even halfwords +void recPINTEH() +{ + MMI_3OP_SETUP(); + // Extract even halfwords from each into scratch — never touch qs/qt. + armAsm->Uzp1(RQSCRATCH.V8H(), qs.V8H(), qs.V8H()); // rs evens in lower 64 + armAsm->Uzp1(RQSCRATCH2.V8H(), qt.V8H(), qt.V8H()); // rt evens in lower 64 + // Zip the lower 64 bits of each into qd. + armAsm->Zip1(qd.V8H(), RQSCRATCH2.V8H(), RQSCRATCH.V8H()); +} + +// ============================================================================ +// Shuffles / Permutations +// ============================================================================ + +// PEXEW: rd = {rt[2], rt[1], rt[0], rt[3]} (lane order) — swap words 0 and 2. +// 2-op idiom (Rev64 + Ext) instead of a scratch snapshot + full copy + 2 lane +// inserts. Both ops read qt fully before writing, so it is alias-safe when the +// allocator hands back qd == qt. +void recPEXEW() +{ + MMI_2OP_SETUP(); + armAsm->Rev64(qd.V4S(), qt.V4S()); // {rt[1],rt[0],rt[3],rt[2]} + armAsm->Ext(qd.V16B(), qd.V16B(), qd.V16B(), 12); // {rt[2],rt[1],rt[0],rt[3]} +} + +// PEXEH: swap halfwords 0↔2 in each 64-bit lane +// rd = {H[2],H[1],H[0],H[3], H[6],H[5],H[4],H[7]} +void recPEXEH() +{ + MMI_2OP_SETUP(); + armAsm->Mov(RQSCRATCH.V16B(), qt.V16B()); + armAsm->Mov(qd.V8H(), RQSCRATCH.V8H()); + armAsm->Mov(qd.V8H(), 0, RQSCRATCH.V8H(), 2); + armAsm->Mov(qd.V8H(), 2, RQSCRATCH.V8H(), 0); + armAsm->Mov(qd.V8H(), 4, RQSCRATCH.V8H(), 6); + armAsm->Mov(qd.V8H(), 6, RQSCRATCH.V8H(), 4); +} + +// PREVH: reverse halfwords within each 64-bit lane +// rd = {H[3],H[2],H[1],H[0], H[7],H[6],H[5],H[4]} +void recPREVH() +{ + MMI_2OP_SETUP(); + armAsm->Rev64(qd.V8H(), qt.V8H()); +} + +// PROT3W: rotate lower 3 words: rd = {rt[1], rt[2], rt[0], rt[3]} (lane order). +// 3-op shuffle (Rev64 + Ext + Zip1) instead of a scratch snapshot + full copy +// + 3 lane inserts. Rev64 and Ext read qt into scratches +// first, so Zip1 → qd is alias-safe when qd == qt. +// rev = {rt[1],rt[0],rt[3],rt[2]} +// ext8 = {rt[2],rt[3],rt[0],rt[1]} +// Zip1(rev,ext8) = {rev[0],ext8[0],rev[1],ext8[1]} = {rt[1],rt[2],rt[0],rt[3]} +void recPROT3W() +{ + MMI_2OP_SETUP(); + armAsm->Rev64(RQSCRATCH.V4S(), qt.V4S()); + armAsm->Ext(RQSCRATCH2.V16B(), qt.V16B(), qt.V16B(), 8); + armAsm->Zip1(qd.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); +} + +// PEXCW: swap words 1 and 2: rd = {rt[0], rt[2], rt[1], rt[3]} (lane order). +// 2-op idiom (Rev64 + Uzp1) instead of a scratch snapshot + full copy + 2 lane +// inserts. Both read their sources fully before +// writing qd, so it is alias-safe when qd == qt. +// rev = {rt[1],rt[0],rt[3],rt[2]} +// Uzp1(qt,rev) = {qt[0],qt[2],rev[0],rev[2]} = {rt[0],rt[2],rt[1],rt[3]} +void recPEXCW() +{ + MMI_2OP_SETUP(); + armAsm->Rev64(RQSCRATCH.V4S(), qt.V4S()); + armAsm->Uzp1(qd.V4S(), qt.V4S(), RQSCRATCH.V4S()); +} + +// PEXCH: swap halfwords 1↔2 within each 64-bit lane +// {H[0],H[2],H[1],H[3], H[4],H[6],H[5],H[7]} +void recPEXCH() +{ + MMI_2OP_SETUP(); + armAsm->Mov(RQSCRATCH.V16B(), qt.V16B()); + armAsm->Mov(qd.V8H(), RQSCRATCH.V8H()); + armAsm->Mov(qd.V8H(), 1, RQSCRATCH.V8H(), 2); + armAsm->Mov(qd.V8H(), 2, RQSCRATCH.V8H(), 1); + armAsm->Mov(qd.V8H(), 5, RQSCRATCH.V8H(), 6); + armAsm->Mov(qd.V8H(), 6, RQSCRATCH.V8H(), 5); +} + +// PEXT5: expand each 32-bit lane's PS2 RGB1555 field into BGRA8 layout. +// Per-lane: +// rd = ((rt & 0x001F) << 3) // R bits [4:0] -> [7:3] +// | ((rt & 0x03E0) << 6) // G bits [9:5] -> [15:11] +// | ((rt & 0x7C00) << 9) // B bits [14:10] -> [23:19] +// | ((rt & 0x8000) << 16); // A bit [15] -> [31] +void recPEXT5() +{ + MMI_2OP_SETUP(); + // Preserve qt in case allocator assigned qd == qt — rt is needed for all + // four shift+mask passes below, but the first write to qd would clobber + // it if they share a slot. + armAsm->Mov(RQSCRATCH3.V16B(), qt.V16B()); + + // Field 0: (rt << 3) & 0x000000F8 -> qd + armAsm->Shl(qd.V4S(), RQSCRATCH3.V4S(), 3); + armAsm->Movi(RQSCRATCH.V4S(), 0xF8); + armAsm->And(qd.V16B(), qd.V16B(), RQSCRATCH.V16B()); + + // Field 1: (rt << 6) & 0x0000F800 -> qd + armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 6); + armAsm->Movi(RQSCRATCH.V4S(), 0xF8, vixl::aarch64::LSL, 8); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); + + // Field 2: (rt << 9) & 0x00F80000 -> qd + armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 9); + armAsm->Movi(RQSCRATCH.V4S(), 0xF8, vixl::aarch64::LSL, 16); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); + + // Field 3: (rt << 16) & 0x80000000 -> qd + armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 16); + armAsm->Movi(RQSCRATCH.V4S(), 0x80, vixl::aarch64::LSL, 24); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); +} + +// PPAC5: pack BGRA8-style 32-bit lanes back into PS2 RGB1555 16-bit layout +// (the inverse of PEXT5). Upper 16 bits of each lane left as garbage from +// the shifted-source — interp does not mask them either. +// Per-lane: +// rd = ((rt >> 3) & 0x001F) +// | ((rt >> 6) & 0x03E0) +// | ((rt >> 9) & 0x7C00) +// | ((rt >> 16) & 0x8000); +void recPPAC5() +{ + MMI_2OP_SETUP(); + armAsm->Mov(RQSCRATCH3.V16B(), qt.V16B()); + + // Field 0: (rt >> 3) & 0x0000001F -> qd + armAsm->Ushr(qd.V4S(), RQSCRATCH3.V4S(), 3); + armAsm->Movi(RQSCRATCH.V4S(), 0x1F); + armAsm->And(qd.V16B(), qd.V16B(), RQSCRATCH.V16B()); + + // Field 1: (rt >> 6) & 0x000003E0 -> qd + // 0x3E0 has two non-zero bytes; vixl's Movi macro materializes it via + // Mov scratch_w + Dup (2 host insns) rather than the single LSL form. + armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 6); + armAsm->Movi(RQSCRATCH.V4S(), 0x3E0); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); + + // Field 2: (rt >> 9) & 0x00007C00 -> qd + armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 9); + armAsm->Movi(RQSCRATCH.V4S(), 0x7C, vixl::aarch64::LSL, 8); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); + + // Field 3: (rt >> 16) & 0x00008000 -> qd + armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 16); + armAsm->Movi(RQSCRATCH.V4S(), 0x80, vixl::aarch64::LSL, 8); + armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B()); + armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B()); +} + +// ============================================================================ +// Variable shifts — operate on words 0 and 2 only, sign-extend to 64 +// ============================================================================ + +// PSLLVW: rd.SD[0] = sign_ext(rt.UL[0] << (rs.UL[0] & 0x1F)) +// rd.SD[1] = sign_ext(rt.UL[2] << (rs.UL[2] & 0x1F)) +void recPSLLVW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + + // Word 0 + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Lsl(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]); + + // Word 2 + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Lsl(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]); +} + +void recPSRLVW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Lsr(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]); + + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Lsr(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]); +} + +void recPSRAVW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Asr(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]); + + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]); + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]); + armAsm->And(a64::w1, a64::w1, 0x1F); + armAsm->Asr(a64::w0, a64::w0, a64::w1); + armAsm->Sxtw(a64::x0, a64::w0); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]); +} + +// ============================================================================ +// Multiply / Divide / MAC — at x86 parity via interpreter fallback +// ============================================================================ + +// These stay as REC_FUNC because production x86 stays REC_FUNC too (the +// MMI2_RECOMPILE native code paths in pcsx2/x86/iMMI.cpp live behind a never- +// defined macro): +// +// PMADDW / PMSUBW — interp models the PS2-hardware multiplication errata +// (`temp2 / 0xFFFFFFFF` + the conditional `+ 0x70000000` voodoo on boundary +// Rt values). Any non-errata fast path — NEON, SSE, or scalar — diverges +// from interp on essentially every input. Upstream keeps interp +// authoritative; the same applies here. +// +// PDIVW / PDIVBW / PDIVUW — AArch64 NEON has no integer divide, and x86's +// commented-out "native" path is itself `recCall(Interp::PDIV*)` after a +// targeted `_deleteEEreg(_Rd_, 0)`. There is no codegen to port. +// +// PMADDUW gets a native impl below — its interp is plain u64 arithmetic (no +// errata), so a NEON port matches interp bit-for-bit. +REC_FUNC(PMADDW); +REC_FUNC(PMSUBW); +REC_FUNC(PDIVW); +REC_FUNC(PDIVBW); +REC_FUNC(PDIVUW); + +// PMULTW: 2-lane signed 32x32->64 multiply on even-indexed source words. +// prod[0] = (s64)Rs.SL[0] * (s64)Rt.SL[0] +// prod[1] = (s64)Rs.SL[2] * (s64)Rt.SL[2] +// LO.UD[0..1] = sign-extended low32 of each product +// HI.UD[0..1] = sign-extended high32 of each product +// Rd.SD[0..1] = full 64-bit products +void recPMULTW() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + // Pack even-indexed 32-bit lanes into the low half (SL[0],SL[2] -> S[0],S[1]) + armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S()); + armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S()); + + // 2-lane signed 32x32->64 -> { prod0, prod1 } as 2x64 + armAsm->Smull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S()); + + // LO = sign-extended low32 of each product + armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D()); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI = sign-extended high32 of each product (shift right narrow + sxtl) + armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + mmiStoreReg(_Rd_, RQSCRATCH3); +} + +// PMULTUW: 2-lane unsigned 32x32->64 multiply on even-indexed source words. +// prod[0] = (u64)Rs.UL[0] * (u64)Rt.UL[0] +// prod[1] = (u64)Rs.UL[2] * (u64)Rt.UL[2] +// LO.UD[0..1] = sign-extended low32 of each product (interp casts (s32)) +// HI.UD[0..1] = sign-extended high32 of each product +// Rd.UD[0..1] = full 64-bit products +void recPMULTUW() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S()); + armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S()); + + armAsm->Umull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S()); + + armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D()); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + mmiStoreReg(_Rd_, RQSCRATCH3); +} + +// PMADDUW: 2-lane unsigned 32x32+64->64 multiply-accumulate on even-indexed +// source words. +// tempu[k] = (u64)(LO.UL[2k] | (HI.UL[2k] << 32)) + (u64)Rs.UL[2k] * Rt.UL[2k] +// LO.UD[k] = sign-extended low32 of tempu[k] +// HI.UD[k] = sign-extended high32 of tempu[k] +// Rd.UD[k] = tempu[k] (full u64) +// +// Interp has no PS2 multiplication errata for the unsigned variant — plain u64 +// arithmetic — so this matches interp bit-for-bit (unlike PMADDW/PMSUBW which +// stay REC_FUNC above). +// +// Bypasses the LO/HI allocator path: EE rec's info-word layout packs EEREC_LO +// and EEREC_HI into the same 5-bit field (the EEREC_LO/EEREC_HI info-word +// macros decode the same bits), so it can't produce two distinct register +// indices. An op that needs both LO and HI live must load them from memory. +void recPMADDUW() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + // LO/HI are never NEON-resident in the EE rec (no opcode passes XMMINFO_*LO/HI), + // so the Ldrs below already see fresh memory — no allocator flush needed. + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + // Pack even-indexed 32-bit lanes into the low half (UL[0],UL[2] -> S[0],S[1]) + armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S()); + armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S()); + + // 2-lane unsigned 32x32->64 product + armAsm->Umull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S()); + + // Compose accumulator: { LO.UL[0] | HI.UL[0]<<32, LO.UL[2] | HI.UL[2]<<32 } + // Trn1.V4S(d, a, b) = { a[0], b[0], a[2], b[2] } -> as V2D, gives LO|HI<<32 per lane. + armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ)); + armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S()); + + // sum = composed + product (2x64 unsigned add) + armAsm->Add(RQSCRATCH3.V2D(), RQSCRATCH.V2D(), RQSCRATCH3.V2D()); + + // LO = sign-extended low32 of each 64-bit lane + armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D()); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI = sign-extended high32 of each 64-bit lane + armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32); + armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + + // Rd = full 2x64 unsigned sum + if (_Rd_) + mmiStoreReg(_Rd_, RQSCRATCH3); +} + +// PHMADH: 8-lane signed 16x16->32 multiply, pair-sum (n + n+1): +// sum[k] = Rs.SH[2k]*Rt.SH[2k] + Rs.SH[2k+1]*Rt.SH[2k+1] for k = 0..3 +// firsttemp[k] = Rs.SH[2k+1]*Rt.SH[2k+1] (the second product of each pair) +// LO = { sum[0], firsttemp[0], sum[2], firsttemp[2] } +// HI = { sum[1], firsttemp[1], sum[3], firsttemp[3] } +// Rd = { sum[0], sum[1], sum[2], sum[3] } (post-update LO/HI even lanes) +void recPHMADH() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + // p_lo = { Rs.SH[i] * Rt.SH[i] } for i = 0..3 (as 4x32) + // p_hi = { Rs.SH[i] * Rt.SH[i] } for i = 4..7 (as 4x32) + armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H()); + armAsm->Smull2(RQSCRATCH.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H()); + + // sums = ADDP(p_lo, p_hi).4S = { p0+p1, p2+p3, p4+p5, p6+p7 } + armAsm->Addp(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + // firsts = UZP2(p_lo, p_hi).4S = { p1, p3, p5, p7 } + armAsm->Uzp2(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + + // LO = TRN1.4S(sums, firsts) = { sum0, p1, sum2, p5 } + armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI = TRN2.4S(sums, firsts) = { sum1, p3, sum3, p7 } + armAsm->Trn2(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + mmiStoreReg(_Rd_, RQSCRATCH2); +} + +// PHMSBH: 8-lane signed 16x16->32 multiply, pair-diff (n+1 - n): +// sum[k] = Rs.SH[2k+1]*Rt.SH[2k+1] - Rs.SH[2k]*Rt.SH[2k] (k = 0..3) +// firsttemp[k] = Rs.SH[2k+1]*Rt.SH[2k+1] (the second product per pair) +// LO = { sum[0], ~firsttemp[0], sum[2], ~firsttemp[2] } (note: bitwise NOT) +// HI = { sum[1], ~firsttemp[1], sum[3], ~firsttemp[3] } +// Rd = { sum[0], sum[1], sum[2], sum[3] } +void recPHMSBH() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H()); + armAsm->Smull2(RQSCRATCH.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H()); + + // odds = UZP2(p_lo, p_hi).4S = { p1, p3, p5, p7 } (firsttemps) + // evens = UZP1(p_lo, p_hi).4S = { p0, p2, p4, p6 } + armAsm->Uzp2(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + armAsm->Uzp1(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S()); + + // sums = odds - evens = { p1-p0, p3-p2, p5-p4, p7-p6 } + armAsm->Sub(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); + + // nfirsts = ~odds (reuse RQSCRATCH3 — evens are dead after Sub) + armAsm->Mvn(RQSCRATCH3.V16B(), RQSCRATCH2.V16B()); + + if (_Rd_) + mmiStoreReg(_Rd_, RQSCRATCH); + + // LO = TRN1.4S(sums, nfirsts) = { sum0, ~p1, sum2, ~p5 } + armAsm->Trn1(RQSCRATCH2.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI = TRN2.4S(sums, nfirsts) = { sum1, ~p3, sum3, ~p7 } + armAsm->Trn2(RQSCRATCH2.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ)); +} + +// PMULTH: 8-lane signed 16x16->32 multiply. +// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7 +// LO = { r0, r1, r4, r5 } +// HI = { r2, r3, r6, r7 } +// Rd = { r0, r2, r4, r6 } (even-indexed products) +void recPMULTH() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + // q29 = SMULL Rs.4H, Rt.4H -> { r0,r1,r2,r3 } as 4x32 + // q31 = SMULL2 Rs.8H, Rt.8H -> { r4,r5,r6,r7 } as 4x32 (in-place over Rt) + armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H()); + armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H()); + + // LO = TRN1.2D(prod_lo, prod_hi) = { prod_lo.D[0], prod_hi.D[0] } = { r0,r1,r4,r5 } + armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI = TRN2.2D(prod_lo, prod_hi) = { prod_lo.D[1], prod_hi.D[1] } = { r2,r3,r6,r7 } + armAsm->Trn2(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + { + // Rd = UZP1.4S(prod_lo, prod_hi) = { r0, r2, r4, r6 } (even-indexed) + armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH2.V4S()); + mmiStoreReg(_Rd_, RQSCRATCH); + } +} + +// PMADDH: 8-lane signed 16x16->32 multiply, accumulate into existing LO/HI. +// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7 +// LO.UL[0..3] += { r0, r1, r4, r5 } +// HI.UL[0..3] += { r2, r3, r6, r7 } +// Rd = { new_LO[0], new_HI[0], new_LO[2], new_HI[2] } +void recPMADDH() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H()); + armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H()); + + // q30 = LO_increment = { r0,r1,r4,r5 } + armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + // q29 = HI_increment = { r2,r3,r6,r7 } + armAsm->Trn2(RQSCRATCH3.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + + // q31 = old LO; add and store + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ)); + armAsm->Add(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH.V4S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + // q31 = old HI; add and store + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ)); + armAsm->Add(RQSCRATCH3.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + { + // Rd = TRN1.4S(new_LO, new_HI) = { LO[0], HI[0], LO[2], HI[2] } + armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); + mmiStoreReg(_Rd_, RQSCRATCH); + } +} + +// PMSUBH: 8-lane signed 16x16->32 multiply, subtract from existing LO/HI. +// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7 +// LO.UL[0..3] -= { r0, r1, r4, r5 } +// HI.UL[0..3] -= { r2, r3, r6, r7 } +// Rd = { new_LO[0], new_HI[0], new_LO[2], new_HI[2] } +void recPMSUBH() +{ + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + if (_Rd_) + mmiInvalidateDest(_Rd_); + + mmiLoadReg(RQSCRATCH, _Rs_); + mmiLoadReg(RQSCRATCH2, _Rt_); + + armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H()); + armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H()); + + armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + armAsm->Trn2(RQSCRATCH3.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D()); + + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ)); + armAsm->Sub(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH.V4S()); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ)); + + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ)); + armAsm->Sub(RQSCRATCH3.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S()); + armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ)); + + if (_Rd_) + { + armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S()); + mmiStoreReg(_Rd_, RQSCRATCH); + } +} + +// ============================================================================ +// QFSRV: Quad Funnel Shift Right Variable +// ============================================================================ + +// QFSRV: Rd = {Rs, Rt} >> (sa * 8), truncated to 128 bits. +// cpuRegs.sa is in bytes (0-15). Concatenate Rt (low) and Rs (high) +// into a 256-bit value, shift right by sa bytes, take lower 128 bits. +// Implementation: store {Rt, Rs} to adjacent memory, unaligned load at offset sa. +// Matches x86 approach using tempqw buffer. +alignas(16) static u8 s_qfsrvTemp[32]; + +void recQFSRV() +{ + if (!_Rd_) return; + + mmiFlushReg(_Rs_); + mmiFlushReg(_Rt_); + mmiInvalidateDest(_Rd_); + + // Adjacent-source fast path: when Rs == Rt+1 the 256-bit + // {Rt:Rs} window already exists contiguously in the GPR array + // (GPR.r[Rt] immediately precedes GPR.r[Rt+1]==GPR.r[Rs], 32 bytes), now + // memory-coherent after the flushes above. Read the unaligned 128 bits + // directly at &GPR.r[Rt] + sa and skip the two temp stores. sa is 0..15 so + // the load stays within the two registers' 32 bytes. Gate on Rt != 0 to avoid + // depending on GPR.r[0] holding zero in memory (the slow path Movi's it). + if (_Rt_ != 0 && _Rs_ == _Rt_ + 1) + { + armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa); + armMoveAddressToReg(RSCRATCHADDR, &cpuRegs.GPR.r[_Rt_]); + armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); + armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rd_])); + return; + } + + // Store Rt at temp[0:15], Rs at temp[16:31] + mmiLoadReg(RQSCRATCH, _Rt_); + armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[0]); + armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + mmiLoadReg(RQSCRATCH, _Rs_); + armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[16]); + armAsm->Str(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + // Load sa (byte offset) + armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa); + + // Unaligned 128-bit load from temp + sa + armMoveAddressToReg(RSCRATCHADDR, &s_qfsrvTemp[0]); + armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // addr = temp + sa + armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + // Store result to Rd + armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rd_])); +} + +// ============================================================================ +// Other MMI +// ============================================================================ + +// PLZCW: count leading sign bits (excluding the sign bit itself) for words 0 and 1 +void recPLZCW() +{ + if (!_Rd_) return; + mmiFlushReg(_Rs_); + mmiInvalidateDest(_Rd_); + + // Word 0: ARM64 CLS counts leading sign bits excluding the MSB sign bit itself, + // which matches the PS2 PLZCW definition (CountLeadingSignBits - 1). + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->Cls(a64::w0, a64::w0); + armStoreEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rd_].UL[0]); + + // Word 1 + armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rs_].UL[1]); + armAsm->Cls(a64::w0, a64::w0); + armStoreEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rd_].UL[1]); +} + +// PMFHL — read LO/HI 128-bit register pair, dispatch on sa: +// 0x00 LW : Rd = { LO.UL[0], HI.UL[0], LO.UL[2], HI.UL[2] } +// 0x01 UW : Rd = { LO.UL[1], HI.UL[1], LO.UL[3], HI.UL[3] } +// 0x02 SLW: composed s64 (HI.UL[2k]:LO.UL[2k]) signed-saturated to s32 +// then sign-extended to s64; written to Rd.UD[k] for k=0,1. +// 0x03 LH : Rd.US lanes from even-indexed LO/HI halfwords, interleaved +// at 32-bit-pair granularity. +// 0x04 SH : per-lane s32→s16 signed saturation of LO/HI words, interleaved +// at 32-bit-pair granularity. +// sa >= 5 — interpreter is a no-op (no default-case in MMI.cpp PMFHL); mirror +// that by early-returning without touching Rd. (x86 rec asserts on this path, +// but the interp doesn't, so the recompiled-vs-interp diff would fail an assert +// rather than catch real divergence — matching the interp is the safer choice.) +void recPMFHL() +{ + if (!_Rd_) + return; + if (_Sa_ > 0x04) + return; + + // LO/HI loaded directly from memory rather than via the allocator + // (XMMINFO_READLO | XMMINFO_READHI). Reason: the arm64 EE rec's + // info-word layout packs PROCESS_EE_SET_LO and PROCESS_EE_SET_HI + // into the SAME 5-bit field at bits 23..27 (see iCore-arm64.h — + // EEREC_LO == EEREC_HI == EEREC_ACC). PMFHL needs both LO and HI as + // simultaneous live inputs, which that field collision cannot represent. + // Bypass with explicit Ldrs. LO/HI are never NEON-resident in the EE rec + // (no opcode passes XMMINFO_*LO/HI), so the memory image is already current. + + int info = eeRecompileCodeXMM(XMMINFO_WRITED); + const a64::VRegister qd = armQRegister(EEREC_D); + (void)info; + + // Pre-loaded into reserved scratch quads (outside the allocator pool). + const a64::VRegister qlo = RQSCRATCH; + const a64::VRegister qhi = RQSCRATCH2; + armAsm->Ldr(qlo, armCpuRegMem(&cpuRegs.LO.UQ)); + armAsm->Ldr(qhi, armCpuRegMem(&cpuRegs.HI.UQ)); + + switch (_Sa_) + { + case 0x00: // LW: pick even-indexed words from LO/HI and interleave + // TRN1.V4S → { LO.S[0], HI.S[0], LO.S[2], HI.S[2] } + armAsm->Trn1(qd.V4S(), qlo.V4S(), qhi.V4S()); + break; + + case 0x01: // UW: pick odd-indexed words from LO/HI and interleave + // TRN2.V4S → { LO.S[1], HI.S[1], LO.S[3], HI.S[3] } + armAsm->Trn2(qd.V4S(), qlo.V4S(), qhi.V4S()); + break; + + case 0x02: // SLW: compose s64 (HI:LO) per even-word lane, saturate to s32, sign-extend back + // TRN1.V4S → V2D { (HI[0]:LO[0]), (HI[2]:LO[2]) } (LO in low 32 of each 64) + // SQXTN.V2S — signed-saturating narrow 2x64 → 2x32 (matches interp's + // "in-range -> (s64)(s32)LO.UL[2k]; saturate to INT32_MIN/MAX" bounds). + // SXTL.V2D — sign-extend 2x32 → 2x64 (= the recorded Rd.UD shape). + armAsm->Trn1(qd.V4S(), qlo.V4S(), qhi.V4S()); + armAsm->Sqxtn(qd.V2S(), qd.V2D()); + armAsm->Sxtl(qd.V2D(), qd.V2S()); + break; + + case 0x03: // LH: even halfwords from LO/HI, interleaved at S-pair granularity + // UZP1.V8H(x, x) gathers x's even halfwords into the low 64 bits of x. + // ZIP1.V4S picks S[0]/S[1] of each input → output S[0..3] = + // { (LO[0]:LO[2]), (HI[0]:HI[2]), (LO[4]:LO[6]), (HI[4]:HI[6]) } + // which as V8H = { LO[0], LO[2], HI[0], HI[2], LO[4], LO[6], HI[4], HI[6] }. + armAsm->Uzp1(qlo.V8H(), qlo.V8H(), qlo.V8H()); + armAsm->Uzp1(qhi.V8H(), qhi.V8H(), qhi.V8H()); + armAsm->Zip1(qd.V4S(), qlo.V4S(), qhi.V4S()); + break; + + case 0x04: // SH: signed-saturating narrow 32→16 per word, interleaved at S-pair granularity + // SQXTN.V4H — 4x32 signed-sat narrowed to 4x16 in low 64 of each scratch. + // ZIP1.V4S → output S[0..3] = { sat(LO[0..1]), sat(HI[0..1]), sat(LO[2..3]), sat(HI[2..3]) } + // which as V8H is exactly the interp's PMFHL_CLAMP-per-lane pattern. + armAsm->Sqxtn(qlo.V4H(), qlo.V4S()); + armAsm->Sqxtn(qhi.V4H(), qhi.V4S()); + armAsm->Zip1(qd.V4S(), qlo.V4S(), qhi.V4S()); + break; + } +} + +// PMTHL.LW: even-indexed words of LO/HI receive Rs's four words; the +// odd-indexed words (UL[1] and UL[3] of each) are preserved. Matches +// interp at MMI.cpp:217-224 and x86 BLENDPS/SHUFPS sequence at +// iMMI.cpp:234-248. Strategy: load LO/HI as Q regs, INS lanes 1+3 from +// the prior values to preserve them; lane 0 and lane 2 come from Rs's +// word 0/2 for LO, word 1/3 for HI. +void recPMTHL() +{ + if (_Sa_ != 0) + return; + + mmiFlushReg(_Rs_); + mmiLoadReg(RQSCRATCH, _Rs_); + + // LO_new = [Rs.UL[0], LO.UL[1], Rs.UL[2], LO.UL[3]] + armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ)); + armAsm->Mov(RQSCRATCH3.V16B(), RQSCRATCH.V16B()); + armAsm->Ins(RQSCRATCH3.V4S(), 1, RQSCRATCH2.V4S(), 1); + armAsm->Ins(RQSCRATCH3.V4S(), 3, RQSCRATCH2.V4S(), 3); + armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.LO.UQ)); + + // HI_new = [Rs.UL[1], HI.UL[1], Rs.UL[3], HI.UL[3]] + armAsm->Ldr(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ)); + armAsm->Ins(RQSCRATCH3.V4S(), 0, RQSCRATCH.V4S(), 1); + armAsm->Ins(RQSCRATCH3.V4S(), 2, RQSCRATCH.V4S(), 3); + armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ)); +} + +} // namespace MMI +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp new file mode 100644 index 0000000000..2c8fe222a8 --- /dev/null +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -0,0 +1,2013 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE (R5900) Dynamic Recompiler Core +// Dispatcher, block management, all instructions as interpreter fallbacks. + +#include +#include +#include +#include + +#include "arm64/iR5900-arm64.h" +#include "arm64/iR5900Analysis.h" +#include "arm64/AsmHelpers.h" +#include "Host.h" +#include "R3000A.h" +#include "R5900.h" +#include "arm64/BaseblockEx-arm64.h" +#include "R5900OpcodeTables.h" +#include "Common.h" +#include "VMManager.h" +#include "Config.h" +#include "vtlb.h" +#include "Dmac.h" +#include "GS.h" +#ifdef PCSX2_RECOMPILER_TESTS +#include "ee_divtrace.h" // diagnostic divergence-trace hooks (test builds only) +#endif + +#include "common/Assertions.h" +#include "common/AlignedMalloc.h" +#include "common/Console.h" +#include "common/FastJmp.h" +#include "common/HeapArray.h" +#include "common/Perf.h" + +#include "DebugTools/Breakpoints.h" + +namespace a64 = vixl::aarch64; + +// ===================================================================================================== +// Global State +// ===================================================================================================== + +u32 maxrecmem = 0; +u32 pc; +int g_branch; +u32 target; +u32 s_nBlockCycles; +bool s_nBlockInterlocked; + +bool g_recompilingDelaySlot = false; +bool g_cpuFlushedPC = false; +bool g_cpuFlushedCode = false; + +// Constant propagation — defined here, declared extern in iR5900-arm64.h + +static uptr recLUT[0x10000]; +static u32 hwLUT[0x10000]; + +static __fi u32 HWADDR(u32 mem) { return hwLUT[mem >> 16] + mem; } + +static BASEBLOCK* recRAM = nullptr; +static BASEBLOCK* recROM = nullptr; +static BASEBLOCK* recROM1 = nullptr; +static Arm64BaseBlocks recBlocks; +static u8* recPtr = nullptr; +static u8* recPtrEnd = nullptr; + +static EEINST* s_pInstCache = nullptr; +static u32 s_nInstCacheSize = 0; + +static BASEBLOCK* s_pCurBlock = nullptr; +static BASEBLOCKEX* s_pCurBlockEx = nullptr; + +static u32 s_nEndBlock = 0; +static u32 s_branchTo; +static bool s_nBlockFF; + +static DynamicHeapArray recLutReserve_RAM; +static DynamicHeapArray recLutUnmapped; +static DynamicHeapArray recRAMCopy; +static size_t recLutEntries = 0; + +static ArmConstantPool s_eeConstantPool; + +// Execution state +static fastjmp_buf m_SetJmp_StateCheck; +static bool eeCpuExecuting = false; +static bool eeRecNeedsReset = false; +static bool eeRecExitRequested = false; + +#ifdef PCSX2_RECOMPILER_TESTS +// Harness-entry state. Set by recEeExecuteBlock before entering the JIT, +// observed by recEventTest at every iBranchTest event-due fall-out. +// Production execution (recExecute) leaves g_eeHarnessActive false; the +// predicate short-circuits on it. Compiled out entirely in release builds +// (ENABLE_RECOMPILER_TEST_HOOKS=OFF) so the live VM path carries no +// test-only symbols or branches. +static bool g_eeHarnessActive = false; +static u32 g_eeHarnessParkPc = 0; +static s32 g_eeHarnessCycleBudget = 0; +static u64 g_eeHarnessCycleStart = 0; +static constexpr s32 kExecuteBlockSafetyCap = 1 << 20; +#endif + +// Self-modifying code detection +static u16 manual_page[Ps2MemSize::MainRam / 4096] = {}; +static u8 manual_counter[Ps2MemSize::MainRam / 4096] = {}; + +// Forward declarations +static void recRecompile(const u32 startpc); +static void recResetRaw(); +static void recExitExecution(); +static void recSafeExitExecution(); +#ifdef PCSX2_RECOMPILER_TESTS +static bool harnessShouldExit(); +#endif +static void recError(u32 error); +static void dyna_block_discard(u32 start, u32 sz); +static void dyna_page_reset(u32 start, u32 sz); +static void iopClearRecLUT(BASEBLOCK* base, int count); + +// recBackpropBSC declared in arm64/iR5900Analysis.h + +// ===================================================================================================== +// Native Codegen Verification Mode +// ===================================================================================================== + +#ifdef VERIFY_NATIVE_CODEGEN + +// Snapshot of GPR + HI/LO state before the native instruction executes +static GPR_reg s_verifyGPR[32]; +static GPR_reg s_verifyHI, s_verifyLO; +static u32 s_verifyMismatchCount = 0; + +// VU0 state snapshot for COP2 verification +static VECTOR s_verifyVF[32]; +static VECTOR s_verifyACC; +static REG_VI s_verifyVI[32]; +static u32 s_verifyClipFlag; + +// Called at runtime BEFORE the native instruction: snapshot all state +static void verifySnapshotPre(u32 code, u32 instPC) +{ + memcpy(s_verifyGPR, cpuRegs.GPR.r, sizeof(s_verifyGPR)); + s_verifyHI = cpuRegs.HI; + s_verifyLO = cpuRegs.LO; + + // COP2: also snapshot VU0 state + if ((code >> 26) == 0x12) // COP2 opcode + { + memcpy(s_verifyVF, VU0.VF, sizeof(s_verifyVF)); + s_verifyACC = VU0.ACC; + memcpy(s_verifyVI, VU0.VI, sizeof(s_verifyVI)); + s_verifyClipFlag = VU0.clipflag; + } +} + +// Called at runtime AFTER the native instruction: re-run via interpreter on the snapshot and compare +static void verifyCheckPost(u32 code, u32 instPC) +{ + const bool isCOP2 = (code >> 26) == 0x12; + + // Save native results + GPR_reg nativeGPR[32]; + GPR_reg nativeHI, nativeLO; + memcpy(nativeGPR, cpuRegs.GPR.r, sizeof(nativeGPR)); + nativeHI = cpuRegs.HI; + nativeLO = cpuRegs.LO; + + // Save native VU0 state for COP2 + VECTOR nativeVF[32]; + VECTOR nativeACC; + REG_VI nativeVI[32]; + u32 nativeClipFlag = 0; + if (isCOP2) + { + memcpy(nativeVF, VU0.VF, sizeof(nativeVF)); + nativeACC = VU0.ACC; + memcpy(nativeVI, VU0.VI, sizeof(nativeVI)); + nativeClipFlag = VU0.clipflag; + } + + // Restore pre-instruction state + memcpy(cpuRegs.GPR.r, s_verifyGPR, sizeof(s_verifyGPR)); + cpuRegs.HI = s_verifyHI; + cpuRegs.LO = s_verifyLO; + if (isCOP2) + { + memcpy(VU0.VF, s_verifyVF, sizeof(s_verifyVF)); + VU0.ACC = s_verifyACC; + memcpy(VU0.VI, s_verifyVI, sizeof(s_verifyVI)); + VU0.clipflag = s_verifyClipFlag; + } + + // Run interpreter + const u32 savedCode = cpuRegs.code; + cpuRegs.code = code; + const R5900::OPCODE& opcode = R5900::GetCurrentInstruction(); + if (opcode.interpret) + opcode.interpret(); + cpuRegs.code = savedCode; + + // Compare results + bool mismatch = false; + static const char* gpr_names[] = { + "zero","at","v0","v1","a0","a1","a2","a3", + "t0","t1","t2","t3","t4","t5","t6","t7", + "s0","s1","s2","s3","s4","s5","s6","s7", + "t8","t9","k0","k1","gp","sp","fp","ra" + }; + + for (int i = 1; i < 32; i++) // skip r0 + { + if (cpuRegs.GPR.r[i].UD[0] != nativeGPR[i].UD[0]) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" %s(r%d): native=0x%016llX interp=0x%016llX (pre=0x%016llX)", + gpr_names[i], i, nativeGPR[i].UD[0], cpuRegs.GPR.r[i].UD[0], s_verifyGPR[i].UD[0]); + } + } + + if (cpuRegs.HI.UD[0] != nativeHI.UD[0]) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" HI: native=0x%016llX interp=0x%016llX", nativeHI.UD[0], cpuRegs.HI.UD[0]); + } + if (cpuRegs.LO.UD[0] != nativeLO.UD[0]) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" LO: native=0x%016llX interp=0x%016llX", nativeLO.UD[0], cpuRegs.LO.UD[0]); + } + + // COP2: compare VU0 state (tolerate 1-ULP float differences) + if (isCOP2) + { + auto ulpDiff = [](u32 a, u32 b) -> u32 { + return (a > b) ? (a - b) : (b - a); + }; + + for (int i = 1; i < 32; i++) // skip VF0 + { + bool vfMismatch = false; + for (int lane = 0; lane < 4; lane++) + { + if (ulpDiff(VU0.VF[i].UL[lane], nativeVF[i].UL[lane]) > 100) + vfMismatch = true; + } + if (vfMismatch) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" VF%d: native=[%08X,%08X,%08X,%08X] interp=[%08X,%08X,%08X,%08X]", + i, nativeVF[i].UL[0], nativeVF[i].UL[1], nativeVF[i].UL[2], nativeVF[i].UL[3], + VU0.VF[i].UL[0], VU0.VF[i].UL[1], VU0.VF[i].UL[2], VU0.VF[i].UL[3]); + } + } + bool accMismatch = false; + for (int lane = 0; lane < 4; lane++) + { + if (ulpDiff(VU0.ACC.UL[lane], nativeACC.UL[lane]) > 100) + accMismatch = true; + } + if (accMismatch) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" ACC: native=[%08X,%08X,%08X,%08X] interp=[%08X,%08X,%08X,%08X]", + nativeACC.UL[0], nativeACC.UL[1], nativeACC.UL[2], nativeACC.UL[3], + VU0.ACC.UL[0], VU0.ACC.UL[1], VU0.ACC.UL[2], VU0.ACC.UL[3]); + } + // Check MAC and status flags + if (VU0.VI[REG_MAC_FLAG].UL != nativeVI[REG_MAC_FLAG].UL) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" MAC_FLAG: native=0x%04X interp=0x%04X", nativeVI[REG_MAC_FLAG].UL, VU0.VI[REG_MAC_FLAG].UL); + } + if (VU0.VI[REG_STATUS_FLAG].UL != nativeVI[REG_STATUS_FLAG].UL) + { + if (!mismatch) { Console.Error("VERIFY MISMATCH at pc=0x%08X code=0x%08X:", instPC, code); mismatch = true; } + Console.Error(" STATUS_FLAG: native=0x%04X interp=0x%04X", nativeVI[REG_STATUS_FLAG].UL, VU0.VI[REG_STATUS_FLAG].UL); + } + } + + if (mismatch) + { + const u32 op = code >> 26; + const u32 rs = (code >> 21) & 0x1f; + const u32 rt = (code >> 16) & 0x1f; + const u32 rd = (code >> 11) & 0x1f; + const u32 sa = (code >> 6) & 0x1f; + const u32 funct = code & 0x3f; + Console.Error(" Decode: op=%d rs=%d rt=%d rd=%d sa=%d funct=%d", + op, rs, rt, rd, sa, funct); + s_verifyMismatchCount++; + // Don't assert — remaining mismatches are rounding-induced flag diffs + // (MAC zero flag differs when result is on the boundary of 0.0). + // Log only, no crash. + } + + // Restore native results so execution continues with native values + memcpy(cpuRegs.GPR.r, nativeGPR, sizeof(nativeGPR)); + cpuRegs.HI = nativeHI; + cpuRegs.LO = nativeLO; + if (isCOP2) + { + memcpy(VU0.VF, nativeVF, sizeof(nativeVF)); + VU0.ACC = nativeACC; + memcpy(VU0.VI, nativeVI, sizeof(nativeVI)); + VU0.clipflag = nativeClipFlag; + } +} + +#endif // VERIFY_NATIVE_CODEGEN + +#define GETBLOCK(x) PC_GETBLOCK_(x, recLUT) + +// ===================================================================================================== +// Dynamically Compiled Dispatchers - R5900 ARM64 +// ===================================================================================================== + +static const void* DispatcherEvent = nullptr; +static const void* DispatcherReg = nullptr; +static const void* JITCompile = nullptr; +static const void* EnterRecompiledCode = nullptr; +static const void* DispatchBlockDiscard = nullptr; +static const void* DispatchPageReset = nullptr; +static const void* UnmappedRecLUTPage = nullptr; + +static void recEventTest() +{ + eeEventTestIsActive = true; + _cpuEventTest_Shared(); + eeEventTestIsActive = false; + + if (eeRecExitRequested) + { + eeRecExitRequested = false; + recExitExecution(); + } + +#ifdef PCSX2_RECOMPILER_TESTS + if (harnessShouldExit()) + recExitExecution(); +#endif + + if (eeRecNeedsReset) + { + eeRecNeedsReset = false; + recResetRaw(); + } +} + +#ifdef PCSX2_RECOMPILER_TESTS +// Harness-exit predicate. Returns true when running under recEeExecuteBlock +// AND either the parking PC has been reached or the cycle budget has been +// exhausted. Test-only — release builds drop the call site entirely. +static bool harnessShouldExit() +{ + if (!g_eeHarnessActive) + return false; + if (cpuRegs.pc == g_eeHarnessParkPc) + return true; + const u64 elapsed = cpuRegs.cycle - g_eeHarnessCycleStart; + return elapsed >= static_cast(g_eeHarnessCycleBudget); +} +#endif + +// ARM64 EE dispatcher — same two-level LUT as IOP but using cpuRegs.pc +static const void* _DynGen_DispatcherReg() +{ + u8* retval = armGetCurrentCodePointer(); + + armAsm->Ldr(a64::w0, armCpuRegMem(&cpuRegs.pc)); + + // Two-level LUT lookup: + // base = recLUT[pc >> 16] + // block = *(BASEBLOCK*)(base + pc * sizeof(BASEBLOCK)/4) + // sizeof(BASEBLOCK) = 8, so /4 = *2, hence: base + pc*2 + // Note: use FULL pc as index (not pc & 0xFFFF) because recLUT_SetPage + // adjusts the base address to account for the upper bits. + armAsm->Lsr(a64::w1, a64::w0, 16); + armMoveAddressToReg(RSCRATCHADDR, recLUT); + armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR, a64::x1, a64::LSL, 3)); + + // Index with full PC: base + pc * 2 (not (pc & 0xFFFF) * 2) + armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, a64::Operand(a64::x0, a64::LSL, 1)); + + armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR)); + + armAsm->Br(RSCRATCHADDR); + + return retval; +} + +static const void* _DynGen_JITCompile() +{ + u8* retval = armGetCurrentCodePointer(); + + // Flush pinned cycle counter before the C call, then reload after — + // recRecompile itself doesn't modify cpuRegs.cycle, but other paths + // (e.g. block discard) might, and the convention is "every C-call + // boundary syncs RECCYCLE both ways". + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Ldr(RWARG1, armCpuRegMem(&cpuRegs.pc)); + armEmitCall((void*)recRecompile); + + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armEmitJmp(DispatcherReg); + + return retval; +} + +static const void* _DynGen_DispatcherEvent() +{ + u8* retval = armGetCurrentCodePointer(); + // Flush pinned cycle for recEventTest (it reads cpuRegs.cycle for + // counter / interrupt scheduling), then reload — the event test may + // modify cpuRegs.cycle (e.g. fast-forwarding to nextEventCycle). + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall((void*)recEventTest); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + return retval; // falls through to DispatcherReg +} + +static const void* _DynGen_EnterRecompiledCode() +{ + u8* retval = armGetCurrentCodePointer(); + + // We never return through this function — we exit via fastjmp_jmp (longjmp). + // fastjmp_set/fastjmp_jmp save and restore callee-saved registers, so we + // don't need armBeginStackFrame. Just align the stack (AArch64 requires + // 16-byte alignment). Match x86 pattern: only adjust SP, don't save regs. + armAsm->Sub(a64::sp, a64::sp, 16); + + // Park PS2 FPU clamp constants in callee-saved scalar NEON registers. + // s8 = +FLT_MAX, s9 = -FLT_MAX. AAPCS64 preserves the lower 64 bits of + // d8-d15 across C calls, so these survive every armEmitCall path inside + // JIT blocks without compile-time tracking. fpuClampResult and iCOP2 + // scalar VDIV/VSQRT/VRSQRT clamps read them directly. v8/v9 are removed + // from the NEON allocator pool (see NEON_RESERVED_FPU_{MAX,MIN} in + // iCore-arm64.cpp), so nothing in JIT codegen can clobber them. + armAsm->Ldr(a64::s8, FLT_MAX); + armAsm->Ldr(a64::s9, -FLT_MAX); + + // Load fastmem base into x19 if enabled + if (CHECK_FASTMEM) + { + armMoveAddressToReg(RSCRATCHADDR, &vtlb_private::vtlbdata.fastmem_base); + armAsm->Ldr(RFASTMEMBASE, a64::MemOperand(RSCRATCHADDR)); + } + + // Load &cpuRegs into RSTATE. Callee-saved, never modified by C, so this + // load happens once per JIT entry. Subsequent cpuRegs.X accesses become + // `Ldr/Str ..., [RSTATE, #offsetof(...)]` instead of materializing the + // full address each time. + armMoveAddressToReg(RSTATE, &cpuRegs); + + // Load pinned cycle counter into RECCYCLE. The convention is that + // RECCYCLE holds cpuRegs.cycle for the entire duration of JIT + // execution, with flush+reload around C calls. + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + // Load &VU0 into RVU0. Same idea as RSTATE: VU0 is a static reference + // (constant address), so iCOP2 codegen can reach every VURegs field via + // [RVU0, #imm12]. Survives both armEmitCall and mVU dispatcher runs. + armMoveAddressToReg(RVU0, &VU0); + + // Jump into dispatcher + armEmitJmp(DispatcherReg); + + // Exit point — restore callee-saved and return + // We get here via fastjmp_jmp, not via normal return + return retval; +} + +static const void* _DynGen_DispatchBlockDiscard() +{ + u8* retval = armGetCurrentCodePointer(); + armEmitCall((void*)dyna_block_discard); + armEmitJmp(DispatcherReg); + return retval; +} + +static const void* _DynGen_DispatchPageReset() +{ + u8* retval = armGetCurrentCodePointer(); + armEmitCall((void*)dyna_page_reset); + armEmitJmp(DispatcherReg); + return retval; +} + +static const void* _DynGen_UnmappedRecLUTPage() +{ + u8* retval = armGetCurrentCodePointer(); + armAsm->Mov(RWARG1, 0); + armEmitCall((void*)(void(*)(u32))recError); + return retval; +} + +static void _DynGen_Dispatchers() +{ + const u8* start = armGetCurrentCodePointer(); + + DispatcherEvent = _DynGen_DispatcherEvent(); + DispatcherReg = _DynGen_DispatcherReg(); + + JITCompile = _DynGen_JITCompile(); + EnterRecompiledCode = _DynGen_EnterRecompiledCode(); + DispatchBlockDiscard = _DynGen_DispatchBlockDiscard(); + DispatchPageReset = _DynGen_DispatchPageReset(); + UnmappedRecLUTPage = _DynGen_UnmappedRecLUTPage(); + + // Block linker needs JITCompile so it can route stale / not-yet-compiled + // link sites through the dispatcher path. + recBlocks.SetJITCompile(JITCompile); + + Perf::any.Register(start, static_cast(armGetCurrentCodePointer() - start), "EE Dispatcher"); +} + +// ===================================================================================================== +// Error handling +// ===================================================================================================== + +static void recError(u32 error) +{ + switch (error) + { + case 0: + Host::ReportErrorAsync("R5900 Exception", + fmt::format("Unrecognized opcode (PC: 0x{:08x})", cpuRegs.pc)); + break; + + case 1: + Host::ReportErrorAsync("R5900 Exception", + fmt::format("Jump to unaligned address (PC: 0x{:08x})", cpuRegs.pc)); + break; + } + + VMManager::SetPaused(true); + Cpu->ExitExecution(); +} + +// ===================================================================================================== +// Code generation helpers +// ===================================================================================================== + +void iFlushCall(int flushtype) +{ + // Free caller-saved registers + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (!arm64gprs[i].inuse) + continue; + + if (!armIsCalleeSavedRegister(i) || + ((flushtype & FLUSH_FREE_NONTEMP_X86) && arm64gprs[i].type != ARM64TYPE_TEMP) || + ((flushtype & FLUSH_FREE_TEMP_X86) && arm64gprs[i].type == ARM64TYPE_TEMP)) + { + _freeArm64GPR(i); + } + } + + // Only the lower 64 bits of v8-v15 are callee-saved per AAPCS64; the + // NEON allocator uses 128-bit slots, so all of them are effectively + // caller-saved across a C call. Always free + writeback. Matches x86 + // iFlushCall (pcsx2/x86/ix86-32/iR5900.cpp:1196-1207) which also + // unconditionally evicts caller-saved XMM regs. + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse) + _freeNEONreg(i); + } + + if (flushtype & FLUSH_ALL_X86) + _flushArm64GPRregs(); + + if (flushtype & FLUSH_CONSTANT_REGS) + _flushConstRegs(true); + + if ((flushtype & FLUSH_PC) && !g_cpuFlushedPC) + { + armAsm->Mov(RWSCRATCH, pc); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + g_cpuFlushedPC = true; + } + + if ((flushtype & FLUSH_CODE) && !g_cpuFlushedCode) + { + armAsm->Mov(RWSCRATCH, cpuRegs.code); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.code)); + g_cpuFlushedCode = true; + } +} + +// Flag set by cpuTlbMiss to signal that a TLB exception occurred during +// an interpreter call. The JIT block checks this after recCall and exits +// to the dispatcher if set, so the exception vector gets dispatched. +u32 s_recTlbMissOccurred = 0; + +// Emit the post-interpreter-call TLB-miss exception dispatch. cpuTlbMiss sets +// s_recTlbMissOccurred and moves cpuRegs.pc to the exception vector; when set we +// clear the flag and exit to DispatcherReg rather than continue the block at the +// wrong PC. DispatcherReg/s_recTlbMissOccurred are file-local here, so this is +// the shared entry point used by recCall and recVTLB-arm64.cpp's recUnalignedCall. +void recEmitInterpTlbMissCheck() +{ + // Dispatch to DispatcherReg (not DispatcherEvent, which runs event + // processing that may interfere with the pending exception state). + a64::Label noException; + armMoveAddressToReg(RSCRATCHADDR, &s_recTlbMissOccurred); + armAsm->Ldr(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + armAsm->Cbz(RWSCRATCH, &noException); + armAsm->Str(a64::wzr, a64::MemOperand(RSCRATCHADDR)); // clear flag + armEmitJmp(DispatcherReg); + armAsm->Bind(&noException); +} + +void recCall(void (*func)()) +{ + iFlushCall(FLUSH_INTERPRETER); + + // Flush RECCYCLE → cpuRegs.cycle so the interpreter sees the live cycle + // value (some opcodes — COP0 Count, TLB miss, branch helpers — read it). + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armEmitCall((void*)func); + + // Reload RECCYCLE in case the interpreter modified cpuRegs.cycle. + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + // After interpreter calls, dispatch a pending TLB-miss exception. + recEmitInterpTlbMissCheck(); +} + +void recBranchCall(void (*func)()) +{ + iFlushCall(FLUSH_INTERPRETER); + + // Apply accumulated block cycles to RECCYCLE, then flush to memory + // before the C call — the interpreter's intEventTest reads + // cpuRegs.cycle. Reload after, so the g_branch=2 exit code that + // follows can keep using RECCYCLE. + u32 cycles = scaleblockcycles_clear(); + if (cycles > 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armEmitCall((void*)func); + g_branch = 2; + + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); +} + +// s_nBlockCycles is 3-bit fixed point. Divide by 8 when done! +// Scaling blocks under 40 cycles seems to produce countless problems, so let's try to avoid them. +// Matches x86 scaleblockcycles_calculation() in ix86-32/iR5900.cpp +#define DEFAULT_SCALED_BLOCKS() (s_nBlockCycles >> 3) + +static u32 scaleblockcycles_calculation() +{ + const bool lowcycles = (s_nBlockCycles <= 40); + const s8 cyclerate = EmuConfig.Speedhacks.EECycleRate; + u32 scale_cycles = 0; + + if (cyclerate == 0 || lowcycles || cyclerate < -99 || cyclerate > 3) + scale_cycles = DEFAULT_SCALED_BLOCKS(); + + else if (cyclerate > 1) + scale_cycles = s_nBlockCycles >> (2 + cyclerate); + + else if (cyclerate == 1) + scale_cycles = DEFAULT_SCALED_BLOCKS() / 1.3f; + + else if (cyclerate == -1) + scale_cycles = (s_nBlockCycles <= 80 || s_nBlockCycles > 168 ? 5 : 7) * s_nBlockCycles / 32; + + else + scale_cycles = ((5 + (-2 * (cyclerate + 1))) * s_nBlockCycles) >> 5; + + return (scale_cycles < 1) ? 1 : scale_cycles; +} + +u32 scaleblockcycles_clear() +{ + const u32 scaled = scaleblockcycles_calculation(); + + const s8 cyclerate = EmuConfig.Speedhacks.EECycleRate; + const bool lowcycles = (s_nBlockCycles <= 40); + + if (!lowcycles && cyclerate > 1) + s_nBlockCycles &= (0x1 << (cyclerate + 2)) - 1; + else + s_nBlockCycles &= 0x7; + + return scaled; +} + +void _eeFlushAllDirty() +{ + _flushConstRegs(false); + _flushArm64GPRregs(); + _flushNEONregs(); +} + +void _eeOnWriteReg(int reg, int signext) +{ + GPR_DEL_CONST(reg); +} + +void _deleteEEreg(int reg, int flush) +{ + if (!reg) + return; + if (flush && GPR_IS_CONST1(reg)) + _flushConstReg(reg); + + GPR_DEL_CONST(reg); + _deleteGPRtoArm64GPR(reg, flush ? DELETE_REG_FREE : DELETE_REG_FREE_NO_WRITEBACK); + // NEON side: ALWAYS writeback before free. EE GPRs are 128-bit and scalar + // MIPS ops only overwrite UD[0]; the slot's UD[1] holds the live upper-64 + // from a prior MMI write (MMI routes through eeRecompileCodeXMM, so Rd stays + // live in the slot with MODE_WRITE). Dropping without writeback silently + // zeros UD[1] in memory and breaks the interpreter's "preserve UD[1]" + // contract for LUI/MFLO/MOVZ/ADDIU/... + _deleteGPRtoNEONreg(reg, DELETE_REG_FREE); +} + +void _deleteEEreg128(int reg) +{ + if (!reg) + return; + if (GPR_IS_CONST1(reg)) + _flushConstReg(reg); + + GPR_DEL_CONST(reg); + _deleteGPRtoArm64GPR(reg, DELETE_REG_FREE_NO_WRITEBACK); + _deleteGPRtoNEONreg(reg, DELETE_REG_FREE); +} + +void _flushEEreg(int reg, bool clear) +{ + if (!reg) + return; + + if (GPR_IS_DIRTY_CONST(reg)) + _flushConstReg(reg); + if (clear) + GPR_DEL_CONST(reg); + + // Per-register flush honoring reg/clear, mirroring x86 _flushEEreg. + // clear=false → writeback but keep the allocation; clear=true → also free. + // (The previous arm64 impl flushed ALL registers and ignored reg/clear — + // a behavior-equivalent superset given the lone caller, but a lying API.) + _deleteGPRtoNEONreg(reg, clear ? DELETE_REG_FLUSH_AND_FREE : DELETE_REG_FLUSH); + _deleteGPRtoArm64GPR(reg, clear ? DELETE_REG_FLUSH_AND_FREE : DELETE_REG_FLUSH); +} + +void _eeMoveGPRtoR(const a64::Register& to, int fromgpr, bool allow_preload) +{ + if (fromgpr == 0) + { + // r0 is always zero + if (to.Is64Bits()) + armAsm->Mov(to, a64::xzr); + else + armAsm->Mov(to, a64::wzr); + return; + } + + if (GPR_IS_CONST1(fromgpr)) + { + // Value known at compile time — emit immediate load + if (to.Is64Bits()) + armAsm->Mov(to, g_cpuConstRegs[fromgpr].SD[0]); + else + armAsm->Mov(to, g_cpuConstRegs[fromgpr].UL[0]); + return; + } + + // Check if the register is currently allocated in an ARM64 GPR with + // MODE_READ — meaning the host register holds the current guest value. + // MODE_WRITE-only means it's a destination allocation; the current value + // was never loaded, so the host register contains stale data. + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == ARM64TYPE_GPR && + arm64gprs[i].reg == fromgpr && (arm64gprs[i].mode & MODE_READ)) + { + if (to.Is64Bits()) + armAsm->Mov(to, armXRegister(i)); + else + armAsm->Mov(to, armWRegister(i)); + return; + } + } + + // Check if allocated in a NEON register. A MODE_WRITE-only slot is also + // authoritative — the MMI op that allocated it has written the live value + // to qreg even though the slot was never MODE_READ-loaded. Reading from + // memory in that case would return the pre-MMI stale value. eeRecompileCodeXMM + // passes MODE_WRITE alone (no MODE_READ unless XMMINFO_READD is set) for Rd, + // so every MMI destination lands here. + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse && arm64neon[i].type == NEONTYPE_GPRREG && + arm64neon[i].reg == fromgpr && (arm64neon[i].mode & (MODE_READ | MODE_WRITE))) + { + if (to.Is64Bits()) + armAsm->Fmov(to, armDRegister(i)); // FMOV Xd, Dn + else + armAsm->Fmov(to, armSRegister(i)); // FMOV Wd, Sn (lower 32 bits) + return; + } + } + + // Not allocated anywhere — load from cpuRegs memory + armLoadEERegPtr(to, &cpuRegs.GPR.r[fromgpr].UD[0]); +} + +// ===================================================================================================== +// Branch handling +// ===================================================================================================== + +void SetBranchReg() +{ + g_branch = 1; + + // Flush all GPR/NEON/constant allocations FIRST, while host registers + // still hold correct guest values. iFlushCall writes back delay slot + // results (like addiu sp) before the branch target is loaded into w0. + iFlushCall(FLUSH_EVERYTHING); + + // Now load branch target from pcWriteback (saved by recJR/recJALR) + armLoadEERegPtr(a64::w0, &cpuRegs.pcWriteback); + + // GoemonTlbHack: recJR/recJALR store the raw virtual register target; the + // JIT dispatches in physical space, so translate it before use. Mirrors + // recJ/recJAL (compile-time vtlb_V2P via SetBranchImm) and x86 + // recJR/recJALR (vtlb_DynV2P). The V2P lives in SetBranchReg, whose only + // EE callers are recJR/recJALR, so no other target gets double-translated. + // The iFlushCall(FLUSH_EVERYTHING) above has already spilled guest state; + // vtlb_V2P preserves callee-saved x25 (RECCYCLE) per AAPCS64, so the + // C-call needs no extra save. w0 (== RWARG1) already holds the virtual + // target and receives the translated paddr. + if (EmuConfig.Gamefixes.GoemonTlbHack) + armEmitCall((void*)vtlb_V2P); + + // Store to cpuRegs.pc + armAsm->Str(a64::w0, armCpuRegMem(&cpuRegs.pc)); + + // Alignment check + a64::Label unaligned; + armAsm->Tst(a64::w0, 3); + armAsm->B(&unaligned, a64::ne); + + // Update pinned cycle counter (RECCYCLE = cpuRegs.cycle). + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + // Check events (RECCYCLE >= nextEventCycle → DispatcherEvent flushes + // RECCYCLE itself before calling recEventTest). + armAsm->Ldr(a64::x3, armCpuRegMem(&cpuRegs.nextEventCycle)); + armAsm->Cmp(RECCYCLE, a64::x3); + armEmitCondBranch(a64::ge, DispatcherEvent); + + armEmitJmp(DispatcherReg); + + armAsm->Bind(&unaligned); + armAsm->Mov(RWARG1, 1); + armEmitCall((void*)recError); +} + +void SetBranchImm(u32 imm) +{ + g_branch = 1; + pxAssert(imm); + + armAsm->Mov(RWSCRATCH, imm); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + + iFlushCall(FLUSH_EVERYTHING); + + // Update pinned cycle counter. + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + // WaitLoop speedhack: when the block scanner detected this block as a + // pure-nop spin branching back to itself (s_nBlockFF), fast-forward + // RECCYCLE to max(RECCYCLE, nextEventCycle) and jump straight to + // DispatcherEvent. Saves the dozens of iterations the loop would + // otherwise burn waiting for an event to fire. Matches the x86 path in + // iR5900.cpp:iBranchTest under EmuConfig.Speedhacks.WaitLoop. + if (EmuConfig.Speedhacks.WaitLoop && s_nBlockFF && imm == s_branchTo) + { + armAsm->Ldr(a64::x3, armCpuRegMem(&cpuRegs.nextEventCycle)); + armAsm->Cmp(RECCYCLE, a64::x3); + armAsm->Csel(RECCYCLE, RECCYCLE, a64::x3, a64::hi); + armEmitJmp(DispatcherEvent); + return; + } + + // Check events. + armAsm->Ldr(a64::x3, armCpuRegMem(&cpuRegs.nextEventCycle)); + armAsm->Cmp(RECCYCLE, a64::x3); + armEmitCondBranch(a64::ge, DispatcherEvent); + + // Block linking: emit a single B as the patch site. Initially routed + // through JITCompile via recBlocks.Link(); once the target block is + // compiled, recBlocks.New() rewrites this B's imm26 to branch to the + // target's fnptr directly, bypassing the dispatcher. + { + a64::SingleEmissionCheckScope guard(armAsm); + u8* patch_site = armGetCurrentCodePointer(); + armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite + recBlocks.Link(HWADDR(imm), patch_site); + } +} + +// ===================================================================================================== +// Block state save/restore for delay slots +// ===================================================================================================== + +static _arm64gprregs s_savedGPRs[NUM_ARM_GPR_REGS]; +static _arm64neonregs s_savedNEON[NUM_ARM_NEON_REGS]; +static GPR_reg64 s_savedConstRegs[32]; +static u32 s_savedHasConstReg, s_savedFlushedConstReg; +static u32 s_savedBlockCycles; +static EEINST* s_savedInstInfo; + +void SaveBranchState() +{ + s_savedBlockCycles = s_nBlockCycles; + memcpy(s_savedConstRegs, g_cpuConstRegs, sizeof(g_cpuConstRegs)); + s_savedHasConstReg = g_cpuHasConstReg; + s_savedFlushedConstReg = g_cpuFlushedConstReg; + s_savedInstInfo = g_pCurInstInfo; + memcpy(s_savedGPRs, arm64gprs, sizeof(arm64gprs)); + memcpy(s_savedNEON, arm64neon, sizeof(arm64neon)); +} + +void LoadBranchState() +{ + s_nBlockCycles = s_savedBlockCycles; + memcpy(g_cpuConstRegs, s_savedConstRegs, sizeof(g_cpuConstRegs)); + g_cpuHasConstReg = s_savedHasConstReg; + g_cpuFlushedConstReg = s_savedFlushedConstReg; + g_pCurInstInfo = s_savedInstInfo; + memcpy(arm64gprs, s_savedGPRs, sizeof(arm64gprs)); + memcpy(arm64neon, s_savedNEON, sizeof(arm64neon)); +} + +// ===================================================================================================== +// Instruction recompilation +// ===================================================================================================== + +void recompileNextInstruction(bool delayslot, bool swapped_delay_slot) +{ + const u32 old_code = cpuRegs.code; + EEINST* old_inst_info = g_pCurInstInfo; + + cpuRegs.code = memRead32(pc); + + if (!delayslot) + { + pc += 4; + g_cpuFlushedPC = false; + g_cpuFlushedCode = false; + } + else + { + // For delay slots, increment pc after recompiling (at the end of this function) + g_recompilingDelaySlot = true; + } + + g_pCurInstInfo++; + + // NOP gets cycle counted but no codegen (matching x86 behavior) + if (cpuRegs.code == 0) + { + s_nBlockCycles += 9 * (2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1)); + } + else + { + const R5900::OPCODE& opcode = R5900::GetCurrentInstruction(); + s_nBlockCycles += opcode.cycles * (2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1)); + +#ifdef VERIFY_NATIVE_CODEGEN + // Verification mode: verify native codegen against interpreter for + // instructions in categories that have native codegen enabled. + // Skip COP0/Memory/FPU which are interpreter-only and may have + // timing-sensitive behaviour (MFC0 Count reads cycle counter). + const u32 verifyOp = cpuRegs.code >> 26; + const bool isVerifiableCategory = + // Verify COP2 instructions (opcode 18 = 0x12) + (verifyOp == 0x12); + + if (isVerifiableCategory && opcode.recompile && opcode.interpret) + { + // Step 1: Flush all registers to cpuRegs BEFORE native codegen + iFlushCall(FLUSH_EVERYTHING); + + // Step 2: Emit call to snapshot pre-instruction state + armAsm->Mov(a64::w0, cpuRegs.code); + armAsm->Mov(a64::w1, pc - 4); // current instruction PC + armEmitCall((void*)verifySnapshotPre); + + // Step 3: Run the native codegen + opcode.recompile(); + + // Step 4: Flush native results to cpuRegs + iFlushCall(FLUSH_EVERYTHING); + + // Step 5: Emit call to verify against interpreter + armAsm->Mov(a64::w0, cpuRegs.code); + armAsm->Mov(a64::w1, pc - 4); + armEmitCall((void*)verifyCheckPost); + } + else +#endif + { + // Guard: branch/jump in a delay slot would cause infinite + // compile-time recursion. Use interpreter for the instruction. + const bool isBranchInDelaySlot = delayslot && (opcode.flags & IS_BRANCH); + if (isBranchInDelaySlot || !opcode.recompile) + { + if ((opcode.flags & IS_BRANCH) && !isBranchInDelaySlot) + recBranchCall(opcode.interpret); + else + recCall(opcode.interpret); + } + else + opcode.recompile(); + } + } + + // SP misalignment check disabled: MMI/COP2 instructions legitimately use + // r29 as SIMD data, causing massive false-positive spam. + + if (!swapped_delay_slot) + { + _clearNeededArm64GPRregs(); + _clearNeededNEONregs(); + } + + if (delayslot) + { + pc += 4; + g_cpuFlushedPC = false; + g_cpuFlushedCode = false; + g_recompilingDelaySlot = false; + } + + // When called from TrySwapDelaySlot (swapped_delay_slot=true), restore + // cpuRegs.code so that the caller's _Rs_/_Rt_/_Rd_ macros still work. + // Matches x86 at iR5900.cpp:1918-1921. + if (swapped_delay_slot) + { + cpuRegs.code = old_code; + g_pCurInstInfo = old_inst_info; + } +} + +bool TrySwapDelaySlot(u32 rs, u32 rt, u32 rd, bool allow_loadstore) +{ + if (g_recompilingDelaySlot) + return false; + + const u32 opcode_encoded = memRead32(pc); + if (opcode_encoded == 0) // NOP + { + recompileNextInstruction(true, true); + return true; + } + + return false; +} + +// ===================================================================================================== +// Memory management and block clearing +// ===================================================================================================== + +static void recClear(u32 addr, u32 size) +{ + addr = HWADDR(addr); + const u32 end = addr + size * 4; + + int blockidx = recBlocks.LastIndex(end - 4); + if (blockidx == -1) + return; + + // Track the EE-address span of all blocks we touch so the post-walk + // tail can reset interior BLOCKs across the *full* extent of the + // removed blocks (a straddler can extend well past `end` or below + // `addr`). `ceiling` clamps the tail at the next surviving block's + // startpc so we never trample its interior. + u32 lowerextent = static_cast(-1); + u32 upperextent = 0; + u32 ceiling = static_cast(-1); + + if (BASEBLOCKEX* peb_above = recBlocks[blockidx + 1]) + ceiling = peb_above->startpc; + + int toRemoveLast = blockidx; + + // Walk down through blocks overlapping [addr, end). For each, reset + // BLOCK->fnptr at the block's actual start (the straddle-from-below + // case is load-bearing — Arm64BaseBlocks::Remove() patches only the + // compiled-code stub, so any BLOCK->fnptr left pointing at a stub + // trips the recRecompile fnptr assertion on the next dispatch). + // + // Skip s_pCurBlock if we hit it: it's the block currently being + // compiled, and yanking it mid-emit corrupts the in-progress block. + // Splitting the Remove range around it preserves it. Mirrors x86 + // recClear (pcsx2/x86/ix86-32/iR5900.cpp:786). + while (BASEBLOCKEX* pexblock = recBlocks[blockidx]) + { + const u32 blockstart = pexblock->startpc; + const u32 blockend = blockstart + pexblock->size * 4; + BASEBLOCK* pblock = GETBLOCK(blockstart); + + if (pblock == s_pCurBlock) + { + if (toRemoveLast != blockidx) + recBlocks.Remove(blockidx + 1, toRemoveLast); + toRemoveLast = --blockidx; + continue; + } + + if (blockend <= addr) + { + lowerextent = std::max(lowerextent, blockend); + break; + } + + lowerextent = std::min(lowerextent, blockstart); + upperextent = std::max(upperextent, blockend); + pblock->SetFnptr((uptr)JITCompile); + + --blockidx; + } + + if (toRemoveLast != blockidx) + recBlocks.Remove(blockidx + 1, toRemoveLast); + + upperextent = std::min(upperextent, ceiling); + + // Reset interior BLOCKs across the full removed-block extent. Without + // this, interior fnptrs of straddler blocks can stay non-JITCompile + // from a prior compilation, leading to wrong dispatch on a later JR + // into the middle of a freshly-recompiled block. + if (upperextent > lowerextent) + iopClearRecLUT(GETBLOCK(lowerextent), upperextent - lowerextent); +} + +static void iopClearRecLUT(BASEBLOCK* base, int count) +{ + for (int i = 0; i < count / 4; i++) + base[i].SetFnptr((uptr)JITCompile); +} + +static void dyna_block_discard(u32 start, u32 sz) +{ + DevCon.WriteLn("%.8X rec block discard (sz=%d)", start, sz); + recClear(start, sz); +} + +static void dyna_page_reset(u32 start, u32 sz) +{ + recClear(start & ~0xFFF, 0x400); // clear 4KB page + manual_counter[start >> 12]++; + mmap_MarkCountedRamPage(start); +} + +// Self-modifying code detection — generates inline memory comparison checks +// for blocks in manually-protected pages, and sets up page protection for new pages. +// Port of x86 memory_protect_recompiled_code(). +static void memory_protect_recompiled_code(u32 startpc, u32 size) +{ + u32 inpage_ptr = HWADDR(startpc); + const u32 inpage_sz = size * 4; + + // The kernel context register is stored @ 0x800010C0-0x80001300 + // The EENULL thread context register is stored @ 0x81000-.... + const bool contains_thread_stack = ((startpc >> 12) == 0x81) || ((startpc >> 12) == 0x80001); + + const vtlb_ProtectionMode PageType = contains_thread_stack ? ProtMode_Manual : mmap_GetRamPageInfo(inpage_ptr); + + switch (PageType) + { + case ProtMode_NotRequired: + break; + + case ProtMode_None: + case ProtMode_Write: + mmap_MarkCountedRamPage(inpage_ptr); + manual_page[inpage_ptr >> 12] = 0; + break; + + case ProtMode_Manual: + { + // Set up arguments for DispatchBlockDiscard (w0=addr, w1=size) + armAsm->Mov(a64::w0, inpage_ptr); + armAsm->Mov(a64::w1, inpage_sz / 4); + + u32 lpc = inpage_ptr; + u32 stg = inpage_sz; + + // Generate inline byte-by-byte comparison of compiled block source with current RAM. + // If any word differs, the block is stale and must be discarded. + while (stg > 0) + { + const u32 expected = *(u32*)PSM(lpc); + + // Load current memory word + armMoveAddressToReg(RSCRATCHADDR, (void*)PSM(lpc)); + armAsm->Ldr(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + + // Compare with compile-time snapshot + armAsm->Mov(a64::w9, expected); + armAsm->Cmp(RWSCRATCH, a64::w9); + armEmitCondBranch(a64::ne, DispatchBlockDiscard); + + stg -= 4; + lpc += 4; + } + + // Counted blocks: track how often this block runs. If the counter overflows, + // reset the page to write-protected mode (faster than manual checks). + if (!contains_thread_stack && manual_counter[inpage_ptr >> 12] <= 3) + { + armMoveAddressToReg(RSCRATCHADDR, &manual_page[inpage_ptr >> 12]); + armAsm->Ldrh(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + armAsm->Add(RWSCRATCH, RWSCRATCH, size); + armAsm->Strh(RWSCRATCH, a64::MemOperand(RSCRATCHADDR)); + // Check for u16 overflow (bit 16+ set means wrapped past 0xFFFF) + armAsm->Tst(RWSCRATCH, 0xFFFF0000u); + armEmitCondBranch(a64::ne, DispatchPageReset); + } + break; + } + } +} + +// ===================================================================================================== +// Reserve / Reset / Shutdown / Execute +// ===================================================================================================== + +static void recReserveRAM() +{ + recLutEntries = (Ps2MemSize::MainRam + Ps2MemSize::Rom + Ps2MemSize::Rom1) / 4; + + if (recLutReserve_RAM.size() != recLutEntries) + recLutReserve_RAM.resize(recLutEntries); + + recLutUnmapped.resize(_64kb / 4); + + BASEBLOCK* curpos = recLutReserve_RAM.data(); + recRAM = curpos; + curpos += (Ps2MemSize::MainRam / 4); + recROM = curpos; + curpos += (Ps2MemSize::Rom / 4); + recROM1 = curpos; + curpos += (Ps2MemSize::Rom1 / 4); + + if (recRAMCopy.size() != Ps2MemSize::MainRam) + recRAMCopy.resize(Ps2MemSize::MainRam); +} + +static void recReserve() +{ + Console.WriteLn(Color_Green, "EE: ARM64 Recompiler reserved."); + recPtr = SysMemory::GetEERec(); + recPtrEnd = SysMemory::GetEERecEnd() - _64kb; + + recReserveRAM(); + + pxAssertRel(!s_pInstCache, "InstCache not allocated"); + s_nInstCacheSize = 128; + s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize); + if (!s_pInstCache) + pxFailRel("Failed to allocate R5900 InstCache array."); + + const u32 poolSize = 65536; + u8* poolBase = SysMemory::GetEERecEnd() - poolSize; + s_eeConstantPool.Init(poolBase, poolSize); +} + +static void recResetRaw() +{ + Console.WriteLn(Color_Green, "iR5900-ARM64 Recompiler reset."); + + armSetAsmPtr(SysMemory::GetEERec(), SysMemory::GetEERecEnd() - SysMemory::GetEERec(), &s_eeConstantPool); + armStartBlock(); + const u8* dispStart = armGetCurrentCodePointer(); + _DynGen_Dispatchers(); + const u8* dispEnd = armGetCurrentCodePointer(); + recPtr = armEndBlock(); + + Console.WriteLn(Color_Green, "EE ARM64: Dispatcher generated at %p (%zu bytes)", dispStart, (size_t)(dispEnd - dispStart)); + + iopClearRecLUT(recLutReserve_RAM.data(), + Ps2MemSize::MainRam + Ps2MemSize::Rom + Ps2MemSize::Rom1); + + BASEBLOCK* unmapped = recLutUnmapped.data(); + + for (int i = 0; i < 0x10000; i++) + recLUT_SetPage(recLUT, hwLUT, unmapped, i, 0, 0); + + for (int i = 0; i < _64kb / 4; i++) + unmapped[i].SetFnptr((uptr)UnmappedRecLUTPage); + + // Map EE RAM (32MB, mirrored) + for (int i = 0; i < 0x200; i++) + { + u32 mask = (Ps2MemSize::MainRam / _64kb) - 1; + recLUT_SetPage(recLUT, hwLUT, recRAM, 0x0000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0x2000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0x3000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0x8000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0xa000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0xb000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0xc000, i, i & mask); + recLUT_SetPage(recLUT, hwLUT, recRAM, 0xd000, i, i & mask); + } + + // Map BIOS ROM + for (int i = 0x1fc0; i < 0x2000; i++) + { + recLUT_SetPage(recLUT, hwLUT, recROM, 0x0000, i, i - 0x1fc0); + recLUT_SetPage(recLUT, hwLUT, recROM, 0x8000, i, i - 0x1fc0); + recLUT_SetPage(recLUT, hwLUT, recROM, 0xa000, i, i - 0x1fc0); + } + + // Map ROM1 + for (int i = 0x1e00; i < 0x1e04; i++) + { + recLUT_SetPage(recLUT, hwLUT, recROM1, 0x0000, i, i - 0x1e00); + recLUT_SetPage(recLUT, hwLUT, recROM1, 0x8000, i, i - 0x1e00); + recLUT_SetPage(recLUT, hwLUT, recROM1, 0xa000, i, i - 0x1e00); + } + + if (s_pInstCache) + memset(s_pInstCache, 0, sizeof(EEINST) * s_nInstCacheSize); + + recBlocks.Reset(); + maxrecmem = 0; + + memset(manual_page, 0, sizeof(manual_page)); + memset(manual_counter, 0, sizeof(manual_counter)); + if (recRAMCopy.data()) + memset(recRAMCopy.data(), 0, recRAMCopy.size()); + + g_branch = 0; +} + +static void recShutdown() +{ + s_eeConstantPool.Destroy(); + recRAMCopy.deallocate(); + recLutReserve_RAM.deallocate(); + recLutUnmapped.deallocate(); + + safe_free(s_pInstCache); + s_nInstCacheSize = 0; + + recPtr = nullptr; + recPtrEnd = nullptr; +} + +static void recResetEE() +{ + if (eeCpuExecuting) + { + eeRecNeedsReset = true; + recSafeExitExecution(); + return; + } + + recResetRaw(); +} + +static void recStep() +{ +} + +static void recExitExecution() +{ + fastjmp_jmp(&m_SetJmp_StateCheck, 1); +} + +static void recSafeExitExecution() +{ + eeRecExitRequested = true; + + if (!eeEventTestIsActive) + { + cpuRegs.nextEventCycle = 0; + } + else + { + if (psxRegs.iopCycleEE > 0) + { + psxRegs.iopBreak += psxRegs.iopCycleEE; + psxRegs.iopCycleEE = 0; + } + } +} + +static void recCancelInstruction() +{ + // Called by interpreter functions (e.g. RaiseAddressError) when an + // exception occurs mid-instruction. For the interpreter, this does a + // longjmp. For the recompiler, set the TLB miss flag so that recCall's + // post-call check dispatches to the exception vector. + s_recTlbMissOccurred = 1; +} + +static void recExecute() +{ + if (eeRecNeedsReset) + { + eeRecNeedsReset = false; + recResetRaw(); + } + + Console.WriteLn(Color_Green, "EE ARM64: Entering recompiled code (pc=0x%08X)", cpuRegs.pc); + + if (!fastjmp_set(&m_SetJmp_StateCheck)) + { + eeCpuExecuting = true; + ((void (*)())EnterRecompiledCode)(); + } + + eeCpuExecuting = false; +} + +#ifdef PCSX2_RECOMPILER_TESTS +// Harness entry. Not part of R5900cpu; called directly by EeRecTestHarness +// for a bounded number of guest cycles ending at park_pc. Forces +// nextEventCycle = cpuRegs.cycle so iBranchTest at every block tail routes +// through DispatcherEvent (where recEventTest's harnessShouldExit check +// can observe parking-PC arrival or cycle exhaust). Returns the cycle +// delta consumed in this run. +s32 recEeExecuteBlock(s32 cycles, u32 park_pc) +{ + const s32 cap = std::min(cycles, kExecuteBlockSafetyCap); + + g_eeHarnessActive = true; + g_eeHarnessParkPc = park_pc; + g_eeHarnessCycleBudget = cap; + g_eeHarnessCycleStart = cpuRegs.cycle; + eeRecExitRequested = false; + + cpuRegs.nextEventCycle = cpuRegs.cycle; + + if (!fastjmp_set(&m_SetJmp_StateCheck)) + { + ((void (*)())EnterRecompiledCode)(); + } + + g_eeHarnessActive = false; + + return static_cast(cpuRegs.cycle - g_eeHarnessCycleStart); +} + +// Test-harness link introspection. Forwards to Arm64BaseBlocks::IsLinked, +// which walks the link multimap for any patch site within the block +// containing src_pc that targets dst_pc. +bool recEeIsBlockLinked(u32 src_pc, u32 dst_pc) +{ + return recBlocks.IsLinked(src_pc, dst_pc); +} +#endif + +// ===================================================================================================== +// Timeout Loop Speedhack +// ===================================================================================================== + +// Detects and skips timeout loops like: +// addiu v0,v0,-1 / nop*N / bne v0,zero,loop / nop +// Instead of spinning, advances the cycle counter and decrements the register. +// Port of x86 recSkipTimeoutLoop(). +static bool recSkipTimeoutLoop(s32 reg, bool is_timeout_loop) +{ + if (!EmuConfig.Speedhacks.WaitLoop || !is_timeout_loop) + return false; + + DevCon.WriteLn("[EE] Skipping timeout loop at 0x%08X -> 0x%08X (reg=%d)", + s_pCurBlockEx->startpc, s_nEndBlock, reg); + + // Logic: skip the loop by advancing cycles based on the register value. + // new_cycles = min(reg * 8 + cycle, nextEventCycle) + // new_reg = reg - (new_cycles - cycle) / 8 + // if new_reg > 0, jump to dispatcher (an event interrupted the loop) + // else loop finished, continue at s_nEndBlock + + // if (cycle >= nextEventCycle) goto DispatcherEvent (u64 comparison) + armAsm->Ldr(a64::x3, armCpuRegMem(&cpuRegs.nextEventCycle)); + armAsm->Cmp(RECCYCLE, a64::x3); + armEmitCondBranch(a64::hs, DispatcherEvent); + + // w4 = reg value (the decrementing counter) + armAsm->Ldr(a64::w4, armCpuRegMem(&cpuRegs.GPR.r[reg].UL[0])); + + // x5 = reg * 8 + cycle (estimated end cycle, u64) + armAsm->Add(a64::x5, RECCYCLE, a64::Operand(a64::x4, a64::LSL, 3)); + + // x5 = min(x5, nextEventCycle) + armAsm->Cmp(a64::x5, a64::x3); + armAsm->Csel(a64::x5, a64::x3, a64::x5, a64::hi); // if x5 > nextEvent, use nextEvent + + // w6 = (new_cycles - old_cycle) >> 3 = iterations consumed (uses old RECCYCLE). + armAsm->Sub(a64::w6, a64::w5, RECCYCLE.W()); + armAsm->Lsr(a64::w6, a64::w6, 3); + + // Commit the new cycle value into RECCYCLE (no memory store — DispatcherEvent + // will flush it if we exit there; otherwise the next block-tail event check + // uses RECCYCLE directly). + armAsm->Mov(RECCYCLE, a64::x5); + + // reg -= iterations consumed + armAsm->Sub(a64::w4, a64::w4, a64::w6); + armAsm->Str(a64::w4, armCpuRegMem(&cpuRegs.GPR.r[reg].UL[0])); + // Also sign-extend to upper 32 bits (EE GPRs are 64-bit for lower half) + armAsm->Sxtw(a64::x4, a64::w4); + armAsm->Str(a64::x4, armCpuRegMem(&cpuRegs.GPR.r[reg].UD[0])); + + // if reg != 0, event interrupted the loop — go to dispatcher + armEmitCbnz(a64::w4, DispatcherEvent); + + // Loop finished — set PC to end of block and dispatch + armAsm->Mov(RWSCRATCH, s_nEndBlock); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + armEmitJmp(DispatcherReg); + + g_branch = 1; + pc = s_nEndBlock; + + return true; +} + +// ===================================================================================================== +// Main Recompilation Loop +// ===================================================================================================== + +static void recRecompile(const u32 startpc) +{ + u32 i; + + // Note: startpc=0 is valid (EE RAM address 0). The x86 rec asserts on this + // but it can legitimately happen during BIOS init (e.g., JR $ra with ra=0). + // We allow it since address 0 is properly mapped in recLUT. + + if (recPtr >= recPtrEnd) + eeRecNeedsReset = true; + + // Signal that the ELF entry point is now compiling, so VMManager flips + // HasBootedELF() and applies the per-game GameDB fixes (both game fixes and + // GS hardware fixes — e.g. Baldur's Gate: Dark Alliance's textureInsideRT, + // which fixes the right-half-black menu render). Must fire before the + // deferred reset below — the hook can change settings and flush the JIT. + // Mirrors iR5900.cpp's recRecompile. + if (HWADDR(startpc) == VMManager::Internal::GetCurrentELFEntryPoint()) + VMManager::Internal::EntryPointCompilingOnCPUThread(); + + if (eeRecNeedsReset) + { + eeRecNeedsReset = false; + recResetRaw(); + } + + armSetAsmPtr(recPtr, recPtrEnd - recPtr + _64kb, &s_eeConstantPool); + armStartBlock(); + + s_pCurBlock = GETBLOCK(startpc); + pxAssert(s_pCurBlock->GetFnptr() == (uptr)JITCompile || s_pCurBlock->GetFnptr() == (uptr)UnmappedRecLUTPage); + + // armStartBlock() aligned armAsmPtr to 16 bytes, so the actual block + // code starts at armGetCurrentCodePointer(), not at recPtr. Block + // linking branches to BASEBLOCKEX::fnptr, so it must be the aligned + // address — using recPtr instead lands the branch on padding bytes + // and triggers SIGILL. + const uptr block_fnptr = (uptr)armGetCurrentCodePointer(); + + s_pCurBlockEx = recBlocks.Get(HWADDR(startpc)); + if (!s_pCurBlockEx || s_pCurBlockEx->startpc != HWADDR(startpc)) + s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr); + + g_branch = 0; + + s_pCurBlock->SetFnptr(block_fnptr); + s_nBlockCycles = 0; + s_nBlockInterlocked = false; + + pc = startpc; + g_cpuHasConstReg = g_cpuFlushedConstReg = 1; + g_cpuFlushedPC = false; + g_cpuFlushedCode = false; + + _initArm64GPRregs(); + _initArm64NEONregs(); + +#ifdef PCSX2_RECOMPILER_TESTS + // Optional block-entry diagnostic hook (test builds only). Emitted only when + // g_emit_block_hook is set before recReset; production recompiles emit + // nothing. Fires on EVERY block entry, including statically-linked ones, + // because linked branches target block_fnptr — i.e. exactly here. At the + // prologue all guest state is memory-resident (the allocator was just + // re-initialized to memory), so the hook's FingerprintCpu() reads correct + // cpuRegs. We pass startpc as an immediate because cpuRegs.pc is not updated + // on a static-linked entry, and flush RECCYCLE -> cpuRegs.cycle so the hook + // sees the live cycle. RECCYCLE (x25) is callee-saved across the C call, so + // no reload is needed. + if (ee_divtrace::g_emit_block_hook) + { + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armAsm->Mov(RWARG1, startpc); + armEmitCall((void*)ee_divtrace_jit_block_hook); + } +#endif + + // EELOAD detection + ELF-load hooks (mirrors iR5900.cpp's recRecompile). + // These compile-time-detected, run-time-emitted calls are how PCSX2 learns + // which game ELF is booting: eeloadHook() -> ELFLoadingOnCPUThread() sets + // s_elf_entry_point / CRC, which gates HasBootedELF() and therefore ALL + // per-game patches, game fixes, GS hardware fixes, widescreen, symbol import + // and achievements. The emitted calls run at block-execution time; at the + // prologue all guest state is memory-resident, so no iFlushCall is needed, + // and the pinned bases (x24/x25) are callee-saved across the C call. + if (HWADDR(startpc) == EELOAD_START) + { + // The EELOAD _start function is the same across all BIOS versions. + const u32 mainjump = memRead32(EELOAD_START + 0x9c); + if (mainjump >> 26 == 3) // JAL + g_eeloadMain = ((EELOAD_START + 0xa0) & 0xf0000000U) | (mainjump << 2 & 0x0fffffffU); + } + + if (g_eeloadMain && HWADDR(startpc) == HWADDR(g_eeloadMain)) + { + armEmitCall((void*)eeloadHook); + if (VMManager::Internal::IsFastBootInProgress()) + { + // Four known EELOAD versions, identified by the location of the 'jal' to + // the EELOAD function that calls ExecPS2(). The function itself is at the + // same address in all BIOSs after v1.00-v1.10. + const u32 typeAexecjump = memRead32(EELOAD_START + 0x470); // v1.00, v1.01?, v1.10? + const u32 typeBexecjump = memRead32(EELOAD_START + 0x5B0); // v1.20, v1.50, v1.60 (3000x models) + const u32 typeCexecjump = memRead32(EELOAD_START + 0x618); // v1.60 (3900x models) + const u32 typeDexecjump = memRead32(EELOAD_START + 0x600); // v1.70, v1.90, v2.00, v2.20, v2.30 + if ((typeBexecjump >> 26 == 3) || (typeCexecjump >> 26 == 3) || (typeDexecjump >> 26 == 3)) // JAL to 0x822B8 + g_eeloadExec = EELOAD_START + 0x2B8; + else if (typeAexecjump >> 26 == 3) // JAL to 0x82170 + g_eeloadExec = EELOAD_START + 0x170; + else // Unexamined BIOS models: 18000, 3500x, 3700x, 5500x, 7900x (and v1.01/v1.10). + 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 && HWADDR(startpc) == HWADDR(g_eeloadExec)) + armEmitCall((void*)eeloadHook2); + + // Scan for block boundary + i = startpc; + s_nEndBlock = 0xffffffff; + s_branchTo = -1; + + // Timeout loop detection (matches x86 recSkipTimeoutLoop pattern): + // addiu reg,reg,-N / nop*N / bne reg,zero,loop / nop + s32 timeout_reg = -1; + bool is_timeout_loop = true; + bool timeout_has_bne = false; + + while (1) + { + BASEBLOCK* pblock = GETBLOCK(i); + if (i != startpc && pblock->GetFnptr() != (uptr)JITCompile) + { + s_nEndBlock = i; + break; + } + + // 4K page boundary + if (i != startpc && (i & 0xffc) == 0) + { + s_nEndBlock = i; + break; + } + + cpuRegs.code = memRead32(i); + + // Timeout loop pattern matching + if (is_timeout_loop) + { + if ((cpuRegs.code >> 26) == 8 || (cpuRegs.code >> 26) == 9) + { + // addi/addiu — must be first non-nop, decrementing same reg + if (timeout_reg >= 0 || _Rs_ != _Rt_ || _Imm_ >= 0) + is_timeout_loop = false; + else + timeout_reg = _Rs_; + } + else if ((cpuRegs.code >> 26) == 5) + { + // bne — must branch back using the timeout reg vs zero + if (timeout_reg != static_cast(_Rs_) || _Rt_ != 0 || memRead32(i + 4) != 0) + is_timeout_loop = false; + else + timeout_has_bne = true; + } + else if (cpuRegs.code != 0) + { + is_timeout_loop = false; + } + } + + switch (cpuRegs.code >> 26) + { + case 0: // SPECIAL + if (_Funct_ == 8 || _Funct_ == 9) // JR, JALR + { + s_nEndBlock = i + 8; + goto StartRecomp; + } + if (_Funct_ == 12 || _Funct_ == 13) // SYSCALL, BREAK + { + s_nEndBlock = i + 4; // no delay slot + goto StartRecomp; + } + break; + + case 1: // REGIMM + if (_Rt_ < 4 || (_Rt_ >= 16 && _Rt_ < 20)) + { + s_branchTo = _Imm_ * 4 + i + 4; + // Backward branch into the current block: end the block at the + // target so the loop head becomes its own linkable block. + // Mirrors x86 iR5900.cpp:2362 and the COP1/COP2 case below. + if (s_branchTo > startpc && s_branchTo < i) + s_nEndBlock = s_branchTo; + else + s_nEndBlock = i + 8; + goto StartRecomp; + } + break; + + case 2: case 3: // J, JAL + s_branchTo = (_InstrucTarget_ << 2) | ((i + 4) & 0xf0000000); + s_nEndBlock = i + 8; + goto StartRecomp; + + case 4: case 5: case 6: case 7: // BEQ, BNE, BLEZ, BGTZ + case 20: case 21: // BEQL, BNEL + case 22: case 23: // BLEZL, BGTZL + s_branchTo = _Imm_ * 4 + i + 4; + // Backward branch into the current block: split so the loop head + // is its own linkable block. Mirrors x86 iR5900.cpp:2387 and + // the COP1/COP2 case below. + if (s_branchTo > startpc && s_branchTo < i) + s_nEndBlock = s_branchTo; + else + s_nEndBlock = i + 8; + goto StartRecomp; + + case 16: // COP0 + if (_Rs_ == 16 && _Funct_ == 24) // ERET (no delay slot) + { + s_nEndBlock = i + 4; + goto StartRecomp; + } + // Fall through: COP0's branch opcodes line up with COP1/COP2's. + [[fallthrough]]; + + case 17: // COP1 + case 18: // COP2 + if (_Rs_ == 8) // BC0/BC1/BC2 F/T/FL/TL + { + s_branchTo = _Imm_ * 4 + i + 4; + if (s_branchTo > startpc && s_branchTo < i) + s_nEndBlock = s_branchTo; + else + s_nEndBlock = i + 8; + goto StartRecomp; + } + break; + } + + i += 4; + } + +StartRecomp: + + // Self-modifying code detection: generate inline memory checks for manual blocks. + memory_protect_recompiled_code(startpc, (s_nEndBlock - startpc) >> 2); + + // Infinite loop detection + s_nBlockFF = false; + if (s_branchTo == startpc) + { + s_nBlockFF = true; + for (i = startpc; i < s_nEndBlock; i += 4) + { + if (i != s_nEndBlock - 8 && memRead32(i) != 0) + { + s_nBlockFF = false; + break; + } + } + } + else + { + // A timeout loop must branch back to its own start (a self-loop). If the + // block's terminating branch targets anywhere else, it is NOT a timeout + // loop and must be recompiled normally. Mirrors x86 iR5900.cpp:2510-2513. + // Without this guard, the early-exit `bne reg,zero,` at the TOP + // of a counted compute loop gets misdetected as a timeout loop and + // recSkipTimeoutLoop fast-forwards the counter to 0 while skipping the + // loop BODY's real work. timeout_has_bne alone is insufficient because it + // matches the forward early-exit branch without checking the branch target. + is_timeout_loop = false; + } + + // Instruction analysis (backward pass) + { + EEINST* pcur; + + if (s_nInstCacheSize < (s_nEndBlock - startpc) / 4 + 1) + { + free(s_pInstCache); + s_nInstCacheSize = (s_nEndBlock - startpc) / 4 + 10; + s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize); + pxAssert(s_pInstCache != NULL); + } + + pcur = s_pInstCache + (s_nEndBlock - startpc) / 4; + _recClearInst(pcur); + pcur->info = 0; + + bool has_cop2_instructions = false; + for (i = s_nEndBlock; i > startpc; i -= 4) + { + cpuRegs.code = memRead32(i - 4); + pcur[-1] = pcur[0]; + recBackpropBSC(cpuRegs.code, pcur - 1, pcur); + pcur--; + + has_cop2_instructions |= (_Opcode_ == 022 || _Opcode_ == 066 || _Opcode_ == 076); + } + + // Run COP2 analysis passes — sets EEINST_COP2_SYNC_VU0/FINISH_VU0 flags + // for conditional VU0 synchronization in transfer ops. + if (has_cop2_instructions) + { + R5900::COP2MicroFinishPass().Run(startpc, s_nEndBlock, s_pInstCache + 1); + + if (EmuConfig.Speedhacks.vuFlagHack) + R5900::COP2FlagHackPass().Run(startpc, s_nEndBlock, s_pInstCache + 1); + } + } + + // Try timeout loop speedhack — if detected, skip normal codegen + // Timer-poll loops (mfc0 Count / subu / sltu / bne) are NOT skipped because + // they need to wait for a specific elapsed time — the correct fix is native codegen. + // Require timeout_reg >= 0 (actually found an addiu) to avoid matching all-nop blocks + const bool doRecompilation = !recSkipTimeoutLoop(timeout_reg, is_timeout_loop && timeout_reg >= 0 && timeout_has_bne); + + // Code generation (forward pass) + if (doRecompilation) + { + g_pCurInstInfo = s_pInstCache; + while (!g_branch && pc < s_nEndBlock) + recompileNextInstruction(false, false); + } + + pxAssert((pc - startpc) >> 2 <= 0xffff); + s_pCurBlockEx->size = (pc - startpc) >> 2; + + if (!(pc & 0x10000000)) + maxrecmem = std::max((pc & ~0xa0000000), maxrecmem); + + // Snapshot current block's source to recRAMCopy for future overlap detection. + // Note: The overlap check (comparing old blocks' recRAMCopy vs current memory) is + // disabled because it causes infinite recompilation loops — recRAMCopy starts zeroed + // but memory has real code, so the memcmp always fails. The inline CMP checks from + // memory_protect_recompiled_code are the primary SMC detection mechanism. + if (HWADDR(pc) <= Ps2MemSize::MainRam) + { + memcpy(&recRAMCopy[HWADDR(startpc) / 4], PSM(startpc), pc - startpc); + } + + if (g_branch == 2) + { + // Branch taken — flush and dispatch. recBranchCall already accumulated + // any pre-call cycles into RECCYCLE and reloaded it after the C call, + // so any further scaleblockcycles_clear() result is the post-call delta. + iFlushCall(FLUSH_EVERYTHING); + + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + armAsm->Ldr(a64::x3, armCpuRegMem(&cpuRegs.nextEventCycle)); + armAsm->Cmp(RECCYCLE, a64::x3); + armEmitCondBranch(a64::ge, DispatcherEvent); + + armEmitJmp(DispatcherReg); + } + else + { + if (g_branch) + { + // g_branch == 1: block ended with a branch instruction. + // pc may not equal s_nEndBlock for branch-likely instructions + // where the not-taken path skips the delay slot. + } + else + { + // Non-branch block end (split / page boundary / cycle cap). + // Mirrors x86 iR5900.cpp:2680-2698: + // long block (>6 insns): SetBranchImm(pc) — event check + static-linked B + // short block (≤6 insns): flush + pc + cycle + bare static-linked B + // Short blocks skip the event check entirely; a few-insn block can't + // have advanced cycles far enough to cross an event boundary, so the + // load+cmp+B.ge is dead-weight code per block tail. + if (pc != s_nEndBlock) + Console.Error("EE ARM64: Block end mismatch! startpc=0x%08X pc=0x%08X s_nEndBlock=0x%08X", startpc, pc, s_nEndBlock); + pxAssert(pc == s_nEndBlock); + + const int numinsts = (pc - startpc) / 4; + if (numinsts > 6) + { + SetBranchImm(pc); + } + else + { + iFlushCall(FLUSH_EVERYTHING); + + armAsm->Mov(RWSCRATCH, pc); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc)); + + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + a64::SingleEmissionCheckScope guard(armAsm); + u8* patch_site = armGetCurrentCodePointer(); + armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite + recBlocks.Link(HWADDR(pc), patch_site); + } + } + } + + pxAssert(armGetCurrentCodePointer() < SysMemory::GetEERecEnd()); + + // Size is from the aligned block_fnptr, not the pre-alignment recPtr — + // keeps Perf::ee.RegisterPC consistent with the linker's view of the block. + s_pCurBlockEx->x86size = static_cast((uptr)armGetCurrentCodePointer() - s_pCurBlockEx->fnptr); + + Perf::ee.RegisterPC((void*)s_pCurBlockEx->fnptr, s_pCurBlockEx->x86size, s_pCurBlockEx->startpc); + + recPtr = armEndBlock(); + + pxAssert((g_cpuHasConstReg & g_cpuFlushedConstReg) == g_cpuHasConstReg); + + s_pCurBlock = NULL; + s_pCurBlockEx = NULL; +} + +// ===================================================================================================== +// Thunk helpers for fastmem backpatching +// ===================================================================================================== + +u8* recBeginThunk() +{ + // Check for recompiler cache overflow + if (recPtr >= recPtrEnd) + eeRecNeedsReset = true; + + // Set up assembler to emit thunk code at the current recompiler pointer. + // No constant pool needed for thunks (they're small and self-contained). + armSetAsmPtr(recPtr, recPtrEnd - recPtr + _64kb, nullptr); + u8* aligned = armStartBlock(); + + // Return the aligned address where code actually starts, not recPtr. + // armStartBlock() aligns to 16 bytes — branching to recPtr would hit padding. + return aligned; +} + +u8* recEndThunk() +{ + recPtr = armEndBlock(); + pxAssert(recPtr < SysMemory::GetEERecEnd()); + return recPtr; +} + +// ===================================================================================================== +// R5900cpu struct — public interface +// ===================================================================================================== + +R5900cpu recCpu = { + recReserve, + recShutdown, + recResetEE, + recStep, + recExecute, + recSafeExitExecution, + recCancelInstruction, + recClear, +}; diff --git a/pcsx2/arm64/iR5900-arm64.h b/pcsx2/arm64/iR5900-arm64.h new file mode 100644 index 0000000000..732b6a3551 --- /dev/null +++ b/pcsx2/arm64/iR5900-arm64.h @@ -0,0 +1,297 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Config.h" +#include "R5900.h" +#include "R5900OpcodeTables.h" +#include "VU.h" +#include "arm64/iCore-arm64.h" + +// Per-category interpreter fallback toggles. +// Comment out a define to enable native ARM64 codegen for that category. +// #define FORCE_INTERP_BRANCH 1 +// #define FORCE_INTERP_JUMP 1 +// #define FORCE_INTERP_MOVE 1 +// #define FORCE_INTERP_SHIFT 1 +// #define FORCE_INTERP_ALU 1 +// #define FORCE_INTERP_ARITIMM 1 +// #define FORCE_INTERP_MULTDIV 1 +// #define FORCE_INTERP_MEMORY 1 +// #define FORCE_INTERP_COP0 1 +// #define FORCE_INTERP_FPU 1 +// #define FORCE_INTERP_COP2 1 + +// Reserved ARM64 registers for the recompiler +// x19: Fastmem base pointer (callee-saved) +#define RFASTMEMBASE vixl::aarch64::x19 +// x20: Pointer to cpuRegs struct (callee-saved). Loaded once at JIT entry by +// EnterRecompiledCode and never modified for the duration of JIT execution. +// Use armCpuRegMem() to construct cpuRegs-relative MemOperands cheaply. +#define RSTATE vixl::aarch64::x20 +// x24: Pinned &VU0 (callee-saved). Loaded once at JIT entry by +// EnterRecompiledCode; used to address VU0.{VF,VI,ACC,q,statusflag,...} +// fields via single [RVU0, #imm12] load/store in iCOP2-arm64.cpp, +// instead of the 3-mov+ldr abs-addr materialization sequence. +// Survives armEmitCall (callee-saved per AAPCS) and the mVU dispatcher's +// outer Stp/Ldp pair, so cross-EE/mVU dispatches preserve it. +#define RVU0 vixl::aarch64::x24 +// x25: Pinned cpuRegs.cycle (callee-saved). Always valid while in JIT; +// flushed to memory only around C calls (DispatcherEvent, JITCompile, +// recBranchCall). Block-to-block control flow keeps it live, so linked +// branches don't reload the cycle counter from memory each iteration. +#define RECCYCLE vixl::aarch64::x25 + +// Build a MemOperand addressing a cpuRegs field via RSTATE. +// Replaces the 3-instruction `armMoveAddressToReg(RSCRATCHADDR, &cpuRegs.X); +// Ldr/Str ..., [RSCRATCHADDR]` pattern with a single Ldr/Str using a +// signed/unsigned-immediate offset on RSTATE. ARM64 LDR with imm12 covers +// offsets up to 32760 bytes (64-bit) / 16380 bytes (32-bit) — easily larger +// than cpuRegs / fpuRegs combined, so a single instruction suffices for +// every reachable field. +static __fi vixl::aarch64::MemOperand armCpuRegMem(const void* field) +{ + const ptrdiff_t off = reinterpret_cast(field) - reinterpret_cast(&cpuRegs); + return vixl::aarch64::MemOperand(RSTATE, static_cast(off)); +} + +// Pinned-base load/store helpers: when the target is anywhere inside +// _cpuRegistersPack (cpuRegs + fpuRegs), reach it via [RSTATE, #off] in one +// instruction; otherwise fall back to the generic 4-inst armLoadPtr/StorePtr. +static __fi bool armIsCpuRegPtr(const void* field) +{ + const u8* base = reinterpret_cast(&_cpuRegistersPack); + const u8* p = reinterpret_cast(field); + return p >= base && p < base + sizeof(cpuRegistersPack); +} +static __fi void armLoadEERegPtr(const vixl::aarch64::CPURegister& reg, const void* field) +{ + if (armIsCpuRegPtr(field)) + armAsm->Ldr(reg, armCpuRegMem(field)); + else + armLoadPtr(reg, field); +} +static __fi void armStoreEERegPtr(const vixl::aarch64::CPURegister& reg, const void* field) +{ + if (armIsCpuRegPtr(field)) + armAsm->Str(reg, armCpuRegMem(field)); + else + armStorePtr(reg, field); +} + +// Build a MemOperand addressing a VU0 field via RVU0. VURegs is < 2 KB, so +// every reachable field fits in imm12 for byte/halfword/word/doubleword/quad +// ldr/str. Mirrors armCpuRegMem for VU0 — used by iCOP2-arm64.cpp. +static __fi vixl::aarch64::MemOperand armVU0Mem(const void* field) +{ + const ptrdiff_t off = reinterpret_cast(field) - reinterpret_cast(&VU0); + return vixl::aarch64::MemOperand(RVU0, static_cast(off)); +} + +// Emit LD1R from a VU0 field, broadcasting to all lanes. ARM64 LD1R does +// NOT support [base, #imm] addressing — only [base] or post-index. vixl's +// LoadStoreStructAddrModeField silently drops the offset (and the assert +// is gated on VIXL_DEBUG, so Devel builds ship the wrong encoding instead +// of trapping). Materialize the address with a single ADD imm12 instead of +// 3-mov: VURegs fields fit within 4 KB of &VU0, so one ADD suffices. +// Total cost: ADD + LD1R = 2 insns, vs the original 4-insn 3-mov + LD1R. +static __fi void armLd1rVU0(const vixl::aarch64::VRegister& vt, const void* field) +{ + const ptrdiff_t off = reinterpret_cast(field) - reinterpret_cast(&VU0); + armAsm->Add(RSCRATCHADDR, RVU0, static_cast(off)); + armAsm->Ld1r(vt, vixl::aarch64::MemOperand(RSCRATCHADDR)); +} + +extern u32 maxrecmem; +extern u32 pc; // recompiler pc +extern int g_branch; // set for branch +extern u32 target; // branch target +extern u32 s_nBlockCycles; // cycles of current block recompiling +extern bool s_nBlockInterlocked; // Current block has VU0 interlocking + +////////////////////////////////////////////////////////////////////////////////////////// +// Interpreter fallback macros + +#define REC_FUNC(f) \ + void rec##f() \ + { \ + /* Delete destination register's const/alloc state before interpreter call. \ + * The interpreter writes directly to cpuRegs, making any cached const or \ + * allocated register stale. SPECIAL ops (Rd), loads/COP (Rt). */ \ + const u32 _op = cpuRegs.code >> 26; \ + const int _dest = (_op == 0 || _op == 0x1C) ? _Rd_ : _Rt_; \ + if (_dest > 0) \ + _deleteEEreg(_dest, 1); \ + recCall(Interp::f); \ + } + +#define REC_FUNC_DEL(f, delreg) \ + void rec##f() \ + { \ + if ((delreg) > 0) \ + _deleteEEreg(delreg, 1); \ + recCall(Interp::f); \ + } + +#define REC_SYS(f) \ + void rec##f() \ + { \ + recBranchCall(Interp::f); \ + } + +#define REC_SYS_DEL(f, delreg) \ + void rec##f() \ + { \ + if ((delreg) > 0) \ + _deleteEEreg(delreg, 1); \ + recBranchCall(Interp::f); \ + } + +extern bool g_recompilingDelaySlot; + +// Used for generating backpatch thunks for fastmem +u8* recBeginThunk(); +u8* recEndThunk(); + +// Branch processing +bool TrySwapDelaySlot(u32 rs, u32 rt, u32 rd, bool allow_loadstore); +void SaveBranchState(); +void LoadBranchState(); + +void recompileNextInstruction(bool delayslot, bool swapped_delay_slot); +void SetBranchReg(); +void SetBranchImm(u32 imm); + +void iFlushCall(int flushtype); +void recBranchCall(void (*func)()); +void recCall(void (*func)()); +// Emit the post-interpreter-call TLB-miss exception dispatch (defined in +// iR5900-arm64.cpp). DispatcherReg/s_recTlbMissOccurred are file-local there, +// so cross-TU interpreter-call sites (recVTLB-arm64.cpp) route through this. +void recEmitInterpTlbMissCheck(); +u32 scaleblockcycles_clear(); + +// COP2 / VU0 sync emit helper (defined in iCOP2-arm64.cpp). +// interlock=true mirrors x86 COP2_Interlock (CFC2/CTC2/QMFC2/QMTC2 path); +// interlock=false mirrors mVUSyncVU0 / mVUFinishVU0 gating used by LQC2/SQC2 +// and the COP2 macro-arithmetic setup. finishFunc is the secondary helper +// to invoke after vu0Sync (typically _vu0FinishMicro or _vu0WaitMicro); +// pass nullptr for "sync only". Emits zero instructions when EEINST analysis +// flags say no sync is needed. +void cop2EmitConditionalSync(bool interlock, void (*finishFunc)()); + +// COP2 macro-mode microVU0 state setup/teardown (defined in microVU-arm64.cpp). +// Mirrors x86 setupMacroOp/endMacroOp's regAlloc reset, microVU0.cop2 = 1, +// prog.IRinfo.curPC/info[0] init, code = cpuRegs.code, and flag scaffolding. +// Required before invoking any mVU emitter (mVU_LQI/SQI/MFIR/...) from a +// COP2 macro-mode dispatch wrapper. eeinstInfo is g_pCurInstInfo->info (or 0 +// when EEINST analysis isn't live for this site). +void mVUmacroSetupCOP2State(int mode, u32 eeinstInfo); +void mVUmacroEndCOP2State(); + +// COP2 macro-mode setup/teardown wrapper (defined in iCOP2-arm64.cpp). +// Calls cop2EmitConditionalSync, emits status-flag denormalize/normalize when +// mode & 0x10, then runs mVUmacroSetup/EndCOP2State to ready microVU0 for the +// mVU emitter pass. REC_COP2_mVU0_ARM64-style wrappers in iR5900Misc-arm64.cpp +// bracket calls to mVUmacroEmit_ with these. +void setupMacroOp_arm64(int mode); +void endMacroOp_arm64(int mode); + +// COP2 macro-mode emit adapters (defined in microVU-arm64.cpp). Each runs the +// pass1+pass2 dispatch x86 uses in REC_COP2_mVU0 (microVU_Macro.inl:127-133). +// mode is the same mode bits passed to setupMacroOp_arm64; only bit 0x04 +// (requires analysis pass) is observed by the adapter. +void mVUmacroEmit_LQI(int mode); +void mVUmacroEmit_SQI(int mode); +void mVUmacroEmit_LQD(int mode); +void mVUmacroEmit_SQD(int mode); +void mVUmacroEmit_MTIR(int mode); +void mVUmacroEmit_MFIR(int mode); +void mVUmacroEmit_ILWR(int mode); +void mVUmacroEmit_ISWR(int mode); +void mVUmacroEmit_RNEXT(int mode); +void mVUmacroEmit_RGET(int mode); +void mVUmacroEmit_RINIT(int mode); +void mVUmacroEmit_RXOR(int mode); + +namespace R5900 +{ + namespace Dynarec + { + extern void recDoBranchImm(u32 branchTo, u32* jmpSkip, bool isLikely = false, bool swappedDelaySlot = false); + } +} + +//////////////////////////////////////////////////////////////////// +// Constant Propagation + +#define GPR_IS_CONST1(reg) (EE_CONST_PROP && (reg) < 32 && (g_cpuHasConstReg & (1 << (reg)))) +#define GPR_IS_CONST2(reg1, reg2) (EE_CONST_PROP && (g_cpuHasConstReg & (1 << (reg1))) && (g_cpuHasConstReg & (1 << (reg2)))) +#define GPR_IS_DIRTY_CONST(reg) (EE_CONST_PROP && (reg) < 32 && (g_cpuHasConstReg & (1 << (reg))) && (!(g_cpuFlushedConstReg & (1 << (reg))))) +#define GPR_SET_CONST(reg) \ + { \ + if ((reg) < 32) \ + { \ + g_cpuHasConstReg |= (1 << (reg)); \ + g_cpuFlushedConstReg &= ~(1 << (reg)); \ + } \ + } + +#define GPR_DEL_CONST(reg) \ + { \ + if ((reg) < 32) \ + g_cpuHasConstReg &= ~(1 << (reg)); \ + } + +alignas(16) extern GPR_reg64 g_cpuConstRegs[32]; +extern u32 g_cpuHasConstReg, g_cpuFlushedConstReg; + +// Move guest GPR value to an ARM64 register +void _eeMoveGPRtoR(const vixl::aarch64::Register& to, int fromgpr, bool allow_preload = true); + +void _eeFlushAllDirty(); +void _eeOnWriteReg(int reg, int signext); + +// Totally deletes from const, NEON, and GPR entries +// if flush is 1, also flushes to memory +void _deleteEEreg(int reg, int flush); +void _deleteEEreg128(int reg); + +void _flushEEreg(int reg, bool clear = false); + +////////////////////////////////////// +// Templates for code recompilation // +////////////////////////////////////// + +typedef void (*R5900FNPTR)(); +typedef void (*R5900FNPTR_INFO)(int info); + +// Memory-based templates — no register allocation, all operands via cpuRegs memory. +void eeRecompileCodeRC0_MEM(R5900FNPTR constcode, R5900FNPTR_INFO constscode, R5900FNPTR_INFO consttcode, R5900FNPTR_INFO noconstcode, int xmminfo); +void eeRecompileCodeRC1_MEM(R5900FNPTR constcode, R5900FNPTR_INFO noconstcode, int xmminfo); +void eeRecompileCodeRC2_MEM(R5900FNPTR constcode, R5900FNPTR_INFO noconstcode, int xmminfo); + +#define EERECOMPILE_CODERC0_MEM(fn, xmminfo) \ + void rec##fn(void) \ + { \ + eeRecompileCodeRC0_MEM(rec##fn##_const, rec##fn##_consts, rec##fn##_constt, rec##fn##_, (xmminfo)); \ + } + +#define EERECOMPILE_CODEX_MEM(codename, fn, xmminfo) \ + void rec##fn(void) \ + { \ + codename(rec##fn##_const, rec##fn##_, (xmminfo)); \ + } + +#define FPURECOMPILE_CONSTCODE(fn, xmminfo) \ + void rec##fn(void) \ + { \ + if (CHECK_FPU_FULL) \ + eeFPURecompileCode(DOUBLE::rec##fn##_xmm, R5900::Interpreter::OpcodeImpl::COP1::fn, xmminfo); \ + else \ + eeFPURecompileCode(rec##fn##_xmm, R5900::Interpreter::OpcodeImpl::COP1::fn, xmminfo); \ + } + +int eeRecompileCodeXMM(int xmminfo); +void eeFPURecompileCode(R5900FNPTR_INFO xmmcode, R5900FNPTR fpucode, int xmminfo); diff --git a/pcsx2/arm64/iR5900Analysis.h b/pcsx2/arm64/iR5900Analysis.h new file mode 100644 index 0000000000..e2f03af9e0 --- /dev/null +++ b/pcsx2/arm64/iR5900Analysis.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 wrapper for the shared instruction analysis pass. +// Routes to ARM64-specific headers instead of x86 iR5900.h/iCore.h. + +#pragma once + +#include "arm64/iR5900-arm64.h" +#include "arm64/iCore-arm64.h" + +// Re-export the shared analysis classes and functions from x86/iR5900Analysis.h +namespace R5900 +{ + class AnalysisPass + { + public: + AnalysisPass(); + virtual ~AnalysisPass(); + + virtual void Run(u32 start, u32 end, EEINST* inst_cache); + + protected: + template + void ForEachInstruction(u32 start, u32 end, EEINST* inst_cache, const F& func); + + template + void DumpAnnotatedBlock(u32 start, u32 end, EEINST* inst_cache, const F& func); + }; + + class COP2FlagHackPass final : public AnalysisPass + { + public: + COP2FlagHackPass(); + ~COP2FlagHackPass(); + + void Run(u32 start, u32 end, EEINST* inst_cache) override; + + private: + void DumpAnnotatedBlock(u32 start, u32 end, EEINST* inst_cache); + + void CommitStatusFlag(); + void CommitMACFlag(); + void CommitClipFlag(); + void CommitAllFlags(); + + bool m_status_denormalized = false; + EEINST* m_last_status_write = nullptr; + EEINST* m_last_mac_write = nullptr; + EEINST* m_last_clip_write = nullptr; + + u32 m_cfc2_pc = 0; + }; + + class COP2MicroFinishPass final : public AnalysisPass + { + public: + COP2MicroFinishPass(); + ~COP2MicroFinishPass(); + + void Run(u32 start, u32 end, EEINST* inst_cache) override; + }; +} // namespace R5900 + +void recBackpropBSC(u32 code, EEINST* prev, EEINST* pinst); diff --git a/pcsx2/arm64/iR5900Arit-arm64.cpp b/pcsx2/arm64/iR5900Arit-arm64.cpp new file mode 100644 index 0000000000..5adcb99f83 --- /dev/null +++ b/pcsx2/arm64/iR5900Arit-arm64.cpp @@ -0,0 +1,456 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE ALU Instruction Codegen — memory-based +// All operands loaded from / stored to cpuRegs.GPR memory. +// No register allocation for scalar ops. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_ALU +REC_FUNC(ADD); +void recADDU() { recADD(); } +REC_FUNC(DADD); +void recDADDU() { recDADD(); } +REC_FUNC(SUB); +void recSUBU() { recSUB(); } +REC_FUNC(DSUB); +void recDSUBU() { recDSUB(); } +REC_FUNC(AND); +REC_FUNC(OR); +REC_FUNC(XOR); +REC_FUNC(NOR); +REC_FUNC(SLT); +REC_FUNC(SLTU); +#else + +// Memory load/store helpers — always use cpuRegs memory +static void memLoadS32() { armLoadEERegPtr(RWARG1, &cpuRegs.GPR.r[_Rs_].UL[0]); } +static void memLoadT32() { armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); } +static void memLoadS64() { armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rs_].UD[0]); } +static void memLoadT64() { armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); } +static void memStoreD() { armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); } + +/********************************************************* + * Register arithmetic — rd = rs OP rt * + * 32-bit ops sign-extend result to 64 bits * + *********************************************************/ + +//// ADD / ADDU + +static void recADD_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = s64(s32(g_cpuConstRegs[_Rs_].UL[0] + g_cpuConstRegs[_Rt_].UL[0])); +} + +static void recADD_consts(int info) +{ + const s32 cval = g_cpuConstRegs[_Rs_].SL[0]; + memLoadT32(); + if (cval != 0) + armAsm->Add(RWSCRATCH, RWSCRATCH, cval); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recADD_constt(int info) +{ + const s32 cval = g_cpuConstRegs[_Rt_].SL[0]; + memLoadS32(); + if (cval != 0) + { + armAsm->Add(RWSCRATCH, RWARG1, cval); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + } + else + { + armAsm->Sxtw(RXSCRATCH, RWARG1); + } + memStoreD(); +} + +static void recADD_(int info) +{ + memLoadS32(); + memLoadT32(); + armAsm->Add(RWSCRATCH, RWARG1, RWSCRATCH); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(ADD, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); + +void recADDU() { recADD(); } + +//// DADD / DADDU + +static void recDADD_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] + g_cpuConstRegs[_Rt_].UD[0]; +} + +static void recDADD_consts(int info) +{ + const s64 cval = g_cpuConstRegs[_Rs_].SD[0]; + memLoadT64(); + if (cval != 0) + { + armAsm->Mov(RXARG1, cval); + armAsm->Add(RXSCRATCH, RXSCRATCH, RXARG1); + } + memStoreD(); +} + +static void recDADD_constt(int info) +{ + const s64 cval = g_cpuConstRegs[_Rt_].SD[0]; + memLoadS64(); + if (cval != 0) + armAsm->Add(RXSCRATCH, RXARG1, cval); + else + armAsm->Mov(RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recDADD_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Add(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(DADD, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +void recDADDU() { recDADD(); } + +//// SUB / SUBU + +static void recSUB_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = s64(s32(g_cpuConstRegs[_Rs_].UL[0] - g_cpuConstRegs[_Rt_].UL[0])); +} + +static void recSUB_consts(int info) +{ + const s32 cval = g_cpuConstRegs[_Rs_].SL[0]; + memLoadT32(); + armAsm->Mov(RWARG1, cval); + armAsm->Sub(RWSCRATCH, RWARG1, RWSCRATCH); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSUB_constt(int info) +{ + const s32 cval = g_cpuConstRegs[_Rt_].SL[0]; + memLoadS32(); + if (cval != 0) + { + armAsm->Sub(RWSCRATCH, RWARG1, cval); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + } + else + { + armAsm->Sxtw(RXSCRATCH, RWARG1); + } + memStoreD(); +} + +static void recSUB_(int info) +{ + // rs - rs == 0 always; emit a single store of zero. + if (_Rs_ == _Rt_) + { + armStoreEERegPtr(a64::xzr, &cpuRegs.GPR.r[_Rd_].UD[0]); + return; + } + memLoadS32(); + memLoadT32(); + armAsm->Sub(RWSCRATCH, RWARG1, RWSCRATCH); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SUB, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); + +void recSUBU() { recSUB(); } + +//// DSUB / DSUBU + +static void recDSUB_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] - g_cpuConstRegs[_Rt_].UD[0]; +} + +static void recDSUB_consts(int info) +{ + const s64 cval = g_cpuConstRegs[_Rs_].SD[0]; + memLoadT64(); + armAsm->Mov(RXARG1, cval); + armAsm->Sub(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +static void recDSUB_constt(int info) +{ + const s64 cval = g_cpuConstRegs[_Rt_].SD[0]; + memLoadS64(); + if (cval != 0) + armAsm->Sub(RXSCRATCH, RXARG1, cval); + else + armAsm->Mov(RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recDSUB_(int info) +{ + // rs - rs == 0 always; emit a single store of zero. + if (_Rs_ == _Rt_) + { + armStoreEERegPtr(a64::xzr, &cpuRegs.GPR.r[_Rd_].UD[0]); + return; + } + memLoadS64(); + memLoadT64(); + armAsm->Sub(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(DSUB, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +void recDSUBU() { recDSUB(); } + +//// AND — 64-bit + +static void recAND_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] & g_cpuConstRegs[_Rt_].UD[0]; +} + +static void recAND_consts(int info) +{ + memLoadT64(); + armAsm->And(RXSCRATCH, RXSCRATCH, g_cpuConstRegs[_Rs_].UD[0]); + memStoreD(); +} + +static void recAND_constt(int info) +{ + memLoadS64(); + armAsm->And(RXSCRATCH, RXARG1, g_cpuConstRegs[_Rt_].UD[0]); + memStoreD(); +} + +static void recAND_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->And(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(AND, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// OR — 64-bit + +static void recOR_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] | g_cpuConstRegs[_Rt_].UD[0]; +} + +static void recOR_consts(int info) +{ + const u64 cval = g_cpuConstRegs[_Rs_].UD[0]; + memLoadT64(); + if (cval != 0) + armAsm->Orr(RXSCRATCH, RXSCRATCH, cval); + memStoreD(); +} + +static void recOR_constt(int info) +{ + const u64 cval = g_cpuConstRegs[_Rt_].UD[0]; + memLoadS64(); + if (cval != 0) + armAsm->Orr(RXSCRATCH, RXARG1, cval); + else + armAsm->Mov(RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recOR_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Orr(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(OR, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// XOR — 64-bit + +static void recXOR_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] ^ g_cpuConstRegs[_Rt_].UD[0]; +} + +static void recXOR_consts(int info) +{ + memLoadT64(); + armAsm->Mov(RXARG1, g_cpuConstRegs[_Rs_].UD[0]); + armAsm->Eor(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recXOR_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]); + armAsm->Eor(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +static void recXOR_(int info) +{ + // rs ^ rs == 0 always; skip the two operand loads (mirrors recSUB_). + if (_Rs_ == _Rt_) + { + armAsm->Mov(RXSCRATCH, 0); + memStoreD(); + return; + } + memLoadS64(); + memLoadT64(); + armAsm->Eor(RXSCRATCH, RXARG1, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(XOR, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// NOR — rd = ~(rs | rt), 64-bit + +static void recNOR_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = ~(g_cpuConstRegs[_Rs_].UD[0] | g_cpuConstRegs[_Rt_].UD[0]); +} + +static void recNOR_consts(int info) +{ + const u64 cval = g_cpuConstRegs[_Rs_].UD[0]; + memLoadT64(); + if (cval != 0) + armAsm->Orr(RXSCRATCH, RXSCRATCH, cval); + armAsm->Mvn(RXSCRATCH, RXSCRATCH); + memStoreD(); +} + +static void recNOR_constt(int info) +{ + const u64 cval = g_cpuConstRegs[_Rt_].UD[0]; + memLoadS64(); + if (cval != 0) + armAsm->Orr(RXSCRATCH, RXARG1, cval); + else + armAsm->Mov(RXSCRATCH, RXARG1); + armAsm->Mvn(RXSCRATCH, RXSCRATCH); + memStoreD(); +} + +static void recNOR_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Orr(RXSCRATCH, RXARG1, RXSCRATCH); + armAsm->Mvn(RXSCRATCH, RXSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(NOR, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// SLT — rd = (rs < rt) ? 1 : 0 (signed 64-bit compare) + +static void recSLT_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = (g_cpuConstRegs[_Rs_].SD[0] < g_cpuConstRegs[_Rt_].SD[0]) ? 1 : 0; +} + +static void recSLT_consts(int info) +{ + memLoadT64(); + armAsm->Mov(RXARG1, g_cpuConstRegs[_Rs_].SD[0]); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lt); + memStoreD(); +} + +static void recSLT_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].SD[0]); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lt); + memStoreD(); +} + +static void recSLT_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lt); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SLT, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// SLTU — rd = (rs < rt) ? 1 : 0 (unsigned 64-bit compare) + +static void recSLTU_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = (g_cpuConstRegs[_Rs_].UD[0] < g_cpuConstRegs[_Rt_].UD[0]) ? 1 : 0; +} + +static void recSLTU_consts(int info) +{ + memLoadT64(); + armAsm->Mov(RXARG1, g_cpuConstRegs[_Rs_].UD[0]); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lo); + memStoreD(); +} + +static void recSLTU_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lo); + memStoreD(); +} + +static void recSLTU_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Cmp(RXARG1, RXSCRATCH); + armAsm->Cset(RXSCRATCH, a64::lo); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SLTU, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +#endif // !FORCE_INTERP_ALU + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900AritImm-arm64.cpp b/pcsx2/arm64/iR5900AritImm-arm64.cpp new file mode 100644 index 0000000000..f4596a8621 --- /dev/null +++ b/pcsx2/arm64/iR5900AritImm-arm64.cpp @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE ALU Immediate Instruction Codegen — memory-based +// rt = rs OP imm16. All operands via cpuRegs memory. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_ARITIMM +REC_FUNC(ADDI); +void recADDIU() { recADDI(); } +REC_FUNC(DADDI); +void recDADDIU() { recDADDI(); } +REC_FUNC(ANDI); +REC_FUNC(ORI); +REC_FUNC(XORI); +REC_FUNC(SLTI); +REC_FUNC(SLTIU); +#else + +// Memory load/store helpers +static void memLoadS32() { armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]); } +static void memLoadS64() { armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); } +static void memStoreT() { armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); } + +//// ADDI / ADDIU — rt = sign_extend(rs + imm) +static void recADDI_const() +{ + g_cpuConstRegs[_Rt_].SD[0] = s64(s32(g_cpuConstRegs[_Rs_].UL[0] + u32(s32(_Imm_)))); +} + +static void recADDI_(int info) +{ + memLoadS32(); + if (_Imm_ != 0) + armAsm->Add(RWSCRATCH, RWSCRATCH, _Imm_); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ADDI, XMMINFO_WRITET | XMMINFO_READS); + +void recADDIU() { recADDI(); } + +//// DADDI / DADDIU — rt = rs + sign_extend(imm) +static void recDADDI_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] + u64(s64(_Imm_)); +} + +static void recDADDI_(int info) +{ + memLoadS64(); + if (_Imm_ != 0) + { + // vixl's Add(int64_t) picks the right ADD/SUB-imm encoding and + // materializes via x16 when the immediate is unencodable. + armAsm->Add(RXSCRATCH, RXSCRATCH, static_cast(static_cast(_Imm_))); + } + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, DADDI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +void recDADDIU() { recDADDI(); } + +//// ANDI — rt = rs & zero_extend(imm16) +static void recANDI_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] & (u64)(u16)_ImmU_; +} + +static void recANDI_(int info) +{ + memLoadS64(); + if (_ImmU_ == 0) + armAsm->Mov(RXSCRATCH, 0); + else + armAsm->And(RXSCRATCH, RXSCRATCH, static_cast(static_cast(_ImmU_))); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ANDI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +//// ORI — rt = rs | zero_extend(imm16) +static void recORI_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] | (u64)(u16)_ImmU_; +} + +static void recORI_(int info) +{ + memLoadS64(); + if (_ImmU_ != 0) + armAsm->Orr(RXSCRATCH, RXSCRATCH, static_cast(static_cast(_ImmU_))); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ORI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +//// XORI — rt = rs ^ zero_extend(imm16) +static void recXORI_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = g_cpuConstRegs[_Rs_].UD[0] ^ (u64)(u16)_ImmU_; +} + +static void recXORI_(int info) +{ + memLoadS64(); + if (_ImmU_ != 0) + armAsm->Eor(RXSCRATCH, RXSCRATCH, static_cast(static_cast(_ImmU_))); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, XORI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +//// SLTI — rt = (rs < sign_extend(imm)) ? 1 : 0 (signed) +static void recSLTI_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = (g_cpuConstRegs[_Rs_].SD[0] < (s64)(s32)_Imm_) ? 1 : 0; +} + +static void recSLTI_(int info) +{ + memLoadS64(); + armAsm->Cmp(RXSCRATCH, static_cast(static_cast(_Imm_))); + armAsm->Cset(RXSCRATCH, a64::lt); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, SLTI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +//// SLTIU — rt = (rs < sign_extend(imm)) ? 1 : 0 (unsigned) +static void recSLTIU_const() +{ + g_cpuConstRegs[_Rt_].UD[0] = (g_cpuConstRegs[_Rs_].UD[0] < (u64)(s64)(s32)_Imm_) ? 1 : 0; +} + +static void recSLTIU_(int info) +{ + memLoadS64(); + // Sign-extended imm — Cmp condition flags are signedness-agnostic; only + // the Cset (lo = unsigned-less-than) differs from SLTI. + armAsm->Cmp(RXSCRATCH, static_cast(static_cast(_Imm_))); + armAsm->Cset(RXSCRATCH, a64::lo); + memStoreT(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, SLTIU, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP); + +#endif // !FORCE_INTERP_ARITIMM + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900Branch-arm64.cpp b/pcsx2/arm64/iR5900Branch-arm64.cpp new file mode 100644 index 0000000000..bbc532d687 --- /dev/null +++ b/pcsx2/arm64/iR5900Branch-arm64.cpp @@ -0,0 +1,530 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Branch Instruction Codegen — NEON-based +// Branches read GPR values for comparison. Values are extracted from +// NEON registers via FMOV or loaded from memory after flush. + +#include "arm64/iR5900-arm64.h" +#include "common/Assertions.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_BRANCH +REC_SYS(BEQ); +REC_SYS(BNE); +REC_SYS(BEQL); +REC_SYS(BNEL); +REC_SYS(BLEZ); +REC_SYS(BGTZ); +REC_SYS(BLTZ); +REC_SYS(BGEZ); +REC_SYS(BLEZL); +REC_SYS(BGTZL); +REC_SYS(BLTZL); +REC_SYS(BGEZL); +REC_SYS(BLTZAL); +REC_SYS(BGEZAL); +REC_SYS(BLTZALL); +REC_SYS(BGEZALL); +#else + +// Thread-local label for the "not taken" forward branch +static thread_local a64::Label* s_pBranchLabel = nullptr; + +// Load a GPR value into an ARM64 X register for comparison. Allocator-aware: +// _eeFlushAllDirty leaves slots in MODE_READ (clean) so any GPR/NEON slot +// holding `gprreg` is still authoritative and can be read via Mov/Fmov, +// saving an LDR per branch operand. Falls back to memory when unallocated. +static void loadGPRtoX(const a64::Register& dst, int gprreg) +{ + _eeMoveGPRtoR(dst, gprreg); +} + +// Emit comparison for BEQ/BNE and set up forward branch. +// +// `bne` selects the emitted forward-skip condition, NOT the instruction: +// bne==0 skips on `ne` (used by BEQ and, with its inverted structure, BNEL); +// bne==1 skips on `eq`. The forward branch jumps over the delay slot on the +// not-taken edge. +// +// Const-operand fast paths (one side const-folded): let vixl pick the optimal +// CMP/CMN-immediate encoding, and for compare-against-zero (BEQ/BNE $zero — the +// most common branch shape) collapse the whole test to a single Cbz/Cbnz with +// no Cmp at all. +// Cbz/Cbnz reach ±1MB, strictly wider than the Tbz already proven safe over +// this same skip distance in recSetBranchL. +static void recSetBranchEQ(int bne, int process) +{ + s_pBranchLabel = new a64::Label(); + + if (process & (PROCESS_CONSTS | PROCESS_CONSTT)) + { + const int constReg = (process & PROCESS_CONSTS) ? _Rs_ : _Rt_; + const int liveReg = (process & PROCESS_CONSTS) ? _Rt_ : _Rs_; + const s64 cval = g_cpuConstRegs[constReg].SD[0]; + + _eeFlushAllDirty(); + loadGPRtoX(RXARG1, liveReg); + + if (cval == 0) + { + // Single test-and-branch against $zero — no Cmp. + // bne==0 skips on ne → live != 0 → Cbnz; bne==1 skips on eq → Cbz. + if (bne) + armAsm->Cbz(RXARG1, s_pBranchLabel); + else + armAsm->Cbnz(RXARG1, s_pBranchLabel); + return; + } + + // vixl emits a single CMP/CMN immediate when cval fits, else materializes. + armAsm->Cmp(RXARG1, cval); + } + else + { + _eeFlushAllDirty(); + loadGPRtoX(RXARG1, _Rs_); + loadGPRtoX(RXSCRATCH, _Rt_); + armAsm->Cmp(RXARG1, RXSCRATCH); + } + + if (bne) + armAsm->B(s_pBranchLabel, a64::eq); + else + armAsm->B(s_pBranchLabel, a64::ne); +} + +// Emit comparison for BLTZ/BGEZ (rs vs 0) and set up forward branch. +// +// The "forward branch" jumps over the delay slot when the BLTZ/BGEZ would +// NOT be taken. For BLTZ (ltz=1) we skip when rs >= 0, i.e. bit 63 of the +// 64-bit GPR is zero → Tbz. For BGEZ (ltz=0) we skip when rs < 0, i.e. +// bit 63 is one → Tbnz. Tbz/Tbnz directly test a bit and branch, so no +// Cmp insn is needed. Shares the centralised setup across all 8 BLTZ/BGEZ/L/AL/ALL +// callers in this file. +static void recSetBranchL(int ltz) +{ + _eeFlushAllDirty(); + loadGPRtoX(RXSCRATCH, _Rs_); + + s_pBranchLabel = new a64::Label(); + if (ltz) + armAsm->Tbz(RXSCRATCH, 63, s_pBranchLabel); + else + armAsm->Tbnz(RXSCRATCH, 63, s_pBranchLabel); +} + +// Bind the forward branch label +static void recBindBranchLabel() +{ + pxAssert(s_pBranchLabel != nullptr); + armAsm->Bind(s_pBranchLabel); + delete s_pBranchLabel; + s_pBranchLabel = nullptr; +} + +//// BEQ — branch if rs == rt +static void recBEQ_const() +{ + u32 branchTo; + if (g_cpuConstRegs[_Rs_].SD[0] == g_cpuConstRegs[_Rt_].SD[0]) + branchTo = ((s32)_Imm_ * 4) + pc; + else + branchTo = pc + 4; + recompileNextInstruction(true, false); + SetBranchImm(branchTo); +} + +static void recBEQ_process(int process) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + if (_Rs_ == _Rt_) + { + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + return; + } + + const bool swap = TrySwapDelaySlot(_Rs_, _Rt_, 0, true); + recSetBranchEQ(0, process); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBranchLabel(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBEQ() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + recBEQ_const(); + else if (GPR_IS_CONST1(_Rs_)) + recBEQ_process(PROCESS_CONSTS); + else if (GPR_IS_CONST1(_Rt_)) + recBEQ_process(PROCESS_CONSTT); + else + recBEQ_process(0); +} + +//// BNE — branch if rs != rt +static void recBNE_const() +{ + u32 branchTo; + if (g_cpuConstRegs[_Rs_].SD[0] != g_cpuConstRegs[_Rt_].SD[0]) + branchTo = ((s32)_Imm_ * 4) + pc; + else + branchTo = pc + 4; + recompileNextInstruction(true, false); + SetBranchImm(branchTo); +} + +static void recBNE_process(int process) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + if (_Rs_ == _Rt_) + { + recompileNextInstruction(true, false); + SetBranchImm(pc); + return; + } + + const bool swap = TrySwapDelaySlot(_Rs_, _Rt_, 0, true); + recSetBranchEQ(1, process); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBranchLabel(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +void recBNE() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + recBNE_const(); + else if (GPR_IS_CONST1(_Rs_)) + recBNE_process(PROCESS_CONSTS); + else if (GPR_IS_CONST1(_Rt_)) + recBNE_process(PROCESS_CONSTT); + else + recBNE_process(0); +} + +//// BEQL — branch likely if rs == rt +static void recBEQL_const() +{ + // Capture the taken target BEFORE recompileNextInstruction advances pc by 4 + // (consistent with recBEQ_const / recBEQL_process). + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + if (g_cpuConstRegs[_Rs_].SD[0] == g_cpuConstRegs[_Rt_].SD[0]) + { + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + } + else + SetBranchImm(pc + 4); +} + +static void recBEQL_process(int process) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + recSetBranchEQ(0, process); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBranchLabel(); + LoadBranchState(); + SetBranchImm(pc); +} + +void recBEQL() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + recBEQL_const(); + else if (GPR_IS_CONST1(_Rs_)) + recBEQL_process(PROCESS_CONSTS); + else if (GPR_IS_CONST1(_Rt_)) + recBEQL_process(PROCESS_CONSTT); + else + recBEQL_process(0); +} + +//// BNEL — branch likely if rs != rt +static void recBNEL_const() +{ + // Capture the taken target BEFORE recompileNextInstruction advances pc by 4 + // (consistent with recBNE_const / recBNEL_process). + const u32 branchTo = ((s32)_Imm_ * 4) + pc; + if (g_cpuConstRegs[_Rs_].SD[0] != g_cpuConstRegs[_Rt_].SD[0]) + { + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + } + else + SetBranchImm(pc + 4); +} + +static void recBNEL_process(int process) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + recSetBranchEQ(0, process); + + SaveBranchState(); + SetBranchImm(pc + 4); + + recBindBranchLabel(); + LoadBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); +} + +void recBNEL() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + recBNEL_const(); + else if (GPR_IS_CONST1(_Rs_)) + recBNEL_process(PROCESS_CONSTS); + else if (GPR_IS_CONST1(_Rt_)) + recBNEL_process(PROCESS_CONSTT); + else + recBNEL_process(0); +} + +/********************************************************* + * Single-register branches: BLTZ, BGEZ, BLEZ, BGTZ * + *********************************************************/ + +static void recBranchSingle(a64::Condition skip_cond) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + if (GPR_IS_CONST1(_Rs_)) + { + bool taken; + s64 val = g_cpuConstRegs[_Rs_].SD[0]; + if (skip_cond == a64::gt) taken = (val <= 0); + else if (skip_cond == a64::le) taken = (val > 0); + else if (skip_cond == a64::ge) taken = (val < 0); + else if (skip_cond == a64::lt) taken = (val >= 0); + else taken = false; + + if (!taken) branchTo = pc + 4; + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + return; + } + + const bool swap = TrySwapDelaySlot(_Rs_, 0, 0, true); + + if (skip_cond == a64::ge || skip_cond == a64::lt) + { + recSetBranchL(skip_cond == a64::ge ? 1 : 0); + } + else + { + _eeFlushAllDirty(); + loadGPRtoX(RXSCRATCH, _Rs_); + armAsm->Cmp(RXSCRATCH, 0); + s_pBranchLabel = new a64::Label(); + armAsm->B(s_pBranchLabel, skip_cond); + } + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBranchLabel(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +static void recBranchSingleLikely(a64::Condition skip_cond) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + if (GPR_IS_CONST1(_Rs_)) + { + bool taken; + s64 val = g_cpuConstRegs[_Rs_].SD[0]; + if (skip_cond == a64::gt) taken = (val <= 0); + else if (skip_cond == a64::le) taken = (val > 0); + else if (skip_cond == a64::ge) taken = (val < 0); + else if (skip_cond == a64::lt) taken = (val >= 0); + else taken = false; + + if (taken) + { + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + } + else + SetBranchImm(pc + 4); + return; + } + + if (skip_cond == a64::ge || skip_cond == a64::lt) + { + recSetBranchL(skip_cond == a64::ge ? 1 : 0); + } + else + { + _eeFlushAllDirty(); + loadGPRtoX(RXSCRATCH, _Rs_); + armAsm->Cmp(RXSCRATCH, 0); + s_pBranchLabel = new a64::Label(); + armAsm->B(s_pBranchLabel, skip_cond); + } + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBranchLabel(); + LoadBranchState(); + SetBranchImm(pc); +} + +void recBLEZ() { recBranchSingle(a64::gt); } +void recBGTZ() { recBranchSingle(a64::le); } +void recBLTZ() { recBranchSingle(a64::ge); } +void recBGEZ() { recBranchSingle(a64::lt); } + +void recBLEZL() { recBranchSingleLikely(a64::gt); } +void recBGTZL() { recBranchSingleLikely(a64::le); } +void recBLTZL() { recBranchSingleLikely(a64::ge); } +void recBGEZL() { recBranchSingleLikely(a64::lt); } + +/********************************************************* + * Branch-and-link: BLTZAL, BGEZAL, BLTZALL, BGEZALL * + *********************************************************/ + +static void recBranchLink(bool ltz) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + _eeOnWriteReg(31, 0); + _eeFlushAllDirty(); + + _deleteEEreg(31, 0); + // Store return address directly to memory + armAsm->Mov(RXSCRATCH, (u64)(pc + 4)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.n.ra.UD[0])); + + if (GPR_IS_CONST1(_Rs_)) + { + bool taken = ltz ? (g_cpuConstRegs[_Rs_].SD[0] < 0) : (g_cpuConstRegs[_Rs_].SD[0] >= 0); + if (!taken) branchTo = pc + 4; + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + return; + } + + const bool swap = TrySwapDelaySlot(_Rs_, 0, 0, true); + recSetBranchL(ltz ? 1 : 0); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBranchLabel(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +static void recBranchLinkLikely(bool ltz) +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + _eeOnWriteReg(31, 0); + _eeFlushAllDirty(); + + _deleteEEreg(31, 0); + armAsm->Mov(RXSCRATCH, (u64)(pc + 4)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.n.ra.UD[0])); + + if (GPR_IS_CONST1(_Rs_)) + { + bool taken = ltz ? (g_cpuConstRegs[_Rs_].SD[0] < 0) : (g_cpuConstRegs[_Rs_].SD[0] >= 0); + if (taken) + { + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + } + else + SetBranchImm(pc + 4); + return; + } + + recSetBranchL(ltz ? 1 : 0); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBranchLabel(); + LoadBranchState(); + SetBranchImm(pc); +} + +void recBLTZAL() { recBranchLink(true); } +void recBGEZAL() { recBranchLink(false); } +void recBLTZALL() { recBranchLinkLikely(true); } +void recBGEZALL() { recBranchLinkLikely(false); } + +#endif // !FORCE_INTERP_BRANCH + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900Jump-arm64.cpp b/pcsx2/arm64/iR5900Jump-arm64.cpp new file mode 100644 index 0000000000..57c4e1b3cb --- /dev/null +++ b/pcsx2/arm64/iR5900Jump-arm64.cpp @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Jump Instruction Codegen + +#include "arm64/iR5900-arm64.h" +#include "Config.h" +#include "vtlb.h" +#include "common/Console.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_JUMP +REC_SYS(J); +REC_SYS(JAL); +REC_SYS(JR); +REC_SYS(JALR); +#else + +/********************************************************* + * Jump to target * + * Format: OP target * + *********************************************************/ + +//// J +void recJ() +{ + u32 newpc = (_InstrucTarget_ << 2) + (pc & 0xf0000000); + recompileNextInstruction(true, false); + if (EmuConfig.Gamefixes.GoemonTlbHack) + SetBranchImm(vtlb_V2P(newpc)); + else + SetBranchImm(newpc); +} + +//// JAL — jump and link (r31 = return address) +void recJAL() +{ + u32 newpc = (_InstrucTarget_ << 2) + (pc & 0xf0000000); + _deleteEEreg(31, 0); + if (EE_CONST_PROP) + { + GPR_SET_CONST(31); + g_cpuConstRegs[31].UL[0] = pc + 4; + g_cpuConstRegs[31].UL[1] = 0; + } + else + { + armAsm->Mov(RXSCRATCH, (u64)(pc + 4)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[31].UD[0])); + } + + recompileNextInstruction(true, false); + if (EmuConfig.Gamefixes.GoemonTlbHack) + SetBranchImm(vtlb_V2P(newpc)); + else + SetBranchImm(newpc); +} + +/********************************************************* + * Register jump * + * Format: OP rs, rd * + *********************************************************/ + +//// JR — jump to address in rs +void recJR() +{ + const u32 rs = _Rs_; + + // Save jump target to memory BEFORE delay slot, so it can't be lost + // if the delay slot evicts registers. + _deleteEEreg(rs, 1); // flush rs to memory + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[rs].UL[0]); + armStoreEERegPtr(RWSCRATCH, &cpuRegs.pcWriteback); + + recompileNextInstruction(true, false); + + SetBranchReg(); +} + +//// JALR — jump to rs, link in rd +void recJALR() +{ + const u32 rs = _Rs_; + const u32 rd = _Rd_; + const u32 newpc = pc + 4; + + // Save jump target to memory BEFORE delay slot. + // Must read rs before writing rd in case rd == rs. + _deleteEEreg(rs, 1); // flush rs to memory + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[rs].UL[0]); + armStoreEERegPtr(RWSCRATCH, &cpuRegs.pcWriteback); + + // Write link address to rd + if (rd) + { + _deleteEEreg(rd, 0); + if (EE_CONST_PROP) + { + GPR_SET_CONST(rd); + g_cpuConstRegs[rd].UL[0] = newpc; + g_cpuConstRegs[rd].UL[1] = 0; + } + else + { + armAsm->Mov(RXSCRATCH, (u64)newpc); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[rd].UD[0]); + } + } + + recompileNextInstruction(true, false); + + SetBranchReg(); +} + +#endif // !FORCE_INTERP_JUMP + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900LoadStore-arm64.cpp b/pcsx2/arm64/iR5900LoadStore-arm64.cpp new file mode 100644 index 0000000000..8c977ea48e --- /dev/null +++ b/pcsx2/arm64/iR5900LoadStore-arm64.cpp @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Load/Store — all recXXX implementations are in recVTLB-arm64.cpp +// This file is intentionally empty. diff --git a/pcsx2/arm64/iR5900Misc-arm64.cpp b/pcsx2/arm64/iR5900Misc-arm64.cpp new file mode 100644 index 0000000000..c2cad31248 --- /dev/null +++ b/pcsx2/arm64/iR5900Misc-arm64.cpp @@ -0,0 +1,550 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "arm64/iR5900-arm64.h" +#include "common/Assertions.h" +#include "common/Console.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { + +// Forward declarations for native COP2 codegen (defined in iCOP2-arm64.cpp) +namespace OpcodeImpl { + // Transfer ops + void recCOP2_QMFC2(); + void recCOP2_QMTC2(); + void recCOP2_CFC2(); + // SIMPLE + void recCOP2_VMOVE(); + void recCOP2_VMR32(); + void recCOP2_VNOP(); + void recCOP2_VWAITQ(); + void recCOP2_VABS(); + // VEC_ARITH + void recCOP2_VADD(); + void recCOP2_VSUB(); + void recCOP2_VMUL(); + void recCOP2_VMAX(); + void recCOP2_VMINI(); + // BC variants + void recCOP2_VADDx(); void recCOP2_VADDy(); void recCOP2_VADDz(); void recCOP2_VADDw(); + void recCOP2_VSUBx(); void recCOP2_VSUBy(); void recCOP2_VSUBz(); void recCOP2_VSUBw(); + void recCOP2_VMULx(); void recCOP2_VMULy(); void recCOP2_VMULz(); void recCOP2_VMULw(); + void recCOP2_VMAXx(); void recCOP2_VMAXy(); void recCOP2_VMAXz(); void recCOP2_VMAXw(); + void recCOP2_VMINIx(); void recCOP2_VMINIy(); void recCOP2_VMINIz(); void recCOP2_VMINIw(); + void recCOP2_VMAXi(); void recCOP2_VMINIi(); + // Q/I variants + void recCOP2_VADDq(); void recCOP2_VSUBq(); void recCOP2_VMULq(); + void recCOP2_VADDi(); void recCOP2_VSUBi(); void recCOP2_VMULi(); + // MADD/MSUB + void recCOP2_VMADD(); void recCOP2_VMSUB(); + void recCOP2_VMADDx(); void recCOP2_VMADDy(); void recCOP2_VMADDz(); void recCOP2_VMADDw(); + void recCOP2_VMSUBx(); void recCOP2_VMSUBy(); void recCOP2_VMSUBz(); void recCOP2_VMSUBw(); + void recCOP2_VMADDq(); void recCOP2_VMSUBq(); + void recCOP2_VMADDi(); void recCOP2_VMSUBi(); + void recCOP2_VOPMSUB(); + // Accumulator + void recCOP2_VADDA(); void recCOP2_VSUBA(); void recCOP2_VMULA(); + void recCOP2_VADDAx(); void recCOP2_VADDAy(); void recCOP2_VADDAz(); void recCOP2_VADDAw(); + void recCOP2_VSUBAx(); void recCOP2_VSUBAy(); void recCOP2_VSUBAz(); void recCOP2_VSUBAw(); + void recCOP2_VMULAx(); void recCOP2_VMULAy(); void recCOP2_VMULAz(); void recCOP2_VMULAw(); + void recCOP2_VMULAq(); void recCOP2_VMULAi(); + void recCOP2_VADDAq(); void recCOP2_VSUBAq(); + void recCOP2_VADDAi(); void recCOP2_VSUBAi(); + void recCOP2_VMADDA(); void recCOP2_VMSUBA(); + void recCOP2_VMADDAx(); void recCOP2_VMADDAy(); void recCOP2_VMADDAz(); void recCOP2_VMADDAw(); + void recCOP2_VMSUBAx(); void recCOP2_VMSUBAy(); void recCOP2_VMSUBAz(); void recCOP2_VMSUBAw(); + void recCOP2_VMADDAq(); void recCOP2_VMSUBAq(); + void recCOP2_VMADDAi(); void recCOP2_VMSUBAi(); + void recCOP2_VOPMULA(); + // Conversion + void recCOP2_VITOF0(); void recCOP2_VITOF4(); void recCOP2_VITOF12(); void recCOP2_VITOF15(); + void recCOP2_VFTOI0(); void recCOP2_VFTOI4(); void recCOP2_VFTOI12(); void recCOP2_VFTOI15(); + // Integer ops + void recCOP2_VIADD(); void recCOP2_VISUB(); void recCOP2_VIADDI(); + void recCOP2_VIAND(); void recCOP2_VIOR(); + // CTC2 + void recCOP2_CTC2(); + // Division ops + void recCOP2_VDIV(); + void recCOP2_VSQRT(); + void recCOP2_VRSQRT(); + // Clip + void recCOP2_VCLIP(); +} // namespace OpcodeImpl + +// Branch helper — not implemented on ARM64. Callers (iCOP0/iFPU/COP2 macro +// paths) drive SaveBranchState/SetBranchImm directly instead. Fail loudly +// rather than silently no-op if a future port wires this in by mistake. +void recDoBranchImm(u32 branchTo, u32* jmpSkip, bool isLikely, bool swappedDelaySlot) +{ + pxFailRel("recDoBranchImm is not implemented on ARM64"); +} + +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +void recPREF() {} + +// SYSCALL and BREAK — flush state and call interpreter +void recSYSCALL() +{ + if (GPR_IS_CONST1(3)) + { + // FlushCache (0x64) / iFlushCache (0x68): the EE cache is not modelled, + // so account for the kernel handler cycles inline and skip the call. + // Cycle count from github.com/F0bes/flushcache-cycles. Mirrors x86 recSYSCALL. + // + // This skip leaves v0/v1/at/t0/t1 and EPC at their pre-syscall values, + // whereas the interpreter actually raises cpuException(0x20) and runs the + // BIOS 0x80000180 trampoline, which clobbers them. That JIT-vs-interp + // divergence is REAL but ABI-benign: FlushCache is a syscall, so under + // the MIPS calling convention those are all caller-saved/temporary regs + // (plus EPC, which user code never reads) — correct code never depends on + // them surviving. Upstream PCSX2-x86 ships this skip as a correct, faster + // optimization. + const u8 syscallNum = g_cpuConstRegs[3].UC[0]; + if (syscallNum == 0x64 || syscallNum == 0x68) + { + s_nBlockCycles += 5650; + return; + } + } + recBranchCall(Interp::SYSCALL); +} + +void recBREAK() +{ + recBranchCall(Interp::BREAK); +} + +// ===================================================================================================== +// COP2 (VU0 macro mode) — dispatch table with per-sub-opcode interpreter fallback +// Mirrors the x86 dispatch structure: recCOP2 → recCOP2t[_Rs_] → SPEC1/SPEC2 +// ===================================================================================================== + +// COP2 macro-mode mVU-reuse wrapper. Drives the existing microVU emitter +// (mVU_ in microVU_Lower-arm64.inl) via the mVUmacroEmit_ adapter +// declared in iR5900-arm64.h. Mirrors x86 REC_COP2_mVU0 (microVU_Macro.inl:122). +// Mode bits per x86 microVU_Macro.inl:158-165: +// 0x01 reads Q reg / 0x02 writes Q reg / 0x04 requires analysis pass +// 0x08 writes CLIP / 0x10 writes status/mac / 0x100 requires x86 regs. +#define REC_COP2_mVU0_ARM64(name, mode) \ + static void recV##name() \ + { \ + setupMacroOp_arm64(mode); \ + mVUmacroEmit_##name(mode); \ + endMacroOp_arm64(mode); \ + } + +// Transfer ops — native codegen for QMFC2/QMTC2/CFC2, CTC2 stays interpreter +static void recVQMFC2() { recCOP2_QMFC2(); } +static void recVQMTC2() { recCOP2_QMTC2(); } +static void recVCFC2() { recCOP2_CFC2(); } +static void recVCTC2() { recCOP2_CTC2(); } + +// Branch ops — native COP2 condition branch. CP2COND = bit 8 of +// VU0.VI[REG_VPU_STAT] (COP2.cpp:11). Mirrors x86 _setupBranchTest +// (microVU_Macro.inl) and the recBC1F FPU-branch shape: a lightweight +// _eeFlushAllDirty + a single Tbz/Tbnz on the flag bit, then the standard EE +// branch-imm machinery — avoiding FLUSH_INTERPRETER + C-call + dispatcher +// round-trip overhead. +static a64::Label* s_pBC2Label = nullptr; + +static void recSetBranchCOP2(bool branchOnTrue) +{ + _eeFlushAllDirty(); + armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_VPU_STAT])); + + // The forward branch skips the taken path: BC2T (branchOnTrue) is taken when + // CP2COND is set → skip when clear → Tbz; BC2F is taken when clear → skip + // when set → Tbnz. Matches x86 JZ32/JNZ32 in recBC2T/recBC2F. + s_pBC2Label = new a64::Label(); + if (branchOnTrue) + armAsm->Tbz(RWSCRATCH, 8, s_pBC2Label); + else + armAsm->Tbnz(RWSCRATCH, 8, s_pBC2Label); +} + +static void recBindBC2Label() +{ + armAsm->Bind(s_pBC2Label); + delete s_pBC2Label; + s_pBC2Label = nullptr; +} + +// Non-likely (BC2F/BC2T): attempt a delay-slot swap (allow_loadstore=false, +// matching x86 _setupBranchTest's TrySwapDelaySlot(0,0,0,false)). +static void recVBC2F() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + const bool swap = TrySwapDelaySlot(0, 0, 0, false); + recSetBranchCOP2(false); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC2Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +static void recVBC2T() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + const bool swap = TrySwapDelaySlot(0, 0, 0, false); + recSetBranchCOP2(true); + + if (!swap) + { + SaveBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(branchTo); + + recBindBC2Label(); + + if (!swap) + { + pc -= 4; + LoadBranchState(); + recompileNextInstruction(true, false); + } + SetBranchImm(pc); +} + +// Likely (BC2FL/BC2TL): delay slot squashed when not taken; no swap, matching +// the x86 isLikely path (and the interp's `else { cpuRegs.pc += 4; }`). +static void recVBC2FL() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + recSetBranchCOP2(false); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC2Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +static void recVBC2TL() +{ + u32 branchTo = ((s32)_Imm_ * 4) + pc; + + recSetBranchCOP2(true); + + SaveBranchState(); + recompileNextInstruction(true, false); + SetBranchImm(branchTo); + + recBindBC2Label(); + LoadBranchState(); + SetBranchImm(pc); +} + +// Upper instructions (SPEC1) — native NEON codegen +// BC variants: VF[fd] = VF[fs] OP VF[ft].bc +static void recVADDx() { recCOP2_VADDx(); } static void recVADDy() { recCOP2_VADDy(); } +static void recVADDz() { recCOP2_VADDz(); } static void recVADDw() { recCOP2_VADDw(); } +static void recVSUBx() { recCOP2_VSUBx(); } static void recVSUBy() { recCOP2_VSUBy(); } +static void recVSUBz() { recCOP2_VSUBz(); } static void recVSUBw() { recCOP2_VSUBw(); } +static void recVMADDx() { recCOP2_VMADDx(); } static void recVMADDy() { recCOP2_VMADDy(); } +static void recVMADDz() { recCOP2_VMADDz(); } static void recVMADDw() { recCOP2_VMADDw(); } +static void recVMSUBx() { recCOP2_VMSUBx(); } static void recVMSUBy() { recCOP2_VMSUBy(); } +static void recVMSUBz() { recCOP2_VMSUBz(); } static void recVMSUBw() { recCOP2_VMSUBw(); } +static void recVMAXx() { recCOP2_VMAXx(); } static void recVMAXy() { recCOP2_VMAXy(); } +static void recVMAXz() { recCOP2_VMAXz(); } static void recVMAXw() { recCOP2_VMAXw(); } +static void recVMINIx() { recCOP2_VMINIx(); } static void recVMINIy() { recCOP2_VMINIy(); } +static void recVMINIz() { recCOP2_VMINIz(); } static void recVMINIw() { recCOP2_VMINIw(); } +static void recVMULx() { recCOP2_VMULx(); } static void recVMULy() { recCOP2_VMULy(); } +static void recVMULz() { recCOP2_VMULz(); } static void recVMULw() { recCOP2_VMULw(); } +static void recVMULq() { recCOP2_VMULq(); } static void recVMAXi() { recCOP2_VMAXi(); } +static void recVMULi() { recCOP2_VMULi(); } static void recVMINIi() { recCOP2_VMINIi(); } +static void recVADDq() { recCOP2_VADDq(); } static void recVMADDq() { recCOP2_VMADDq(); } +static void recVADDi() { recCOP2_VADDi(); } static void recVMADDi() { recCOP2_VMADDi(); } +static void recVSUBq() { recCOP2_VSUBq(); } static void recVMSUBq() { recCOP2_VMSUBq(); } +static void recVSUBi() { recCOP2_VSUBi(); } static void recVMSUBi() { recCOP2_VMSUBi(); } +static void recVADD() { recCOP2_VADD(); } static void recVMADD() { recCOP2_VMADD(); } +static void recVMUL() { recCOP2_VMUL(); } static void recVMAX() { recCOP2_VMAX(); } +static void recVSUB() { recCOP2_VSUB(); } static void recVMSUB() { recCOP2_VMSUB(); } +static void recVOPMSUB(){ recCOP2_VOPMSUB(); } static void recVMINI() { recCOP2_VMINI(); } +// Integer ops — native +static void recVIADD() { recCOP2_VIADD(); } static void recVISUB() { recCOP2_VISUB(); } +static void recVIADDI() { recCOP2_VIADDI(); } +static void recVIAND() { recCOP2_VIAND(); } static void recVIOR() { recCOP2_VIOR(); } +// CALLMS/CALLMSR kick off a VU0 microprogram via the interpreter — they are +// NOT EE branches (x86 iR5900Analysis case 56/57 just `break;`) so they +// must NOT exit the recompiled block the way recBranchCall does. Mirror +// x86's INTERPRETATE_COP2_FUNC(CALLMS) (microVU_Macro.inl:142): full +// FLUSH_INTERPRETER flush (so cpuRegs.code is current — VCALLMS reads +// the start PC from `(cpuRegs.code >> 6) & 0x7FFF`), apply pending block +// cycles, call the interpreter (which itself runs _vu0FinishMicro + +// vu0ExecMicro), then reload RECCYCLE in case the interp advanced +// cpuRegs.cycle. Block execution continues at the next opcode. +// Using iFlushCall(FLUSH_INTERPRETER) inline avoids the g_branch=2 block +// exit that recBranchCall would trigger on every CALLMS; the flush cost +// is the same, with no dispatcher round-trip. +static void recVCallmsImpl(void (*func)()) +{ + iFlushCall(FLUSH_INTERPRETER); + + u32 cycles = scaleblockcycles_clear(); + if (cycles != 0) + armAsm->Add(RECCYCLE, RECCYCLE, cycles); + + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall((void*)func); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); +} + +static void recVCALLMS() { recVCallmsImpl(VCALLMS); } +static void recVCALLMSR() { recVCallmsImpl(VCALLMSR); } + +// Lower instructions (SPEC2) — native NEON codegen for accumulator/conversion/simple ops +// Accumulator BC variants +static void recVADDAx() { recCOP2_VADDAx(); } static void recVADDAy() { recCOP2_VADDAy(); } +static void recVADDAz() { recCOP2_VADDAz(); } static void recVADDAw() { recCOP2_VADDAw(); } +static void recVSUBAx() { recCOP2_VSUBAx(); } static void recVSUBAy() { recCOP2_VSUBAy(); } +static void recVSUBAz() { recCOP2_VSUBAz(); } static void recVSUBAw() { recCOP2_VSUBAw(); } +static void recVMADDAx(){ recCOP2_VMADDAx(); } static void recVMADDAy(){ recCOP2_VMADDAy(); } +static void recVMADDAz(){ recCOP2_VMADDAz(); } static void recVMADDAw(){ recCOP2_VMADDAw(); } +static void recVMSUBAx(){ recCOP2_VMSUBAx(); } static void recVMSUBAy(){ recCOP2_VMSUBAy(); } +static void recVMSUBAz(){ recCOP2_VMSUBAz(); } static void recVMSUBAw(){ recCOP2_VMSUBAw(); } +// Conversions — native NEON +static void recVITOF0() { recCOP2_VITOF0(); } static void recVITOF4() { recCOP2_VITOF4(); } +static void recVITOF12() { recCOP2_VITOF12(); } static void recVITOF15() { recCOP2_VITOF15(); } +static void recVFTOI0() { recCOP2_VFTOI0(); } static void recVFTOI4() { recCOP2_VFTOI4(); } +static void recVFTOI12() { recCOP2_VFTOI12(); } static void recVFTOI15() { recCOP2_VFTOI15(); } +// Accumulator MULAx/y/z/w +static void recVMULAx() { recCOP2_VMULAx(); } static void recVMULAy() { recCOP2_VMULAy(); } +static void recVMULAz() { recCOP2_VMULAz(); } static void recVMULAw() { recCOP2_VMULAw(); } +static void recVMULAq() { recCOP2_VMULAq(); } static void recVABS() { recCOP2_VABS(); } +static void recVMULAi() { recCOP2_VMULAi(); } +// CLIP — still interpreter fallback (complex flag logic) +static void recVCLIP() { recCOP2_VCLIP(); } +// Accumulator Q/I variants +static void recVADDAq() { recCOP2_VADDAq(); } static void recVMADDAq(){ recCOP2_VMADDAq(); } +static void recVADDAi() { recCOP2_VADDAi(); } static void recVMADDAi(){ recCOP2_VMADDAi(); } +static void recVSUBAq() { recCOP2_VSUBAq(); } static void recVMSUBAq(){ recCOP2_VMSUBAq(); } +static void recVSUBAi() { recCOP2_VSUBAi(); } static void recVMSUBAi(){ recCOP2_VMSUBAi(); } +// Accumulator full-vector variants +static void recVADDA() { recCOP2_VADDA(); } static void recVMADDA() { recCOP2_VMADDA(); } +static void recVMULA() { recCOP2_VMULA(); } +static void recVSUBA() { recCOP2_VSUBA(); } static void recVMSUBA() { recCOP2_VMSUBA(); } +static void recVOPMULA(){ recCOP2_VOPMULA(); } static void recVNOP() { recCOP2_VNOP(); } +// Simple data movement — native +static void recVMOVE() { recCOP2_VMOVE(); } static void recVMR32() { recCOP2_VMR32(); } +// Load/store — full group native via mVU emit (mode bits from x86 microVU_Macro.inl:276-279). +REC_COP2_mVU0_ARM64(LQI, 0x104); REC_COP2_mVU0_ARM64(SQI, 0x100); +REC_COP2_mVU0_ARM64(LQD, 0x104); REC_COP2_mVU0_ARM64(SQD, 0x100); +// Division ops — native +static void recVDIV() { recCOP2_VDIV(); } +static void recVSQRT() { recCOP2_VSQRT(); } +static void recVRSQRT(){ recCOP2_VRSQRT(); } +static void recVWAITQ() { recCOP2_VWAITQ(); } +REC_COP2_mVU0_ARM64(MTIR, 0x104); REC_COP2_mVU0_ARM64(MFIR, 0x104); +REC_COP2_mVU0_ARM64(ILWR, 0x104); REC_COP2_mVU0_ARM64(ISWR, 0x100); +REC_COP2_mVU0_ARM64(RNEXT, 0x104); REC_COP2_mVU0_ARM64(RGET, 0x104); +REC_COP2_mVU0_ARM64(RINIT, 0x100); REC_COP2_mVU0_ARM64(RXOR, 0x100); + +static void rec_C2UNK() { Console.Error("EE: Unrecognized COP2 opcode %08X", cpuRegs.code); } + +// Dispatch tables — mirror x86 structure +static void recCOP2_BC2(); +static void recCOP2_SPEC1(); +static void recCOP2_SPEC2(); + +static void (*recCOP2t[32])() = { + rec_C2UNK, recVQMFC2, recVCFC2, rec_C2UNK, rec_C2UNK, recVQMTC2, recVCTC2, rec_C2UNK, + recCOP2_BC2, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, + recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, recCOP2_SPEC1, +}; + +static void (*recCOP2_BC2t[32])() = { + recVBC2F, recVBC2T, recVBC2FL, recVBC2TL, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, +}; + +static void (*recCOP2SPECIAL1t[64])() = { + recVADDx, recVADDy, recVADDz, recVADDw, recVSUBx, recVSUBy, recVSUBz, recVSUBw, + recVMADDx, recVMADDy, recVMADDz, recVMADDw, recVMSUBx, recVMSUBy, recVMSUBz, recVMSUBw, + recVMAXx, recVMAXy, recVMAXz, recVMAXw, recVMINIx, recVMINIy, recVMINIz, recVMINIw, + recVMULx, recVMULy, recVMULz, recVMULw, recVMULq, recVMAXi, recVMULi, recVMINIi, + recVADDq, recVMADDq, recVADDi, recVMADDi, recVSUBq, recVMSUBq, recVSUBi, recVMSUBi, + recVADD, recVMADD, recVMUL, recVMAX, recVSUB, recVMSUB, recVOPMSUB, recVMINI, + recVIADD, recVISUB, recVIADDI, rec_C2UNK, recVIAND, recVIOR, rec_C2UNK, rec_C2UNK, + recVCALLMS, recVCALLMSR,rec_C2UNK, rec_C2UNK, recCOP2_SPEC2, recCOP2_SPEC2, recCOP2_SPEC2, recCOP2_SPEC2, +}; + +static void (*recCOP2SPECIAL2t[128])() = { + recVADDAx, recVADDAy, recVADDAz, recVADDAw, recVSUBAx, recVSUBAy, recVSUBAz, recVSUBAw, + recVMADDAx,recVMADDAy, recVMADDAz, recVMADDAw, recVMSUBAx, recVMSUBAy, recVMSUBAz, recVMSUBAw, + recVITOF0, recVITOF4, recVITOF12, recVITOF15, recVFTOI0, recVFTOI4, recVFTOI12, recVFTOI15, + recVMULAx, recVMULAy, recVMULAz, recVMULAw, recVMULAq, recVABS, recVMULAi, recVCLIP, + recVADDAq, recVMADDAq,recVADDAi, recVMADDAi, recVSUBAq, recVMSUBAq, recVSUBAi, recVMSUBAi, + recVADDA, recVMADDA, recVMULA, rec_C2UNK, recVSUBA, recVMSUBA, recVOPMULA, recVNOP, + recVMOVE, recVMR32, rec_C2UNK, rec_C2UNK, recVLQI, recVSQI, recVLQD, recVSQD, + recVDIV, recVSQRT, recVRSQRT, recVWAITQ, recVMTIR, recVMFIR, recVILWR, recVISWR, + recVRNEXT, recVRGET, recVRINIT, recVRXOR, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, + rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, rec_C2UNK, +}; + +static void recCOP2_BC2() { recCOP2_BC2t[_Rt_](); } +static void recCOP2_SPEC1() { recCOP2SPECIAL1t[cpuRegs.code & 0x3f](); } +static void recCOP2_SPEC2() { recCOP2SPECIAL2t[(cpuRegs.code & 0x3) | ((cpuRegs.code >> 4) & 0x7c)](); } + +void recCOP2() +{ +#ifdef FORCE_INTERP_COP2 + // Use interpreter for all COP2 — but branches need recBranchCall + if (_Rs_ == 8) // BC2 branch instructions + recBranchCall(Interp::COP2); + else + recCall(Interp::COP2); +#else + recCOP2t[_Rs_](); +#endif +} + +void recSYNC() {} + +// MFSA — rd = sa (shift amount register) +void recMFSA() +{ + if (!_Rd_) return; + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa); + armAsm->Mov(RXSCRATCH, RWSCRATCH); // zero-extend to 64 + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +// MTSA — sa = rs +void recMTSA() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rs_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } +} + +// MTSAB — sa = (rs[3:0] ^ imm[3:0]) +void recMTSAB() +{ + if (GPR_IS_CONST1(_Rs_)) + { + u32 val = (g_cpuConstRegs[_Rs_].UL[0] & 0xF) ^ (_Imm_ & 0xF); + armAsm->Mov(RWSCRATCH, val); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->And(RWSCRATCH, RWSCRATCH, 0xF); + armAsm->Eor(RWSCRATCH, RWSCRATCH, _Imm_ & 0xF); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } +} + +// MTSAH — sa = ((rs[2:0] ^ imm[2:0]) << 1) +void recMTSAH() +{ + if (GPR_IS_CONST1(_Rs_)) + { + u32 val = ((g_cpuConstRegs[_Rs_].UL[0] & 0x7) ^ (_Imm_ & 0x7)) << 1; + armAsm->Mov(RWSCRATCH, val); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]); + armAsm->Eor(RWSCRATCH, RWSCRATCH, _Imm_ & 0x7); + // ubfiz w, w, #1, #3 extracts bits[2:0] and places them at bit 1 + armAsm->Ubfiz(RWSCRATCH, RWSCRATCH, 1, 3); + armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.sa)); + } +} + +void recNULL() +{ + Console.Error("EE: Unimplemented op %x", cpuRegs.code); +} + +void recUnknown() +{ + Console.Error("EE: Unrecognized op %x", cpuRegs.code); +} + +void recMMI_Unknown() +{ + Console.Error("EE: Unrecognized MMI op %x", cpuRegs.code); +} + +void recCOP0_Unknown() +{ + Console.Error("EE: Unrecognized COP0 op %x", cpuRegs.code); +} + +void recCOP1_Unknown() +{ + Console.Error("EE: Unrecognized FPU/COP1 op %x", cpuRegs.code); +} + +void recCACHE() {} + +REC_SYS(TGE); +REC_SYS(TGEU); +REC_SYS(TLT); +REC_SYS(TLTU); +REC_SYS(TEQ); +REC_SYS(TNE); +REC_SYS(TGEI); +REC_SYS(TGEIU); +REC_SYS(TLTI); +REC_SYS(TLTIU); +REC_SYS(TEQI); +REC_SYS(TNEI); + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 + +// recBackpropBSC is provided by the shared x86/iR5900Analysis.cpp +// (compiled for ARM64 via ARCH_ARM64 conditional include). + diff --git a/pcsx2/arm64/iR5900Move-arm64.cpp b/pcsx2/arm64/iR5900Move-arm64.cpp new file mode 100644 index 0000000000..786570f678 --- /dev/null +++ b/pcsx2/arm64/iR5900Move-arm64.cpp @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Move Instruction Codegen — memory-based +// All operands via cpuRegs memory. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_MOVE +REC_FUNC(LUI); +REC_FUNC(MFLO); +REC_FUNC(MFHI); +REC_FUNC(MTLO); +REC_FUNC(MTHI); +REC_FUNC(MFLO1); +REC_FUNC(MFHI1); +REC_FUNC(MTLO1); +REC_FUNC(MTHI1); +REC_FUNC(MOVZ); +REC_FUNC(MOVN); +#else + +//// LUI — rt = imm16 << 16 (sign-extended to 64 bits) +void recLUI() +{ + if (!_Rt_) return; + + _deleteEEreg(_Rt_, 0); + + if (EE_CONST_PROP) + { + g_cpuConstRegs[_Rt_].SD[0] = s64(s32((u32)_ImmU_ << 16)); + GPR_SET_CONST(_Rt_); + } + else + { + GPR_DEL_CONST(_Rt_); + armAsm->Mov(RXSCRATCH, s64(s32((u32)_ImmU_ << 16))); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + } +} + +//// MFLO / MFHI — rd = LO/HI (memory to memory) +void recMFLO() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +void recMFHI() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +//// MTLO / MTHI — LO/HI = rs (memory to memory) +void recMTLO() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rs_].SD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[0]); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[0]); + } +} + +void recMTHI() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rs_].SD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[0]); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[0]); + } +} + +//// MFLO1/MFHI1 — rd = LO1/HI1 (upper 64 bits of LO/HI) +void recMFLO1() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[1]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +void recMFHI1() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[1]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +//// MTLO1/MTHI1 — LO1/HI1 = rs +void recMTLO1() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rs_].SD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[1]); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[1]); + } +} + +void recMTHI1() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rs_].SD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[1]); + } + else + { + _deleteEEreg(_Rs_, 1); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.HI.UD[1]); + } +} + +//// MOVZ — if (rt == 0) then rd = rs +// Memory-based: all loads/stores via cpuRegs + +static void recMOVZtemp_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0]; +} + +static void recMOVZtemp_consts(int info) +{ + // S is const — load T from memory, compare, conditionally store + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + armAsm->Cmp(RXSCRATCH, 0); + + armAsm->Mov(RXARG1, g_cpuConstRegs[_Rs_].SD[0]); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); + armAsm->Csel(RXSCRATCH, RXARG1, RXSCRATCH, a64::eq); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static void recMOVZtemp_constt(int info) +{ + // T is constant and zero (checked in wrapper) — unconditional move + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static void recMOVZtemp_(int info) +{ + // Load T for comparison + armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rt_].UD[0]); + armAsm->Cmp(RXARG1, 0); + + // Load S + armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rs_].UD[0]); + + // Load current D, conditional select, store back + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); + armAsm->Csel(RXSCRATCH, RXARG1, RXSCRATCH, a64::eq); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static EERECOMPILE_CODERC0_MEM(MOVZtemp, XMMINFO_READS | XMMINFO_READT | XMMINFO_READD | XMMINFO_WRITED | XMMINFO_NORENAME); + +void recMOVZ() +{ + if (_Rs_ == _Rd_) + return; + + if (GPR_IS_CONST1(_Rt_) && g_cpuConstRegs[_Rt_].UD[0] != 0) + return; + + recMOVZtemp(); +} + +//// MOVN — if (rt != 0) then rd = rs + +static void recMOVNtemp_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rs_].UD[0]; +} + +static void recMOVNtemp_consts(int info) +{ + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); + armAsm->Cmp(RXSCRATCH, 0); + + armAsm->Mov(RXARG1, g_cpuConstRegs[_Rs_].SD[0]); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); + armAsm->Csel(RXSCRATCH, RXARG1, RXSCRATCH, a64::ne); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static void recMOVNtemp_constt(int info) +{ + // T is constant and non-zero (checked in wrapper) — unconditional move + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static void recMOVNtemp_(int info) +{ + armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rt_].UD[0]); + armAsm->Cmp(RXARG1, 0); + + armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rs_].UD[0]); + + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); + armAsm->Csel(RXSCRATCH, RXARG1, RXSCRATCH, a64::ne); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +static EERECOMPILE_CODERC0_MEM(MOVNtemp, XMMINFO_READS | XMMINFO_READT | XMMINFO_READD | XMMINFO_WRITED | XMMINFO_NORENAME); + +void recMOVN() +{ + if (_Rs_ == _Rd_) + return; + + if (GPR_IS_CONST1(_Rt_) && g_cpuConstRegs[_Rt_].UD[0] == 0) + return; + + recMOVNtemp(); +} + +#endif // !FORCE_INTERP_MOVE + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900MultDiv-arm64.cpp b/pcsx2/arm64/iR5900MultDiv-arm64.cpp new file mode 100644 index 0000000000..c3374a7d4a --- /dev/null +++ b/pcsx2/arm64/iR5900MultDiv-arm64.cpp @@ -0,0 +1,594 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Multiply/Divide Instruction Codegen — memory-based +// MULT/DIV write to HI:LO registers, optionally Rd. +// ARM64 has native SMULL/UMULL and SDIV/UDIV. +// All operands via cpuRegs memory. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_MULTDIV +REC_FUNC(MULT); +REC_FUNC(MULTU); +REC_FUNC(DIV); +REC_FUNC(DIVU); +#else + +// Load Rs/Rt lower 32 bits from memory (or const) +static void loadRs32() +{ + if (GPR_IS_CONST1(_Rs_)) + armAsm->Mov(a64::w1, g_cpuConstRegs[_Rs_].UL[0]); + else + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]); +} + +static void loadRt32() +{ + if (GPR_IS_CONST1(_Rt_)) + armAsm->Mov(a64::w2, g_cpuConstRegs[_Rt_].UL[0]); + else + armLoadEERegPtr(a64::w2, &cpuRegs.GPR.r[_Rt_].UL[0]); +} + +// Write LO and HI from 64-bit result in x0 +// lo = lower 32, hi = upper 32, both sign-extended to 64 bits +static void recWritebackHILO(bool upper) +{ + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(upper ? &cpuRegs.LO.UD[1] : &cpuRegs.LO.UD[0])); + + armAsm->Asr(a64::x0, a64::x0, 32); + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(upper ? &cpuRegs.HI.UD[1] : &cpuRegs.HI.UD[0])); +} + +// Write Rd from LO (memory-based — no register allocation) +static void recWritebackRd() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[0]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +//// MULT — signed 32-bit multiply, result in HI:LO, optionally Rd +void recMULT() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s64 result = (s64)(s32)g_cpuConstRegs[_Rs_].UL[0] * (s64)(s32)g_cpuConstRegs[_Rt_].UL[0]; + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)result); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)(result >> 32)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); + + if (_Rd_) + { + _deleteEEreg(_Rd_, 0); + g_cpuConstRegs[_Rd_].SD[0] = (s32)(u32)result; + GPR_SET_CONST(_Rd_); + } + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Smull(a64::x0, a64::w1, a64::w2); + + recWritebackHILO(false); + recWritebackRd(); +} + +//// MULTU — unsigned 32-bit multiply +void recMULTU() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u64 result = (u64)g_cpuConstRegs[_Rs_].UL[0] * (u64)g_cpuConstRegs[_Rt_].UL[0]; + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)result); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)(result >> 32)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); + + if (_Rd_) + { + _deleteEEreg(_Rd_, 0); + g_cpuConstRegs[_Rd_].SD[0] = (s32)(u32)result; + GPR_SET_CONST(_Rd_); + } + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Umull(a64::x0, a64::w1, a64::w2); + + recWritebackHILO(false); + recWritebackRd(); +} + +//// DIV — signed 32-bit divide. LO = quotient, HI = remainder. +// PS2 div-by-zero: LO = (rs >= 0 ? -1 : 1), HI = rs (sign-extended into 64-bit +// HI/LO). Matches the interpreter and the PS2 hardware spec. +void recDIV() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s32 rs = g_cpuConstRegs[_Rs_].SL[0]; + s32 rt = g_cpuConstRegs[_Rt_].SL[0]; + + s32 lo, hi; + if (rt == 0) + { + lo = (rs >= 0) ? -1 : 1; + hi = rs; + } + else if (rs == (s32)0x80000000 && rt == -1) + { + lo = (s32)0x80000000; + hi = 0; + } + else + { + lo = rs / rt; + hi = rs % rt; + } + + armAsm->Mov(RXSCRATCH, (s64)lo); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + armAsm->Mov(RXSCRATCH, (s64)hi); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + // Branch on rt == 0 → div-by-zero handler. + a64::Label divByZero; + a64::Label done; + armAsm->Cbz(a64::w2, &divByZero); + + // Normal path: SDIV w0, w1, w2; MSUB w3 = w1 - w0 * w2 (remainder). + armAsm->Sdiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w3, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + + // Div-by-zero: w0 = (rs >= 0 ? -1 : 1), w3 = rs. + // Cneg w0, w0, lt: if rs < 0, w0 = -(-1) = 1; else w0 = -1. + armAsm->Bind(&divByZero); + armAsm->Mov(a64::w0, -1); + armAsm->Cmp(a64::w1, 0); + armAsm->Cneg(a64::w0, a64::w0, a64::lt); + armAsm->Mov(a64::w3, a64::w1); // HI = rs + + armAsm->Bind(&done); + + // Store LO = sign_extend(quotient or -1/1) + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + // Store HI = sign_extend(remainder or rs) + armAsm->Sxtw(RXSCRATCH, a64::w3); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); +} + +//// DIVU — unsigned 32-bit divide. PS2 div-by-zero: LO = -1 (0xffffffff +//// sign-extended), HI = rs. +void recDIVU() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u32 rs = g_cpuConstRegs[_Rs_].UL[0]; + u32 rt = g_cpuConstRegs[_Rt_].UL[0]; + + s32 lo, hi; + if (rt == 0) + { + lo = -1; + hi = (s32)rs; + } + else + { + lo = (s32)(rs / rt); + hi = (s32)(rs % rt); + } + + armAsm->Mov(RXSCRATCH, (s64)lo); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + armAsm->Mov(RXSCRATCH, (s64)hi); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + a64::Label divByZero; + a64::Label done; + armAsm->Cbz(a64::w2, &divByZero); + + // Normal path: UDIV w0, w1, w2; MSUB w1 = w1 - w0 * w2 (remainder in-place + // over Rs scratch; Msub permits rd==ra). On div-by-zero w1 still holds Rs, + // so the slow path doesn't need a separate Mov w3, w1. + armAsm->Udiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w1, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + + // Div-by-zero: w0 = -1; w1 already holds Rs (HI). + armAsm->Bind(&divByZero); + armAsm->Mov(a64::w0, -1); + + armAsm->Bind(&done); + + // Store LO = sign_extend(quotient or -1) + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[0])); + + // Store HI = sign_extend(remainder or rs) + armAsm->Sxtw(RXSCRATCH, a64::w1); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[0])); +} + +#endif // !FORCE_INTERP_MULTDIV + +// Write Rd from LO1 (pipeline 1) +static void recWritebackRd1() +{ + if (!_Rd_) return; + + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + armLoadEERegPtr(RXSCRATCH, &cpuRegs.LO.UD[1]); + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +//// MULT1 — signed 32-bit multiply, pipeline 1 (HI1:LO1) +void recMULT1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s64 result = (s64)(s32)g_cpuConstRegs[_Rs_].UL[0] * (s64)(s32)g_cpuConstRegs[_Rt_].UL[0]; + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)result); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)(result >> 32)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); + + if (_Rd_) + { + _deleteEEreg(_Rd_, 0); + g_cpuConstRegs[_Rd_].SD[0] = (s32)(u32)result; + GPR_SET_CONST(_Rd_); + } + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Smull(a64::x0, a64::w1, a64::w2); + + recWritebackHILO(true); + recWritebackRd1(); +} + +//// MULTU1 — unsigned 32-bit multiply, pipeline 1 +void recMULTU1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u64 result = (u64)g_cpuConstRegs[_Rs_].UL[0] * (u64)g_cpuConstRegs[_Rt_].UL[0]; + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)result); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Mov(RXSCRATCH, (s64)(s32)(u32)(result >> 32)); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); + + if (_Rd_) + { + _deleteEEreg(_Rd_, 0); + g_cpuConstRegs[_Rd_].SD[0] = (s32)(u32)result; + GPR_SET_CONST(_Rd_); + } + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Umull(a64::x0, a64::w1, a64::w2); + + recWritebackHILO(true); + recWritebackRd1(); +} + +//// DIV1 — signed 32-bit divide, pipeline 1. Same div-by-zero spec as DIV. +void recDIV1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s32 rs = g_cpuConstRegs[_Rs_].SL[0]; + s32 rt = g_cpuConstRegs[_Rt_].SL[0]; + + s32 lo, hi; + if (rt == 0) + { + lo = (rs >= 0) ? -1 : 1; + hi = rs; + } + else if (rs == (s32)0x80000000 && rt == -1) + { + lo = (s32)0x80000000; + hi = 0; + } + else + { + lo = rs / rt; + hi = rs % rt; + } + + armAsm->Mov(RXSCRATCH, (s64)lo); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Mov(RXSCRATCH, (s64)hi); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + a64::Label divByZero; + a64::Label done; + armAsm->Cbz(a64::w2, &divByZero); + + armAsm->Sdiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w3, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + + // Div-by-zero: w0 = (rs >= 0 ? -1 : 1), w3 = rs. See recDIV for Cneg rationale. + armAsm->Bind(&divByZero); + armAsm->Mov(a64::w0, -1); + armAsm->Cmp(a64::w1, 0); + armAsm->Cneg(a64::w0, a64::w0, a64::lt); + armAsm->Mov(a64::w3, a64::w1); // HI = rs + + armAsm->Bind(&done); + + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Sxtw(RXSCRATCH, a64::w3); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); +} + +//// DIVU1 — unsigned 32-bit divide, pipeline 1. Same div-by-zero spec as DIVU. +void recDIVU1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u32 rs = g_cpuConstRegs[_Rs_].UL[0]; + u32 rt = g_cpuConstRegs[_Rt_].UL[0]; + + s32 lo, hi; + if (rt == 0) + { + lo = -1; + hi = (s32)rs; + } + else + { + lo = (s32)(rs / rt); + hi = (s32)(rs % rt); + } + + armAsm->Mov(RXSCRATCH, (s64)lo); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Mov(RXSCRATCH, (s64)hi); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + a64::Label divByZero; + a64::Label done; + armAsm->Cbz(a64::w2, &divByZero); + + // See recDIVU for the Msub-into-w1 rationale. + armAsm->Udiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w1, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + + armAsm->Bind(&divByZero); + armAsm->Mov(a64::w0, -1); + + armAsm->Bind(&done); + + armAsm->Sxtw(RXSCRATCH, a64::w0); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.LO.UD[1])); + + armAsm->Sxtw(RXSCRATCH, a64::w1); + armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.HI.UD[1])); +} + +//// MADD — signed multiply-add: HI:LO += Rs * Rt, Rd = LO +void recMADD() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s64 result = (s64)(s32)g_cpuConstRegs[_Rs_].UL[0] * (s64)(s32)g_cpuConstRegs[_Rt_].UL[0]; + + // Add to existing HI:LO — load, add, store + _eeFlushAllDirty(); + armLoadEERegPtr(a64::w1, &cpuRegs.LO.UL[0]); + armLoadEERegPtr(a64::w2, &cpuRegs.HI.UL[0]); + armAsm->Orr(a64::x1, a64::x1, a64::Operand(a64::x2, a64::LSL, 32)); + armAsm->Mov(RXSCRATCH, result); + armAsm->Add(a64::x0, a64::x1, RXSCRATCH); + + recWritebackHILO(false); + recWritebackRd(); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + // x0 = Rs * Rt (signed 32x32→64) + armAsm->Smull(a64::x0, a64::w1, a64::w2); + + // Load existing HI:LO into x1 + armLoadEERegPtr(a64::w3, &cpuRegs.LO.UL[0]); + armLoadEERegPtr(a64::w4, &cpuRegs.HI.UL[0]); + armAsm->Orr(a64::x3, a64::x3, a64::Operand(a64::x4, a64::LSL, 32)); + + // Add + armAsm->Add(a64::x0, a64::x0, a64::x3); + + recWritebackHILO(false); + recWritebackRd(); +} + +//// MADDU — unsigned multiply-add: HI:LO += Rs * Rt, Rd = LO +void recMADDU() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u64 result = (u64)g_cpuConstRegs[_Rs_].UL[0] * (u64)g_cpuConstRegs[_Rt_].UL[0]; + + _eeFlushAllDirty(); + armLoadEERegPtr(a64::w1, &cpuRegs.LO.UL[0]); + armLoadEERegPtr(a64::w2, &cpuRegs.HI.UL[0]); + armAsm->Orr(a64::x1, a64::x1, a64::Operand(a64::x2, a64::LSL, 32)); + armAsm->Mov(RXSCRATCH, result); + armAsm->Add(a64::x0, a64::x1, RXSCRATCH); + + recWritebackHILO(false); + recWritebackRd(); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Umull(a64::x0, a64::w1, a64::w2); + + armLoadEERegPtr(a64::w3, &cpuRegs.LO.UL[0]); + armLoadEERegPtr(a64::w4, &cpuRegs.HI.UL[0]); + armAsm->Orr(a64::x3, a64::x3, a64::Operand(a64::x4, a64::LSL, 32)); + + armAsm->Add(a64::x0, a64::x0, a64::x3); + + recWritebackHILO(false); + recWritebackRd(); +} + +//// MADD1 — signed multiply-add, pipeline 1: HI1:LO1 += Rs * Rt, Rd = LO1 +void recMADD1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + s64 result = (s64)(s32)g_cpuConstRegs[_Rs_].UL[0] * (s64)(s32)g_cpuConstRegs[_Rt_].UL[0]; + + _eeFlushAllDirty(); + armLoadEERegPtr(a64::w1, &cpuRegs.LO.UL[2]); + armLoadEERegPtr(a64::w2, &cpuRegs.HI.UL[2]); + armAsm->Orr(a64::x1, a64::x1, a64::Operand(a64::x2, a64::LSL, 32)); + armAsm->Mov(RXSCRATCH, result); + armAsm->Add(a64::x0, a64::x1, RXSCRATCH); + + recWritebackHILO(true); + recWritebackRd1(); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Smull(a64::x0, a64::w1, a64::w2); + + armLoadEERegPtr(a64::w3, &cpuRegs.LO.UL[2]); // LO1 = LO.UL[2] (upper 64 bits) + armLoadEERegPtr(a64::w4, &cpuRegs.HI.UL[2]); // HI1 = HI.UL[2] + armAsm->Orr(a64::x3, a64::x3, a64::Operand(a64::x4, a64::LSL, 32)); + + armAsm->Add(a64::x0, a64::x0, a64::x3); + + recWritebackHILO(true); + recWritebackRd1(); +} + +//// MADDU1 — unsigned multiply-add, pipeline 1 +void recMADDU1() +{ + if (GPR_IS_CONST2(_Rs_, _Rt_)) + { + u64 result = (u64)g_cpuConstRegs[_Rs_].UL[0] * (u64)g_cpuConstRegs[_Rt_].UL[0]; + + _eeFlushAllDirty(); + armLoadEERegPtr(a64::w1, &cpuRegs.LO.UL[2]); + armLoadEERegPtr(a64::w2, &cpuRegs.HI.UL[2]); + armAsm->Orr(a64::x1, a64::x1, a64::Operand(a64::x2, a64::LSL, 32)); + armAsm->Mov(RXSCRATCH, result); + armAsm->Add(a64::x0, a64::x1, RXSCRATCH); + + recWritebackHILO(true); + recWritebackRd1(); + return; + } + + _eeFlushAllDirty(); + loadRs32(); + loadRt32(); + + armAsm->Umull(a64::x0, a64::w1, a64::w2); + + armLoadEERegPtr(a64::w3, &cpuRegs.LO.UL[2]); + armLoadEERegPtr(a64::w4, &cpuRegs.HI.UL[2]); + armAsm->Orr(a64::x3, a64::x3, a64::Operand(a64::x4, a64::LSL, 32)); + + armAsm->Add(a64::x0, a64::x0, a64::x3); + + recWritebackHILO(true); + recWritebackRd1(); +} + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900Shift-arm64.cpp b/pcsx2/arm64/iR5900Shift-arm64.cpp new file mode 100644 index 0000000000..5cda95986f --- /dev/null +++ b/pcsx2/arm64/iR5900Shift-arm64.cpp @@ -0,0 +1,427 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Shift Instruction Codegen — memory-based +// All operands loaded from / stored to cpuRegs.GPR memory. +// No register allocation for scalar ops. + +#include "arm64/iR5900-arm64.h" + +namespace a64 = vixl::aarch64; + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +#ifdef FORCE_INTERP_SHIFT +REC_FUNC(SLL); +REC_FUNC(SRL); +REC_FUNC(SRA); +REC_FUNC(DSLL); +REC_FUNC(DSRL); +REC_FUNC(DSRA); +REC_FUNC(DSLL32); +REC_FUNC(DSRL32); +REC_FUNC(DSRA32); +REC_FUNC(SLLV); +REC_FUNC(SRLV); +REC_FUNC(SRAV); +REC_FUNC(DSLLV); +REC_FUNC(DSRLV); +REC_FUNC(DSRAV); +#else + +// Memory load/store helpers — always use cpuRegs memory +static void memLoadS32() +{ + armLoadEERegPtr(RWARG1, &cpuRegs.GPR.r[_Rs_].UL[0]); +} + +static void memLoadS64() +{ + armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rs_].UD[0]); +} + +static void memLoadT32() +{ + armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]); +} + +static void memLoadT64() +{ + armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); +} + +static void memStoreD() +{ + armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]); +} + +/********************************************************* + * Shift with constant amount — rd = rt SHIFT sa * + * Uses eeRecompileCodeRC2_MEM * + *********************************************************/ + +//// SLL — rd = sign_extend(rt << sa) +static void recSLL_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].UL[0] << _Sa_); +} + +static void recSLL_(int info) +{ + memLoadT32(); + if (_Sa_ != 0) + armAsm->Lsl(RWSCRATCH, RWSCRATCH, _Sa_); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SLL, XMMINFO_WRITED | XMMINFO_READT); + +//// SRL — rd = sign_extend(rt >> sa) (logical) +static void recSRL_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].UL[0] >> _Sa_); +} + +static void recSRL_(int info) +{ + memLoadT32(); + if (_Sa_ != 0) + armAsm->Lsr(RWSCRATCH, RWSCRATCH, _Sa_); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SRL, XMMINFO_WRITED | XMMINFO_READT); + +//// SRA — rd = sign_extend(rt >> sa) (arithmetic) +static void recSRA_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].SL[0] >> _Sa_); +} + +static void recSRA_(int info) +{ + memLoadT32(); + if (_Sa_ != 0) + armAsm->Asr(RWSCRATCH, RWSCRATCH, _Sa_); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SRA, XMMINFO_WRITED | XMMINFO_READT); + +//// DSLL — rd = rt << sa (64-bit) +static void recDSLL_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] << _Sa_; +} + +static void recDSLL_(int info) +{ + memLoadT64(); + if (_Sa_ != 0) + armAsm->Lsl(RXSCRATCH, RXSCRATCH, _Sa_); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSLL, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRL +static void recDSRL_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] >> _Sa_; +} + +static void recDSRL_(int info) +{ + memLoadT64(); + if (_Sa_ != 0) + armAsm->Lsr(RXSCRATCH, RXSCRATCH, _Sa_); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRL, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRA +static void recDSRA_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = g_cpuConstRegs[_Rt_].SD[0] >> _Sa_; +} + +static void recDSRA_(int info) +{ + memLoadT64(); + if (_Sa_ != 0) + armAsm->Asr(RXSCRATCH, RXSCRATCH, _Sa_); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRA, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSLL32 — rd = rt << (sa + 32) +static void recDSLL32_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] << (_Sa_ + 32); +} + +static void recDSLL32_(int info) +{ + memLoadT64(); + armAsm->Lsl(RXSCRATCH, RXSCRATCH, _Sa_ + 32); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSLL32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRL32 +static void recDSRL32_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] >> (_Sa_ + 32); +} + +static void recDSRL32_(int info) +{ + memLoadT64(); + armAsm->Lsr(RXSCRATCH, RXSCRATCH, _Sa_ + 32); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRL32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRA32 +static void recDSRA32_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = g_cpuConstRegs[_Rt_].SD[0] >> (_Sa_ + 32); +} + +static void recDSRA32_(int info) +{ + memLoadT64(); + armAsm->Asr(RXSCRATCH, RXSCRATCH, _Sa_ + 32); + memStoreD(); +} + +EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRA32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP); + +/********************************************************* + * Variable shifts — rd = rt SHIFT rs * + * Uses eeRecompileCodeRC0_MEM * + *********************************************************/ + +//// SLLV — rd = sign_extend((rt << rs[4:0])) +static void recSLLV_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].UL[0] << (g_cpuConstRegs[_Rs_].UL[0] & 0x1f)); +} + +static void recSLLV_consts(int info) +{ + memLoadT32(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f; + if (sa != 0) + armAsm->Lsl(RWSCRATCH, RWSCRATCH, sa); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSLLV_constt(int info) +{ + memLoadS32(); + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + armAsm->Lsl(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSLLV_(int info) +{ + memLoadS32(); + memLoadT32(); + armAsm->Lsl(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SLLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); + +//// SRLV +static void recSRLV_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].UL[0] >> (g_cpuConstRegs[_Rs_].UL[0] & 0x1f)); +} + +static void recSRLV_consts(int info) +{ + memLoadT32(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f; + if (sa != 0) + armAsm->Lsr(RWSCRATCH, RWSCRATCH, sa); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSRLV_constt(int info) +{ + memLoadS32(); + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]); + armAsm->Lsr(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSRLV_(int info) +{ + memLoadS32(); + memLoadT32(); + armAsm->Lsr(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SRLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); + +//// SRAV +static void recSRAV_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = (s32)(g_cpuConstRegs[_Rt_].SL[0] >> (g_cpuConstRegs[_Rs_].UL[0] & 0x1f)); +} + +static void recSRAV_consts(int info) +{ + memLoadT32(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f; + if (sa != 0) + armAsm->Asr(RWSCRATCH, RWSCRATCH, sa); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSRAV_constt(int info) +{ + memLoadS32(); + armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].SL[0]); + armAsm->Asr(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +static void recSRAV_(int info) +{ + memLoadS32(); + memLoadT32(); + armAsm->Asr(RWSCRATCH, RWSCRATCH, RWARG1); + armAsm->Sxtw(RXSCRATCH, RWSCRATCH); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(SRAV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT); + +//// DSLLV — rd = rt << rs[5:0] (64-bit) +static void recDSLLV_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] << (g_cpuConstRegs[_Rs_].UL[0] & 0x3f); +} + +static void recDSLLV_consts(int info) +{ + memLoadT64(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f; + if (sa != 0) + armAsm->Lsl(RXSCRATCH, RXSCRATCH, sa); + memStoreD(); +} + +static void recDSLLV_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]); + armAsm->Lsl(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recDSLLV_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Lsl(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(DSLLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRLV +static void recDSRLV_const() +{ + g_cpuConstRegs[_Rd_].UD[0] = g_cpuConstRegs[_Rt_].UD[0] >> (g_cpuConstRegs[_Rs_].UL[0] & 0x3f); +} + +static void recDSRLV_consts(int info) +{ + memLoadT64(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f; + if (sa != 0) + armAsm->Lsr(RXSCRATCH, RXSCRATCH, sa); + memStoreD(); +} + +static void recDSRLV_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]); + armAsm->Lsr(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recDSRLV_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Lsr(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(DSRLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +//// DSRAV +static void recDSRAV_const() +{ + g_cpuConstRegs[_Rd_].SD[0] = g_cpuConstRegs[_Rt_].SD[0] >> (g_cpuConstRegs[_Rs_].UL[0] & 0x3f); +} + +static void recDSRAV_consts(int info) +{ + memLoadT64(); + u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f; + if (sa != 0) + armAsm->Asr(RXSCRATCH, RXSCRATCH, sa); + memStoreD(); +} + +static void recDSRAV_constt(int info) +{ + memLoadS64(); + armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].SD[0]); + armAsm->Asr(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +static void recDSRAV_(int info) +{ + memLoadS64(); + memLoadT64(); + armAsm->Asr(RXSCRATCH, RXSCRATCH, RXARG1); + memStoreD(); +} + +EERECOMPILE_CODERC0_MEM(DSRAV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP); + +#endif // !FORCE_INTERP_SHIFT + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/arm64/iR5900Templates-arm64.cpp b/pcsx2/arm64/iR5900Templates-arm64.cpp new file mode 100644 index 0000000000..956f6475dd --- /dev/null +++ b/pcsx2/arm64/iR5900Templates-arm64.cpp @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE Code Generation Templates +// Ports of x86/ix86-32/iR5900Templates.cpp for ARM64 register allocator. +// These templates handle register allocation, constant propagation dispatch, +// and register renaming for the standard instruction patterns. + +#include "arm64/iR5900-arm64.h" +#include "Common.h" +#include "Memory.h" +#include "VU.h" +#include "VUmicro.h" + +namespace a64 = vixl::aarch64; + +//////////////////// +// Code Templates // +//////////////////// + + +//////////////////////////////////// +// Memory-based scalar templates // +// No register allocation — all // +// operands via cpuRegs memory. // +//////////////////////////////////// + +// rd = rs OP rt (memory-based) +void eeRecompileCodeRC0_MEM(R5900FNPTR constcode, R5900FNPTR_INFO constscode, R5900FNPTR_INFO consttcode, R5900FNPTR_INFO noconstcode, int xmminfo) +{ + if (!_Rd_ && (xmminfo & XMMINFO_WRITED)) + return; + + const bool s_is_const = GPR_IS_CONST1(_Rs_); + const bool t_is_const = GPR_IS_CONST1(_Rt_); + + // Both-const: compile-time evaluation + if (s_is_const && t_is_const) + { + if (_Rd_ && (xmminfo & XMMINFO_WRITED)) + { + _deleteEEreg(_Rd_, 0); + GPR_SET_CONST(_Rd_); + } + constcode(); + return; + } + + // Flush source registers to memory (writeback if dirty, then free) + if ((xmminfo & XMMINFO_READS) && !s_is_const) + _deleteEEreg(_Rs_, 1); + if ((xmminfo & XMMINFO_READT) && !t_is_const) + _deleteEEreg(_Rt_, 1); + + // Handle dest register + if (xmminfo & XMMINFO_READD) + _deleteEEreg(_Rd_, 1); // flush so we can read current value + else if (xmminfo & XMMINFO_WRITED) + _deleteEEreg(_Rd_, 0); // discard — we're about to overwrite + + if (xmminfo & XMMINFO_WRITED) + GPR_DEL_CONST(_Rd_); + + u32 info = 0; // No register allocation — codegen reads/writes memory + + if (s_is_const) + { + constscode(info); + return; + } + + if (t_is_const) + { + consttcode(info); + return; + } + + noconstcode(info); +} + +// rt = rs OP imm16 (memory-based) +void eeRecompileCodeRC1_MEM(R5900FNPTR constcode, R5900FNPTR_INFO noconstcode, int xmminfo) +{ + pxAssert((xmminfo & (XMMINFO_READS | XMMINFO_WRITET)) == (XMMINFO_READS | XMMINFO_WRITET)); + + if (!_Rt_) + return; + + // Const: compile-time evaluation + if (GPR_IS_CONST1(_Rs_)) + { + _deleteEEreg(_Rt_, 0); + GPR_SET_CONST(_Rt_); + constcode(); + return; + } + + // Flush source to memory + _deleteEEreg(_Rs_, 1); + + // Discard dest (about to overwrite) + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + + u32 info = 0; + noconstcode(info); +} + +// rd = rt OP sa (memory-based) +void eeRecompileCodeRC2_MEM(R5900FNPTR constcode, R5900FNPTR_INFO noconstcode, int xmminfo) +{ + pxAssert((xmminfo & (XMMINFO_READT | XMMINFO_WRITED)) == (XMMINFO_READT | XMMINFO_WRITED)); + + if (!_Rd_) + return; + + // Const: compile-time evaluation + if (GPR_IS_CONST1(_Rt_)) + { + _deleteEEreg(_Rd_, 0); + GPR_SET_CONST(_Rd_); + constcode(); + return; + } + + // Flush source to memory + _deleteEEreg(_Rt_, 1); + + // Discard dest (about to overwrite) + _deleteEEreg(_Rd_, 0); + GPR_DEL_CONST(_Rd_); + + u32 info = 0; + noconstcode(info); +} + +// 128-bit NEON allocation for MMI/XMM operations +int eeRecompileCodeXMM(int xmminfo) +{ + int info = PROCESS_EE_XMM; + + // EEREC_LO, EEREC_HI and EEREC_ACC all decode from the same 5-bit info-word + // field (see iCore-arm64.h): five distinct 5-bit register fields plus the + // presence flags do not fit in the 32-bit info word. This is safe only because + // no op needs two of {LO, HI, ACC} live at once through the allocator — integer + // MULT/DIV/MADD and PMFHL load LO/HI directly from memory, and ACC is FPU-only. + // Requesting both LO and HI here would OR two register indices into one field + // and silently miscompile, so guard it; an op that genuinely needs both must + // bypass the allocator (see recPMFHL). + pxAssertRel(!((xmminfo & (XMMINFO_READLO | XMMINFO_WRITELO)) && + (xmminfo & (XMMINFO_READHI | XMMINFO_WRITEHI))), + "eeRecompileCodeXMM: LO and HI share an info-word field; an op needing both " + "must bypass the allocator (see recPMFHL)."); + + if (xmminfo & (XMMINFO_READLO | XMMINFO_WRITELO)) + _addNeededGPRtoNEONreg(NEONGPR_LO); + if (xmminfo & (XMMINFO_READHI | XMMINFO_WRITEHI)) + _addNeededGPRtoNEONreg(NEONGPR_HI); + if (xmminfo & XMMINFO_READS) + _addNeededGPRtoNEONreg(_Rs_); + if (xmminfo & XMMINFO_READT) + _addNeededGPRtoNEONreg(_Rt_); + if (xmminfo & XMMINFO_WRITED) + _addNeededGPRtoNEONreg(_Rd_); + + if (xmminfo & XMMINFO_READS) + { + const int reg = _allocGPRtoNEONreg(_Rs_, MODE_READ); + info |= PROCESS_EE_SET_S(reg); + } + if (xmminfo & XMMINFO_READT) + { + const int reg = _allocGPRtoNEONreg(_Rt_, MODE_READ); + info |= PROCESS_EE_SET_T(reg); + } + + if (xmminfo & XMMINFO_WRITED) + { + int readd = MODE_WRITE | ((xmminfo & XMMINFO_READD) ? MODE_READ : 0); + + int regd = _checkNEONreg(NEONTYPE_GPRREG, _Rd_, readd); + if (regd < 0) + { + // TODO: register renaming for NEON + regd = _allocGPRtoNEONreg(_Rd_, readd); + } + info |= PROCESS_EE_SET_D(regd); + } + + // INVARIANT: no EE opcode currently passes XMMINFO_*LO/HI, so these two + // branches never execute and LO/HI are never NEON-resident — which is why + // the MMI mul/mac handlers (recPMADDUW/PMFHL/PMTHI/PMTLO in iMMI-arm64.cpp) + // can Str/Ldr LO/HI straight to memory without an allocator flush. If a + // future op DOES request XMMINFO_*LO/HI, every direct-memory LO/HI handler + // in iMMI-arm64.cpp must regain a _deleteGPRtoNEONreg(NEONGPR_LO/HI) flush. + if (xmminfo & (XMMINFO_READLO | XMMINFO_WRITELO)) + { + info |= PROCESS_EE_SET_LO(_allocGPRtoNEONreg(NEONGPR_LO, + ((xmminfo & XMMINFO_READLO) ? MODE_READ : 0) | ((xmminfo & XMMINFO_WRITELO) ? MODE_WRITE : 0))); + } + if (xmminfo & (XMMINFO_READHI | XMMINFO_WRITEHI)) + { + info |= PROCESS_EE_SET_HI(_allocGPRtoNEONreg(NEONGPR_HI, + ((xmminfo & XMMINFO_READHI) ? MODE_READ : 0) | ((xmminfo & XMMINFO_WRITEHI) ? MODE_WRITE : 0))); + } + + if (xmminfo & XMMINFO_WRITED) + GPR_DEL_CONST(_Rd_); + + _validateRegs(); + return info; +} + +// FPU allocation template +#define _Ft_ _Rt_ +#define _Fs_ _Rd_ +#define _Fd_ _Sa_ + +void eeFPURecompileCode(R5900FNPTR_INFO xmmcode, R5900FNPTR fpucode, int xmminfo) +{ + int mmregs = -1, mmregt = -1, mmregd = -1, mmregacc = -1; + int info = PROCESS_EE_XMM; + + if (xmminfo & XMMINFO_READS) + _addNeededFPtoNEONreg(_Fs_); + if (xmminfo & XMMINFO_READT) + _addNeededFPtoNEONreg(_Ft_); + if (xmminfo & (XMMINFO_WRITED | XMMINFO_READD)) + _addNeededFPtoNEONreg(_Fd_); + if (xmminfo & (XMMINFO_WRITEACC | XMMINFO_READACC)) + _addNeededFPACCtoNEONreg(); + + if (xmminfo & XMMINFO_READT) + mmregt = _allocFPtoNEONreg(_Ft_, MODE_READ); + + if (xmminfo & XMMINFO_READS) + { + mmregs = _allocFPtoNEONreg(_Fs_, MODE_READ); + if ((xmminfo & XMMINFO_READT) && _Fs_ == _Ft_) + mmregt = mmregs; + } + + if (xmminfo & XMMINFO_READD) + { + pxAssert(xmminfo & XMMINFO_WRITED); + mmregd = _allocFPtoNEONreg(_Fd_, MODE_READ); + } + + if (xmminfo & XMMINFO_READACC) + mmregacc = _allocFPACCtoNEONreg(MODE_READ); + + if (xmminfo & XMMINFO_WRITEACC) + { + int readacc = MODE_WRITE | ((xmminfo & XMMINFO_READACC) ? MODE_READ : 0); + mmregacc = _checkNEONreg(NEONTYPE_FPACC, 0, readacc); + if (mmregacc < 0) + mmregacc = _allocFPACCtoNEONreg(readacc); + arm64neon[mmregacc].mode |= MODE_WRITE; + } + else if (xmminfo & XMMINFO_WRITED) + { + int readd = MODE_WRITE | ((xmminfo & XMMINFO_READD) ? MODE_READ : 0); + if (xmminfo & XMMINFO_READD) + mmregd = _allocFPtoNEONreg(_Fd_, readd); + else + mmregd = _checkNEONreg(NEONTYPE_FPREG, _Fd_, readd); + + if (mmregd < 0) + mmregd = _allocFPtoNEONreg(_Fd_, readd); + } + + pxAssert(mmregs >= 0 || mmregt >= 0 || mmregd >= 0 || mmregacc >= 0); + + if (xmminfo & XMMINFO_WRITED) + { + pxAssert(mmregd >= 0); + info |= PROCESS_EE_SET_D(mmregd); + } + if (xmminfo & (XMMINFO_WRITEACC | XMMINFO_READACC)) + { + if (mmregacc >= 0) + info |= PROCESS_EE_SET_ACC(mmregacc) | PROCESS_EE_ACC; + else + pxAssert(!(xmminfo & XMMINFO_WRITEACC)); + } + + if (xmminfo & XMMINFO_READS) + { + if (mmregs >= 0) + info |= PROCESS_EE_SET_S(mmregs); + } + if (xmminfo & XMMINFO_READT) + { + if (mmregt >= 0) + info |= PROCESS_EE_SET_T(mmregt); + } + + xmmcode(info); +} + +#undef _Ft_ +#undef _Fs_ +#undef _Fd_ diff --git a/pcsx2/arm64/recVTLB-arm64.cpp b/pcsx2/arm64/recVTLB-arm64.cpp new file mode 100644 index 0000000000..1cfc8ba91b --- /dev/null +++ b/pcsx2/arm64/recVTLB-arm64.cpp @@ -0,0 +1,1214 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 EE VTLB Dynamic Code Generation +// +// Two paths for load/store instructions: +// +// 1. Fastmem (primary): Single LDR/STR via RFASTMEMBASE (x19). +// The fastmem area is a 4GB region mapped to PS2 memory. If the page +// is unmapped (MMIO, etc.), the SIGSEGV handler backpatches the faulting +// instruction with a branch to a slow-path thunk. +// +// 2. Softmem (fallback): Inline VTLB page table lookup. Used when fastmem +// is disabled or when a PC has previously faulted (vtlb_IsFaultingPC). + +// FORCE_INTERP_MEMORY is defined in iR5900-arm64.h to force interpreter fallback + +#include "arm64/iR5900-arm64.h" +#include "arm64/AsmHelpers.h" +#include "vtlb.h" +#include "VU.h" +#include "Hw.h" +#include "Memory.h" +#include "common/Assertions.h" + +extern void vu0Sync(); + +namespace a64 = vixl::aarch64; + +using namespace vtlb_private; + + +// ===================================================================================================== +// Softmem — Inline VTLB Lookup (fallback for faulting PCs) +// ===================================================================================================== + +// Generates inline vtlb read code. Result in w0/x0. +// +// Algorithm: +// vmv = vmap[addr >> VTLB_PAGE_BITS] (page table lookup) +// ppf = addr + vmv (combine with mapping) +// if (ppf >= 0) result = *(DataType*)ppf (fast path: direct read) +// else call vtlb_memRead(addr) (slow path: handler dispatch) +// +// addr_reg: ARM64 W register index containing the EE virtual address +// Clobbers: w0, x0, x8, x9, x17 +// Result: in w0/x0 +static void vtlbSoftmemRead(int addr_wreg, u32 bits, bool sign) +{ + pxAssert(bits == 8 || bits == 16 || bits == 32 || bits == 64); + + // Save the original address in w9 (needed for slow path) + if (addr_wreg != 9) + armAsm->Mov(a64::w9, armWRegister(addr_wreg)); + + // Page index: w8 = addr >> VTLB_PAGE_BITS + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + + // Load vmap base address into x17 + armMoveAddressToReg(RSCRATCHADDR, vtlbdata.vmap); + + // Load vmap entry: x8 = vmap[page_index] (each entry is 8 bytes = sptr) + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + + // Compute ppf: x0 = addr + vmv (use 64-bit add, addr zero-extended from w9). + // ADDS sets N from bit 63 of the result, so B.mi handles the slow-path + // branch on the sign bit without a separate Tbnz. + armAsm->Adds(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->B(&slow_path, a64::mi); + + // --- Fast path: direct memory read from host pointer ppf --- + switch (bits) + { + case 8: + if (sign) + armAsm->Ldrsb(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldrb(a64::w0, a64::MemOperand(a64::x0)); + break; + case 16: + if (sign) + armAsm->Ldrsh(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldrh(a64::w0, a64::MemOperand(a64::x0)); + break; + case 32: + if (sign) + armAsm->Ldrsw(a64::x0, a64::MemOperand(a64::x0)); + else + armAsm->Ldr(a64::w0, a64::MemOperand(a64::x0)); + break; + case 64: + armAsm->Ldr(a64::x0, a64::MemOperand(a64::x0)); + break; + } + armAsm->B(&done); + + // --- Slow path: call vtlb_memRead(addr) --- + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); // restore original address as argument + + // Spill/reload RECCYCLE around the handler call: page-0F INTC_STAT + // reads invoke IntCHackCheck which mutates cpuRegs.cycle. Without + // this the JIT's pinned x25 stays stale and block-end cycle compare + // never trips on tight INTC polls. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + switch (bits) + { + case 8: armEmitCall((void*)vtlb_memRead); break; + case 16: armEmitCall((void*)vtlb_memRead); break; + case 32: armEmitCall((void*)vtlb_memRead); break; + case 64: armEmitCall((void*)vtlb_memRead); break; + } + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + // Sign-extend if needed (vtlb_memRead returns zero-extended) + if (sign && bits == 8) + armAsm->Sxtb(a64::x0, a64::w0); + else if (sign && bits == 16) + armAsm->Sxth(a64::x0, a64::w0); + else if (sign && bits == 32) + armAsm->Sxtw(a64::x0, a64::w0); + + armAsm->Bind(&done); +} + +// Generates inline vtlb write code. +// addr_reg: W register index with EE virtual address +// value_reg: W/X register index with value to write +// Clobbers: w0, x0, w1, x1, x8, x9, x17 +static void vtlbSoftmemWrite(int addr_wreg, int value_reg, u32 bits) +{ + pxAssert(bits == 8 || bits == 16 || bits == 32 || bits == 64); + + if (addr_wreg != 9) + armAsm->Mov(a64::w9, armWRegister(addr_wreg)); + if (value_reg != 10) + { + if (bits <= 32) + armAsm->Mov(a64::w10, armWRegister(value_reg)); + else + armAsm->Mov(a64::x10, armXRegister(value_reg)); + } + + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + // ADDS sets N from bit 63 of ppf; B.mi (=N) branches on the sign bit + // without the separate Tbnz, saving one instruction per softmem op. + armAsm->Adds(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->B(&slow_path, a64::mi); + + // --- Fast path: direct memory write --- + switch (bits) + { + case 8: armAsm->Strb(a64::w10, a64::MemOperand(a64::x0)); break; + case 16: armAsm->Strh(a64::w10, a64::MemOperand(a64::x0)); break; + case 32: armAsm->Str(a64::w10, a64::MemOperand(a64::x0)); break; + case 64: armAsm->Str(a64::x10, a64::MemOperand(a64::x0)); break; + } + armAsm->B(&done); + + // --- Slow path: call vtlb_memWrite --- + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + if (bits <= 32) + armAsm->Mov(a64::w1, a64::w10); + else + armAsm->Mov(a64::x1, a64::x10); + + // Spill/reload RECCYCLE: write-side handlers are symmetric to reads — + // any cycle-mutating handler reachable from MMIO must keep the JIT's + // pinned x25 coherent. See vtlbSoftmemRead for full rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + switch (bits) + { + case 8: armEmitCall((void*)vtlb_memWrite); break; + case 16: armEmitCall((void*)vtlb_memWrite); break; + case 32: armEmitCall((void*)vtlb_memWrite); break; + case 64: armEmitCall((void*)vtlb_memWrite); break; + } + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&done); +} + +// ===================================================================================================== +// Load/Store Instruction Implementations +// ===================================================================================================== + +namespace R5900 { +namespace Dynarec { +namespace OpcodeImpl { + +namespace Interp = R5900::Interpreter::OpcodeImpl; + +// ===================================================================================================== +// Fastmem helpers +// ===================================================================================================== + +// Build bitmasks of currently allocated ARM64 GPR and NEON registers. +// The backpatch thunk uses these to save/restore live registers around +// the vtlb C call, preventing corruption of JIT register allocator state. +static void vtlbGetLiveRegisterMasks(u32& gpr_bitmask, u32& fpr_bitmask) +{ + gpr_bitmask = 0; + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse) + gpr_bitmask |= (1u << i); + } + + fpr_bitmask = 0; + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + if (arm64neon[i].inuse) + fpr_bitmask |= (1u << i); + } +} + +// Emit a single fastmem load instruction and register backpatch info. +// addr_wreg: W register index holding the 32-bit guest virtual address +// dest_reg: register index where the result goes (W or X based on bits) +// Result is in dest_reg after the load (or after backpatch thunk on fault). +static void vtlbFastmemRead(int addr_wreg, int dest_reg, u32 bits, bool sign) +{ + u32 gpr_bitmask, fpr_bitmask; + vtlbGetLiveRegisterMasks(gpr_bitmask, fpr_bitmask); + + const u8* codeStart = armGetCurrentCodePointer(); + + a64::MemOperand mem(RFASTMEMBASE, armWRegister(addr_wreg), a64::UXTW); + switch (bits) + { + case 8: + if (sign) + armAsm->Ldrsb(armXRegister(dest_reg), mem); + else + armAsm->Ldrb(armWRegister(dest_reg), mem); + break; + case 16: + if (sign) + armAsm->Ldrsh(armXRegister(dest_reg), mem); + else + armAsm->Ldrh(armWRegister(dest_reg), mem); + break; + case 32: + if (sign) + armAsm->Ldrsw(armXRegister(dest_reg), mem); + else + armAsm->Ldr(armWRegister(dest_reg), mem); + break; + case 64: + armAsm->Ldr(armXRegister(dest_reg), mem); + break; + } + + vtlb_AddLoadStoreInfo((uptr)codeStart, 4, pc, gpr_bitmask, fpr_bitmask, + static_cast(addr_wreg), static_cast(dest_reg), + static_cast(bits), sign, true, false); +} + +// Emit a single fastmem store instruction and register backpatch info. +static void vtlbFastmemWrite(int addr_wreg, int value_reg, u32 bits) +{ + u32 gpr_bitmask, fpr_bitmask; + vtlbGetLiveRegisterMasks(gpr_bitmask, fpr_bitmask); + + const u8* codeStart = armGetCurrentCodePointer(); + + a64::MemOperand mem(RFASTMEMBASE, armWRegister(addr_wreg), a64::UXTW); + switch (bits) + { + case 8: armAsm->Strb(armWRegister(value_reg), mem); break; + case 16: armAsm->Strh(armWRegister(value_reg), mem); break; + case 32: armAsm->Str(armWRegister(value_reg), mem); break; + case 64: armAsm->Str(armXRegister(value_reg), mem); break; + } + + vtlb_AddLoadStoreInfo((uptr)codeStart, 4, pc, gpr_bitmask, fpr_bitmask, + static_cast(addr_wreg), static_cast(value_reg), + static_cast(bits), false, false, false); +} + +// Emit a single 128-bit fastmem load (LDR Q0, [RFASTMEMBASE, w_addr, UXTW]). +// Result in q0. Mirrors x86 PCSX2's MOVAPS-via-RFASTMEMBASE pattern +// (ix86-32/recVTLB.cpp). Backpatch thunk extended in RecStubs.cpp. +static void vtlbFastmemRead128(int addr_wreg) +{ + u32 gpr_bitmask, fpr_bitmask; + vtlbGetLiveRegisterMasks(gpr_bitmask, fpr_bitmask); + + const u8* codeStart = armGetCurrentCodePointer(); + armAsm->Ldr(a64::q0, a64::MemOperand(RFASTMEMBASE, armWRegister(addr_wreg), a64::UXTW)); + + vtlb_AddLoadStoreInfo((uptr)codeStart, 4, pc, gpr_bitmask, fpr_bitmask, + static_cast(addr_wreg), /*data_register*/ 0, + /*size_in_bits*/ 128, /*is_signed*/ false, /*is_load*/ true, /*is_fpr*/ true); +} + +// Emit a single 128-bit fastmem store (STR Q0, [RFASTMEMBASE, w_addr, UXTW]). +// Value in q0. Backpatch thunk extended in RecStubs.cpp. +static void vtlbFastmemWrite128(int addr_wreg) +{ + u32 gpr_bitmask, fpr_bitmask; + vtlbGetLiveRegisterMasks(gpr_bitmask, fpr_bitmask); + + const u8* codeStart = armGetCurrentCodePointer(); + armAsm->Str(a64::q0, a64::MemOperand(RFASTMEMBASE, armWRegister(addr_wreg), a64::UXTW)); + + vtlb_AddLoadStoreInfo((uptr)codeStart, 4, pc, gpr_bitmask, fpr_bitmask, + static_cast(addr_wreg), /*data_register*/ 0, + /*size_in_bits*/ 128, /*is_signed*/ false, /*is_load*/ false, /*is_fpr*/ true); +} + +// ===================================================================================================== +// Address computation helpers +// ===================================================================================================== + +// Compute load/store address (rs + imm) into w9. +// Does NOT flush — reads from wherever the value currently lives +// (const propagation, ARM64 GPR, NEON register, or cpuRegs memory). +// For const Rs, the full address is computed at compile time. +static void recComputeAddr() +{ + if (GPR_IS_CONST1(_Rs_)) + { + armAsm->Mov(a64::w9, g_cpuConstRegs[_Rs_].UL[0] + _Imm_); + } + else + { + _eeMoveGPRtoR(a64::w9, _Rs_); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + } +} + +// Prepare store value in w10/x10. +// Does NOT flush — reads from wherever the value currently lives. +static void recPrepStoreValue(u32 bits) +{ + _eeMoveGPRtoR(bits <= 32 ? a64::w10 : a64::x10, _Rt_); +} + +// Store load result from x0 to guest register Rt. +// Invalidates any existing allocation for Rt (const/GPR/NEON) and +// writes the result to cpuRegs memory. +static void recStoreLoadResult() +{ + if (_Rt_) + { + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rt_].UD[0]); + } +} + +// ===================================================================================================== +// Load implementations +// ===================================================================================================== + +// Const-paddr MMIO shortcut. When Rs is constant and the resolved page is a +// handler (MMIO), emit a direct BL to the registered handler instead of going +// through fastmem-fault → backpatch thunk → vtlb_memRead → page-table dispatch. +// Mirrors x86 vtlb_DynGenReadNonQuad_Const (ix86-32/recVTLB.cpp). +// +// Direct (RAM-backed) const-paddr loads stay on the fastmem path — a single +// LDR off RFASTMEMBASE is already optimal for those. +// +// Returns true if the shortcut emitted the load; caller should bail out. +static bool recLoadConstPaddrMMIOShortcut(u32 bits, bool sign) +{ + if (!GPR_IS_CONST1(_Rs_)) + return false; + + const u32 addr_const = g_cpuConstRegs[_Rs_].UL[0] + _Imm_; + const auto vmv = vtlbdata.vmap[addr_const >> VTLB_PAGE_BITS]; + if (!vmv.isHandler(addr_const)) + return false; + + const u32 paddr = vmv.assumeHandlerGetPAddr(addr_const); + + iFlushCall(FLUSH_INTERPRETER); + + // INTC_STAT inline-load when the speedhack is disabled. With it on + // (the default), fall through to a direct BL of the registered + // hwRead32_page_0F_INTC_HACK handler. + if (bits == 32 && !EmuConfig.Speedhacks.IntcStat && paddr == INTC_STAT) + { + armLoadPtr(a64::w0, &psHu32(INTC_STAT)); + if (sign) + armAsm->Sxtw(a64::x0, a64::w0); + recStoreLoadResult(); + return true; + } + + int szidx = 0; + switch (bits) + { + case 8: szidx = 0; break; + case 16: szidx = 1; break; + case 32: szidx = 2; break; + case 64: szidx = 3; break; + } + armAsm->Mov(a64::w0, paddr); + // Spill/reload RECCYCLE around the registered handler: the const-paddr + // MMIO shortcut targets the same handler set as vtlbSoftmemRead's slow + // path, including page-0F INTC_STAT → IntCHackCheck which mutates + // cpuRegs.cycle. See vtlbSoftmemRead for full rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall(vmv.assumeHandlerGetRaw(szidx, false)); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + // Extend handler return value into x0 for the 64-bit cpuRegs.GPR store. + // AAPCS64 leaves the upper bits of x0 unspecified for sub-word returns. + if (bits < 64) + { + if (sign) + { + switch (bits) + { + case 8: armAsm->Sxtb(a64::x0, a64::w0); break; + case 16: armAsm->Sxth(a64::x0, a64::w0); break; + case 32: armAsm->Sxtw(a64::x0, a64::w0); break; + } + } + else + { + armAsm->Uxtw(a64::x0, a64::w0); + } + } + + recStoreLoadResult(); + return true; +} + +// Generic load: fastmem (primary) with softmem fallback for faulting PCs. +// +// Fastmem path: no flush — the backpatch thunk in RecStubs.cpp saves/ +// restores live regs around the slow-path C call. +// +// Softmem path: FLUSH_CONSTANT_REGS only. iFlushCall already evicts all +// caller-saved GPRs + NEON unconditionally; constants must additionally +// be written back so post-call emit re-reads from cpuRegs.GPR rather than +// trusting now-stale const tracking. PC and CODE are not load-bearing — +// vtlb_memRead doesn't read them, and exception handlers that fire +// from the slow path stash their own PC. +static void recLoad(u32 bits, bool sign) +{ + // Force an event test on EE counter-range reads (the EE timers at + // 0x10000000..0x10001FFF) to improve read + interrupt syncing — namely + // ESPN Games. Follows the upstream x86-master fix + // (iR5900LoadStore.cpp: needs_flush → iFlushCall(FLUSH_INTERPRETER) + + // g_branch=2). Setting g_branch=2 makes the block finalizer end the block + // with the event-test exit (it does the FLUSH_EVERYTHING + cycle accumulate + + // nextEventCycle dispatch itself), so no explicit iFlushCall is needed here. + bool forceEventTest = false; + if (GPR_IS_CONST1(_Rs_) && bits <= 32) + { + const u32 srcadr = g_cpuConstRegs[_Rs_].UL[0] + _Imm_; + forceEventTest = (srcadr & 0xFFFFE000) == 0x10000000; + } + + if (recLoadConstPaddrMMIOShortcut(bits, sign)) + { + if (forceEventTest) + g_branch = 2; + return; + } + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + if (GPR_IS_CONST1(_Rs_)) + { + iFlushCall(FLUSH_CONSTANT_REGS); + armAsm->Mov(a64::w9, g_cpuConstRegs[_Rs_].UL[0] + _Imm_); + } + else + { + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + } + + if (useFastmem) + { + vtlbFastmemRead(9, 0, bits, sign); + } + else + { + vtlbSoftmemRead(9, bits, sign); + } + recStoreLoadResult(); + + if (forceEventTest) + g_branch = 2; +} + +#ifdef FORCE_INTERP_MEMORY +REC_FUNC(LB); REC_FUNC(LBU); REC_FUNC(LH); REC_FUNC(LHU); +REC_FUNC(LW); REC_FUNC(LWU); REC_FUNC(LD); +#else +void recLB() { recLoad(8, true); } +void recLBU() { recLoad(8, false); } +void recLH() { recLoad(16, true); } +void recLHU() { recLoad(16, false); } +void recLW() { recLoad(32, true); } +void recLWU() { recLoad(32, false); } +void recLD() { recLoad(64, false); } +#endif + +// ===================================================================================================== +// Store implementations +// ===================================================================================================== + +// Symmetric to recLoadConstPaddrMMIOShortcut: when Rs is constant and the +// resolved page is a handler (MMIO), emit a direct BL to the registered write +// handler instead of going through fastmem-fault → backpatch thunk → +// vtlb_memWrite → page-table dispatch. Mirrors x86 vtlb_DynGenWrite_Const +// (ix86-32/recVTLB.cpp). +// +// Direct (RAM-backed) const-paddr stores stay on the fastmem path — a single +// STR off RFASTMEMBASE is already optimal for those. +// +// Returns true if the shortcut emitted the store; caller should bail out. +static bool recStoreConstPaddrMMIOShortcut(u32 bits) +{ + if (!GPR_IS_CONST1(_Rs_)) + return false; + + const u32 addr_const = g_cpuConstRegs[_Rs_].UL[0] + _Imm_; + const auto vmv = vtlbdata.vmap[addr_const >> VTLB_PAGE_BITS]; + if (!vmv.isHandler(addr_const)) + return false; + + const u32 paddr = vmv.assumeHandlerGetPAddr(addr_const); + + iFlushCall(FLUSH_INTERPRETER); + + int szidx = 0; + switch (bits) + { + case 8: szidx = 0; break; + case 16: szidx = 1; break; + case 32: szidx = 2; break; + case 64: szidx = 3; break; + } + + // AAPCS64: w0 = paddr, w1/x1 = value. After FLUSH_INTERPRETER (0xfff) + // all guest reg state is in memory, so armLoadEERegPtr is correct for + // both const and non-const Rt (including Rt == 0). + armAsm->Mov(a64::w0, paddr); + if (bits <= 32) + armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rt_].UL[0]); + else + armLoadEERegPtr(a64::x1, &cpuRegs.GPR.r[_Rt_].UD[0]); + + // RECCYCLE coherence — same rationale as recLoadConstPaddrMMIOShortcut. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall(vmv.assumeHandlerGetRaw(szidx, true)); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + return true; +} + +static void recStore(u32 bits) +{ + if (recStoreConstPaddrMMIOShortcut(bits)) + return; + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + // Flush rationale: see recLoad. FLUSH_CONSTANT_REGS only. + if (GPR_IS_CONST1(_Rt_)) + { + if (bits <= 32) + armAsm->Mov(a64::w10, g_cpuConstRegs[_Rt_].UL[0]); + else + armAsm->Mov(a64::x10, g_cpuConstRegs[_Rt_].UD[0]); + } + else + { + _eeMoveGPRtoR(bits <= 32 ? a64::w10 : a64::x10, _Rt_); + } + + if (GPR_IS_CONST1(_Rs_)) + { + iFlushCall(FLUSH_CONSTANT_REGS); + armAsm->Mov(a64::w9, g_cpuConstRegs[_Rs_].UL[0] + _Imm_); + } + else + { + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + } + + if (useFastmem) + { + vtlbFastmemWrite(9, 10, bits); + } + else + { + vtlbSoftmemWrite(9, 10, bits); + } +} + +#ifdef FORCE_INTERP_MEMORY +REC_FUNC(SB); REC_FUNC(SH); REC_FUNC(SW); REC_FUNC(SD); +#else +void recSB() { recStore(8); } +void recSH() { recStore(16); } +void recSW() { recStore(32); } +void recSD() { recStore(64); } +#endif + +// ===================================================================================================== +// LQ / SQ — 128-bit quad load/store +// addr = (rs + imm) & ~0xF (silently aligned to 16 bytes) +// ===================================================================================================== + +// Inline VTLB 128-bit read. Result in q0. +static void vtlbSoftmemRead128(int addr_wreg) +{ + if (addr_wreg != 9) + armAsm->Mov(a64::w9, armWRegister(addr_wreg)); + + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + // ADDS sets N from bit 63 of ppf; B.mi (=N) branches on the sign bit + // without the separate Tbnz, saving one instruction per softmem op. + armAsm->Adds(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->B(&slow_path, a64::mi); + + // Fast path: LDR q0, [x0] + armAsm->Ldr(a64::q0, a64::MemOperand(a64::x0)); + armAsm->B(&done); + + // Slow path: call vtlb_memRead128(addr) — returns r128 in q0 + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + // See vtlbSoftmemRead for the RECCYCLE coherence rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall((void*)vtlb_memRead128); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&done); +} + +// Inline VTLB 128-bit write. Value in q0. +static void vtlbSoftmemWrite128(int addr_wreg) +{ + if (addr_wreg != 9) + armAsm->Mov(a64::w9, armWRegister(addr_wreg)); + + armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS); + armMoveAddressToReg(RSCRATCHADDR, vtlbdata.vmap); + armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3)); + // ADDS sets N from bit 63 of ppf; B.mi (=N) branches on the sign bit + // without the separate Tbnz, saving one instruction per softmem op. + armAsm->Adds(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW)); + + a64::Label slow_path, done; + armAsm->B(&slow_path, a64::mi); + + // Fast path: STR q0, [x0] + armAsm->Str(a64::q0, a64::MemOperand(a64::x0)); + armAsm->B(&done); + + // Slow path: call vtlb_memWrite128(addr, value) + // addr in w0, value in q0 (ARM64 ABI: 128-bit passed in q0) + armAsm->Bind(&slow_path); + armAsm->Mov(a64::w0, a64::w9); + // See vtlbSoftmemRead for the RECCYCLE coherence rationale. + armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + armEmitCall((void*)vtlb_memWrite128); + armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); + + armAsm->Bind(&done); +} + +void recLQ() +{ + // addr = (rs + imm) & ~0xF — compute from live registers, then flush + recComputeAddr(); + armAsm->And(a64::w9, a64::w9, (u32)~0xF); + iFlushCall(FLUSH_CONSTANT_REGS); + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + if (useFastmem) + vtlbFastmemRead128(9); + else + vtlbSoftmemRead128(9); + + // Store full 128-bit result to GPR[rt] via memory + if (_Rt_) + { + _deleteEEreg(_Rt_, 0); + GPR_DEL_CONST(_Rt_); + armAsm->Str(a64::q0, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UD[0])); + } +} + +void recSQ() +{ + // Flush rationale: see recLoad. FLUSH_CONSTANT_REGS only. + // Rt and Rs are read from memory after the flush. iFlushCall has + // freed all NEON unconditionally (writeback-on-dirty), so any EE + // GPR allocated in NEON is in memory. EE GPRs in arm64gprs[] are + // not written back by FLUSH_CONSTANT_REGS — relies on allocator + // preferring NEON for full-128-bit guest GPRs. + iFlushCall(FLUSH_CONSTANT_REGS); + armLoadEERegPtr(a64::q0, &cpuRegs.GPR.r[_Rt_].UQ); + + armLoadEERegPtr(a64::w9, &cpuRegs.GPR.r[_Rs_].UL[0]); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + armAsm->And(a64::w9, a64::w9, (u32)~0xF); + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + if (useFastmem) + vtlbFastmemWrite128(9); + else + vtlbSoftmemWrite128(9); +} + +// ===================================================================================================== +// LWC1 / SWC1 — FPU 32-bit load/store +// LWC1: fpr[ft] = mem32(rs + imm) +// SWC1: mem32(rs + imm) = fpr[ft] +// ===================================================================================================== + +void recLWC1() +{ + // On the fast path a single inline LDR off RFASTMEMBASE + backpatch, + // no iFlushCall and no vtlb C call. The result lands in w0 (a plain GPR + // rather than an allocated FPR host reg). Softmem stays as the + // faulting-PC fallback. + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + // Compute address into w9 from live registers. + recComputeAddr(); + + if (useFastmem) + { + vtlbFastmemRead(9, 0, 32, false); + } + else + { + iFlushCall(FLUSH_CONSTANT_REGS); + vtlbSoftmemRead(9, 32, false); + } + + // fpr[ft] in memory is about to be overwritten; the allocator's slot + // (if any) is now stale and must not flush back over the write. + _deleteFPtoNEONreg(_Rt_, DELETE_REG_FREE_NO_WRITEBACK); + // Store to fpuRegs.fpr[ft] + armStoreEERegPtr(a64::w0, &fpuRegs.fpr[_Rt_].UL); +} + +void recSWC1() +{ + // fpr[ft] may be live in NEON with MODE_WRITE-only state; flush dirty + // content to memory and drop the slot before reading via armLoad. + _deleteFPtoNEONreg(_Rt_, DELETE_REG_FLUSH_AND_FREE); + + // Load FPU register value into w10 + armLoadEERegPtr(a64::w10, &fpuRegs.fpr[_Rt_].UL); + + // Inline STR off RFASTMEMBASE + backpatch on the fast path, softmem + // fallback otherwise. + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + // Compute address from live registers. + recComputeAddr(); + + if (useFastmem) + { + vtlbFastmemWrite(9, 10, 32); + } + else + { + iFlushCall(FLUSH_CONSTANT_REGS); + vtlbSoftmemWrite(9, 10, 32); + } +} + +// ===================================================================================================== +// Unaligned load/store (LWL/LWR/LDL/LDR/SWL/SWR/SDL/SDR) — inline fastmem +// read-modify-write codegen. Mirrors x86 master's REC_LOADS/REC_STORES paths: +// the inline path needs only FLUSH_CONSTANT_REGS plus fastmem accesses +// (no C call on the fast path), avoiding a full FLUSH_INTERPRETER eviction. +// ===================================================================================================== + +// Inline LWL/LWR codegen. Mirrors x86 recLWL/recLWR (ix86-32/iR5900LoadStore.cpp). +// +// addr = Rs + imm +// shift8 = (addr & 3) * 8 // kept in a callee-saved temp across the read +// aligned = addr & ~3 +// loaded = mem32(aligned) +// +// LWL: Rt = sign_ext_32_to_64( (Rt & (0xffffff >> shift8)) | (loaded << (24 - shift8)) ) +// LWR: if shift8 == 0: Rt = sign_ext_32_to_64(loaded) +// else : Rt[31:0] = (Rt[31:0] & (0xffffff00 << (24 - shift8))) | (loaded >> shift8) +// Rt[63:32] preserved (see interpreter LWL/LWR in R5900OpcodeImpl.cpp) +// +// Uses fastmem when available (the backpatch thunk spills the live temp around +// its slow-path C call); softmem path's slow-path C call obeys AAPCS so the +// callee-saved temp survives there too. +static void recUnalignedWord(bool is_lwl) +{ + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + // Compute Rs+imm in w9. Mirrors recLoad: load Rs first, then flush, then add imm. + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + + // shift8 lives in a callee-saved temp so it survives vtlb's slow-path + // C call (fastmem backpatch thunk OR softmem slow path). + const int shift8 = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + + armAsm->And(armWRegister(shift8), a64::w9, 3); + armAsm->Lsl(armWRegister(shift8), armWRegister(shift8), 3); + armAsm->And(a64::w9, a64::w9, ~3u); + + // 32-bit aligned read; result in w0. + if (useFastmem) + vtlbFastmemRead(9, 0, 32, false); + else + vtlbSoftmemRead(9, 32, false); + + if (!_Rt_) + { + _freeArm64GPR(shift8); + return; + } + + const int rt = _allocArm64GPR(ARM64TYPE_GPR, _Rt_, MODE_READ | MODE_WRITE); + + if (is_lwl) + { + // mask = 0xffffff >> shift8 + armAsm->Mov(RWSCRATCH, 0xffffff); + armAsm->Lsr(RWSCRATCH, RWSCRATCH, armWRegister(shift8)); + armAsm->And(armWRegister(rt), armWRegister(rt), RWSCRATCH); + + // shifted_loaded = loaded << (24 - shift8); reuse RWSCRATCH as shift amount. + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, armWRegister(shift8)); + armAsm->Lsl(a64::w0, a64::w0, RWSCRATCH); + + // Merge and sign-extend the 32-bit result into the 64-bit guest reg. + armAsm->Orr(armWRegister(rt), armWRegister(rt), a64::w0); + armAsm->Sxtw(armXRegister(rt), armWRegister(rt)); + } + else + { + a64::Label nomask, done; + armAsm->Cbz(armWRegister(shift8), &nomask); + + // mask = 0xffffff00 << (24 - shift8); held in RSCRATCHADDR.W() since RWSCRATCH carries the shift amount. + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, armWRegister(shift8)); + armAsm->Mov(RSCRATCHADDR.W(), 0xffffff00u); + armAsm->Lsl(RSCRATCHADDR.W(), RSCRATCHADDR.W(), RWSCRATCH); + armAsm->And(RWSCRATCH, armWRegister(rt), RSCRATCHADDR.W()); + + armAsm->Lsr(a64::w0, a64::w0, armWRegister(shift8)); + armAsm->Orr(a64::w0, a64::w0, RWSCRATCH); + + // Per interp: when shift8 != 0, only Rt[31:0] changes; upper 32 preserved. + armAsm->Bfi(armXRegister(rt), a64::x0, 0, 32); + armAsm->B(&done); + + // shift8 == 0 (aligned): straight sign-extend, full 64-bit overwrite. + armAsm->Bind(&nomask); + armAsm->Sxtw(armXRegister(rt), a64::w0); + + armAsm->Bind(&done); + } + + _freeArm64GPR(shift8); +} + +// Inline SWL/SWR codegen (32-bit unaligned store, read-modify-write). Mirrors +// x86 recSWL/recSWR (ix86-32/iR5900LoadStore.cpp) and interp R5900OpcodeImpl.cpp SWL/SWR. +// +// addr = Rs + imm ; shift8 = (addr & 3) * 8 ; aligned = addr & ~3 +// mem = mem32(aligned) +// SWL: mem32(aligned) = (Rt >> (24 - shift8)) | (mem & (0xffffff00 << shift8)) +// SWR: mem32(aligned) = (Rt << shift8 ) | (mem & (0x00ffffff >> (24 - shift8))) +// +// aligned and shift8 live in callee-saved temps so they survive the read's +// slow-path C call (fastmem backpatch thunk OR softmem slow path). The Rt load +// and merged value never cross a call. The implementation always performs a +// full RMW (no shift8==24 full-overwrite skip): the aligned word is the same +// one written, so reading it is always safe, and the general shifts already +// collapse to "store Rt" at that alignment. +static void recUnalignedStoreWord(bool is_swl) +{ + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + + const int addrTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + const int shiftTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + + armAsm->And(armWRegister(addrTemp), a64::w9, ~3u); // aligned addr + armAsm->And(armWRegister(shiftTemp), a64::w9, 3); + armAsm->Lsl(armWRegister(shiftTemp), armWRegister(shiftTemp), 3); // shift8 + + // 32-bit aligned read; mem -> w0. + armAsm->Mov(a64::w9, armWRegister(addrTemp)); + if (useFastmem) + vtlbFastmemRead(9, 0, 32, false); + else + vtlbSoftmemRead(9, 32, false); + + // Load Rt after the read (never crosses a call). Handles Rt==0 -> 0. + _eeMoveGPRtoR(a64::w1, _Rt_); + + if (is_swl) + { + armAsm->Mov(RWSCRATCH, 0xffffff00u); + armAsm->Lsl(RWSCRATCH, RWSCRATCH, armWRegister(shiftTemp)); // 0xffffff00 << shift8 + armAsm->And(a64::w0, a64::w0, RWSCRATCH); // mem & mask + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, armWRegister(shiftTemp)); // 24 - shift8 + armAsm->Lsr(a64::w1, a64::w1, RWSCRATCH); // Rt >> (24 - shift8) + } + else + { + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, armWRegister(shiftTemp)); // 24 - shift8 + armAsm->Mov(RSCRATCHADDR.W(), 0x00ffffffu); + armAsm->Lsr(RSCRATCHADDR.W(), RSCRATCHADDR.W(), RWSCRATCH); // 0x00ffffff >> (24 - shift8) + armAsm->And(a64::w0, a64::w0, RSCRATCHADDR.W()); // mem & mask + armAsm->Lsl(a64::w1, a64::w1, armWRegister(shiftTemp)); // Rt << shift8 + } + + armAsm->Orr(a64::w0, a64::w0, a64::w1); // merged -> w0 + + armAsm->Mov(a64::w9, armWRegister(addrTemp)); + if (useFastmem) + vtlbFastmemWrite(9, 0, 32); + else + vtlbSoftmemWrite(9, 0, 32); + + _freeArm64GPR(shiftTemp); + _freeArm64GPR(addrTemp); +} + +// Inline LDL/LDR codegen (64-bit unaligned load). Mirrors x86 recLDL/recLDR and +// interp R5900OpcodeImpl.cpp LDL/LDR. +// +// addr = Rs + imm ; s = addr & 7 ; shift8 = s * 8 ; aligned = addr & ~7 +// mem = mem64(aligned) +// LDL: Rt = (Rt & (~0 >> (shift8 + 8))) | (mem << (56 - shift8)) [s==7: Rt = mem] +// LDR: Rt = (Rt & (~0 << (64 - shift8))) | (mem >> shift8) [s==0: Rt = mem] +// +// The degenerate alignment (LDL s==7 / LDR s==0) needs a shift by 64, which the +// AArch64 variable-shift uses mod-64 — so those map to a straight Rt = mem and +// are branched out, exactly like x86's CMOVE/skip. +static void recUnalignedLoadDouble(bool is_ldl) +{ + if (!_Rt_) + return; + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + + // mem must be parked in a callee-saved temp before the Rt alloc: under + // register pressure the alloc can spill a guest reg or land Rt in x0, + // clobbering x0 between the fastmem read and the merge. s = addr & 7 also + // lives in a callee-saved temp. The store path parks all its operands in + // callee-saved temps for the same reason. + const int sTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + const int memTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + armAsm->And(armWRegister(sTemp), a64::w9, 7); + armAsm->And(a64::w9, a64::w9, ~7u); // aligned + + if (useFastmem) + vtlbFastmemRead(9, 0, 64, false); // mem -> x0 + else + vtlbSoftmemRead(9, 64, false); + + armAsm->Mov(armXRegister(memTemp), a64::x0); // park mem (x0 unsafe across Rt alloc) + + const int rt = _allocArm64GPR(ARM64TYPE_GPR, _Rt_, MODE_READ | MODE_WRITE); + + a64::Label special, done; + armAsm->Cmp(armWRegister(sTemp), is_ldl ? 7 : 0); + armAsm->B(&special, a64::eq); + + armAsm->Lsl(RWSCRATCH, armWRegister(sTemp), 3); // x8 = shift8 (<=56) + + if (is_ldl) + { + // value: mem << (56 - shift8) + armAsm->Mov(RSCRATCHADDR, 56); + armAsm->Sub(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // 56 - shift8 + armAsm->Lsl(armXRegister(memTemp), armXRegister(memTemp), RSCRATCHADDR); + // mask: Rt & (~0 >> (shift8 + 8)) + armAsm->Add(RXSCRATCH, RXSCRATCH, 8); // shift8 + 8 + armAsm->Mov(RSCRATCHADDR, UINT64_C(0xFFFFFFFFFFFFFFFF)); + armAsm->Lsr(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); + armAsm->And(armXRegister(rt), armXRegister(rt), RSCRATCHADDR); + } + else + { + // mask amount = 64 - shift8 + armAsm->Mov(RSCRATCHADDR, 64); + armAsm->Sub(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // 64 - shift8 + // value: mem >> shift8 + armAsm->Lsr(armXRegister(memTemp), armXRegister(memTemp), RXSCRATCH); + // mask: Rt & (~0 << (64 - shift8)) + armAsm->Mov(RXSCRATCH, UINT64_C(0xFFFFFFFFFFFFFFFF)); + armAsm->Lsl(RXSCRATCH, RXSCRATCH, RSCRATCHADDR); + armAsm->And(armXRegister(rt), armXRegister(rt), RXSCRATCH); + } + + armAsm->Orr(armXRegister(rt), armXRegister(rt), armXRegister(memTemp)); + armAsm->B(&done); + + armAsm->Bind(&special); + armAsm->Mov(armXRegister(rt), armXRegister(memTemp)); // Rt = mem + + armAsm->Bind(&done); + _freeArm64GPR(memTemp); + _freeArm64GPR(sTemp); +} + +// Inline SDL/SDR codegen (64-bit unaligned store, read-modify-write). Mirrors +// x86 recSDL/recSDR and interp R5900OpcodeImpl.cpp SDL/SDR. +// +// addr = Rs + imm ; s = addr & 7 ; shift8 = s * 8 ; aligned = addr & ~7 +// mem = mem64(aligned) +// SDL: mem64(aligned) = (Rt >> (56 - shift8)) | (mem & (~0 << (shift8 + 8))) [s==7: store Rt] +// SDR: mem64(aligned) = (Rt << shift8 ) | (mem & (~0 >> (64 - shift8))) [s==0: store Rt] +// +// aligned, s and Rt all live in callee-saved temps across the read. The +// degenerate alignment (SDL s==7 / SDR s==0) stores Rt whole and skips the read. +static void recUnalignedStoreDouble(bool is_sdl) +{ + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + + _eeMoveGPRtoR(a64::w9, _Rs_); + iFlushCall(FLUSH_CONSTANT_REGS); + if (_Imm_ != 0) + armAsm->Add(a64::w9, a64::w9, _Imm_); + + const int addrTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + const int sTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + const int valTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + + armAsm->And(armWRegister(addrTemp), a64::w9, ~7u); // aligned + armAsm->And(armWRegister(sTemp), a64::w9, 7); // s + _eeMoveGPRtoR(armXRegister(valTemp), _Rt_); // Rt (64-bit), handles Rt==0 + + a64::Label special, merged; + armAsm->Cmp(armWRegister(sTemp), is_sdl ? 7 : 0); + armAsm->B(&special, a64::eq); + + // General path: read aligned word, merge with Rt. + armAsm->Mov(a64::w9, armWRegister(addrTemp)); + if (useFastmem) + vtlbFastmemRead(9, 0, 64, false); // mem -> x0 + else + vtlbSoftmemRead(9, 64, false); + + armAsm->Lsl(RWSCRATCH, armWRegister(sTemp), 3); // x8 = shift8 + + if (is_sdl) + { + // Rt >> (56 - shift8) + armAsm->Mov(RSCRATCHADDR, 56); + armAsm->Sub(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // 56 - shift8 + armAsm->Lsr(armXRegister(valTemp), armXRegister(valTemp), RSCRATCHADDR); + // mem & (~0 << (shift8 + 8)) + armAsm->Add(RXSCRATCH, RXSCRATCH, 8); // shift8 + 8 + armAsm->Mov(RSCRATCHADDR, UINT64_C(0xFFFFFFFFFFFFFFFF)); + armAsm->Lsl(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); + armAsm->And(a64::x0, a64::x0, RSCRATCHADDR); + } + else + { + // Rt << shift8 + armAsm->Lsl(armXRegister(valTemp), armXRegister(valTemp), RXSCRATCH); + // mem & (~0 >> (64 - shift8)) + armAsm->Mov(RSCRATCHADDR, 64); + armAsm->Sub(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH); // 64 - shift8 + armAsm->Mov(RXSCRATCH, UINT64_C(0xFFFFFFFFFFFFFFFF)); + armAsm->Lsr(RXSCRATCH, RXSCRATCH, RSCRATCHADDR); + armAsm->And(a64::x0, a64::x0, RXSCRATCH); + } + + armAsm->Orr(a64::x0, a64::x0, armXRegister(valTemp)); // merged -> x0 + armAsm->B(&merged); + + armAsm->Bind(&special); + armAsm->Mov(a64::x0, armXRegister(valTemp)); // store Rt whole + + armAsm->Bind(&merged); + armAsm->Mov(a64::w9, armWRegister(addrTemp)); + if (useFastmem) + vtlbFastmemWrite(9, 0, 64); + else + vtlbSoftmemWrite(9, 0, 64); + + _freeArm64GPR(valTemp); + _freeArm64GPR(sTemp); + _freeArm64GPR(addrTemp); +} + +void recLWL() { recUnalignedWord(true); } +void recLWR() { recUnalignedWord(false); } +void recLDL() { recUnalignedLoadDouble(true); } +void recLDR() { recUnalignedLoadDouble(false); } +void recSWL() { recUnalignedStoreWord(true); } +void recSWR() { recUnalignedStoreWord(false); } +void recSDL() { recUnalignedStoreDouble(true); } +void recSDR() { recUnalignedStoreDouble(false); } +// ===================================================================================================== +// LQC2 / SQC2 — 128-bit COP2 (VU0) register load/store +// LQC2: VU0.VF[ft] = mem128((rs + imm) & ~0xF) +// SQC2: mem128((rs + imm) & ~0xF) = VU0.VF[ft] +// Same as LQ/SQ but target is VU0.VF[ft] instead of cpuRegs.GPR[rt]. +// Requires vu0Sync() before access. +// ===================================================================================================== + +void recLQC2() +{ + // Sync VU0 before COP2 register access. Gated on EEINST analysis — + // no emit at all when the analysis says no sync is needed (the common + // case for quad-load-heavy code like vertex streaming). Mirrors x86 + // recLQC2 (mVUSyncVU0 / mVUFinishVU0 gating on EEINST_COP2_SYNC_VU0 / + // EEINST_COP2_FINISH_VU0). The helper handles iFlushCall, RECCYCLE + // save/reload, runtime VPU_STAT check, and cycle accounting. + cop2EmitConditionalSync(false, _vu0FinishMicro); + + // addr = (rs + imm) & ~0xF + recComputeAddr(); + armAsm->And(a64::w9, a64::w9, (u32)~0xF); + + // Match recLQ/recSQ: spill constant tracking before the softmem C call + // can fire (vtlb_memRead128 may dispatch through an MMIO handler that + // reads cpuRegs). Redundant when cop2EmitConditionalSync already + // emitted FLUSH_INTERPRETER, harmless otherwise. + iFlushCall(FLUSH_CONSTANT_REGS); + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + if (useFastmem) + vtlbFastmemRead128(9); + else + vtlbSoftmemRead128(9); + + // Store 128-bit result to VU0.VF[rt] (COP2 ft field = rt field) + if (_Rt_) + { + armMoveAddressToReg(RSCRATCHADDR, &VU0.VF[_Rt_].UQ); + armAsm->Str(a64::q0, a64::MemOperand(RSCRATCHADDR)); + } +} + +void recSQC2() +{ + // EEINST-gated VU0 sync — see recLQC2 above. + cop2EmitConditionalSync(false, _vu0FinishMicro); + + // addr = (rs + imm) & ~0xF — allocator-aware (rs may still be live + // in a host reg if the sync above emitted nothing). + recComputeAddr(); + armAsm->And(a64::w9, a64::w9, (u32)~0xF); + + // Flush before the q0 load. iFlushCall unconditionally evicts all NEON + // (iR5900-arm64.cpp:765-769) and writes back any dirty cache; after + // this point q0 is allocator-detached and safe to touch directly. + iFlushCall(FLUSH_CONSTANT_REGS); + + // Load 128-bit VU0.VF[rt] into q0 (COP2 ft field = rt field). MUST be + // after iFlushCall: q0 may otherwise be tracked by the EE NEON allocator + // as caching a live FPREG, in which case this load stomps q0's runtime + // contents while leaving arm64neon[0] still marked dirty. The next + // allocator flush would then write back the stomped value (VU0.VF[rt]) + // to the FPREG's memory slot, corrupting the FPREG. + armLoadPtr(a64::q0, &VU0.VF[_Rt_].UQ); + + const bool useFastmem = CHECK_FASTMEM && !vtlb_IsFaultingPC(pc); + if (useFastmem) + vtlbFastmemWrite128(9); + else + vtlbSoftmemWrite128(9); +} + +} // namespace OpcodeImpl +} // namespace Dynarec +} // namespace R5900 diff --git a/pcsx2/x86/iR5900Analysis.cpp b/pcsx2/x86/iR5900Analysis.cpp index ea33fa90bb..a17322a3ba 100644 --- a/pcsx2/x86/iR5900Analysis.cpp +++ b/pcsx2/x86/iR5900Analysis.cpp @@ -1,7 +1,11 @@ // SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ +#ifdef ARCH_ARM64 +#include "arm64/iR5900Analysis.h" +#else #include "iR5900Analysis.h" +#endif #include "Memory.h" #include "DebugTools/Debug.h" From 459671214e603a16cdecc13fe9f3f514de6c3525 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 005/292] arm64: IOP (R3000A) recompiler ARM64 IOP dynarec and opcode tables, plus the s32 cycle-delta cast fix (host FCVTZS saturation differs from x86 CVTTSS2SI on out-of-range inputs). Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/IopBios.cpp | 2 +- pcsx2/R3000A.cpp | 9 +- pcsx2/arm64/iR3000A-arm64.cpp | 1440 ++++++++++++++++++++++++++ pcsx2/arm64/iR3000A-arm64.h | 156 +++ pcsx2/arm64/iR3000Atables-arm64.cpp | 1456 +++++++++++++++++++++++++++ 5 files changed, 3061 insertions(+), 2 deletions(-) create mode 100644 pcsx2/arm64/iR3000A-arm64.cpp create mode 100644 pcsx2/arm64/iR3000A-arm64.h create mode 100644 pcsx2/arm64/iR3000Atables-arm64.cpp diff --git a/pcsx2/IopBios.cpp b/pcsx2/IopBios.cpp index 4ed09d970f..14bd75ee3e 100644 --- a/pcsx2/IopBios.cpp +++ b/pcsx2/IopBios.cpp @@ -854,7 +854,7 @@ namespace R3000A v0 = file->read(buf.get(), count); [[likely]] - if (v0 >= 0 && iopMemSafeWriteBytes(data, buf.get(), v0)) + if (static_cast(v0) >= 0 && iopMemSafeWriteBytes(data, buf.get(), v0)) { psxCpu->Clear(data, (v0 + 3) / 4); } diff --git a/pcsx2/R3000A.cpp b/pcsx2/R3000A.cpp index 331259ac0d..dc6191384f 100644 --- a/pcsx2/R3000A.cpp +++ b/pcsx2/R3000A.cpp @@ -137,7 +137,14 @@ __fi void PSX_INT( IopEventId n, s32 ecycle ) psxSetNextBranchDelta(ecycle); const float mutiplier = static_cast(PS2CLK) / static_cast(PSXCLK); - const s32 iopDelta = (psxRegs.iopNextEventCycle - psxRegs.cycle) * mutiplier; + // Cast the u32 cycle delta to s32 *before* the float multiply. When `cycle` + // briefly leads `iopNextEventCycle`, the u32 subtraction underflows to ~4e9; + // multiplied by ~2.97f the float result is ~1.2e10, out of int range. The + // cast to int is host-defined: x86 CVTTSS2SI returns INT_MIN, ARM64 FCVTZS + // saturates to INT_MAX. Casting first lets the multiply happen in signed + // arithmetic where the small-negative case rounds correctly on both hosts. + const s32 iopCyclesUntilEvent = static_cast(psxRegs.iopNextEventCycle - psxRegs.cycle); + const s32 iopDelta = static_cast(iopCyclesUntilEvent * mutiplier); if (psxRegs.iopCycleEE < iopDelta) { diff --git a/pcsx2/arm64/iR3000A-arm64.cpp b/pcsx2/arm64/iR3000A-arm64.cpp new file mode 100644 index 0000000000..82fff3637b --- /dev/null +++ b/pcsx2/arm64/iR3000A-arm64.cpp @@ -0,0 +1,1440 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "arm64/iR3000A-arm64.h" +#include "arm64/AsmHelpers.h" +#include "Host.h" +#include "R3000A.h" +#include "arm64/BaseblockEx-arm64.h" +#include "R5900OpcodeTables.h" +#include "IopBios.h" +#include "IopHw.h" +#include "DebugTools/SymbolGuardian.h" +#include "Common.h" +#include "VMManager.h" +#include "Config.h" + +#include "common/Assertions.h" +#include "common/AlignedMalloc.h" +#include "common/Console.h" +#include "common/FastJmp.h" +#include "common/HeapArray.h" +#include "common/Perf.h" + +namespace a64 = vixl::aarch64; + +using namespace R3000A; + +extern void psxBREAK(); + +u32 g_psxMaxRecMem = 0; + +uptr psxRecLUT[0x10000]; +u32 psxhwLUT[0x10000]; + +static __fi u32 HWADDR(u32 mem) { return psxhwLUT[mem >> 16] + mem; } + +static BASEBLOCK* recRAM = nullptr; +static BASEBLOCK* recROM = nullptr; +static BASEBLOCK* recROM1 = nullptr; +static BASEBLOCK* recROM2 = nullptr; +static Arm64BaseBlocks recBlocks; +static u8* recPtr = nullptr; +static u8* recPtrEnd = nullptr; +u32 psxpc; +int psxbranch; +u32 g_iopCyclePenalty; + +static EEINST* s_pInstCache = nullptr; +static u32 s_nInstCacheSize = 0; + +static BASEBLOCK* s_pCurBlock = nullptr; +static BASEBLOCKEX* s_pCurBlockEx = nullptr; + +static u32 s_nEndBlock = 0; +static u32 s_branchTo; +static bool s_nBlockFF; + +static u32 s_saveConstRegs[32]; +static u32 s_saveHasConstReg = 0, s_saveFlushedConstReg = 0; +static EEINST* s_psaveInstInfo = nullptr; + +u32 s_psxBlockCycles = 0; +static u32 s_savenBlockCycles = 0; +static bool s_recompilingDelaySlot = false; + +static void iPsxBranchTest(u32 newpc, u32 cpuBranch); +void psxRecompileNextInstruction(bool delayslot, bool swapped_delayslot); + +extern void (*rpsxBSC[64])(); +void rpsxpropBSC(EEINST* prev, EEINST* pinst); + +static void iopClearRecLUT(BASEBLOCK* base, int count); +static void iopRecError(int err); + +#define PSX_GETBLOCK(x) PC_GETBLOCK_(x, psxRecLUT) + +#define PSXREC_CLEARM(mem) \ + (((mem) < g_psxMaxRecMem && (psxRecLUT[(mem) >> 16] + (mem))) ? \ + psxRecClearMem(mem) : \ + 4) + +// LUT page management — same as x86 (architecture-independent) +static DynamicHeapArray recLutReserve; +static DynamicHeapArray recLutUnmapped; +static size_t recLutEntries = 0; +static bool extraRam = false; + +// Constant pool for the IOP recompiler +static ArmConstantPool s_iopConstantPool; + +// ===================================================================================================== +// Dynamically Compiled Dispatchers - R3000A ARM64 +// ===================================================================================================== + +static void iopRecRecompile(u32 startpc); + +static const void* iopDispatcherEvent = nullptr; +static const void* iopDispatcherReg = nullptr; +static const void* iopJITCompile = nullptr; +static const void* iopEnterRecompiledCode = nullptr; +static const void* iopExitRecompiledCode = nullptr; +static const void* iopUnmappedRecLUTPage = nullptr; +static void recEventTest() +{ + _cpuEventTest_Shared(); +} + +// ARM64 dispatcher: Load PC → two-level LUT lookup → jump to block +// +// psxRecLUT[pc >> 16] gives a base pointer to the BASEBLOCK array for that 64K page. +// Each BASEBLOCK is 8 bytes (one uptr function pointer). +// Index within page: (pc & 0xFFFF) >> 2 (since instructions are 4-byte aligned). +// So: base + ((pc & 0xFFFF) >> 2) * 8 = base + (pc & 0xFFFF) * 2 +// +// ARM64 codegen: +// ldr w0, [addr of psxRegs.pc] // load PC +// lsr w1, w0, #16 // page index +// adr x2, psxRecLUT +// ldr x2, [x2, x1, lsl #3] // page base = psxRecLUT[pc >> 16] +// and w0, w0, #0xFFFF // low 16 bits +// add x2, x2, w0, uxtw #1 // base + (pc & 0xFFFF) * 2 +// ldr x2, [x2] // block fnptr +// br x2 // jump to compiled code + +static const void* _DynGen_DispatcherReg() +{ + u8* retval = armGetCurrentCodePointer(); + + // Load psxRegs.pc + armAsm->Ldr(a64::w0, armPsxRegMem(&psxRegs.pc)); + + // Two-level LUT lookup + armAsm->Lsr(a64::w1, a64::w0, 16); + armMoveAddressToReg(RSCRATCHADDR, psxRecLUT); + armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR, a64::x1, a64::LSL, 3)); + + // Index with full PC: base + pc * sizeof(BASEBLOCK)/4 = base + pc * 2 + // recLUT_SetPage adjusts base to account for upper bits, so use full pc. + armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, a64::Operand(a64::x0, a64::LSL, 1)); + + // Load block function pointer and jump + armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR)); + armAsm->Br(RSCRATCHADDR); + + return retval; +} + +// Called when a block hasn't been compiled yet — compile it, then dispatch +static const void* _DynGen_JITCompile() +{ + u8* retval = armGetCurrentCodePointer(); + + // Call iopRecRecompile(psxRegs.pc) + armAsm->Ldr(RWARG1, armPsxRegMem(&psxRegs.pc)); + armEmitCall((void*)iopRecRecompile); + + // Now dispatch to the newly compiled block + armEmitJmp(iopDispatcherReg); + + return retval; +} + +// Entry point called from C code — sets up stack frame, enters dispatcher loop +static const void* _DynGen_EnterRecompiledCode() +{ + u8* retval = armGetCurrentCodePointer(); + + // Save callee-saved registers and set up stack frame + armBeginStackFrame(false); + + // Pin &psxRegs into RPSXSTATE for the duration of IOP JIT execution. Like + // EE's RSTATE, this turns "armMoveAddressToReg(scratch, &psxRegs.X); ldr" + // into a single ldr [RPSXSTATE, #off]. Callee-saved across all C calls. + armMoveAddressToReg(RPSXSTATE, &psxRegs); + + // Jump into the dispatcher + armEmitJmp(iopDispatcherReg); + + // Exit point — restore callee-saved registers and return + iopExitRecompiledCode = armGetCurrentCodePointer(); + armEndStackFrame(false); + armAsm->Ret(); + + return retval; +} + +// Error handler for jumps to unmapped memory +static const void* _DynGen_UnmappedRecLUTPage() +{ + u8* retval = armGetCurrentCodePointer(); + + armAsm->Mov(RWARG1, 0); + armEmitCall((void*)iopRecError); + armEmitJmp(iopExitRecompiledCode); + + return retval; +} + +// Generate all dispatcher stubs during reset +static void _DynGen_Dispatchers() +{ + const u8* start = armGetCurrentCodePointer(); + + // Event test: call recEventTest, then fall through to dispatcher + iopDispatcherEvent = armGetCurrentCodePointer(); + armEmitCall((void*)recEventTest); + + iopDispatcherReg = _DynGen_DispatcherReg(); + iopJITCompile = _DynGen_JITCompile(); + iopEnterRecompiledCode = _DynGen_EnterRecompiledCode(); + iopUnmappedRecLUTPage = _DynGen_UnmappedRecLUTPage(); + + // Block linker needs iopJITCompile so it can route stale / not-yet- + // compiled link sites through the dispatcher path. Mirrors EE rec + // at iR5900-arm64.cpp:674 and x86 IOP rec at iR3000A.cpp:257. + recBlocks.SetJITCompile(iopJITCompile); + + Perf::any.Register(start, static_cast(armGetCurrentCodePointer() - start), "IOP Dispatcher"); +} + +static void iopRecError(int err) +{ + switch (err) + { + case 0: + Host::ReportErrorAsync("R3000A Exception", + fmt::format("Unrecognized opcode (PC: 0x{:08x})", psxRegs.pc)); + break; + + case 1: + Host::ReportErrorAsync("R3000A Exception", + fmt::format("Jump to unaligned address (PC: 0x{:08x})", psxRegs.pc)); + break; + } + + VMManager::SetPaused(true); + Cpu->ExitExecution(); +} + +// ===================================================================================================== +// Register allocation and code generation helpers +// ===================================================================================================== + +void _psxFlushConstReg(int reg) +{ + if (PSX_IS_CONST1(reg) && !(g_psxFlushedConstReg & (1 << reg))) + { + armAsm->Mov(RWSCRATCH, g_psxConstRegs[reg]); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.r[reg])); + g_psxFlushedConstReg |= (1 << reg); + } +} + +void _psxFlushConstRegs() +{ + for (int i = 1; i < 32; ++i) + { + if (g_psxHasConstReg & (1 << i)) + { + if (!(g_psxFlushedConstReg & (1 << i))) + { + armAsm->Mov(RWSCRATCH, g_psxConstRegs[i]); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.r[i])); + g_psxFlushedConstReg |= 1 << i; + } + + if (g_psxHasConstReg == g_psxFlushedConstReg) + break; + } + } +} + +void _psxDeleteReg(int reg, int flush) +{ + if (!reg) + return; + if (flush && PSX_IS_CONST1(reg)) + _psxFlushConstReg(reg); + + PSX_DEL_CONST(reg); + _deletePSXtoArm64GPR(reg, flush ? DELETE_REG_FREE : DELETE_REG_FREE_NO_WRITEBACK); +} + +void _psxMoveGPRtoR(const a64::Register& to, int fromgpr) +{ + if (PSX_IS_CONST1(fromgpr)) + { + armAsm->Mov(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), g_psxConstRegs[fromgpr]); + } + else + { + const int reg = EEINST_USEDTEST(fromgpr) + ? _allocArm64GPR(ARM64TYPE_PSX, fromgpr, MODE_READ) + : _checkArm64GPR(ARM64TYPE_PSX, fromgpr, MODE_READ); + if (reg >= 0) + armAsm->Mov(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), armWRegister(reg)); + else + armLoadPsxRegPtr(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), &psxRegs.GPR.r[fromgpr]); + } +} + +void _psxFlushCall(int flushtype) +{ + // Free caller-saved registers (and optionally others per flushtype) + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (!arm64gprs[i].inuse) + continue; + + if (!armIsCalleeSavedRegister(i) || + ((flushtype & FLUSH_FREE_NONTEMP_X86) && arm64gprs[i].type != ARM64TYPE_TEMP) || + ((flushtype & FLUSH_FREE_TEMP_X86) && arm64gprs[i].type == ARM64TYPE_TEMP)) + { + _freeArm64GPR(i); + } + } + + if (flushtype & FLUSH_ALL_X86) + _flushArm64GPRregs(); + + if (flushtype & FLUSH_CONSTANT_REGS) + _psxFlushConstRegs(); + + if (flushtype & FLUSH_PC) + { + armAsm->Mov(RWSCRATCH, psxpc); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + } +} + +void _psxFlushAllDirty() +{ + for (u32 i = 0; i < 32; ++i) + { + if (PSX_IS_CONST1(i)) + _psxFlushConstReg(i); + } + + _flushArm64GPRregs(); +} + +void _psxOnWriteReg(int reg) +{ + PSX_DEL_CONST(reg); +} + +void psxSaveBranchState() +{ + s_savenBlockCycles = s_psxBlockCycles; + memcpy(s_saveConstRegs, g_psxConstRegs, sizeof(g_psxConstRegs)); + s_saveHasConstReg = g_psxHasConstReg; + s_saveFlushedConstReg = g_psxFlushedConstReg; + s_psaveInstInfo = g_pCurInstInfo; + memcpy(s_saveArm64GPRregs, arm64gprs, sizeof(arm64gprs)); +} + +void psxLoadBranchState() +{ + s_psxBlockCycles = s_savenBlockCycles; + memcpy(g_psxConstRegs, s_saveConstRegs, sizeof(g_psxConstRegs)); + g_psxHasConstReg = s_saveHasConstReg; + g_psxFlushedConstReg = s_saveFlushedConstReg; + g_pCurInstInfo = s_psaveInstInfo; + memcpy(arm64gprs, s_saveArm64GPRregs, sizeof(arm64gprs)); +} + +// ===================================================================================================== +// Constant Propagation Code Templates +// ===================================================================================================== + +// rd = rs op rt — dispatch based on which operands are constant +// rd = rs op rt — matching x86 ordering: const check before _addNeeded +void psxRecompileCodeConst0(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int xmminfo) +{ + if (!_Rd_) + return; + + if (PSX_IS_CONST2(_Rs_, _Rt_)) + { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + constcode(); + return; + } + + int info = 0; + if (PSX_IS_CONST1(_Rs_)) + info |= PROCESS_CONSTS; + if (PSX_IS_CONST1(_Rt_)) + info |= PROCESS_CONSTT; + + _addNeededPSXtoArm64GPR(_Rs_); + _addNeededPSXtoArm64GPR(_Rt_); + _addNeededPSXtoArm64GPR(_Rd_); + + if (xmminfo & XMMINFO_READS) + { + if (!(info & PROCESS_CONSTS)) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_S(reg); + } + } + if (xmminfo & XMMINFO_READT) + { + if (!(info & PROCESS_CONSTT)) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_T(reg); + } + } + if (xmminfo & XMMINFO_WRITED) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + if (reg >= 0) + info |= PROCESS_EE_SET_D(reg); + } + + PSX_DEL_CONST(_Rd_); + + if (info & PROCESS_CONSTS) + constscode(info); + else if (info & PROCESS_CONSTT) + consttcode(info); + else + noconstcode(info); + + _clearNeededArm64GPRregs(); +} + +// rt = rs op imm16 +// Matches x86 pattern: const check before _addNeeded, PSX_DEL_CONST before noconstcode +void psxRecompileCodeConst1(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo) +{ + if (!_Rt_) + return; + + // x86 checks const BEFORE _addNeeded + if (PSX_IS_CONST1(_Rs_)) + { + _psxDeleteReg(_Rt_, 0); + PSX_SET_CONST(_Rt_); + constcode(); + return; + } + + _addNeededPSXtoArm64GPR(_Rs_); + _addNeededPSXtoArm64GPR(_Rt_); + + int info = 0; + + if (xmminfo & XMMINFO_READS) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_S(reg); + } + if (xmminfo & XMMINFO_WRITET) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE); + if (reg >= 0) + info |= PROCESS_EE_SET_T(reg); + } + + // x86 deletes const BEFORE noconstcode + PSX_DEL_CONST(_Rt_); + noconstcode(info); + + _clearNeededArm64GPRregs(); +} + +// rd = rt op sa +void psxRecompileCodeConst2(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo) +{ + if (!_Rd_) + return; + + _addNeededPSXtoArm64GPR(_Rt_); + _addNeededPSXtoArm64GPR(_Rd_); + + int info = 0; + + if (PSX_IS_CONST1(_Rt_)) + { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + constcode(); + _clearNeededArm64GPRregs(); + return; + } + + if (xmminfo & XMMINFO_READT) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_T(reg); + } + if (xmminfo & XMMINFO_WRITED) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + if (reg >= 0) + info |= PROCESS_EE_SET_D(reg); + } + + noconstcode(info); + + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +// [lo,hi] = rt op rs +void psxRecompileCodeConst3(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int LOHI) +{ + _addNeededPSXtoArm64GPR(_Rs_); + _addNeededPSXtoArm64GPR(_Rt_); + + int info = 0; + + if (PSX_IS_CONST2(_Rs_, _Rt_)) + { + constcode(); + _clearNeededArm64GPRregs(); + return; + } + + if (PSX_IS_CONST1(_Rs_)) + info |= PROCESS_CONSTS; + if (PSX_IS_CONST1(_Rt_)) + info |= PROCESS_CONSTT; + + if (!(info & PROCESS_CONSTS)) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_S(reg); + } + if (!(info & PROCESS_CONSTT)) + { + int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (reg >= 0) + info |= PROCESS_EE_SET_T(reg); + } + + if (info & PROCESS_CONSTS) + constscode(info); + else if (info & PROCESS_CONSTT) + consttcode(info); + else + noconstcode(info); + + _clearNeededArm64GPRregs(); +} + +// ===================================================================================================== +// Branch handling +// ===================================================================================================== + +void psxSetBranchReg() +{ + psxbranch = 1; + + // Flush all register allocations first, then load branch target from pcWriteback. + // This matches the EE SetBranchReg pattern — ensures delay slot results are + // written back before the branch target overwrites w0. + _psxFlushCall(FLUSH_EVERYTHING); + + // Load branch target from pcWriteback + armLoadPsxRegPtr(a64::w0, &psxRegs.pcWriteback); + + // Store to psxRegs.pc + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.pc)); + + // Check alignment + a64::Label unaligned; + armAsm->Tst(a64::w0, 3); + armAsm->B(&unaligned, a64::ne); + + iPsxBranchTest(0xffffffff, 1); + + armEmitJmp(iopDispatcherReg); + + armAsm->Bind(&unaligned); + armAsm->Mov(RWARG1, 1); + armEmitCall((void*)iopRecError); + armEmitJmp(iopExitRecompiledCode); +} + +void psxSetBranchImm(u32 imm) +{ + psxbranch = 1; + pxAssert(imm); + + armAsm->Mov(RWSCRATCH, imm); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + _psxFlushCall(FLUSH_EVERYTHING); + iPsxBranchTest(imm, imm <= psxpc); + + // Block linking: emit a single B as the patch site. Initially routed + // through iopJITCompile via recBlocks.Link(); once the target block is + // compiled, recBlocks.New() rewrites this B's imm26 to branch to the + // target's fnptr directly, bypassing the dispatcher. Mirrors EE rec + // at iR5900-arm64.cpp:1064. + { + a64::SingleEmissionCheckScope guard(armAsm); + u8* patch_site = armGetCurrentCodePointer(); + armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite + recBlocks.Link(HWADDR(imm), patch_site); + } +} + +static __fi u32 psxScaleBlockCycles() +{ + return s_psxBlockCycles; +} + +static void iPsxAddEECycles(u32 blockCycles) +{ + // Subtract cycles * 8 from iopCycleEE + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE)); + + if (blockCycles != 0xFFFFFFFF) + { + if (blockCycles * 8 < 4096) + armAsm->Sub(RWSCRATCH, RWSCRATCH, blockCycles * 8); + else + { + armAsm->Mov(a64::w1, blockCycles * 8); + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); + } + } + else + { + // blockCycles in w0 (from wait loop optimization) + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w0); + } + + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE)); +} + +static void iPsxBranchTest(u32 newpc, u32 cpuBranch) +{ + u32 blockCycles = psxScaleBlockCycles(); + + if (EmuConfig.Speedhacks.WaitLoop && s_nBlockFF && newpc == s_branchTo) + { + // WaitLoop fast-forward: tight busy-wait loop detected. + // Advance cycle counter to consume the remaining IOP timeslice, + // clamped to iopNextEventCycle. Matches x86 iR3000A.cpp:1179. + // new_cycle = old_cycle + (iopCycleEE + 7) / 8 + // new_cycle = min(new_cycle, iopNextEventCycle) + armAsm->Ldr(a64::x2, armPsxRegMem(&psxRegs.cycle)); // x2 = old cycle + armAsm->Mov(a64::x4, a64::x2); // x4 = old cycle (saved) + + // iopCycleEE is the ONLY signed quantity in this block — it can go + // negative, so the timeslice divide uses Asr (arithmetic shift). The + // cycle/iopNextEventCycle clamp below is unsigned (Csel hi); don't + // swap predicates between the two. + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE)); // w8 = iopCycleEE (s32) + armAsm->Add(RWSCRATCH, RWSCRATCH, 7); + armAsm->Asr(RWSCRATCH, RWSCRATCH, 3); // w8 = (iopCycleEE + 7) >> 3 (signed) + armAsm->Add(a64::x2, a64::x2, a64::x8); // x2 = cycle + advance + + // Clamp to iopNextEventCycle + armAsm->Ldr(a64::x3, armPsxRegMem(&psxRegs.iopNextEventCycle)); // x3 = iopNextEventCycle + armAsm->Cmp(a64::x2, a64::x3); + // Both cycle counters are u32; use unsigned predicate so the clamp + // stays correct after either operand crosses 2^31. x86 CMOVA / JL + // (the upstream reference) are likewise unsigned. + armAsm->Csel(a64::x2, a64::x3, a64::x2, a64::hi); // x2 = min(x2, x3) unsigned + + // Store new cycle + armAsm->Str(a64::x2, armPsxRegMem(&psxRegs.cycle)); + + // consumed = (new_cycle - old_cycle) << 3, in w0 for iPsxAddEECycles + armAsm->Sub(a64::x0, a64::x2, a64::x4); + armAsm->Lsl(a64::w0, a64::w0, 3); + + // Subtract consumed cycles from iopCycleEE + iPsxAddEECycles(0xFFFFFFFF); // uses w0 as the cycle count + + armAsm->Cmp(RWSCRATCH, 0); + armEmitCondBranch(a64::le, iopExitRecompiledCode); + + // Call event test + armEmitCall((void*)iopEventTest); + + if (newpc != 0xffffffff) + { + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + armAsm->Cmp(RWSCRATCH, newpc); + armEmitCondBranch(a64::ne, iopDispatcherReg); + } + } + else + { + // Normal path: add block cycles and check events + armAsm->Ldr(a64::x2, armPsxRegMem(&psxRegs.cycle)); + if (blockCycles < 4096) + armAsm->Add(a64::x2, a64::x2, blockCycles); + else + { + armAsm->Mov(a64::x3, static_cast(blockCycles)); + armAsm->Add(a64::x2, a64::x2, a64::x3); + } + armAsm->Str(a64::x2, armPsxRegMem(&psxRegs.cycle)); + + // Subtract from iopCycleEE — exit if <= 0 + iPsxAddEECycles(blockCycles); + armAsm->Cmp(RWSCRATCH, 0); + armEmitCondBranch(a64::le, iopExitRecompiledCode); + + // Check if event is pending: cycle >= iopNextEventCycle + armAsm->Ldr(a64::x3, armPsxRegMem(&psxRegs.iopNextEventCycle)); + armAsm->Cmp(a64::x2, a64::x3); + a64::Label noEvent; + // Unsigned predicate — see clamp note above. Both operands are u32; + // signed lt would mis-fire after either crosses 2^31. + armAsm->B(&noEvent, a64::lo); + + // Event pending — call event test + armEmitCall((void*)iopEventTest); + + if (newpc != 0xffffffff) + { + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + armAsm->Cmp(RWSCRATCH, newpc); + armEmitCondBranch(a64::ne, iopDispatcherReg); + } + + armAsm->Bind(&noEvent); + } +} + +// ===================================================================================================== +// Delay slot swap optimization +// ===================================================================================================== + +// Hoist the branch's delay-slot instruction ahead of the branch test when it +// provably doesn't read/write any of the branch's source/dest registers. +// Skips the psxSaveBranchState/recompile/psxLoadBranchState/recompile-again +// ceremony that the branch handler otherwise pays. Mirrors x86 iR3000A.cpp:439 +// opcode-for-opcode. +bool psxTrySwapDelaySlot(u32 rs, u32 rt, u32 rd) +{ + if (s_recompilingDelaySlot) + return false; + + const u32 opcode_encoded = iopMemRead32(psxpc); + if (opcode_encoded == 0) + { + psxRecompileNextInstruction(true, true); + return true; + } + + const u32 opcode_rs = ((opcode_encoded >> 21) & 0x1F); + const u32 opcode_rt = ((opcode_encoded >> 16) & 0x1F); + const u32 opcode_rd = ((opcode_encoded >> 11) & 0x1F); + + switch (opcode_encoded >> 26) + { + case 8: // ADDI + case 9: // ADDIU + case 10: // SLTI + case 11: // SLTIU + case 12: // ANDI + case 13: // ORI + case 14: // XORI + case 15: // LUI + case 32: // LB + case 33: // LH + case 34: // LWL + case 35: // LW + case 36: // LBU + case 37: // LHU + case 38: // LWR + case 39: // LWU + case 40: // SB + case 41: // SH + case 42: // SWL + case 43: // SW + case 46: // SWR + { + if ((rs != 0 && rs == opcode_rt) || (rt != 0 && rt == opcode_rt) || (rd != 0 && (rd == opcode_rs || rd == opcode_rt))) + return false; + } + break; + + case 50: // LWC2 + case 58: // SWC2 + break; + + case 0: // SPECIAL + { + switch (opcode_encoded & 0x3F) + { + case 0: // SLL + case 2: // SRL + case 3: // SRA + case 4: // SLLV + case 6: // SRLV + case 7: // SRAV + case 32: // ADD + case 33: // ADDU + case 34: // SUB + case 35: // SUBU + case 36: // AND + case 37: // OR + case 38: // XOR + case 39: // NOR + case 42: // SLT + case 43: // SLTU + { + if ((rs != 0 && rs == opcode_rd) || (rt != 0 && rt == opcode_rd) || (rd != 0 && (rd == opcode_rs || rd == opcode_rt))) + return false; + } + break; + + case 15: // SYNC + case 24: // MULT + case 25: // MULTU + case 26: // DIV + case 27: // DIVU + break; + + default: + return false; + } + } + break; + + case 16: // COP0 + case 17: // COP1 + case 18: // COP2 + case 19: // COP3 + { + switch ((opcode_encoded >> 21) & 0x1F) + { + case 0: // MFC + case 2: // CFC + { + if ((rs != 0 && rs == opcode_rt) || (rt != 0 && rt == opcode_rt) || (rd != 0 && rd == opcode_rt)) + return false; + } + break; + + case 4: // MTC + case 6: // CTC + break; + + default: + // GTE (COP2) ops are safe. + if ((opcode_encoded >> 26) != 18) + return false; + break; + } + } + break; + + default: + return false; + } + + psxRecompileNextInstruction(true, true); + return true; +} + +// ===================================================================================================== +// Instruction recompilation +// ===================================================================================================== + +// delayslot = true when called from a branch handler to compile its delay +// slot. swapped_delayslot = true when called via psxTrySwapDelaySlot — +// the delay slot is being hoisted ahead of the branch test, so we must +// snapshot psxRegs.code + g_pCurInstInfo so the enclosing branch handler's +// codegen continues against its own instruction, and suppress the trailing +// _clearNeededArm64GPRregs because the branch hasn't emitted its flush yet. +void psxRecompileNextInstruction(bool delayslot, bool swapped_delayslot) +{ + // IOP recompiler breakpoint support is not implemented on this target. + + const u32 old_code = psxRegs.code; + EEINST* const old_inst_info = g_pCurInstInfo; + s_recompilingDelaySlot = delayslot; + + // Read the instruction + psxRegs.code = iopMemRead32(psxpc); + s_psxBlockCycles++; + psxpc += 4; + g_pCurInstInfo++; + + g_iopCyclePenalty = 0; + + // Dispatch to the appropriate recompiler function + rpsxBSC[psxRegs.code >> 26](); + + s_psxBlockCycles += g_iopCyclePenalty; + + if (!swapped_delayslot) + { + _clearNeededArm64GPRregs(); + } + else + { + psxRegs.code = old_code; + g_pCurInstInfo = old_inst_info; + } + + s_recompilingDelaySlot = false; +} + +// SYSCALL and BREAK +void rpsxSYSCALL() +{ + armAsm->Mov(RWSCRATCH, psxRegs.code); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); + + armAsm->Mov(RWSCRATCH, psxpc - 4); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + + _psxFlushCall(FLUSH_NODESTROY); + + armAsm->Mov(RWARG1, 0x20); + armAsm->Mov(RWARG2, psxbranch == 1 ? 1 : 0); + armEmitCall((void*)psxException); + + // Check if PC changed + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + armAsm->Cmp(RWSCRATCH, psxpc - 4); + a64::Label noChange; + armAsm->B(&noChange, a64::eq); + + // PC changed — update cycles and re-dispatch + armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle)); + if (psxScaleBlockCycles() < 4096) + armAsm->Add(a64::x0, a64::x0, psxScaleBlockCycles()); + else + { + armAsm->Mov(a64::x1, static_cast(psxScaleBlockCycles())); + armAsm->Add(a64::x0, a64::x0, a64::x1); + } + armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle)); + iPsxAddEECycles(psxScaleBlockCycles()); + armEmitJmp(iopDispatcherReg); + + armAsm->Bind(&noChange); +} + +void rpsxBREAK() +{ + armAsm->Mov(RWSCRATCH, psxRegs.code); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); + + armAsm->Mov(RWSCRATCH, psxpc - 4); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + + _psxFlushCall(FLUSH_NODESTROY); + + armAsm->Mov(RWARG1, 0x24); + armAsm->Mov(RWARG2, psxbranch == 1 ? 1 : 0); + armEmitCall((void*)psxException); + + armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + armAsm->Cmp(RWSCRATCH, psxpc - 4); + a64::Label noChange; + armAsm->B(&noChange, a64::eq); + + armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle)); + if (psxScaleBlockCycles() < 4096) + armAsm->Add(a64::x0, a64::x0, psxScaleBlockCycles()); + else + { + armAsm->Mov(a64::x1, static_cast(psxScaleBlockCycles())); + armAsm->Add(a64::x0, a64::x0, a64::x1); + } + armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle)); + iPsxAddEECycles(psxScaleBlockCycles()); + armEmitJmp(iopDispatcherReg); + + armAsm->Bind(&noChange); +} + +// ===================================================================================================== +// Memory management and block clearing +// ===================================================================================================== + +// recLUT_SetPage is defined in BaseblockEx.h (architecture-independent) + +static __fi u32 psxRecClearMem(u32 pc) +{ + // Look up the containing block via recBlocks instead of testing the + // per-instruction LUT fnptr. The LUT only patches block-head slots away + // from iopJITCompile; mid-block words still hold the trampoline, so a + // fnptr-based early-exit would leak mid-block writes through to stale + // compiled code. + int blockidx = recBlocks.Index(HWADDR(pc)); + if (blockidx == -1) + return 4; + + u32 lowerextent = pc, upperextent = pc + 4; + + while (BASEBLOCKEX* pexblock = recBlocks[blockidx - 1]) + { + if (pexblock->startpc + pexblock->size * 4 <= HWADDR(lowerextent)) + break; + + lowerextent = std::min(lowerextent, pexblock->startpc); + blockidx--; + } + + while (BASEBLOCKEX* pexblock = recBlocks[blockidx]) + { + if (pexblock->startpc >= HWADDR(upperextent)) + break; + + lowerextent = std::min(lowerextent, pexblock->startpc); + upperextent = std::max(upperextent, pexblock->startpc + pexblock->size * 4); + recBlocks.Remove(blockidx, blockidx); + } + + // Clear all blocks in the range + for (u32 addr = lowerextent; addr < upperextent; addr += 4) + { + BASEBLOCK* p = PSX_GETBLOCK(addr); + p->SetFnptr((uptr)iopJITCompile); + } + + return upperextent - pc; +} + +static void recClearIOP(u32 Addr, u32 Size) +{ + u32 end = Addr + Size * 4; + for (u32 i = Addr; i < end; i += PSXREC_CLEARM(i)) + ; +} + +static void iopClearRecLUT(BASEBLOCK* base, int count) +{ + for (int i = 0; i < count / 4; i++) + base[i].SetFnptr((uptr)iopJITCompile); +} + +// ===================================================================================================== +// Reserve / Reset / Shutdown / Execute +// ===================================================================================================== + +static void recReserveRAM() +{ + recLutEntries = + ((Ps2MemSize::ExposedIopRam + Ps2MemSize::Rom + Ps2MemSize::Rom1 + Ps2MemSize::Rom2) / 4); + + if (recLutReserve.size() != recLutEntries) + recLutReserve.resize(recLutEntries); + + recLutUnmapped.resize(_64kb / 4); + + BASEBLOCK* curpos = recLutReserve.data(); + recRAM = curpos; + curpos += (Ps2MemSize::ExposedIopRam / 4); + recROM = curpos; + curpos += (Ps2MemSize::Rom / 4); + recROM1 = curpos; + curpos += (Ps2MemSize::Rom1 / 4); + recROM2 = curpos; + curpos += (Ps2MemSize::Rom2 / 4); +} + +static void recReserve() +{ + Console.WriteLn(Color_Green, "IOP: ARM64 Recompiler reserved."); + recPtr = SysMemory::GetIOPRec(); + recPtrEnd = SysMemory::GetIOPRecEnd() - _64kb; + + recReserveRAM(); + + pxAssertRel(!s_pInstCache, "InstCache not allocated"); + s_nInstCacheSize = 128; + s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize); + if (!s_pInstCache) + pxFailRel("Failed to allocate R3000A InstCache array."); + + // Initialize constant pool + // Reserve some space at the end of the IOP rec region for the constant pool + const u32 poolSize = 65536; + u8* poolBase = SysMemory::GetIOPRecEnd() - poolSize; + s_iopConstantPool.Init(poolBase, poolSize); +} + +void recResetIOP() +{ + Console.WriteLn(Color_Green, "iR3000A-ARM64 Recompiler reset."); + + if (CHECK_EXTRAMEM != extraRam) + { + recReserveRAM(); + extraRam = !extraRam; + } + + armSetAsmPtr(SysMemory::GetIOPRec(), SysMemory::GetIOPRecEnd() - SysMemory::GetIOPRec(), &s_iopConstantPool); + armStartBlock(); + _DynGen_Dispatchers(); + recPtr = armEndBlock(); + + iopClearRecLUT(reinterpret_cast(recLutReserve.data()), + Ps2MemSize::ExposedIopRam + Ps2MemSize::Rom + Ps2MemSize::Rom1 + Ps2MemSize::Rom2); + + BASEBLOCK* unmapped = recLutUnmapped.data(); + + for (int i = 0; i < 0x10000; i++) + recLUT_SetPage(psxRecLUT, psxhwLUT, unmapped, i, 0, 0); + + for (int i = 0; i < _64kb / 4; i++) + unmapped[i].SetFnptr((uptr)iopUnmappedRecLUTPage); + + // Map IOP RAM (with mirrors) + for (int i = 0; i < 0x80; i++) + { + u32 mask = (Ps2MemSize::ExposedIopRam / _64kb) - 1; + recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0x0000, i, i & mask); + recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0x8000, i, i & mask); + recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0xa000, i, i & mask); + } + + // Map ROM + for (int i = 0x1fc0; i < 0x2000; i++) + { + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0x0000, i, i - 0x1fc0); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0x8000, i, i - 0x1fc0); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0xa000, i, i - 0x1fc0); + } + + for (int i = 0x1e00; i < 0x1e40; i++) + { + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0x0000, i, i - 0x1e00); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0x8000, i, i - 0x1e00); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0xa000, i, i - 0x1e00); + } + + for (int i = 0x1e40; i < 0x1e48; i++) + { + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0x0000, i, i - 0x1e40); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0x8000, i, i - 0x1e40); + recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0xa000, i, i - 0x1e40); + } + + if (s_pInstCache) + memset(s_pInstCache, 0, sizeof(EEINST) * s_nInstCacheSize); + + recBlocks.Reset(); + g_psxMaxRecMem = 0; + + psxbranch = 0; +} + +static void recShutdown() +{ + s_iopConstantPool.Destroy(); + recLutReserve.deallocate(); + + safe_free(s_pInstCache); + s_nInstCacheSize = 0; + + recPtr = nullptr; + recPtrEnd = nullptr; +} + +static __noinline s32 recExecuteBlock(s32 eeCycles) +{ + psxRegs.iopBreak = 0; + psxRegs.iopCycleEE = eeCycles; + + ((void (*)())iopEnterRecompiledCode)(); + + return psxRegs.iopBreak + psxRegs.iopCycleEE; +} + +// ===================================================================================================== +// Main Recompilation Loop +// ===================================================================================================== + +static void iopRecRecompile(const u32 startpc) +{ + u32 i; + u32 link_next_block = 0; + + // BIOS hacks + if (startpc == 0x890) + { + DevCon.WriteLn(Color_Gray, "R3000 Debugger: Branch to 0x890 (SYSMEM). Clearing modules."); + R3000SymbolGuardian.ClearIrxModules(); + } + + if (startpc == 0x1630 && EmuConfig.CurrentIRX.length() > 3) + { + if (iopMemRead32(0x20018) == 0x1F) + iopMemWrite32(0x20094, 0xbffc0000); + } + + if (startpc == 0xbfc4a000) + psxRegs.GPR.n.a0 = Ps2MemSize::ExposedIopRam >> 20; + + pxAssert(startpc); + + // Reset code buffer if full + if (recPtr >= recPtrEnd) + recResetIOP(); + + armSetAsmPtr(recPtr, recPtrEnd - recPtr + _64kb, &s_iopConstantPool); + armStartBlock(); + + s_pCurBlock = PSX_GETBLOCK(startpc); + pxAssert(s_pCurBlock->GetFnptr() == (uptr)iopJITCompile); + + // armStartBlock() aligned armAsmPtr to 16 bytes, so the actual block + // code starts at armGetCurrentCodePointer(), not at recPtr. Block + // linking branches to BASEBLOCKEX::fnptr, so it must be the aligned + // address — using recPtr instead lands the branch on padding bytes + // and triggers SIGILL. Same fix as EE rec at iR5900-arm64.cpp:1751. + const uptr block_fnptr = (uptr)armGetCurrentCodePointer(); + + s_pCurBlockEx = recBlocks.Get(HWADDR(startpc)); + if (!s_pCurBlockEx || s_pCurBlockEx->startpc != HWADDR(startpc)) + s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr); + + psxbranch = 0; + + s_pCurBlock->SetFnptr(block_fnptr); + s_psxBlockCycles = 0; + // Reset recomp state + psxpc = startpc; + g_psxHasConstReg = g_psxFlushedConstReg = 1; + + _initArm64GPRregs(); + + // BIOS call interception + if ((psxHu32(HW_ICFG) & 8) && (HWADDR(startpc) == 0xa0 || HWADDR(startpc) == 0xb0 || HWADDR(startpc) == 0xc0)) + { + armEmitCall((void*)psxBiosCall); + // If psxBiosCall returns non-zero, skip to dispatcher + armAsm->Tst(RWRET, RWRET); + armEmitCondBranch(a64::ne, iopDispatcherReg); + } + + // Scan for block boundary + i = startpc; + s_nEndBlock = 0xffffffff; + s_branchTo = -1; + + while (1) + { + BASEBLOCK* pblock = PSX_GETBLOCK(i); + if (i != startpc && pblock->GetFnptr() != (uptr)iopJITCompile) + { + link_next_block = 1; + s_nEndBlock = i; + break; + } + + psxRegs.code = iopMemRead32(i); + + switch (psxRegs.code >> 26) + { + case 0: // special + if (_Funct_ == 8 || _Funct_ == 9) // JR, JALR + { + s_nEndBlock = i + 8; + goto StartRecomp; + } + break; + + case 1: // regimm + if (_Rt_ == 0 || _Rt_ == 1 || _Rt_ == 16 || _Rt_ == 17) + { + s_branchTo = _Imm_ * 4 + i + 4; + if (s_branchTo > startpc && s_branchTo < i) + s_nEndBlock = s_branchTo; + else + s_nEndBlock = i + 8; + goto StartRecomp; + } + break; + + case 2: // J + case 3: // JAL + s_branchTo = (_InstrucTarget_ << 2) | ((i + 4) & 0xf0000000); + s_nEndBlock = i + 8; + goto StartRecomp; + + case 4: case 5: case 6: case 7: // BEQ, BNE, BLEZ, BGTZ + s_branchTo = _Imm_ * 4 + i + 4; + if (s_branchTo > startpc && s_branchTo < i) + s_nEndBlock = s_branchTo; + else + s_nEndBlock = i + 8; + goto StartRecomp; + } + + i += 4; + } + +StartRecomp: + + // Detect infinite loops (NOP loops) + s_nBlockFF = false; + if (s_branchTo == startpc) + { + s_nBlockFF = true; + for (i = startpc; i < s_nEndBlock; i += 4) + { + if (i != s_nEndBlock - 8) + { + if (iopMemRead32(i) != 0) + { + s_nBlockFF = false; + break; + } + } + } + } + + // Instruction liveness analysis (backward pass) + { + EEINST* pcur; + + if (s_nInstCacheSize < (s_nEndBlock - startpc) / 4 + 1) + { + free(s_pInstCache); + s_nInstCacheSize = (s_nEndBlock - startpc) / 4 + 10; + s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize); + pxAssert(s_pInstCache != NULL); + } + + pcur = s_pInstCache + (s_nEndBlock - startpc) / 4; + _recClearInst(pcur); + pcur->info = 0; + + for (i = s_nEndBlock; i > startpc; i -= 4) + { + psxRegs.code = iopMemRead32(i - 4); + pcur[-1] = pcur[0]; + rpsxpropBSC(pcur - 1, pcur); + pcur--; + } + } + + // Compile instructions (forward pass) + g_pCurInstInfo = s_pInstCache; + while (!psxbranch && psxpc < s_nEndBlock) + { + psxRecompileNextInstruction(false, false); + } + + pxAssert((psxpc - startpc) >> 2 <= 0xffff); + s_pCurBlockEx->size = (psxpc - startpc) >> 2; + + if (!(psxpc & 0x10000000)) + g_psxMaxRecMem = std::max((psxpc & ~0xa0000000), g_psxMaxRecMem); + + if (psxbranch == 2) + { + _psxFlushCall(FLUSH_EVERYTHING); + iPsxBranchTest(0xffffffff, 1); + armEmitJmp(iopDispatcherReg); + } + else + { + if (psxbranch) + pxAssert(!link_next_block); + else + { + // Non-branch block end: add cycles. + // + // The flush MUST precede the cycle accounting below: the accounting + // uses x0/x1 as scratch, but the register allocator can still hold + // an unflushed write-back in x0 from the block's last instruction + // (e.g. SRA writing rd via _allocArm64GPR MODE_WRITE). Without this + // hoist, the deferred flush at the link-next-block site stores the + // cycle counter's low bits into the dirty guest register's psxRegs + // slot. + _psxFlushCall(FLUSH_EVERYTHING); + + armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle)); + u32 scaledCycles = psxScaleBlockCycles(); + if (scaledCycles < 4096) + armAsm->Add(a64::x0, a64::x0, scaledCycles); + else + { + armAsm->Mov(a64::x1, static_cast(scaledCycles)); + armAsm->Add(a64::x0, a64::x0, a64::x1); + } + armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle)); + iPsxAddEECycles(psxScaleBlockCycles()); + } + + if (link_next_block || !psxbranch) + { + pxAssert(psxpc == s_nEndBlock); + _psxFlushCall(FLUSH_EVERYTHING); + + armAsm->Mov(RWSCRATCH, psxpc); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc)); + + // Block linking — see psxSetBranchImm. + { + a64::SingleEmissionCheckScope guard(armAsm); + u8* patch_site = armGetCurrentCodePointer(); + armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite + recBlocks.Link(HWADDR(psxpc), patch_site); + } + psxbranch = 3; + } + } + + pxAssert(armGetCurrentCodePointer() < SysMemory::GetIOPRecEnd()); + + s_pCurBlockEx->x86size = static_cast(armGetCurrentCodePointer() - recPtr); + + Perf::iop.RegisterPC((void*)s_pCurBlockEx->fnptr, s_pCurBlockEx->x86size, s_pCurBlockEx->startpc); + + recPtr = armEndBlock(); + + pxAssert((g_psxHasConstReg & g_psxFlushedConstReg) == g_psxHasConstReg); + + s_pCurBlock = NULL; + s_pCurBlockEx = NULL; +} + +// ===================================================================================================== +// R3000Acpu struct — the public interface +// ===================================================================================================== + +R3000Acpu psxRec = { + recReserve, + recResetIOP, + recExecuteBlock, + recClearIOP, + recShutdown, +}; diff --git a/pcsx2/arm64/iR3000A-arm64.h b/pcsx2/arm64/iR3000A-arm64.h new file mode 100644 index 0000000000..ea43297f7e --- /dev/null +++ b/pcsx2/arm64/iR3000A-arm64.h @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "R3000A.h" +#include "arm64/iCore-arm64.h" +#include "arm64/AsmHelpers.h" + +// x21: Pointer to psxRegs struct (callee-saved). Loaded once at IOP JIT entry +// by EnterRecompiledCode and never modified for the duration of IOP execution. +// Use armPsxRegMem() to construct psxRegs-relative MemOperands cheaply. +#define RPSXSTATE vixl::aarch64::x21 + +// Build a MemOperand addressing a psxRegs field via RPSXSTATE. +// Mirrors the EE armCpuRegMem pattern. ARM64 LDR with imm12 covers offsets up +// to 32760 bytes (64-bit) — easily larger than psxRegs, so a single instruction +// suffices for every reachable field. +static __fi vixl::aarch64::MemOperand armPsxRegMem(const void* field) +{ + const ptrdiff_t off = reinterpret_cast(field) - reinterpret_cast(&psxRegs); + return vixl::aarch64::MemOperand(RPSXSTATE, static_cast(off)); +} + +static __fi bool armIsPsxRegPtr(const void* field) +{ + const u8* base = reinterpret_cast(&psxRegs); + const u8* p = reinterpret_cast(field); + return p >= base && p < base + sizeof(psxRegs); +} +static __fi void armLoadPsxRegPtr(const vixl::aarch64::CPURegister& reg, const void* field) +{ + if (armIsPsxRegPtr(field)) + armAsm->Ldr(reg, armPsxRegMem(field)); + else + armLoadPtr(reg, field); +} +static __fi void armStorePsxRegPtr(const vixl::aarch64::CPURegister& reg, const void* field) +{ + if (armIsPsxRegPtr(field)) + armAsm->Str(reg, armPsxRegMem(field)); + else + armStorePtr(reg, field); +} + +// Cycle penalties for particularly slow IOP instructions. +static const int psxInstCycles_Mult = 7; +static const int psxInstCycles_Div = 40; + +static const int psxInstCycles_Peephole_Store = 0; +static const int psxInstCycles_Store = 0; +static const int psxInstCycles_Load = 0; + +// HI/LO register indices — consistent with EE naming +#define PSX_HI NEONGPR_HI +#define PSX_LO NEONGPR_LO + +extern uptr psxRecLUT[]; + +void _psxFlushConstReg(int reg); +void _psxFlushConstRegs(); + +void _psxDeleteReg(int reg, int flush); +void _psxFlushCall(int flushtype); +void _psxFlushAllDirty(); + +void _psxOnWriteReg(int reg); + +void _psxMoveGPRtoR(const vixl::aarch64::Register& to, int fromgpr); + +extern u32 psxpc; // recompiler pc +extern int psxbranch; // set for branch +extern u32 g_iopCyclePenalty; + +void psxSaveBranchState(); +void psxLoadBranchState(); + +extern void psxSetBranchReg(); +extern void psxSetBranchImm(u32 imm); +extern void psxRecompileNextInstruction(bool delayslot, bool swapped_delayslot); + +//////////////////////////////////////////////////////////////////// +// IOP Constant Propagation + +#define PSX_IS_CONST1(reg) ((reg) < 32 && (g_psxHasConstReg & (1 << (reg)))) +#define PSX_IS_CONST2(reg1, reg2) ((g_psxHasConstReg & (1 << (reg1))) && (g_psxHasConstReg & (1 << (reg2)))) +#define PSX_IS_DIRTY_CONST(reg) ((reg) < 32 && (g_psxHasConstReg & (1 << (reg))) && (!(g_psxFlushedConstReg & (1 << (reg))))) +#define PSX_SET_CONST(reg) \ + { \ + if ((reg) < 32) \ + { \ + g_psxHasConstReg |= (1u << (reg)); \ + g_psxFlushedConstReg &= ~(1u << (reg)); \ + } \ + } + +#define PSX_DEL_CONST(reg) \ + { \ + if ((reg) < 32) \ + g_psxHasConstReg &= ~(1 << (reg)); \ + } + +extern u32 g_psxConstRegs[32]; +extern u32 g_psxHasConstReg, g_psxFlushedConstReg; + +typedef void (*R3000AFNPTR)(); +typedef void (*R3000AFNPTR_INFO)(int info); + +bool psxTrySwapDelaySlot(u32 rs, u32 rt, u32 rd); + +//////////////////////////////////////////////////////////////////// +// Constant propagation code generation macros + +// rd = rs op rt +#define PSXRECOMPILE_CONSTCODE0(fn, info) \ + void rpsx##fn(void) \ + { \ + psxRecompileCodeConst0(rpsx##fn##_const, rpsx##fn##_consts, rpsx##fn##_constt, rpsx##fn##_, info); \ + } + +// rt = rs op imm16 +#define PSXRECOMPILE_CONSTCODE1(fn, info) \ + void rpsx##fn(void) \ + { \ + psxRecompileCodeConst1(rpsx##fn##_const, rpsx##fn##_, info); \ + } + +// rd = rt op sa +#define PSXRECOMPILE_CONSTCODE2(fn, info) \ + void rpsx##fn(void) \ + { \ + psxRecompileCodeConst2(rpsx##fn##_const, rpsx##fn##_, info); \ + } + +// [lo,hi] = rt op rs +#define PSXRECOMPILE_CONSTCODE3(fn, LOHI) \ + void rpsx##fn(void) \ + { \ + psxRecompileCodeConst3(rpsx##fn##_const, rpsx##fn##_consts, rpsx##fn##_constt, rpsx##fn##_, LOHI); \ + } + +#define PSXRECOMPILE_CONSTCODE3_PENALTY(fn, LOHI, cycles) \ + void rpsx##fn(void) \ + { \ + psxRecompileCodeConst3(rpsx##fn##_const, rpsx##fn##_consts, rpsx##fn##_constt, rpsx##fn##_, LOHI); \ + g_iopCyclePenalty = cycles; \ + } + +// rd = rs op rt +void psxRecompileCodeConst0(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int xmminfo); +// rt = rs op imm16 +void psxRecompileCodeConst1(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo); +// rd = rt op sa +void psxRecompileCodeConst2(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo); +// [lo,hi] = rt op rs +void psxRecompileCodeConst3(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int LOHI); diff --git a/pcsx2/arm64/iR3000Atables-arm64.cpp b/pcsx2/arm64/iR3000Atables-arm64.cpp new file mode 100644 index 0000000000..05386f65a7 --- /dev/null +++ b/pcsx2/arm64/iR3000Atables-arm64.cpp @@ -0,0 +1,1456 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ARM64 IOP Opcode Implementations +// ALU/shift/mult-div/move/load-store/LWL-LWR/branch/COP0 have native codegen; +// only GTE (COP2) ops fall back to the interpreter via REC_GTE_FUNC. + +#include "arm64/iR3000A-arm64.h" +#include "arm64/AsmHelpers.h" +#include "IopMem.h" +#include "IopDma.h" // also declares iopTestIntc() +#include "IopGte.h" + +#include "common/Assertions.h" +#include "common/Console.h" + +namespace a64 = vixl::aarch64; + +extern int g_psxWriteOk; +extern u32 g_psxMaxRecMem; + +// IOP interpreter function declarations (defined in R3000AOpcodeTables.cpp) +extern void psxADDI(); extern void psxADDIU(); extern void psxSLTI(); extern void psxSLTIU(); +extern void psxANDI(); extern void psxORI(); extern void psxXORI(); extern void psxLUI(); +extern void psxADD(); extern void psxADDU(); extern void psxSUB(); extern void psxSUBU(); +extern void psxAND(); extern void psxOR(); extern void psxXOR(); extern void psxNOR(); +extern void psxSLT(); extern void psxSLTU(); +extern void psxSLL(); extern void psxSRL(); extern void psxSRA(); +extern void psxSLLV(); extern void psxSRLV(); extern void psxSRAV(); +extern void psxMULT(); extern void psxMULTU(); extern void psxDIV(); extern void psxDIVU(); +extern void psxMFHI(); extern void psxMTHI(); extern void psxMFLO(); extern void psxMTLO(); +extern void psxLB(); extern void psxLH(); extern void psxLW(); +extern void psxLBU(); extern void psxLHU(); extern void psxLWL(); extern void psxLWR(); +extern void psxSB(); extern void psxSH(); extern void psxSW(); +extern void psxSWL(); extern void psxSWR(); +extern void psxMFC0(); extern void psxMTC0(); extern void psxCFC0(); extern void psxCTC0(); +extern void psxRFE(); + +// GTE functions (defined in IopGte.cpp) +extern void gteMFC2(); extern void gteMTC2(); extern void gteCFC2(); extern void gteCTC2(); +extern void gteLWC2(); extern void gteSWC2(); +extern void gteRTPS(); extern void gteNCLIP(); extern void gteOP(); +extern void gteDPCS(); extern void gteINTPL(); extern void gteMVMVA(); extern void gteNCDS(); +extern void gteCDP(); extern void gteNCDT(); extern void gteNCCS(); extern void gteCC(); +extern void gteNCS(); extern void gteNCT(); extern void gteSQR(); extern void gteDCPL(); +extern void gteDPCT(); extern void gteAVSZ3(); extern void gteAVSZ4(); extern void gteRTPT(); +extern void gteGPF(); extern void gteGPL(); extern void gteNCCT(); + +//////////////////////////////////////////////////////////////////// +// Interpreter Fallback Macro + +#define REC_FUNC(f) \ + static void rpsx##f() \ + { \ + armAsm->Mov(RWSCRATCH, (u32)psxRegs.code); \ + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); \ + _psxFlushCall(FLUSH_EVERYTHING); \ + armEmitCall((void*)(uptr)psx##f); \ + PSX_DEL_CONST(_Rt_); \ + } + +#define REC_GTE_FUNC(f) \ + static void rgte##f() \ + { \ + armAsm->Mov(RWSCRATCH, (u32)psxRegs.code); \ + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); \ + _psxFlushCall(FLUSH_EVERYTHING); \ + armEmitCall((void*)(uptr)gte##f); \ + } + +//////////////////////////////////////////////////////////////////// +// ALU Immediate Instructions — rt = rs op imm16 + +//////////////////////////////////////////////////////////////////// +// ALU Instructions — allocator-aware native codegen with const propagation +// +// These keep results live in allocated host regs across IOP instruction +// boundaries, flushing only at block end / before C calls. +// This mirrors the proven PSX_REG_OP path below. + +// Allocator-aware 2-op IOP immediate macro: rt = rs OP imm. +// Const-folds when Rs is const; otherwise allocates Rs (MODE_READ) and Rt +// (MODE_WRITE) in host GPRs and emits codeGen, which sees host-reg indices +// rs (Rs) and rt (Rt). The result is left live in the allocator. +#define PSX_IMM_OP(name, constExpr, codeGen) \ + static void rpsx##name() \ + { \ + if (!_Rt_) return; \ + if (PSX_IS_CONST1(_Rs_)) \ + { \ + const u32 result = (constExpr); \ + _psxDeleteReg(_Rt_, 0); \ + PSX_SET_CONST(_Rt_); \ + g_psxConstRegs[_Rt_] = result; \ + return; \ + } \ + _addNeededPSXtoArm64GPR(_Rs_); \ + _addNeededPSXtoArm64GPR(_Rt_); \ + const int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); \ + const int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE); \ + codeGen; \ + _clearNeededArm64GPRregs(); \ + PSX_DEL_CONST(_Rt_); \ + } + +// PS2 IOP doesn't trap signed overflow, so ADDI is an exact alias of ADDIU. +PSX_IMM_OP(ADDIU, g_psxConstRegs[_Rs_] + _Imm_, + { armAsm->Add(armWRegister(rt), armWRegister(rs), static_cast(static_cast(_Imm_))); }) +static void rpsxADDI() { rpsxADDIU(); } +PSX_IMM_OP(ANDI, g_psxConstRegs[_Rs_] & _ImmU_, + { armAsm->And(armWRegister(rt), armWRegister(rs), static_cast(_ImmU_)); }) +PSX_IMM_OP(ORI, g_psxConstRegs[_Rs_] | _ImmU_, + { armAsm->Orr(armWRegister(rt), armWRegister(rs), static_cast(_ImmU_)); }) +PSX_IMM_OP(XORI, g_psxConstRegs[_Rs_] ^ _ImmU_, + { armAsm->Eor(armWRegister(rt), armWRegister(rs), static_cast(_ImmU_)); }) +PSX_IMM_OP(SLTI, ((s32)g_psxConstRegs[_Rs_] < (s32)_Imm_) ? 1u : 0u, + { armAsm->Cmp(armWRegister(rs), static_cast(static_cast(_Imm_))); armAsm->Cset(armWRegister(rt), a64::lt); }) +PSX_IMM_OP(SLTIU, (g_psxConstRegs[_Rs_] < (u32)(s32)_Imm_) ? 1u : 0u, + { armAsm->Cmp(armWRegister(rs), static_cast(static_cast(_Imm_))); armAsm->Cset(armWRegister(rt), a64::lo); }) + +static void rpsxLUI() +{ + if (!_Rt_) return; + _psxDeleteReg(_Rt_, 0); + PSX_SET_CONST(_Rt_); + g_psxConstRegs[_Rt_] = psxRegs.code << 16; +} + +// Allocator-aware 3-op IOP macro: rd = rs OP rt. +// +// Const-folds when both Rs and Rt are const (matches x86 +// PSXRECOMPILE_CONSTCODE0); otherwise allocates Rd in a host GPR and uses +// `codeGen` to emit one of three branches: +// rs_const → emit `op(rd, rt, const_Rs)` (or per-op flipped form) +// rt_const → emit `op(rd, rs, const_Rt)` +// else → emit `op(rd, rs, rt)` +// The codeGen block sees five locals: bool rs_const, rt_const, int rs, rt, rd +// (rs/rt are -1 when their const fast-path is taken). +#define PSX_REG_OP(name, foldExpr, codeGen) \ + static void rpsx##name() \ + { \ + if (!_Rd_) return; \ + if (PSX_IS_CONST2(_Rs_, _Rt_)) \ + { \ + _psxDeleteReg(_Rd_, 0); \ + PSX_SET_CONST(_Rd_); \ + g_psxConstRegs[_Rd_] = (foldExpr); \ + return; \ + } \ + _addNeededPSXtoArm64GPR(_Rs_); \ + _addNeededPSXtoArm64GPR(_Rt_); \ + _addNeededPSXtoArm64GPR(_Rd_); \ + const bool rs_const = PSX_IS_CONST1(_Rs_); \ + const bool rt_const = PSX_IS_CONST1(_Rt_); \ + const int rs = rs_const ? -1 : _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); \ + const int rt = rt_const ? -1 : _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); \ + const int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); \ + (void)rs; (void)rt; \ + codeGen; \ + _clearNeededArm64GPRregs(); \ + PSX_DEL_CONST(_Rd_); \ + } + +PSX_REG_OP(ADD, + g_psxConstRegs[_Rs_] + g_psxConstRegs[_Rt_], + { + if (rs_const) + armAsm->Add(armWRegister(rd), armWRegister(rt), static_cast(static_cast(g_psxConstRegs[_Rs_]))); + else if (rt_const) + armAsm->Add(armWRegister(rd), armWRegister(rs), static_cast(static_cast(g_psxConstRegs[_Rt_]))); + else + armAsm->Add(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(ADDU, + g_psxConstRegs[_Rs_] + g_psxConstRegs[_Rt_], + { + if (rs_const) + armAsm->Add(armWRegister(rd), armWRegister(rt), static_cast(static_cast(g_psxConstRegs[_Rs_]))); + else if (rt_const) + armAsm->Add(armWRegister(rd), armWRegister(rs), static_cast(static_cast(g_psxConstRegs[_Rt_]))); + else + armAsm->Add(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(SUB, + g_psxConstRegs[_Rs_] - g_psxConstRegs[_Rt_], + { + if (_Rs_ == _Rt_) // Rs - Rs == 0 + armAsm->Mov(armWRegister(rd), 0); + else if (rs_const) + { + const u32 cv = g_psxConstRegs[_Rs_]; + if (cv == 0) + armAsm->Neg(armWRegister(rd), armWRegister(rt)); + else + { + // rd may alias rt (Rd == Rt); materialize the const minuend in a + // non-allocatable scratch so the Mov can't clobber the subtrahend. + armAsm->Mov(RWSCRATCH, cv); + armAsm->Sub(armWRegister(rd), RWSCRATCH, armWRegister(rt)); + } + } + else if (rt_const) + armAsm->Sub(armWRegister(rd), armWRegister(rs), static_cast(static_cast(g_psxConstRegs[_Rt_]))); + else + armAsm->Sub(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(SUBU, + g_psxConstRegs[_Rs_] - g_psxConstRegs[_Rt_], + { + if (_Rs_ == _Rt_) // Rs - Rs == 0 + armAsm->Mov(armWRegister(rd), 0); + else if (rs_const) + { + const u32 cv = g_psxConstRegs[_Rs_]; + if (cv == 0) + armAsm->Neg(armWRegister(rd), armWRegister(rt)); + else + { + // rd may alias rt (Rd == Rt); materialize the const minuend in a + // non-allocatable scratch so the Mov can't clobber the subtrahend. + armAsm->Mov(RWSCRATCH, cv); + armAsm->Sub(armWRegister(rd), RWSCRATCH, armWRegister(rt)); + } + } + else if (rt_const) + armAsm->Sub(armWRegister(rd), armWRegister(rs), static_cast(static_cast(g_psxConstRegs[_Rt_]))); + else + armAsm->Sub(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(AND, + g_psxConstRegs[_Rs_] & g_psxConstRegs[_Rt_], + { + if (rs_const) + armAsm->And(armWRegister(rd), armWRegister(rt), static_cast(g_psxConstRegs[_Rs_])); + else if (rt_const) + armAsm->And(armWRegister(rd), armWRegister(rs), static_cast(g_psxConstRegs[_Rt_])); + else + armAsm->And(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(OR, + g_psxConstRegs[_Rs_] | g_psxConstRegs[_Rt_], + { + if (rs_const) + armAsm->Orr(armWRegister(rd), armWRegister(rt), static_cast(g_psxConstRegs[_Rs_])); + else if (rt_const) + armAsm->Orr(armWRegister(rd), armWRegister(rs), static_cast(g_psxConstRegs[_Rt_])); + else + armAsm->Orr(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(XOR, + g_psxConstRegs[_Rs_] ^ g_psxConstRegs[_Rt_], + { + if (rs_const) + armAsm->Eor(armWRegister(rd), armWRegister(rt), static_cast(g_psxConstRegs[_Rs_])); + else if (rt_const) + armAsm->Eor(armWRegister(rd), armWRegister(rs), static_cast(g_psxConstRegs[_Rt_])); + else + armAsm->Eor(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + }) + +PSX_REG_OP(NOR, + ~(g_psxConstRegs[_Rs_] | g_psxConstRegs[_Rt_]), + { + if (rs_const) + armAsm->Orr(armWRegister(rd), armWRegister(rt), static_cast(g_psxConstRegs[_Rs_])); + else if (rt_const) + armAsm->Orr(armWRegister(rd), armWRegister(rs), static_cast(g_psxConstRegs[_Rt_])); + else + armAsm->Orr(armWRegister(rd), armWRegister(rs), armWRegister(rt)); + armAsm->Mvn(armWRegister(rd), armWRegister(rd)); + }) + +PSX_REG_OP(SLT, + ((s32)g_psxConstRegs[_Rs_] < (s32)g_psxConstRegs[_Rt_]) ? 1u : 0u, + { + // Rs < Rt (signed). When Rs is const k, equivalent to Rt > k → cset gt. + if (rs_const) + { + armAsm->Cmp(armWRegister(rt), static_cast(static_cast(g_psxConstRegs[_Rs_]))); + armAsm->Cset(armWRegister(rd), a64::gt); + } + else if (rt_const) + { + armAsm->Cmp(armWRegister(rs), static_cast(static_cast(g_psxConstRegs[_Rt_]))); + armAsm->Cset(armWRegister(rd), a64::lt); + } + else + { + armAsm->Cmp(armWRegister(rs), armWRegister(rt)); + armAsm->Cset(armWRegister(rd), a64::lt); + } + }) + +PSX_REG_OP(SLTU, + (g_psxConstRegs[_Rs_] < g_psxConstRegs[_Rt_]) ? 1u : 0u, + { + // Rs u k → cset hi. + if (rs_const) + { + armAsm->Cmp(armWRegister(rt), static_cast(g_psxConstRegs[_Rs_])); + armAsm->Cset(armWRegister(rd), a64::hi); + } + else if (rt_const) + { + armAsm->Cmp(armWRegister(rs), static_cast(g_psxConstRegs[_Rt_])); + armAsm->Cset(armWRegister(rd), a64::lo); + } + else + { + armAsm->Cmp(armWRegister(rs), armWRegister(rt)); + armAsm->Cset(armWRegister(rd), a64::lo); + } + }) + +//////////////////////////////////////////////////////////////////// +// Shift Instructions — rd = rt << sa (or variable shift by rs) + +static void rpsxSLL() +{ + if (!_Rd_) return; + if (PSX_IS_CONST1(_Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] << _Sa_; + return; + } + _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + if (_Sa_) + armAsm->Lsl(armWRegister(rd), armWRegister(rt), _Sa_); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +static void rpsxSRL() +{ + if (!_Rd_) return; + if (PSX_IS_CONST1(_Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] >> _Sa_; + return; + } + _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + if (_Sa_) + armAsm->Lsr(armWRegister(rd), armWRegister(rt), _Sa_); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +static void rpsxSRA() +{ + if (!_Rd_) return; + if (PSX_IS_CONST1(_Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = (s32)g_psxConstRegs[_Rt_] >> _Sa_; + return; + } + _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + if (_Sa_) + armAsm->Asr(armWRegister(rd), armWRegister(rt), _Sa_); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +// Variable shifts: rd = rt (rs & 0x1F). When Rs is const, fold to a +// fixed-immediate shift — same emit as rpsxSLL/SRL/SRA in the imm form. +static void rpsxSLLV() +{ + if (!_Rd_) return; + if (PSX_IS_CONST2(_Rs_, _Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] << (g_psxConstRegs[_Rs_] & 0x1F); + return; + } + _addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (PSX_IS_CONST1(_Rs_)) { + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + const u32 sa = g_psxConstRegs[_Rs_] & 0x1F; + if (sa) + armAsm->Lsl(armWRegister(rd), armWRegister(rt), sa); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + } + else { + // Alloc Rs BEFORE Rd: when Rd == Rs, allocating Rd MODE_WRITE first + // would grab a fresh write-only slot (no load), and the subsequent + // Rs MODE_READ alloc would reuse that empty slot without loading. + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + armAsm->Lsl(armWRegister(rd), armWRegister(rt), armWRegister(rs)); + } + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +static void rpsxSRLV() +{ + if (!_Rd_) return; + if (PSX_IS_CONST2(_Rs_, _Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] >> (g_psxConstRegs[_Rs_] & 0x1F); + return; + } + _addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (PSX_IS_CONST1(_Rs_)) { + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + const u32 sa = g_psxConstRegs[_Rs_] & 0x1F; + if (sa) + armAsm->Lsr(armWRegister(rd), armWRegister(rt), sa); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + } + else { + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + armAsm->Lsr(armWRegister(rd), armWRegister(rt), armWRegister(rs)); + } + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +static void rpsxSRAV() +{ + if (!_Rd_) return; + if (PSX_IS_CONST2(_Rs_, _Rt_)) { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = (s32)g_psxConstRegs[_Rt_] >> (g_psxConstRegs[_Rs_] & 0x1F); + return; + } + _addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + if (PSX_IS_CONST1(_Rs_)) { + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + const u32 sa = g_psxConstRegs[_Rs_] & 0x1F; + if (sa) + armAsm->Asr(armWRegister(rd), armWRegister(rt), sa); + else + armAsm->Mov(armWRegister(rd), armWRegister(rt)); + } + else { + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + armAsm->Asr(armWRegister(rd), armWRegister(rt), armWRegister(rs)); + } + _clearNeededArm64GPRregs(); + PSX_DEL_CONST(_Rd_); +} + +//////////////////////////////////////////////////////////////////// +// Multiply/Divide — write to HI:LO + +// Sources for MULT/MULTU/DIV/DIVU are loaded directly from psxRegs.GPR +// (not via _psxMoveGPRtoR / _allocArm64GPR) on purpose. The hi-half of the +// product / quotient is written into w0/x0, which collides with the host +// register the allocator would otherwise pick for $Rs or $Rt. Going through +// the allocator leaves a stale "$Rt lives in w0" mapping that downstream +// instructions then trust — e.g. the GCC divide-by-zero check +// `bne $Rt, $zero, +2 / break 7` reads w0 (now the quotient, often 0) and +// fires BREAK even though $Rt was nonzero. Mirrors the x86 IOP rec, which +// also bypasses the allocator with `xMOV(eax, ptr32[&psxRegs.GPR.r[_Rs_]])`. + +static void rpsxMULT() +{ + _psxFlushCall(FLUSH_EVERYTHING); + armLoadPsxRegPtr(a64::w1, &psxRegs.GPR.r[_Rs_]); + armLoadPsxRegPtr(a64::w2, &psxRegs.GPR.r[_Rt_]); + armAsm->Smull(a64::x0, a64::w1, a64::w2); + // IOP HI:LO are 32-bit registers + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.lo)); + armAsm->Lsr(a64::x0, a64::x0, 32); + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.hi)); + g_iopCyclePenalty = psxInstCycles_Mult; +} + +static void rpsxMULTU() +{ + _psxFlushCall(FLUSH_EVERYTHING); + armLoadPsxRegPtr(a64::w1, &psxRegs.GPR.r[_Rs_]); + armLoadPsxRegPtr(a64::w2, &psxRegs.GPR.r[_Rt_]); + armAsm->Umull(a64::x0, a64::w1, a64::w2); + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.lo)); + armAsm->Lsr(a64::x0, a64::x0, 32); + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.hi)); + g_iopCyclePenalty = psxInstCycles_Mult; +} + +static void rpsxDIV() +{ + _psxFlushCall(FLUSH_EVERYTHING); + armLoadPsxRegPtr(a64::w1, &psxRegs.GPR.r[_Rs_]); + armLoadPsxRegPtr(a64::w2, &psxRegs.GPR.r[_Rt_]); + a64::Label zero_case, done; + armAsm->Cbz(a64::w2, &zero_case); + // Normal path: SDIV is defined on aarch64 for the (INT_MIN / -1) overflow + // case (returns INT_MIN, remainder 0) which matches psxDIV()'s explicit + // overflow branch in R3000AOpcodeTables.cpp:69, so only the divide-by-zero + // case needs fixing here. + armAsm->Sdiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w3, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + armAsm->Bind(&zero_case); + // LO = sign(Rs) ? 1 : 0xFFFFFFFF; HI = Rs. Matches psxDIV(_rRt_==0). + armAsm->Mov(a64::w0, -1); + armAsm->Cmp(a64::w1, 0); + armAsm->Cneg(a64::w0, a64::w0, a64::mi); + armAsm->Mov(a64::w3, a64::w1); + armAsm->Bind(&done); + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.lo)); + armAsm->Str(a64::w3, armPsxRegMem(&psxRegs.GPR.n.hi)); + g_iopCyclePenalty = psxInstCycles_Div; +} + +static void rpsxDIVU() +{ + _psxFlushCall(FLUSH_EVERYTHING); + armLoadPsxRegPtr(a64::w1, &psxRegs.GPR.r[_Rs_]); + armLoadPsxRegPtr(a64::w2, &psxRegs.GPR.r[_Rt_]); + a64::Label zero_case, done; + armAsm->Cbz(a64::w2, &zero_case); + armAsm->Udiv(a64::w0, a64::w1, a64::w2); + armAsm->Msub(a64::w3, a64::w0, a64::w2, a64::w1); + armAsm->B(&done); + armAsm->Bind(&zero_case); + // LO = 0xFFFFFFFF; HI = Rs. Matches psxDIVU(_rRt_==0). + armAsm->Mov(a64::w0, -1); + armAsm->Mov(a64::w3, a64::w1); + armAsm->Bind(&done); + armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.GPR.n.lo)); + armAsm->Str(a64::w3, armPsxRegMem(&psxRegs.GPR.n.hi)); + g_iopCyclePenalty = psxInstCycles_Div; +} + +//////////////////////////////////////////////////////////////////// +// Move from/to HI/LO + +static void rpsxMFHI() +{ + if (!_Rd_) return; + _psxDeleteReg(_Rd_, 0); + PSX_DEL_CONST(_Rd_); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + armLoadPsxRegPtr(armWRegister(rd), &psxRegs.GPR.n.hi); + _clearNeededArm64GPRregs(); +} + +static void rpsxMTHI() +{ + // const Rs: store immediate to hi directly. + if (PSX_IS_CONST1(_Rs_)) + { + armAsm->Mov(RWSCRATCH, g_psxConstRegs[_Rs_]); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.n.hi)); + return; + } + // Otherwise read Rs via the allocator — no FLUSH_EVERYTHING needed here. + _addNeededPSXtoArm64GPR(_Rs_); + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + armAsm->Str(armWRegister(rs), armPsxRegMem(&psxRegs.GPR.n.hi)); + _clearNeededArm64GPRregs(); +} + +static void rpsxMFLO() +{ + if (!_Rd_) return; + _psxDeleteReg(_Rd_, 0); + PSX_DEL_CONST(_Rd_); + int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); + armLoadPsxRegPtr(armWRegister(rd), &psxRegs.GPR.n.lo); + _clearNeededArm64GPRregs(); +} + +static void rpsxMTLO() +{ + if (PSX_IS_CONST1(_Rs_)) + { + armAsm->Mov(RWSCRATCH, g_psxConstRegs[_Rs_]); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.n.lo)); + return; + } + _addNeededPSXtoArm64GPR(_Rs_); + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + armAsm->Str(armWRegister(rs), armPsxRegMem(&psxRegs.GPR.n.lo)); + _clearNeededArm64GPRregs(); +} + +//////////////////////////////////////////////////////////////////// +// Load/Store +// +// Loads: compute address in w0, flush, call iopMemReadN, sign/zero extend, store result +// Stores: compute address in w0, value in w1, flush, call iopMemWriteN + +static void rpsxLoadGeneric(int size, bool sign) +{ + // Read Rs const value FIRST — before deleting Rt (critical when Rs==Rt, + // since _psxDeleteReg clears const state and frees the host register). + const bool rs_const = PSX_IS_CONST1(_Rs_); + const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0; + + // Delete destination register (flush=1 to write back, in case Rs==Rt + // and Rs is in a host register — need the value in memory). + if (_Rt_) + _psxDeleteReg(_Rt_, 1); + + _psxFlushCall(FLUSH_EVERYTHING); + + // Compute address: base + imm16 (after flush, safe to use w0) + if (rs_const) + { + armAsm->Mov(RWARG1, rs_val + _Imm_); + } + else + { + armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]); + if (_Imm_ != 0) + armAsm->Add(RWARG1, RWARG1, static_cast(static_cast(_Imm_))); + } + + // Call iopMemRead — address in w0, result returned in w0 + switch (size) + { + case 8: armEmitCall((void*)iopMemRead8); break; + case 16: armEmitCall((void*)iopMemRead16); break; + case 32: armEmitCall((void*)iopMemRead32); break; + } + + if (!_Rt_) + return; // dummy read + + // Sign/zero extend result (w0) + switch (size) + { + case 8: + if (sign) + armAsm->Sxtb(RWARG1, RWARG1); + else + armAsm->Uxtb(RWARG1, RWARG1); + break; + case 16: + if (sign) + armAsm->Sxth(RWARG1, RWARG1); + else + armAsm->Uxth(RWARG1, RWARG1); + break; + case 32: + break; // no extension needed + } + + // Store result to destination register + armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]); +} + +static void rpsxStoreGeneric(int size) +{ + // Read const values before flush + const bool rs_const = PSX_IS_CONST1(_Rs_); + const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0; + const bool rt_const = PSX_IS_CONST1(_Rt_); + const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0; + + // Flush all registers BEFORE computing operands + _psxFlushCall(FLUSH_EVERYTHING); + + // Compute address: base + imm16 + if (rs_const) + { + armAsm->Mov(RWARG1, rs_val + _Imm_); + } + else + { + armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]); + if (_Imm_ != 0) + armAsm->Add(RWARG1, RWARG1, static_cast(static_cast(_Imm_))); + } + + // Load store value into w1 + if (rt_const) + armAsm->Mov(RWARG2, rt_val); + else + armLoadPsxRegPtr(RWARG2, &psxRegs.GPR.r[_Rt_]); + + // Call iopMemWrite — address in w0, value in w1 + switch (size) + { + case 8: armEmitCall((void*)iopMemWrite8); break; + case 16: armEmitCall((void*)iopMemWrite16); break; + case 32: armEmitCall((void*)iopMemWrite32); break; + } +} + +static void rpsxLB() { rpsxLoadGeneric(8, true); } +static void rpsxLBU() { rpsxLoadGeneric(8, false); } +static void rpsxLH() { rpsxLoadGeneric(16, true); } +static void rpsxLHU() { rpsxLoadGeneric(16, false); } +static void rpsxLW() { rpsxLoadGeneric(32, false); } +static void rpsxSB() { rpsxStoreGeneric(8); } +static void rpsxSH() { rpsxStoreGeneric(16); } +static void rpsxSW() { rpsxStoreGeneric(32); } + +// ===================================================================================================== +// Unaligned word load/store: LWL / LWR / SWL / SWR +// ---------------------------------------------------------------------------------------------------- +// These are partial-word merges keyed on the low two bits of the byte +// address — *not* generic unaligned loads. Compiler-emitted LWL+LWR or +// SWL+SWR pairs construct an unaligned 32-bit access; in isolation each +// op merges memory bytes with the existing register/memory contents per +// the formulae in pcsx2/R3000AOpcodeTables.cpp:psxLWL/LWR/SWL/SWR. +// +// byte_addr = rs + imm; (addr & 3) is saved to the stack across the +// iopMemRead32/Write32 C call; then the mask + shift + or merge is done +// inline, replacing the REC_FUNC interp fallback. +// ===================================================================================================== + +// Compute byte address (rs + imm) into RWARG1, leaving (byte_addr & 3) in +// RWSCRATCH for the caller's later use *before* the C call clobbers w0. +// On return, RWARG1 holds the aligned address (byte_addr & ~3) ready for +// iopMemRead32/iopMemWrite32. +static void rpsxComputeUnalignedAddr() +{ + const bool rs_const = PSX_IS_CONST1(_Rs_); + const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0; + + if (rs_const) + { + const u32 byte_addr = rs_val + _Imm_; + armAsm->Mov(RWARG1, byte_addr & ~3u); // aligned address for memRead/Write32 + armAsm->Mov(RWSCRATCH, byte_addr & 3u); // shift_input + } + else + { + armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]); // w0 = rs + if (_Imm_ != 0) + armAsm->Add(RWARG1, RWARG1, static_cast(static_cast(_Imm_))); // w0 = rs + imm (byte_addr) + armAsm->And(RWSCRATCH, RWARG1, 3); // RWSCRATCH = byte_addr & 3 + armAsm->Bic(RWARG1, RWARG1, 3); // w0 = byte_addr & ~3 + } +} + +static void rpsxLWL() +{ + if (_Rt_) + _psxDeleteReg(_Rt_, 1); + + _psxFlushCall(FLUSH_EVERYTHING); + + rpsxComputeUnalignedAddr(); + + // Save shift_input across the C call (callee-saved would also work, + // but the IOP rec doesn't reserve any of x19-x28 for the emitter). + armAsm->Sub(a64::sp, a64::sp, 16); + armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp)); + + armEmitCall((void*)iopMemRead32); // w0 = mem (aligned word) + + armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp)); + armAsm->Add(a64::sp, a64::sp, 16); + + if (!_Rt_) + return; // dummy read — preserve memory side effects only + + // shift = (byte_addr & 3) * 8. + armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift + + // mask = 0x00ffffff >> shift, mem_shift = 24 - shift. + armAsm->Mov(a64::w2, 0x00ffffffu); + armAsm->Lsr(a64::w2, a64::w2, a64::w1); // w2 = mask + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift + armAsm->Lsl(RWARG1, RWARG1, RWSCRATCH); // w0 = mem << (24 - shift) + + // Merge: rt = (rt & mask) | (mem << (24 - shift)). + armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]); + armAsm->And(a64::w3, a64::w3, a64::w2); + armAsm->Orr(RWARG1, RWARG1, a64::w3); + armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]); +} + +static void rpsxLWR() +{ + if (_Rt_) + _psxDeleteReg(_Rt_, 1); + + _psxFlushCall(FLUSH_EVERYTHING); + + rpsxComputeUnalignedAddr(); + + armAsm->Sub(a64::sp, a64::sp, 16); + armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp)); + + armEmitCall((void*)iopMemRead32); // w0 = mem + + armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp)); + armAsm->Add(a64::sp, a64::sp, 16); + + if (!_Rt_) + return; + + // shift = (byte_addr & 3) * 8. + armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift + + // mask = 0xffffff00 << (24 - shift); mem_shift = shift. + armAsm->Mov(a64::w2, 0xffffff00u); + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift + armAsm->Lsl(a64::w2, a64::w2, RWSCRATCH); // w2 = mask + armAsm->Lsr(RWARG1, RWARG1, a64::w1); // w0 = mem >> shift + + armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]); + armAsm->And(a64::w3, a64::w3, a64::w2); + armAsm->Orr(RWARG1, RWARG1, a64::w3); + armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]); +} + +static void rpsxSWL() +{ + const bool rt_const = PSX_IS_CONST1(_Rt_); + const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0; + + _psxFlushCall(FLUSH_EVERYTHING); + + rpsxComputeUnalignedAddr(); + + // Save aligned addr (RWARG1) and shift_input (RWSCRATCH) across the + // memRead call. Both needed for the subsequent memWrite + merge. + armAsm->Sub(a64::sp, a64::sp, 16); + armAsm->Str(RWARG1, a64::MemOperand(a64::sp, 0)); + armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp, 4)); + + armEmitCall((void*)iopMemRead32); // w0 = mem + + // Reload shift_input and aligned addr; mem stays in w0. + armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp, 4)); // w1 = shift_input + + // shift = (byte_addr & 3) * 8. + armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift + + // rt_shifted = rt >> (24 - shift) + if (rt_const) + armAsm->Mov(a64::w3, rt_val); + else + armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]); + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift + armAsm->Lsr(a64::w3, a64::w3, RWSCRATCH); // w3 = rt >> (24 - shift) + + // mem_masked = mem & (0xffffff00 << shift) + armAsm->Mov(a64::w2, 0xffffff00u); + armAsm->Lsl(a64::w2, a64::w2, a64::w1); // w2 = mask + armAsm->And(a64::w0, a64::w0, a64::w2); // w0 = mem & mask + armAsm->Orr(a64::w0, a64::w0, a64::w3); // merged value + + // Now write back. iopMemWrite32(addr, value): w0 = addr, w1 = value. + armAsm->Mov(RWARG2, a64::w0); // w1 = value + armAsm->Ldr(RWARG1, a64::MemOperand(a64::sp, 0)); // w0 = aligned addr + armAsm->Add(a64::sp, a64::sp, 16); + + armEmitCall((void*)iopMemWrite32); +} + +static void rpsxSWR() +{ + const bool rt_const = PSX_IS_CONST1(_Rt_); + const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0; + + _psxFlushCall(FLUSH_EVERYTHING); + + rpsxComputeUnalignedAddr(); + + armAsm->Sub(a64::sp, a64::sp, 16); + armAsm->Str(RWARG1, a64::MemOperand(a64::sp, 0)); + armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp, 4)); + + armEmitCall((void*)iopMemRead32); // w0 = mem + + armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp, 4)); + armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift + + // rt_shifted = rt << shift + if (rt_const) + armAsm->Mov(a64::w3, rt_val); + else + armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]); + armAsm->Lsl(a64::w3, a64::w3, a64::w1); // w3 = rt << shift + + // mem_masked = mem & (0x00ffffff >> (24 - shift)) + armAsm->Mov(a64::w2, 0x00ffffffu); + armAsm->Mov(RWSCRATCH, 24); + armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift + armAsm->Lsr(a64::w2, a64::w2, RWSCRATCH); // w2 = mask + armAsm->And(a64::w0, a64::w0, a64::w2); // w0 = mem & mask + armAsm->Orr(a64::w0, a64::w0, a64::w3); // merged value + + armAsm->Mov(RWARG2, a64::w0); + armAsm->Ldr(RWARG1, a64::MemOperand(a64::sp, 0)); + armAsm->Add(a64::sp, a64::sp, 16); + + armEmitCall((void*)iopMemWrite32); +} + +//////////////////////////////////////////////////////////////////// +// Branch/Jump Instructions + +static void rpsxJ() +{ + u32 newpc = _InstrucTarget_ * 4 + (psxpc & 0xf0000000); + psxRecompileNextInstruction(true, false); + psxSetBranchImm(newpc); +} + +static void rpsxJAL() +{ + u32 newpc = _InstrucTarget_ * 4 + (psxpc & 0xf0000000); + _psxDeleteReg(31, 0); + PSX_SET_CONST(31); + g_psxConstRegs[31] = psxpc + 4; + + psxRecompileNextInstruction(true, false); + psxSetBranchImm(newpc); +} + +static void rpsxJR() +{ + // Save branch target to pcWriteback before delay slot — the delay slot's + // recCall will clobber w0 via _psxFlushCall. + _psxMoveGPRtoR(RWSCRATCH, _Rs_); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pcWriteback)); + _psxFlushCall(FLUSH_EVERYTHING); + + const bool swap = psxTrySwapDelaySlot(_Rs_, 0, 0); + if (!swap) + psxRecompileNextInstruction(true, false); + psxSetBranchReg(); +} + +static void rpsxJALR() +{ + // Save branch target to pcWriteback before delay slot + _psxMoveGPRtoR(RWSCRATCH, _Rs_); + armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pcWriteback)); + + // Capture link before swap advances psxpc past the delay slot. + const u32 newpc = psxpc + 4; + + // Rd == Rs disables swap — the delay slot reading Rs would observe the + // post-link value instead of the pre-link one. + const bool swap = (_Rd_ == _Rs_) ? false : psxTrySwapDelaySlot(_Rs_, 0, _Rd_); + + // Save return address + if (_Rd_) + { + _psxDeleteReg(_Rd_, 0); + PSX_SET_CONST(_Rd_); + g_psxConstRegs[_Rd_] = newpc; + } + + _psxFlushCall(FLUSH_EVERYTHING); + if (!swap) + psxRecompileNextInstruction(true, false); + psxSetBranchReg(); +} + +// Helper for conditional branches: compare Rs and Rt, branch if condition met +static void rpsxBranchCompare(a64::Condition cond) +{ + u32 branchTo = ((s32)(s16)_Imm_ * 4) + psxpc; + + // Compare Rs and Rt + if (PSX_IS_CONST2(_Rs_, _Rt_)) + { + // Both constant — evaluate at compile time + bool taken = false; + if (cond == a64::eq) taken = (g_psxConstRegs[_Rs_] == g_psxConstRegs[_Rt_]); + else if (cond == a64::ne) taken = (g_psxConstRegs[_Rs_] != g_psxConstRegs[_Rt_]); + _psxFlushAllDirty(); + psxRecompileNextInstruction(true, false); + psxSetBranchImm(taken ? branchTo : psxpc); + return; + } + + // Hoist delay slot ahead of compare when it doesn't reference Rs/Rt. + // Flush AFTER swap so any cache dirties left by the swapped delay-slot + // instruction commit to memory before the compare/branch — the runtime + // taken path otherwise won't emit those flushes. + const bool swap = psxTrySwapDelaySlot(_Rs_, _Rt_, 0); + _psxFlushAllDirty(); + + // Runtime comparison + if (PSX_IS_CONST1(_Rs_)) + { + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + armAsm->Cmp(armWRegister(rt), g_psxConstRegs[_Rs_]); + // Condition is symmetric for eq/ne so operand reversal is safe + } + else if (PSX_IS_CONST1(_Rt_)) + { + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + armAsm->Cmp(armWRegister(rs), g_psxConstRegs[_Rt_]); + } + else + { + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); + armAsm->Cmp(armWRegister(rs), armWRegister(rt)); + } + + _clearNeededArm64GPRregs(); + + a64::Label taken; + armAsm->B(&taken, cond); + + // Not taken path + if (!swap) + { + psxSaveBranchState(); + psxRecompileNextInstruction(true, false); + } + psxSetBranchImm(psxpc); + + // Taken path — recompile delay slot from the correct PC + armAsm->Bind(&taken); + if (!swap) + { + psxpc -= 4; + psxLoadBranchState(); + psxRecompileNextInstruction(true, false); + } + psxSetBranchImm(branchTo); +} + +static void rpsxBEQ() { rpsxBranchCompare(a64::eq); } +static void rpsxBNE() { rpsxBranchCompare(a64::ne); } + +// BLEZ / BGTZ / BLTZ / BGEZ — compare Rs against zero +static void rpsxBranchZero(a64::Condition cond) +{ + u32 branchTo = ((s32)(s16)_Imm_ * 4) + psxpc; + + if (PSX_IS_CONST1(_Rs_)) + { + bool taken = false; + s32 val = (s32)g_psxConstRegs[_Rs_]; + if (cond == a64::le) taken = (val <= 0); + else if (cond == a64::gt) taken = (val > 0); + else if (cond == a64::lt) taken = (val < 0); + else if (cond == a64::ge) taken = (val >= 0); + // No _psxFlushAllDirty() here: the branch is resolved statically (single + // successor, no compare needing clean regs and no Save/LoadBranchState + // snapshot), so the explicit flush is redundant — the delay-slot recompile + // manages its own dirties and block-end flushes the rest. + psxRecompileNextInstruction(true, false); + psxSetBranchImm(taken ? branchTo : psxpc); + return; + } + + // Hoist delay slot ahead of compare when it doesn't reference Rs. + // Flush AFTER swap so any cache dirties left by the swapped delay-slot + // instruction commit to memory before the compare/branch. + const bool swap = psxTrySwapDelaySlot(_Rs_, 0, 0); + _psxFlushAllDirty(); + + int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); + armAsm->Cmp(armWRegister(rs), 0); + _clearNeededArm64GPRregs(); + + a64::Label taken; + armAsm->B(&taken, cond); + + if (!swap) + { + psxSaveBranchState(); + psxRecompileNextInstruction(true, false); + } + psxSetBranchImm(psxpc); + + armAsm->Bind(&taken); + if (!swap) + { + psxpc -= 4; + psxLoadBranchState(); + psxRecompileNextInstruction(true, false); + } + psxSetBranchImm(branchTo); +} + +static void rpsxBLEZ() { rpsxBranchZero(a64::le); } +static void rpsxBGTZ() { rpsxBranchZero(a64::gt); } +static void rpsxBLTZ() { rpsxBranchZero(a64::lt); } +static void rpsxBGEZ() { rpsxBranchZero(a64::ge); } + +static void rpsxBLTZAL() +{ + _psxDeleteReg(31, 0); + PSX_SET_CONST(31); + g_psxConstRegs[31] = psxpc + 4; + rpsxBranchZero(a64::lt); +} + +static void rpsxBGEZAL() +{ + _psxDeleteReg(31, 0); + PSX_SET_CONST(31); + g_psxConstRegs[31] = psxpc + 4; + rpsxBranchZero(a64::ge); +} + +//////////////////////////////////////////////////////////////////// +// COP0 + +// MFC0/CFC0: Rt = CP0[Rd] +static void rpsxMFC0() +{ + if (!_Rt_) + return; + + // Mirrors x86 rpsxMFC0: allocate Rt as a write target and load CP0[Rd] into + // it. CP0 is never register-allocated, so its memory copy is always current; + // and nothing here calls a C function, so no flush is needed. + const int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE); + armLoadPsxRegPtr(armWRegister(rt), &psxRegs.CP0.r[_Rd_]); +} + +static void rpsxCFC0() { rpsxMFC0(); } + +// MTC0/CTC0: CP0[Rd] = Rt +static void rpsxMTC0() +{ + // Mirrors x86 rpsxMTC0: read Rt allocator-aware (const / dirty host reg / + // memory, via _psxMoveGPRtoR) and store to CP0[Rd]. No flush — no C call + // follows, and CP0 is not register-allocated so the memory store stands. + _psxMoveGPRtoR(RWSCRATCH, _Rt_); + armStorePsxRegPtr(RWSCRATCH, &psxRegs.CP0.r[_Rd_]); +} + +static void rpsxCTC0() { rpsxMTC0(); } + +// RFE: Status = (Status & 0xFFFFFFF0) | ((Status & 0x3C) >> 2) +// Then test IOP INTC to raise any pending interrupts. +static void rpsxRFE() +{ + _psxFlushCall(FLUSH_EVERYTHING); + + armLoadPsxRegPtr(RWSCRATCH, &psxRegs.CP0.n.Status); + armAsm->Ubfx(RWARG1, RWSCRATCH, 2, 4); // (Status >> 2) & 0xF == (Status & 0x3C) >> 2 + armAsm->Bfi(RWSCRATCH, RWARG1, 0, 4); // replace low 4 bits of Status + armStorePsxRegPtr(RWSCRATCH, &psxRegs.CP0.n.Status); + + armEmitCall((void*)iopTestIntc); +} + +//////////////////////////////////////////////////////////////////// +// GTE (COP2) + +REC_GTE_FUNC(MFC2); +REC_GTE_FUNC(MTC2); +REC_GTE_FUNC(CFC2); +REC_GTE_FUNC(CTC2); +REC_GTE_FUNC(LWC2); +REC_GTE_FUNC(SWC2); + +REC_GTE_FUNC(RTPS); +REC_GTE_FUNC(NCLIP); +REC_GTE_FUNC(OP); +REC_GTE_FUNC(DPCS); +REC_GTE_FUNC(INTPL); +REC_GTE_FUNC(MVMVA); +REC_GTE_FUNC(NCDS); +REC_GTE_FUNC(CDP); +REC_GTE_FUNC(NCDT); +REC_GTE_FUNC(NCCS); +REC_GTE_FUNC(CC); +REC_GTE_FUNC(NCS); +REC_GTE_FUNC(NCT); +REC_GTE_FUNC(SQR); +REC_GTE_FUNC(DCPL); +REC_GTE_FUNC(DPCT); +REC_GTE_FUNC(AVSZ3); +REC_GTE_FUNC(AVSZ4); +REC_GTE_FUNC(RTPT); +REC_GTE_FUNC(GPF); +REC_GTE_FUNC(GPL); +REC_GTE_FUNC(NCCT); + +//////////////////////////////////////////////////////////////////// +// rpsxSYSCALL and rpsxBREAK are defined in iR3000A-arm64.cpp + +//////////////////////////////////////////////////////////////////// +// Dispatch Tables + +extern void (*rpsxBSC[64])(); +extern void (*rpsxSPC[64])(); +extern void (*rpsxREG[32])(); +extern void (*rpsxCP0[32])(); +extern void (*rpsxCP2[64])(); +extern void (*rpsxCP2BSC[32])(); + +// Defined in iR3000A-arm64.cpp +extern void rpsxSYSCALL(); +extern void rpsxBREAK(); + +static void rpsxSPECIAL() { rpsxSPC[_Funct_](); } +static void rpsxREGIMM() { rpsxREG[_Rt_](); } +static void rpsxCOP0() { rpsxCP0[_Rs_](); } +static void rpsxCOP2() { rpsxCP2[_Funct_](); } +static void rpsxBASIC() { rpsxCP2BSC[_Rs_](); } + +static void rpsxNULL() +{ + Console.WriteLn("psxUNK: %8.8x", psxRegs.code); +} + +// clang-format off +void (*rpsxBSC[64])() = { + rpsxSPECIAL, rpsxREGIMM, rpsxJ , rpsxJAL , rpsxBEQ , rpsxBNE , rpsxBLEZ, rpsxBGTZ, + rpsxADDI , rpsxADDIU , rpsxSLTI, rpsxSLTIU, rpsxANDI, rpsxORI , rpsxXORI, rpsxLUI , + rpsxCOP0 , rpsxNULL , rpsxCOP2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxLB , rpsxLH , rpsxLWL , rpsxLW , rpsxLBU , rpsxLHU , rpsxLWR , rpsxNULL, + rpsxSB , rpsxSH , rpsxSWL , rpsxSW , rpsxNULL, rpsxNULL, rpsxSWR , rpsxNULL, + rpsxNULL , rpsxNULL , rgteLWC2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL , rpsxNULL , rgteSWC2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, +}; + +void (*rpsxSPC[64])() = { + rpsxSLL , rpsxNULL, rpsxSRL , rpsxSRA , rpsxSLLV , rpsxNULL , rpsxSRLV, rpsxSRAV, + rpsxJR , rpsxJALR, rpsxNULL, rpsxNULL, rpsxSYSCALL, rpsxBREAK, rpsxNULL, rpsxNULL, + rpsxMFHI, rpsxMTHI, rpsxMFLO, rpsxMTLO, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, + rpsxMULT, rpsxMULTU, rpsxDIV, rpsxDIVU, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, + rpsxADD , rpsxADDU, rpsxSUB , rpsxSUBU, rpsxAND , rpsxOR , rpsxXOR , rpsxNOR , + rpsxNULL, rpsxNULL, rpsxSLT , rpsxSLTU, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, +}; + +void (*rpsxREG[32])() = { + rpsxBLTZ , rpsxBGEZ , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxBLTZAL, rpsxBGEZAL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, +}; + +void (*rpsxCP0[32])() = { + rpsxMFC0, rpsxNULL, rpsxCFC0, rpsxNULL, rpsxMTC0, rpsxNULL, rpsxCTC0, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxRFE , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, +}; + +void (*rpsxCP2[64])() = { + rpsxBASIC, rgteRTPS , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rgteNCLIP, rpsxNULL, // 00 + rpsxNULL , rpsxNULL , rpsxNULL , rpsxNULL, rgteOP , rpsxNULL , rpsxNULL , rpsxNULL, // 08 + rgteDPCS , rgteINTPL, rgteMVMVA, rgteNCDS, rgteCDP , rpsxNULL , rgteNCDT , rpsxNULL, // 10 + rpsxNULL , rpsxNULL , rpsxNULL , rgteNCCS, rgteCC , rpsxNULL , rgteNCS , rpsxNULL, // 18 + rgteNCT , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, // 20 + rgteSQR , rgteDCPL , rgteDPCT , rpsxNULL, rpsxNULL, rgteAVSZ3, rgteAVSZ4, rpsxNULL, // 28 + rgteRTPT , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, // 30 + rpsxNULL , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rgteGPF , rgteGPL , rgteNCCT, // 38 +}; + +void (*rpsxCP2BSC[32])() = { + rgteMFC2, rpsxNULL, rgteCFC2, rpsxNULL, rgteMTC2, rpsxNULL, rgteCTC2, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, + rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, +}; +// clang-format on + +//////////////////////////////////////////////////////////////////// +// Back-Propagation Analysis Tables (architecture-independent) +//////////////////////////////////////////////////////////////////// + +#define rpsxpropSetRead(reg) \ + { \ + if (!(pinst->regs[reg] & EEINST_USED)) \ + pinst->regs[reg] |= EEINST_LASTUSE; \ + prev->regs[reg] |= EEINST_LIVE | EEINST_USED; \ + pinst->regs[reg] |= EEINST_USED; \ + _recFillRegister(*pinst, NEONTYPE_GPRREG, reg, 0); \ + } + +#define rpsxpropSetWrite(reg) \ + { \ + prev->regs[reg] &= ~(EEINST_LIVE | EEINST_USED); \ + if (!(pinst->regs[reg] & EEINST_USED)) \ + pinst->regs[reg] |= EEINST_LASTUSE; \ + pinst->regs[reg] |= EEINST_USED; \ + _recFillRegister(*pinst, NEONTYPE_GPRREG, reg, 1); \ + } + +void rpsxpropBSC(EEINST* prev, EEINST* pinst); +void rpsxpropSPECIAL(EEINST* prev, EEINST* pinst); +void rpsxpropREGIMM(EEINST* prev, EEINST* pinst); +void rpsxpropCP0(EEINST* prev, EEINST* pinst); +void rpsxpropCP2(EEINST* prev, EEINST* pinst); + +void rpsxpropBSC(EEINST* prev, EEINST* pinst) +{ + switch (psxRegs.code >> 26) + { + case 0: rpsxpropSPECIAL(prev, pinst); break; + case 1: rpsxpropREGIMM(prev, pinst); break; + case 2: break; // J + case 3: rpsxpropSetWrite(31); break; // JAL + case 4: case 5: // BEQ, BNE + rpsxpropSetRead(_Rs_); + rpsxpropSetRead(_Rt_); + break; + case 6: case 7: // BLEZ, BGTZ + rpsxpropSetRead(_Rs_); + break; + case 15: // LUI + rpsxpropSetWrite(_Rt_); + break; + case 16: rpsxpropCP0(prev, pinst); break; + case 18: rpsxpropCP2(prev, pinst); break; + case 40: case 41: case 42: case 43: case 46: // stores + rpsxpropSetRead(_Rt_); + rpsxpropSetRead(_Rs_); + break; + case 50: case 58: break; // LWC2, SWC2 + default: + rpsxpropSetWrite(_Rt_); + rpsxpropSetRead(_Rs_); + break; + } +} + +void rpsxpropSPECIAL(EEINST* prev, EEINST* pinst) +{ + switch (_Funct_) + { + case 0: case 2: case 3: // SLL, SRL, SRA + rpsxpropSetWrite(_Rd_); + rpsxpropSetRead(_Rt_); + break; + case 8: // JR + rpsxpropSetRead(_Rs_); + break; + case 9: // JALR + rpsxpropSetWrite(_Rd_); + rpsxpropSetRead(_Rs_); + break; + case 12: case 13: // SYSCALL, BREAK + _recClearInst(prev); + prev->info = 0; + break; + case 15: break; // SYNC + case 16: // MFHI + rpsxpropSetWrite(_Rd_); + rpsxpropSetRead(PSX_HI); + break; + case 17: // MTHI + rpsxpropSetWrite(PSX_HI); + rpsxpropSetRead(_Rs_); + break; + case 18: // MFLO + rpsxpropSetWrite(_Rd_); + rpsxpropSetRead(PSX_LO); + break; + case 19: // MTLO + rpsxpropSetWrite(PSX_LO); + rpsxpropSetRead(_Rs_); + break; + case 24: case 25: case 26: case 27: // MULT, MULTU, DIV, DIVU + rpsxpropSetWrite(PSX_LO); + rpsxpropSetWrite(PSX_HI); + rpsxpropSetRead(_Rs_); + rpsxpropSetRead(_Rt_); + break; + case 32: case 33: case 34: case 35: // ADD, ADDU, SUB, SUBU + rpsxpropSetWrite(_Rd_); + if (_Rs_) rpsxpropSetRead(_Rs_); + if (_Rt_) rpsxpropSetRead(_Rt_); + break; + default: + rpsxpropSetWrite(_Rd_); + rpsxpropSetRead(_Rs_); + rpsxpropSetRead(_Rt_); + break; + } +} + +void rpsxpropREGIMM(EEINST* prev, EEINST* pinst) +{ + switch (_Rt_) + { + case 0: case 1: // BLTZ, BGEZ + rpsxpropSetRead(_Rs_); + break; + case 16: case 17: // BLTZAL, BGEZAL + rpsxpropSetRead(_Rs_); + break; + default: + break; + } +} + +void rpsxpropCP0(EEINST* prev, EEINST* pinst) +{ + switch (_Rs_) + { + case 0: case 2: // MFC0, CFC0 + rpsxpropSetWrite(_Rt_); + break; + case 4: case 6: // MTC0, CTC0 + rpsxpropSetRead(_Rt_); + break; + case 16: break; // RFE + default: break; + } +} + +static void rpsxpropCP2_basic(EEINST* prev, EEINST* pinst) +{ + switch (_Rs_) + { + case 0: case 2: // MFC2, CFC2 + rpsxpropSetWrite(_Rt_); + break; + case 4: case 6: // MTC2, CTC2 + rpsxpropSetRead(_Rt_); + break; + default: break; + } +} + +void rpsxpropCP2(EEINST* prev, EEINST* pinst) +{ + switch (_Funct_) + { + case 0: rpsxpropCP2_basic(prev, pinst); break; + default: break; + } +} From 44b776ddf70dbe8ae4c6db4636e508b812ca3844 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 006/292] arm64: microVU (VU0/VU1) recompiler core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARM64 microVU template JIT — dispatcher, compiler, upper/lower pipelines, flag and clamp handling, allocator and IR. Wires the VU recompilers into VMManager's CPU providers, ungates REC_VU1/THREAD_VU1 for arm64, and registers the arm64 source set (VUops.cpp/FPU.cpp get -ffp-contract=off so the interpreter stays bit-exact vs the two-rounding JIT). Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/CMakeLists.txt | 42 + pcsx2/Config.h | 5 - pcsx2/VMManager.cpp | 13 +- pcsx2/arm64/microVU-arm64.cpp | 1817 ++++++++++++++++++++ pcsx2/arm64/microVU-arm64.h | 708 ++++++++ pcsx2/arm64/microVU_Alloc-arm64.inl | 143 ++ pcsx2/arm64/microVU_Branch-arm64.inl | 1087 ++++++++++++ pcsx2/arm64/microVU_Clamp-arm64.inl | 76 + pcsx2/arm64/microVU_Compile-arm64.inl | 1172 +++++++++++++ pcsx2/arm64/microVU_Flags-arm64.inl | 409 +++++ pcsx2/arm64/microVU_IR-arm64.h | 868 ++++++++++ pcsx2/arm64/microVU_Lower-arm64.inl | 2280 +++++++++++++++++++++++++ pcsx2/arm64/microVU_Misc-arm64.h | 352 ++++ pcsx2/arm64/microVU_Misc-arm64.inl | 356 ++++ pcsx2/arm64/microVU_Upper-arm64.inl | 975 +++++++++++ 15 files changed, 10289 insertions(+), 14 deletions(-) create mode 100644 pcsx2/arm64/microVU-arm64.cpp create mode 100644 pcsx2/arm64/microVU-arm64.h create mode 100644 pcsx2/arm64/microVU_Alloc-arm64.inl create mode 100644 pcsx2/arm64/microVU_Branch-arm64.inl create mode 100644 pcsx2/arm64/microVU_Clamp-arm64.inl create mode 100644 pcsx2/arm64/microVU_Compile-arm64.inl create mode 100644 pcsx2/arm64/microVU_Flags-arm64.inl create mode 100644 pcsx2/arm64/microVU_IR-arm64.h create mode 100644 pcsx2/arm64/microVU_Lower-arm64.inl create mode 100644 pcsx2/arm64/microVU_Misc-arm64.h create mode 100644 pcsx2/arm64/microVU_Misc-arm64.inl create mode 100644 pcsx2/arm64/microVU_Upper-arm64.inl diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 5d4ec9837c..28fce32f29 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1051,13 +1051,42 @@ set(pcsx2x86Headers # ARM64 set(pcsx2arm64Sources arm64/AsmHelpers.cpp + arm64/iCore-arm64.cpp + arm64/iR3000A-arm64.cpp + arm64/iR3000Atables-arm64.cpp + arm64/iR5900-arm64.cpp + arm64/iR5900Templates-arm64.cpp + x86/BaseblockEx.cpp # arch-neutral baseblock manager, shared with the x86 build + arm64/iR5900Arit-arm64.cpp + arm64/iR5900AritImm-arm64.cpp + arm64/iR5900Branch-arm64.cpp + arm64/iR5900Jump-arm64.cpp + arm64/iR5900LoadStore-arm64.cpp + arm64/recVTLB-arm64.cpp + arm64/iR5900Move-arm64.cpp + arm64/iR5900MultDiv-arm64.cpp + arm64/iR5900Shift-arm64.cpp + arm64/iR5900Misc-arm64.cpp + arm64/iCOP2-arm64.cpp + arm64/iCOP0-arm64.cpp + arm64/iFPU-arm64.cpp + arm64/iFPUd-arm64.cpp + arm64/iMMI-arm64.cpp arm64/Vif_Dynarec.cpp arm64/Vif_UnpackNEON.cpp arm64/RecStubs.cpp + arm64/microVU-arm64.cpp + x86/iR5900Analysis.cpp # arch-neutral EE instruction-liveness analysis, shared with the x86 build ) set(pcsx2arm64Headers arm64/AsmHelpers.h + arm64/iCore-arm64.h + arm64/iR3000A-arm64.h + arm64/iR5900-arm64.h + arm64/iR5900Analysis.h + arm64/microVU-arm64.h + arm64/microVU_Misc-arm64.h ) # These ones benefit a lot from LTO @@ -1215,6 +1244,19 @@ target_include_directories(PCSX2_FLAGS INTERFACE ) set_source_files_properties(PrecompiledHeader.cpp PROPERTIES HEADER_FILE_ONLY TRUE) + +# VUops.cpp (VU interp) and FPU.cpp (EE COP1 interp) must produce bit-exact +# results matching PS2 hardware (which has no FMA). The project-wide +# -ffp-contract=fast is fine on x86 (no FMA emitted without -mfma) but on aarch64 +# it lets the compiler contract `acc + fs * ft` to `fmadd` (single-rounded), +# breaking bit-exactness vs the recompilers (separate fmul + fadd). In VUops.cpp +# this would produce 1-ULP MADDA divergences. FPU.cpp has the identical hazard in +# MADDA_S/MSUBA_S (`_FAValf_ += fs*ft` / `-= fs*ft` are single-expression +# accumulates that fuse on aarch64 while the EE FPU rec emits two roundings) — a +# +1-ULP float drift vs the rec. +if(NOT MSVC) + set_source_files_properties(VUops.cpp FPU.cpp PROPERTIES COMPILE_OPTIONS "-ffp-contract=off") +endif() if(COMMAND target_precompile_headers) message("Using precompiled headers.") target_precompile_headers(PCSX2_FLAGS INTERFACE PrecompiledHeader.h) diff --git a/pcsx2/Config.h b/pcsx2/Config.h index db240e9284..1274ba06af 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1489,13 +1489,8 @@ namespace EmuFolders // ------------ CPU / Recompiler Options --------------- -#ifdef _M_X86 // TODO: Remove me once EE/VU/IOP recs are added. #define REC_VU1 (EmuConfig.Cpu.Recompiler.EnableVU1) #define THREAD_VU1 (REC_VU1 && EmuConfig.Speedhacks.vuThread) -#else -#define THREAD_VU1 false -#define REC_VU1 false -#endif #define INSTANT_VU1 (EmuConfig.Speedhacks.vu1Instant) #define CHECK_EEREC (EmuConfig.Cpu.Recompiler.EnableEE) #define CHECK_CACHE (EmuConfig.Cpu.Recompiler.EnableEECache) diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 404a342dab..d416ebc085 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -2670,16 +2670,13 @@ void VMManager::LogCPUCapabilities() void VMManager::InitializeCPUProviders() { -#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added. +#if defined(_M_X86) || defined(ARCH_ARM64) recCpu.Reserve(); psxRec.Reserve(); CpuMicroVU0.Reserve(); CpuMicroVU1.Reserve(); #else - // Despite not having any VU recompilers on ARM64, therefore no MTVU, - // we still need the thread alive. Otherwise the read and write positions - // of the ring buffer wont match, and various systems in the emulator end up deadlocked. vu1Thread.Open(); #endif @@ -2694,15 +2691,13 @@ void VMManager::ShutdownCPUProviders() dVifRelease(0); } -#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added. +#if defined(_M_X86) || defined(ARCH_ARM64) CpuMicroVU1.Shutdown(); CpuMicroVU0.Shutdown(); psxRec.Shutdown(); recCpu.Shutdown(); #else - // See the comment in the InitializeCPUProviders for an explaination why we - // still need to manage the MTVU thread. if (vu1Thread.IsOpen()) vu1Thread.WaitVU(); #endif @@ -2719,7 +2714,7 @@ void VMManager::UpdateCPUImplementations() return; } -#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added. +#if defined(_M_X86) || defined(ARCH_ARM64) Cpu = CHECK_EEREC ? &recCpu : &intCpu; psxCpu = CHECK_IOPREC ? &psxRec : &psxInt; @@ -2739,7 +2734,7 @@ void VMManager::Internal::ClearCPUExecutionCaches() Cpu->Reset(); psxCpu->Reset(); -#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added. +#if defined(_M_X86) || defined(ARCH_ARM64) // mVU's VU0 needs to be properly initialized for macro mode even if it's not used for micro mode! if (CHECK_EEREC && !EmuConfig.Cpu.Recompiler.EnableVU0) CpuMicroVU0.Reset(); diff --git a/pcsx2/arm64/microVU-arm64.cpp b/pcsx2/arm64/microVU-arm64.cpp new file mode 100644 index 0000000000..5e2fb9c1b5 --- /dev/null +++ b/pcsx2/arm64/microVU-arm64.cpp @@ -0,0 +1,1817 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "microVU-arm64.h" + +#include "microVU_ProgCache-arm64.h" +#include "arm64/iCore-arm64.h" +#include "vtlb.h" +#include "common/AlignedMalloc.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/Perf.h" +#include "common/StringUtil.h" +#include "SaveState.h" +#include "VU1Trace.h" +#include "vu_capture.h" + +// Program-cache telemetry. Uncomment and rebuild to enable; off in +// shipped builds. Same pattern as mVUlogProg / mVUprofileProg in microVU.h. +//#define mVUcacheTrace + +#include +#include +#include +#include +#include +#include +#ifdef mVUcacheTrace +#include +#include +#include +#endif + +#include "fmt/format.h" + +//------------------------------------------------------------------ +// Micro VU - Globals +//------------------------------------------------------------------ + +alignas(16) microVU microVU0; +alignas(16) microVU microVU1; + +//------------------------------------------------------------------ +// Micro VU - Observed-entry-PC tracking on microProgram. Single- +// threaded per VU; the dispatcher records each `startPC` it hands +// off into the resolved program so additional entry trampolines +// can be emitted for previously-unseen PCs. +//------------------------------------------------------------------ + +bool MvuObservedEntries::record(u32 startPC_bytes) +{ + for (u32 i = 0; i < count; ++i) + { + if (pcs[i] == startPC_bytes) + return false; + } + if (count >= kMax) + return false; + pcs[count++] = startPC_bytes; + ++version; + return true; +} + +void MvuObservedEntries::clear() +{ + count = 0; + version = 0; + for (u32 i = 0; i < kMax; ++i) + pcs[i] = 0; +} + +//------------------------------------------------------------------ +// Micro VU - Program-cache range overlap helper +// +// Returns true iff any compiled range in `prog->ranges` overlaps the byte +// interval [addr, addr+size). Used by both the cache-trace telemetry path +// (mVUCacheTraceObserveClear) and the range-aware mVUclear fast path. +//------------------------------------------------------------------ + +static __fi bool mVUProgRangesOverlap(const microProgram* prog, u32 addr, u32 size) +{ + if (!prog || !prog->ranges) + return false; + const s32 lo = static_cast(addr); + const s32 hi = static_cast(addr + size); + for (const auto& r : *prog->ranges) + { + if (r.start < hi && r.end > lo) + return true; + } + return false; +} + +//------------------------------------------------------------------ +// Micro VU - Program-cache instrumentation (mVUcacheTrace) +// +// Enabled by uncommenting `#define mVUcacheTrace` near the top of this file. +// Tracks program-creation/clear/reset counts, deque-walk lengths, and per-VU +// dup histograms via mVUrangesHash — same machinery as x86 mVUprintUniqueRatio. +// Dumped on mVUreset and mVUclose; per-window counters reset after each dump, +// lifetime counters (resets, programs_created) persist. When the define is +// commented out (the default), every helper and call site below compiles to +// nothing. +//------------------------------------------------------------------ + +#ifdef mVUcacheTrace +u64 mVUrangesHash(microVU& mVU, microProgram& prog); + +namespace +{ + struct mVUCacheTrace + { + u64 programs_created = 0; + u64 reset_calls = 0; + u64 clear_calls = 0; + u64 clear_real = 0; + u64 clear_quick_nuked = 0; + u64 clear_quick_would_keep = 0; + u64 search_walks = 0; + u64 search_walk_total = 0; + u64 search_walk_max = 0; + u64 search_walk_min = std::numeric_limits::max(); + u64 search_matches = 0; + u64 search_match_pos_total = 0; + }; + + alignas(64) mVUCacheTrace g_mVUCacheTrace[2]; + + __fi void mVUCacheTraceObserveWalk(u32 vuIdx, u64 iterations, bool matched, u64 matchPos) + { + auto& t = g_mVUCacheTrace[vuIdx & 1]; + ++t.search_walks; + t.search_walk_total += iterations; + if (iterations > t.search_walk_max) + t.search_walk_max = iterations; + if (iterations < t.search_walk_min) + t.search_walk_min = iterations; + if (matched) + { + ++t.search_matches; + t.search_match_pos_total += matchPos; + } + } + + void mVUCacheTraceObserveClear(microVU& mVU, u32 addr, u32 size, bool wasRealClear) + { + auto& t = g_mVUCacheTrace[mVU.index & 1]; + ++t.clear_calls; + if (!wasRealClear) + return; + ++t.clear_real; + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + const microProgram* p = mVU.prog.quick[i].prog; + if (!p) + continue; + ++t.clear_quick_nuked; + if (!mVUProgRangesOverlap(p, addr, size)) + ++t.clear_quick_would_keep; + } + } + + void mVUCacheTraceDump(microVU& mVU, const char* tag) + { + auto& t = g_mVUCacheTrace[mVU.index & 1]; + u32 dequeSlots = 0; + u32 dequeProgs = 0; + u32 maxBucket = 0; + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + const microProgramList* list = mVU.prog.prog[i]; + if (!list || list->empty()) + continue; + ++dequeSlots; + const u32 sz = static_cast(list->size()); + dequeProgs += sz; + if (sz > maxBucket) + maxBucket = sz; + } + const u64 walkAvg = t.search_walks ? (t.search_walk_total / t.search_walks) : 0; + const u64 matchAvg = t.search_matches ? (t.search_match_pos_total / t.search_matches) : 0; + const u64 walkMin = (t.search_walk_min == std::numeric_limits::max()) ? 0 : t.search_walk_min; + const ConsoleColors color = mVU.index ? Color_Orange : Color_Magenta; + DevCon.WriteLn(color, + "mVU%u trace [%s]: created=%llu resets=%llu clears=%llu (real=%llu nuked=%llu wouldKeep=%llu) " + "walks=%llu walkLen(min/avg/max)=%llu/%llu/%llu matches=%llu matchPosAvg=%llu " + "liveSlots=%u liveProgs=%u maxBucket=%u", + mVU.index, tag, + (unsigned long long)t.programs_created, + (unsigned long long)t.reset_calls, + (unsigned long long)t.clear_calls, + (unsigned long long)t.clear_real, + (unsigned long long)t.clear_quick_nuked, + (unsigned long long)t.clear_quick_would_keep, + (unsigned long long)t.search_walks, + (unsigned long long)walkMin, + (unsigned long long)walkAvg, + (unsigned long long)t.search_walk_max, + (unsigned long long)t.search_matches, + (unsigned long long)matchAvg, + dequeSlots, dequeProgs, maxBucket); + + std::vector v; + v.reserve(dequeProgs); + for (u32 pc = 0; pc < (mVU.progSize / 2); pc++) + { + microProgramList* list = mVU.prog.prog[pc]; + if (!list) + continue; + for (auto it = list->begin(); it != list->end(); ++it) + v.push_back(mVUrangesHash(mVU, *it[0])); + } + const u32 total = static_cast(v.size()); + std::sort(v.begin(), v.end()); + v.erase(std::unique(v.begin(), v.end()), v.end()); + if (total) + { + DevCon.WriteLn(color, + "mVU%u trace [%s]: dup ratio %u unique / %u total [%3.1f%% dup]", + mVU.index, tag, + static_cast(v.size()), total, + 100.0 - (double)v.size() / (double)total * 100.0); + } + } + + void mVUCacheTraceResetWindow(u32 vuIdx) + { + auto& t = g_mVUCacheTrace[vuIdx & 1]; + const u64 keep_resets = t.reset_calls; + const u64 keep_created = t.programs_created; + t = mVUCacheTrace{}; + t.reset_calls = keep_resets; + t.programs_created = keep_created; + } +} +#endif // mVUcacheTrace + +//------------------------------------------------------------------ +// Micro VU - Content-hash plumbing (xxhash3-128 program identity) +// +// Builds two hashes: +// +// mVU.optionsSentinel — 128-bit hash of every codegen-affecting build-time +// constexpr (doRegAlloc / noFlagOpts / doSFlagInsts +// / doMFlagInsts / doCFlagInsts / doBranchInDelaySlot +// / doConstProp / doJumpCaching / doJumpAsSameProgram +// / doDBitHandling / doWholeProgCompare) plus the +// runtime knobs the arm64 emitter branches on (VU0/1 +// clamp modes, FPCR bitmasks, vuFlagHack, EECycleRate +// / EECycleSkip, IbitHack, VUSyncHack / +// FullVU0SyncHack, VuAddSubHack, VUOverflowHack). +// Rebuilt at init and reset. +// +// prog.contentHash — 128-bit hash of (kMvuCompilerAbiVersion | +// optionsSentinel | VU index | whole microMem image +// as cached on prog.data). Computed by mVUcacheProg +// after data is filled. This is the cross-process +// identity used as the on-disk cache key and the +// in-process contentMap key. +// +// contentHash is only *populated* here — it is not yet wired into the search +// fast path, because under doWholeProgCompare=false (the default) the bytes +// outside the recorded ranges are stale and short-circuiting on the whole-image +// hash would over-restrict matches. The contentMap consumes the hash with +// whole-image semantics. +//------------------------------------------------------------------ + +void mVUbuildOptionsSentinel(microVU& mVU) +{ + // 64-byte fixed-layout snapshot. Order is load-bearing for stability across + // rebuilds: changing it bumps the ABI version (kMvuCompilerAbiVersion). + struct alignas(8) Snapshot + { + u32 abiVersion; + u32 vuIndex; // 0/1 — guards against accidentally sharing sentinels across VUs + + // Build-time constexprs (microVU_Misc.h). One byte each, packed. + u8 doRegAlloc_; + u8 noFlagOpts_; + u8 doSFlagInsts_; + u8 doMFlagInsts_; + u8 doCFlagInsts_; + u8 doBranchInDelaySlot_; + u8 doConstProp_; + u8 doJumpCaching_; + u8 doJumpAsSameProgram_; + u8 doDBitHandling_; + u8 doWholeProgCompare_; + u8 pad0; + + // Clamp modes (Cpu.Recompiler.vu{0,1}{Overflow,ExtraOverflow,SignOverflow,Underflow}). + u8 vu0Overflow; + u8 vu0ExtraOverflow; + u8 vu0SignOverflow; + u8 vu0Underflow; + u8 vu1Overflow; + u8 vu1ExtraOverflow; + u8 vu1SignOverflow; + u8 vu1Underflow; + + // Speedhacks / Gamefixes that gate emit shape. + u8 vuFlagHack; + s8 EECycleRate; + u8 EECycleSkip; + u8 IbitHack; + u8 VUSyncHack; + u8 FullVU0SyncHack; + u8 VuAddSubHack; + u8 VUOverflowHack; + + // FPCR bitmasks. mVU emits MSR FPCR loads that pick between EE / VU0 / + // VU1 FPCRs based on the configured rounding/flush bits. + u32 fpuFpcr; + u32 vu0Fpcr; + u32 vu1Fpcr; + // mVUPersist emit-time recording (the persisted-JIT program cache). + // Recording changes emitted code forms — canonical movz+movk×3 for + // self-block pointers, forced-long cross-chunk cond branches — so a + // recording-enabled cache must never be matched against a recording- + // disabled run. This field reclaims a zeroed reserved byte, so the + // recording-OFF sentinel is bit-identical to the pre-recording one. + u8 progCacheRecording; + // Reserved tail so adding a future option byte doesn't shift downstream + // fields. Any expansion that consumes these bytes is a codegen-shape + // change → bump kMvuCompilerAbiVersion in the same commit. + u8 reserved[11]; + }; + static_assert(sizeof(Snapshot) == 64, "options sentinel layout drifted — bump kMvuCompilerAbiVersion"); + + Snapshot s = {}; + s.abiVersion = kMvuCompilerAbiVersion; + s.vuIndex = mVU.index; + + s.doRegAlloc_ = doRegAlloc ? 1 : 0; + s.noFlagOpts_ = noFlagOpts ? 1 : 0; + s.doSFlagInsts_ = doSFlagInsts ? 1 : 0; + s.doMFlagInsts_ = doMFlagInsts ? 1 : 0; + s.doCFlagInsts_ = doCFlagInsts ? 1 : 0; + s.doBranchInDelaySlot_ = doBranchInDelaySlot ? 1 : 0; + s.doConstProp_ = doConstProp ? 1 : 0; + s.doJumpCaching_ = doJumpCaching ? 1 : 0; + s.doJumpAsSameProgram_ = doJumpAsSameProgram ? 1 : 0; + s.doDBitHandling_ = doDBitHandling ? 1 : 0; + s.doWholeProgCompare_ = doWholeProgCompare ? 1 : 0; + + s.vu0Overflow = EmuConfig.Cpu.Recompiler.vu0Overflow ? 1 : 0; + s.vu0ExtraOverflow = EmuConfig.Cpu.Recompiler.vu0ExtraOverflow ? 1 : 0; + s.vu0SignOverflow = EmuConfig.Cpu.Recompiler.vu0SignOverflow ? 1 : 0; + s.vu0Underflow = EmuConfig.Cpu.Recompiler.vu0Underflow ? 1 : 0; + s.vu1Overflow = EmuConfig.Cpu.Recompiler.vu1Overflow ? 1 : 0; + s.vu1ExtraOverflow = EmuConfig.Cpu.Recompiler.vu1ExtraOverflow ? 1 : 0; + s.vu1SignOverflow = EmuConfig.Cpu.Recompiler.vu1SignOverflow ? 1 : 0; + s.vu1Underflow = EmuConfig.Cpu.Recompiler.vu1Underflow ? 1 : 0; + + s.vuFlagHack = EmuConfig.Speedhacks.vuFlagHack ? 1 : 0; + s.EECycleRate = static_cast(EmuConfig.Speedhacks.EECycleRate); + s.EECycleSkip = static_cast(EmuConfig.Speedhacks.EECycleSkip); + s.IbitHack = EmuConfig.Gamefixes.IbitHack ? 1 : 0; + s.VUSyncHack = EmuConfig.Gamefixes.VUSyncHack ? 1 : 0; + s.FullVU0SyncHack = EmuConfig.Gamefixes.FullVU0SyncHack ? 1 : 0; + s.VuAddSubHack = EmuConfig.Gamefixes.VuAddSubHack ? 1 : 0; + s.VUOverflowHack = EmuConfig.Gamefixes.VUOverflowHack ? 1 : 0; + + s.fpuFpcr = EmuConfig.Cpu.FPUFPCR.bitmask; + s.vu0Fpcr = EmuConfig.Cpu.VU0FPCR.bitmask; + s.vu1Fpcr = EmuConfig.Cpu.VU1FPCR.bitmask; + + s.progCacheRecording = mVUPersist::IsRecordingEnabled() ? 1 : 0; + + mVU.optionsSentinel = XXH3_128bits(&s, sizeof(s)); + mVU.optionsSentinelValid = true; +} + +XXH128_hash_t mVUcomputeProgramHash(microVU& mVU) +{ + if (!mVU.optionsSentinelValid) + mVUbuildOptionsSentinel(mVU); + + // Streaming hash: ABI | sentinel | VU index | whole microMem snapshot. + // Total input is 16 KB + ~28 B for VU1, 4 KB + ~28 B for VU0 — single shot + // would force a temporary buffer; streaming lets us fold the prologue + // without allocation. + XXH3_state_t state; + XXH3_128bits_reset(&state); + + const u32 abi = kMvuCompilerAbiVersion; + XXH3_128bits_update(&state, &abi, sizeof(abi)); + XXH3_128bits_update(&state, &mVU.optionsSentinel, sizeof(mVU.optionsSentinel)); + + const u8 idx = static_cast(mVU.index); + XXH3_128bits_update(&state, &idx, sizeof(idx)); + + XXH3_128bits_update(&state, mVU.regs().Micro, mVU.microMemSize); + + return XXH3_128bits_digest(&state); +} + +//------------------------------------------------------------------ +// Micro VU - contentMap helpers +// +// The contentMap is the single owner of every live microProgram. Per-startPC +// deques and quick slots are non-owning references whose lifetimes are bounded +// by mVUreset / explicit eviction. These helpers keep the map / refcount / +// deque invariants in one place so mVUsearchProg / mVUcreateProg don't open- +// code the bookkeeping. +//------------------------------------------------------------------ + +// Insert `prog` into the contentMap. Caller must have set `prog->contentHash` +// + `contentHashValid` already (mVUcreateProg does this immediately after +// computing the hash). Asserts the entry is unique: emplace on the unordered_map +// is a no-op if the hash already maps a program, which would silently drop the +// new prog while leaving the stale one in place. +static __fi void mVUcontentMapInsert(microVU& mVU, microProgram* prog) +{ + [[maybe_unused]] const bool inserted = mVU.mvuContentMap.emplace(prog->contentHash, prog).second; + pxAssert(inserted); +} + +// Push `prog` onto the front of `list` if not already present; bump refcount +// once per insertion. Idempotent — re-finding via contentMap shouldn't grow +// the deque past a single entry per program-per-PC. +static __fi void mVUdequePushUnique(microProgramList* list, microProgram* prog) +{ + for (microProgram* p : *list) + { + if (p == prog) + return; + } + list->push_front(prog); + ++prog->refcount; +} + +//------------------------------------------------------------------ +// Micro VU - Main Functions +//------------------------------------------------------------------ + +void mVUinit(microVU& mVU, uint vuIndex) +{ + std::memset(&mVU.prog, 0, sizeof(mVU.prog)); + + mVU.index = vuIndex; + mVU.cop2 = 0; + mVU.vuMemSize = (mVU.index ? 0x4000 : 0x1000); + mVU.microMemSize = (mVU.index ? 0x4000 : 0x1000); + mVU.progSize = (mVU.index ? 0x4000 : 0x1000) / 4; + mVU.progMemMask = mVU.progSize-1; + mVU.cache = vuIndex ? SysMemory::GetVU1Rec() : SysMemory::GetVU0Rec(); + mVU.prog.x86end = (vuIndex ? SysMemory::GetVU1RecEnd() : SysMemory::GetVU0RecEnd()) - (mVUcacheSafeZone * _1mb); + + mVU.regAlloc.reset(new microRegAlloc(mVU.index)); + + // Persisted-JIT recording follows the config bool — established before the + // sentinel (which bakes the recording byte). At boot this runs before + // settings finish loading, so it typically latches off and the first + // mVUreset corrects it. No-op under the test-manual override. See mVUreset. + mVUPersist::SyncRecordingFromConfig(EmuConfig.Cpu.Recompiler.EnableVUProgramCache); + + // Seed options sentinel from current config snapshot. Reset will rebuild it + // in case the user toggled clamp / FPCR / speedhack settings since init. + mVUbuildOptionsSentinel(mVU); + + // Open the on-disk program cache for this VU. Must run after + // mVUbuildOptionsSentinel because the VERSION-header handshake mixes the + // sentinel; a cache built with a different options layout is evicted here. + mVUProgCache::Init(mVU); +} + +//------------------------------------------------------------------ +// ARM64 Stub Dispatchers +//------------------------------------------------------------------ + +// Real dispatchers that enter/exit JIT blocks properly. +// Matches x86 mVUdispatcherAB pattern: save callee regs, call execute +// (returns block ptr), load VU state, jump to block, exit saves state +// and calls cleanup. + +// Emit: ldr x9, [addr]; msr FPCR, x9 — switches FPCR to the value stored at +// `addr`. Called at dispatcher entry to force the VU's FPCR before JIT blocks +// run, and at exit to restore the EE's FPCR. +static void mVUemitLoadFPCR(const u64* addr) +{ + armMoveAddressToReg(a64::x8, (void*)addr); + armAsm->Ldr(a64::x9, a64::MemOperand(a64::x8)); + armAsm->Msr(a64::FPCR, a64::x9); +} + +// Mirrors x86 microVU_Execute.inl:mvuNeedsFPCRUpdate. The MTVU thread starts +// with a stale FPCR so we always reload there; otherwise reload is only +// needed when the configured EE/VU rounding modes differ. +static bool mvuNeedsFPCRUpdate(mV) +{ + if (isVU1 && THREAD_VU1) + return true; + + return EmuConfig.Cpu.FPUFPCR.bitmask != (isVU0 ? EmuConfig.Cpu.VU0FPCR.bitmask : EmuConfig.Cpu.VU1FPCR.bitmask); +} + +static void mVUdispatcherAB(mV) +{ + mVU.startFunct = armStartBlock(); + + // Save callee-saved GPRs and LR + armAsm->Stp(a64::x29, a64::x30, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::x19, a64::x20, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::x21, a64::x22, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::x23, a64::x24, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::x25, a64::x26, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::x27, a64::x28, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + // Save callee-saved NEON (d8-d15) + armAsm->Stp(a64::d8, a64::d9, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::d10, a64::d11, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::d12, a64::d13, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + armAsm->Stp(a64::d14, a64::d15, a64::MemOperand(a64::sp, -16, a64::PreIndex)); + + // Park PS2 FPU clamp constants. AAPCS64 preserves the lower 64 bits of + // d8/d9 across mVUexecuteVU0/1, mVUcompile, and every other C call + // reachable from inside this dispatcher. Matches the EE dispatcher's + // convention so iCOP2 scalar clamps (which can execute inside macro-mode + // VU emissions) and any future scalar FPU work share s8/s9. + armAsm->Ldr(a64::s8, FLT_MAX); + armAsm->Ldr(a64::s9, -FLT_MAX); + + // Inline mVUlookupProg fast path: stash w0/w1 into callee-saved + // w26/w27, BL the lookup-only wrapper (mVUlookupProg_VU0/1) which + // does the cycle setup + lookup but not the slow path. If the + // lookup returns nullptr, fall through to the full mVUexecuteVU0/1 + // BL with the original args restored from w26/w27. This adds one + // BL on the rare miss path (the slow path re-runs the cycle setup + // harmlessly) and keeps the hit-path callee body to just the + // lookup; the BL count on the hit path is unchanged. + // + // Returns compiled block entry point in x0; x0 may be nullptr if + // the slow path's compile failed (cbz exitLabel below catches it). + armAsm->Mov(a64::w26, a64::w0); // stash startPC + armAsm->Mov(a64::w27, a64::w1); // stash cycles + + armEmitCall(isVU1 ? (void*)mVUlookupProg_VU1 : (void*)mVUlookupProg_VU0); + + a64::Label gotHostEntry; + armAsm->Cbnz(a64::x0, &gotHostEntry); + + // Miss: restore args and run the full slow path. + armAsm->Mov(a64::w0, a64::w26); + armAsm->Mov(a64::w1, a64::w27); + armEmitCall(isVU1 ? (void*)mVUexecuteVU1 : (void*)mVUexecuteVU0); + + armAsm->Bind(&gotHostEntry); + // x0 holds the block pointer; we keep it there until the Br below. + // FPCR setup, gprVUState pin, and the flag/PQ loads emit no calls and + // don't touch x0, so it survives across them. + + // Pin gprVUState (x19) = &mVU.regs(). All subsequent regs() field accesses + // (here in the dispatcher and in compiled blocks) use [gprVUState, #off] + // instead of paying the 3-insn movz/movk/movk address-materialization tax. + // Address is constant per-VU (= &vuRegs[mVU.index]), so we set this once + // per dispatch and never re-pin. Survives armEmitCall (callee-saved). + armMoveAddressToReg(gprVUState, &mVU.regs()); + + // Pin gprMVUFlag (x24) = &mVU.macFlag[0]. Reaches statFlag / macFlag / + // clipFlag / neonCTemp / neonBackup via signed [+/-imm12] (see + // microVU_Misc-arm64.h). Lets every flag-touching FMAC drop the 3-insn + // abs-addr materialization down to a single ldr/str. + armMoveAddressToReg(gprMVUFlag, mVU.macFlag); + + // Pin gprMVUglob (x25) = &mVUglob. Every clamp / FTOI / ITOF / EATAN / + // SQRT et al. constant load goes via [gprMVUglob, #imm12] instead of + // materializing the global's absolute address. + armMoveAddressToReg(gprMVUglob, (void*)&mVUglob); + + // Load VU-specific FPCR (round-toward-zero + FZ/DaZ) — only when needed. + // PS2 VU float ops require this rounding mode; the gating skips the + // reload when EE and VU FPCR configs already match (the default). + if (mvuNeedsFPCRUpdate(mVU)) + mVUemitLoadFPCR(isVU0 ? &EmuConfig.Cpu.VU0FPCR.bitmask : &EmuConfig.Cpu.VU1FPCR.bitmask); + + // Load macro/clip flags from VU state into microVU shadow copies via the + // pinned base. Shadow copies live in `microVU` not `VURegs`. + armAsm->Ldr(a64::q0, mVUstateMem(offsetof(VURegs, micro_macflags))); + armAsm->Str(a64::q0, a64::MemOperand(gprMVUFlag)); + + armAsm->Ldr(a64::q0, mVUstateMem(offsetof(VURegs, micro_clipflags))); + armAsm->Str(a64::q0, a64::MemOperand(gprMVUFlag, 16)); + + // Load status flag instances into callee-saved GPRs + armAsm->Ldr(gprF0, mVUstateMem(offsetof(VURegs, micro_statusflags) + 0)); + armAsm->Ldr(gprF1, mVUstateMem(offsetof(VURegs, micro_statusflags) + 4)); + armAsm->Ldr(gprF2, mVUstateMem(offsetof(VURegs, micro_statusflags) + 8)); + armAsm->Ldr(gprF3, mVUstateMem(offsetof(VURegs, micro_statusflags) + 12)); + + // Load P/Q into qmmPQ + // x86 packs P, Q, pending_q, pending_p into xmmPQ via shuffles. + // For now, load Q and pending_q into lanes 0,1 (P is VU1-only). + armAsm->Ldr(a64::s0, mVUstateMem(offsetof(VURegs, VI) + REG_Q * sizeof(REG_VI))); + armAsm->Ldr(a64::s1, mVUstateMem(offsetof(VURegs, pending_q))); + // Pack into qmmPQ: [0]=Q, [1]=pending_q, [2]=P, [3]=pending_p + armAsm->Ins(qmmPQ.V4S(), 0, a64::q0.V4S(), 0); + armAsm->Ins(qmmPQ.V4S(), 1, a64::q1.V4S(), 0); + if (isVU1) + { + armAsm->Ldr(a64::s0, mVUstateMem(offsetof(VURegs, VI) + REG_P * sizeof(REG_VI))); + armAsm->Ldr(a64::s1, mVUstateMem(offsetof(VURegs, pending_p))); + armAsm->Ins(qmmPQ.V4S(), 2, a64::q0.V4S(), 0); + armAsm->Ins(qmmPQ.V4S(), 3, a64::q1.V4S(), 0); + } + + // Jump to compiled block (address still in x0 from mVUexecuteVU return). + // Safety: if block ptr is NULL, fall through to exit path + a64::Label exitLabel; + armAsm->Cbz(a64::x0, &exitLabel); + armAsm->Br(a64::x0); + + // === Exit path === (blocks jump here when done) + armAsm->Bind(&exitLabel); + mVU.exitFunct = armGetCurrentCodePointer(); + + // Restore EE FPCR before returning to C++ (mVUcleanUp + caller) — same + // gating as the entry path. + if (mvuNeedsFPCRUpdate(mVU)) + mVUemitLoadFPCR(&EmuConfig.Cpu.FPUFPCR.bitmask); + + // Save status flags back to VU state via gprVUState (still pinned across + // the block-exit path; restored only by the final Ldp below). + armAsm->Str(gprF0, mVUstateMem(offsetof(VURegs, micro_statusflags) + 0)); + armAsm->Str(gprF1, mVUstateMem(offsetof(VURegs, micro_statusflags) + 4)); + armAsm->Str(gprF2, mVUstateMem(offsetof(VURegs, micro_statusflags) + 8)); + armAsm->Str(gprF3, mVUstateMem(offsetof(VURegs, micro_statusflags) + 12)); + + // mVUcleanUp logic inlined in this exit stub. The C++ helper (mVUcleanUpVU0/1) + // is only called on the rare bounds-violation path (program cache exhausted → + // reset). Cycle accounting and the EE-cycle-skip math (the common cost) + // are emitted inline so dispatch costs no `bl` on the hot path. + // + // 1) Cycle accounting (always hot). + // 2) Cache-bounds check (rare false): if out-of-range, tail to C++ which + // runs mVUreset + EE-skip math + profiler.Print(). + // 3) EE cycle skip math (inline; default EECycleSkip=0 short-circuits at + // the first cbz). VU1+THREAD_VU1 skips the math (matches C++). + // 4) profiler.Print() is __fi {} in default builds → elided. + // + // gprVUState (x19) stays pinned to &mVU.regs() across this whole stub. + a64::Label cleanUpReturn, cleanUpResetTail, eeSkipDone; + + // (1) Cache-bounds check: out-of-range → reset tail. This MUST precede the + // inline cycle math below. On the reset path the C++ mVUcleanUp re-runs the + // same cycle accounting (microVU_Execute.inl: mVU.cycles = totalCycles - + // max(0,mVU.cycles); mVU.regs().cycle += mVU.cycles), so if the inline math + // ran first regs().cycle would be incremented twice — double-counting VU + // cycles up to totalCycles. Branching before the inline math leaves exactly + // one cycle update on each path: inline on the normal path, C++ on the reset + // path. x86ptr / x86start / x86end are three consecutive 8-byte + // fields in microProgManager. + static_assert(offsetof(microProgManager, x86start) == offsetof(microProgManager, x86ptr) + 8, + "inline bounds check expects x86ptr/x86start/x86end adjacent"); + static_assert(offsetof(microProgManager, x86end) == offsetof(microProgManager, x86ptr) + 16, + "inline bounds check expects x86ptr/x86start/x86end adjacent"); + armMoveAddressToReg(a64::x8, &mVU.prog.x86ptr); + armAsm->Ldr(a64::x9, a64::MemOperand(a64::x8)); // x86ptr + armAsm->Ldr(a64::x10, a64::MemOperand(a64::x8, 8)); // x86start + armAsm->Cmp(a64::x9, a64::x10); + armAsm->B(&cleanUpResetTail, a64::lt); + armAsm->Ldr(a64::x10, a64::MemOperand(a64::x8, 16)); // x86end + armAsm->Cmp(a64::x9, a64::x10); + armAsm->B(&cleanUpResetTail, a64::ge); + + // (2) Cycle math (inline; normal in-range path only — the reset tail lets + // the C++ mVUcleanUp do this exactly once instead). + armMoveAddressToReg(a64::x8, &mVU.totalCycles); + armAsm->Ldr(a64::w10, a64::MemOperand(a64::x8)); // totalCycles + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8, 4)); // cycles + armAsm->Cmp(a64::w9, 0); + armAsm->Csel(a64::w9, a64::w9, a64::wzr, a64::gt); // max(0, cycles) + armAsm->Sub(a64::w9, a64::w10, a64::w9); // totalCycles - max(0,c) + armAsm->Str(a64::w9, a64::MemOperand(a64::x8, 4)); // mVU.cycles = ... + // VURegs::cycle is u64 (matching cpuRegs.cycle); the add MUST be 64-bit or + // the carry is dropped once the low 32 bits wrap (~every 14s of EE time), + // leaving VU0.cycle ~4 billion below the EE clock and detonating every + // _vu0run (s64)(cpuRegs.cycle - VU0.cycle) sync. x86 mVUcleanUp does the same + // u64 += s32 cycle add; a 32-bit add is only correct when cycle fields + // are u32 (prior to widening to u64). w9 holds the consumed + // cycle count (s32, non-negative here) — sign-extend into the 64-bit add. + armAsm->Ldr(a64::x10, mVUstateMem(offsetof(VURegs, cycle))); + armAsm->Add(a64::x10, a64::x10, a64::Operand(a64::w9, a64::SXTW)); + armAsm->Str(a64::x10, mVUstateMem(offsetof(VURegs, cycle))); + + // (3) EE cycle skip math (inline). Equivalent C++ (mVUcleanUp body): + // u32 cycles_passed = std::min(mVU.cycles, 3000) * EECycleSkip; + // if (cycles_passed > 0) { + // cpuRegs.cycle += cycles_passed; // u64 += u32 + // VU0.cycle += cycles_passed; // u64 += u32 + // } + // Both arms of the C++ if (`!vuIndex` and `else`) collapse to the same + // arithmetic effect (VU0.cycle += cycles_passed) because + // VU0.cycle = (cpuRegs.cycle + cycles_passed) + (VU0.cycle - cpuRegs.cycle) + // = VU0.cycle + cycles_passed. + // The 64-bit add is preserved here so the long-running cpuRegs.cycle + // counter doesn't lose the carry across its low-32-bit boundary + // (cpuRegs.cycle wraps low-32 every ~14 s of EE time at 1.0×). + // + // VU1+THREAD_VU1 skips the math entirely — MTVU runs the dispatcher on + // the VU1 thread, where touching cpuRegs.cycle is wrong (matches C++). + armMoveAddressToReg(a64::x8, &EmuConfig.Speedhacks.EECycleSkip); + armAsm->Ldrb(a64::w11, a64::MemOperand(a64::x8)); + armAsm->Cbz(a64::w11, &eeSkipDone); // EECycleSkip == 0 → skip + + if (isVU1) + { + // THREAD_VU1 = REC_VU1 && Speedhacks.vuThread. + // EnableVU1 = bit 3 of EmuConfig.Cpu.Recompiler.bitset. + // vuThread = bit 4 of EmuConfig.Speedhacks.bitset. + a64::Label vu1DoEEAdjust; + armMoveAddressToReg(a64::x8, &EmuConfig.Cpu.Recompiler.bitset); + armAsm->Ldr(a64::w10, a64::MemOperand(a64::x8)); + armAsm->Tbz(a64::w10, 3, &vu1DoEEAdjust); // !EnableVU1 → !THREAD_VU1 + armMoveAddressToReg(a64::x8, &EmuConfig.Speedhacks.bitset); + armAsm->Ldr(a64::w10, a64::MemOperand(a64::x8)); + armAsm->Tbnz(a64::w10, 4, &eeSkipDone); // vuThread → THREAD_VU1, skip + armAsm->Bind(&vu1DoEEAdjust); + } + + // cycles_passed = min(mVU.cycles, 3000) * EECycleSkip (w11) + armMoveAddressToReg(a64::x8, &mVU.totalCycles); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8, 4)); // mVU.cycles (post-update) + armAsm->Mov(a64::w10, 3000); + armAsm->Cmp(a64::w9, a64::w10); + armAsm->Csel(a64::w9, a64::w9, a64::w10, a64::lt); // min(cycles, 3000) + armAsm->Mul(a64::w9, a64::w9, a64::w11); // * EECycleSkip; W-write zeroes top of x9 + armAsm->Cbz(a64::w9, &eeSkipDone); // cycles_passed == 0 → skip + + // cpuRegs.cycle (u64) += cycles_passed (zero-extended in x9 by the Mul). + armMoveAddressToReg(a64::x8, &cpuRegs.cycle); + armAsm->Ldr(a64::x10, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x10, a64::x10, a64::x9); + armAsm->Str(a64::x10, a64::MemOperand(a64::x8)); + + // VU0.cycle (u64) += cycles_passed. + if (isVU0) + { + // gprVUState already points at &VU0; piggyback off the pin. + armAsm->Ldr(a64::x10, mVUstateMem(offsetof(VURegs, cycle))); + armAsm->Add(a64::x10, a64::x10, a64::x9); + armAsm->Str(a64::x10, mVUstateMem(offsetof(VURegs, cycle))); + } + else + { + // VU1 dispatcher: gprVUState points at &VU1, so address &VU0 directly. + armMoveAddressToReg(a64::x8, &VU0.cycle); + armAsm->Ldr(a64::x10, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x10, a64::x10, a64::x9); + armAsm->Str(a64::x10, a64::MemOperand(a64::x8)); + } + + armAsm->Bind(&eeSkipDone); + armAsm->B(&cleanUpReturn); + + // Rare reset tail. C++ helper does the bounds check + mVUreset + the cycle + // accounting + the EE-skip math + profiler.Print(). On this bounds-violated + // path the inline cycle math (2) and inline EE-skip (3) above are BOTH + // skipped — the bounds branch precedes them — so the C++ mVUcleanUp performs + // each exactly once with no double-counting. ≪0.01% of dispatches. + armAsm->Bind(&cleanUpResetTail); + armEmitCall(isVU1 ? (void*)mVUcleanUpVU1 : (void*)mVUcleanUpVU0); + + armAsm->Bind(&cleanUpReturn); + + // Restore callee-saved NEON + armAsm->Ldp(a64::d14, a64::d15, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::d12, a64::d13, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::d10, a64::d11, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::d8, a64::d9, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + // Restore callee-saved GPRs and LR + armAsm->Ldp(a64::x27, a64::x28, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::x25, a64::x26, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::x23, a64::x24, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::x21, a64::x22, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::x19, a64::x20, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + armAsm->Ldp(a64::x29, a64::x30, a64::MemOperand(a64::sp, 16, a64::PostIndex)); + + armAsm->Ret(); + + u8* end = armEndBlock(); + + Perf::any.Register(mVU.startFunct, static_cast(end - mVU.startFunct), + mVU.index ? "VU1StartFunc" : "VU0StartFunc"); +} + +static void mVUdispatcherCD(mV) +{ + // XGkick resume dispatcher: a bare Ret. The resume-from-XGKICK-break path + // that this would jump into is #if-0'd out upstream (resumePtrXG is only + // written from that dead block), so there are no callers and the stub never + // needs to restore state or jump anywhere. + mVU.startFunctXG = armStartBlock(); + armAsm->Ret(); + u8* end = armEndBlock(); + mVU.exitFunctXG = end; + + Perf::any.Register(mVU.startFunctXG, static_cast(end - mVU.startFunctXG), + mVU.index ? "VU1StartFuncXG" : "VU0StartFuncXG"); +} + +static void mVUGenerateWaitMTVU(mV) +{ + mVU.waitMTVU = armStartBlock(); + armAsm->Ret(); + armEndBlock(); +} + +static void mVUGenerateCopyPipelineState(mV) +{ + mVU.copyPLState = armStartBlock(); + + // x0 = source pointer to microRegInfo (96 bytes) + // Copy 96 bytes (6 x 16-byte loads) to mVU.prog.lpState + const a64::Register src = a64::x0; + + armMoveAddressToReg(a64::x1, &mVU.prog.lpState); + + // 96 bytes = 6 x LDR/STR Q or 3 x LDP/STP Q + armAsm->Ldp(a64::q0, a64::q1, a64::MemOperand(src, 0)); + armAsm->Ldp(a64::q2, a64::q3, a64::MemOperand(src, 32)); + armAsm->Ldp(a64::q4, a64::q5, a64::MemOperand(src, 64)); + + armAsm->Stp(a64::q0, a64::q1, a64::MemOperand(a64::x1, 0)); + armAsm->Stp(a64::q2, a64::q3, a64::MemOperand(a64::x1, 32)); + armAsm->Stp(a64::q4, a64::q5, a64::MemOperand(a64::x1, 64)); + + armAsm->Ret(); + armEndBlock(); +} + +// Emit the two SFLAGc + microflag tail helpers used by mVUendProgram. +// Each exit thunk calls one of these helpers with a single mov+bl pair +// rather than inlining the full STATUS-denorm + micro_flag +// backup-or-broadcast sequence, keeping per-thunk code size small. +// +// ABI: +// Input : w11 (gprT3) = caller-evaluated getFlagReg(fStatus) value +// Clobbers: w9 (gprT1), w11, q0/v0 +// Reads : pinned x19 (gprVUState), x24 (gprMVUFlag), w20..w23 (gprF0..3) +// Returns via ret using LR set by the bl at the caller +static void mVUGenerateEndProgramFlagsHelper(mV) +{ + auto emitSFLAGc = [&]() { + // Mirrors mVUallocSFLAGc body byte-for-byte but with reg=w9, regT=w11 + armAsm->Mov(a64::w9, 0); + auto setBit = [&](int bitTest, int bitSet) { + armAsm->Tst(a64::w11, bitTest); + a64::Label skip; + armAsm->B(&skip, a64::eq); + armAsm->Orr(a64::w9, a64::w9, bitSet); + armAsm->Bind(&skip); + }; + setBit(0x0f00, 0x0001); // Z bit + setBit(0xf000, 0x0002); // S bit + setBit(0x000f, 0x0040); // ZS bit + setBit(0x00f0, 0x0080); // SS bit + armAsm->And(a64::w11, a64::w11, 0xffff0000u); + armAsm->Lsr(a64::w11, a64::w11, 14); + armAsm->Orr(a64::w9, a64::w9, a64::w11); + armAsm->Str(a64::w9, + mVUstateMem(offsetof(VURegs, VI) + REG_STATUS_FLAG * sizeof(REG_VI))); + }; + + // Helper A — non-Ebit (isEbit == 0 || isEbit == 3): backup all 4 flag + // instances into micro_*flags[] for block-link restore. + mVU.endProgramFlagsA = armStartBlock(); + { + emitSFLAGc(); + armAsm->Ldr(a64::q0, a64::MemOperand(gprMVUFlag)); + armAsm->Str(a64::q0, mVUstateMem(offsetof(VURegs, micro_macflags))); + armAsm->Ldr(a64::q0, a64::MemOperand(gprMVUFlag, 16)); + armAsm->Str(a64::q0, mVUstateMem(offsetof(VURegs, micro_clipflags))); + armAsm->Str(gprF0, mVUstateMem(offsetof(VURegs, micro_statusflags) + 0)); + armAsm->Str(gprF1, mVUstateMem(offsetof(VURegs, micro_statusflags) + 4)); + armAsm->Str(gprF2, mVUstateMem(offsetof(VURegs, micro_statusflags) + 8)); + armAsm->Str(gprF3, mVUstateMem(offsetof(VURegs, micro_statusflags) + 12)); + armAsm->Ret(); + } + armEndBlock(); + + // Helper B — Ebit (isEbit && isEbit != 3): broadcast the just-stored + // MAC/CLIP/STATUS values across all 4 instances. The caller must have + // already stored MAC_FLAG and CLIP_FLAG to VURegs before calling + // (per-callsite, because fMac/fClip vary). Broadcast happens before + // SFLAGc because SFLAGc destroys w11. + mVU.endProgramFlagsB = armStartBlock(); + { + armAsm->Ldr(a64::w9, + mVUstateMem(offsetof(VURegs, VI) + REG_CLIP_FLAG * sizeof(REG_VI))); + armAsm->Dup(a64::q0.V4S(), a64::w9); + armAsm->Str(a64::q0, mVUstateMem(offsetof(VURegs, micro_clipflags))); + armAsm->Ldr(a64::w9, + mVUstateMem(offsetof(VURegs, VI) + REG_MAC_FLAG * sizeof(REG_VI))); + armAsm->Dup(a64::q0.V4S(), a64::w9); + armAsm->Str(a64::q0, mVUstateMem(offsetof(VURegs, micro_macflags))); + armAsm->Dup(a64::q0.V4S(), a64::w11); + armAsm->Str(a64::q0, mVUstateMem(offsetof(VURegs, micro_statusflags))); + + emitSFLAGc(); + armAsm->Ret(); + } + armEndBlock(); +} + +// Resets Rec Data +void mVUreset(microVU& mVU, bool resetReserve) +{ +#ifdef mVUcacheTrace + mVUCacheTraceDump(mVU, "pre-reset"); + ++g_mVUCacheTrace[mVU.index & 1].reset_calls; +#endif + + // Persisted-JIT recording follows the EnableVUProgramCache config bool, and + // MUST be established here — before mVUbuildOptionsSentinel bakes the + // recording byte and before any gameplay program compiles. This is the + // authoritative sync point: InitializeCPUProviders runs before settings + // load the bool, so a one-shot enable there would latch the default (off) + // and never correct, leaving the disk cache writing telemetry-only entries + // with no payloads. The disk Init below is re-synced on the same reset, so + // recording and the cache move in lockstep. No-op under the test-manual + // override (the recompiler-test harness drives recording itself). + mVUPersist::SyncRecordingFromConfig(EmuConfig.Cpu.Recompiler.EnableVUProgramCache); + + // Rebuild options sentinel before any program rebuilds — config may have + // changed since the last init/reset (clamp flips, FPCR edits, speedhack + // toggles, gamefix overrides). New programs created after this reset will + // hash against the up-to-date sentinel. + mVUbuildOptionsSentinel(mVU); + + // Re-enter the disk-cache init: a no-op when already up (or when the + // EnableVUProgramCache config bool is off), but the activation point when + // the user toggled the cache on — settings changes funnel through + // ClearCPUExecutionCaches → this reset. + mVUProgCache::Init(mVU); + + if (THREAD_VU1) + { + DevCon.Warning("mVU Reset"); + if (VU0.VI[REG_VPU_STAT].UL & 0x100) + { + CpuVU1->Execute(vu1RunCycles); + } + VU0.VI[REG_VPU_STAT].UL &= ~0x100; + } + + // Set up the code cache for vixl emission + const size_t cacheCapacity = static_cast(mVU.prog.x86end - mVU.cache); + armSetAsmPtr(mVU.cache, cacheCapacity, nullptr); + + mVUdispatcherAB(mVU); + mVUdispatcherCD(mVU); + mVUGenerateWaitMTVU(mVU); + mVUGenerateCopyPipelineState(mVU); + mVUGenerateEndProgramFlagsHelper(mVU); + + mVU.regs().nextBlockCycles = 0; + memset(&mVU.prog.lpState, 0, sizeof(mVU.prog.lpState)); + mVU.profiler.Reset(mVU.index); + + // Program Variables + mVU.prog.cleared = 1; + mVU.prog.isSame = -1; + mVU.prog.cur = NULL; + mVU.prog.total = 0; + mVU.prog.curFrame = 0; + + // Setup Dynarec Cache Limits for Each Program + // Note: armAsm is null between blocks, so use armGetAsmPtr() directly + mVU.prog.x86start = armGetAsmPtr(); + mVU.prog.x86ptr = mVU.prog.x86start; + + // Build the persistent MacroAssembler over the post-dispatcher region + // of the code cache. From here on, mVUopenCodeCache binds armAsm to + // this MA instead of allocating a fresh one per dispatch. + { + namespace a64 = vixl::aarch64; + const size_t blockCacheCapacity = static_cast(mVU.prog.x86end - mVU.prog.x86start); + mVU.jitAsm = std::make_unique( + static_cast(mVU.prog.x86start), blockCacheCapacity); + mVU.jitAsm->GetScratchVRegisterList()->Remove(31); + mVU.jitAsm->GetScratchRegisterList()->Remove(RSCRATCHADDR.GetCode()); + } + + // Checkpoint the live programs to the on-disk program cache before we + // free them. Subsequent process boots can hit this VU image without + // re-emitting. SaveAllPrograms is idempotent (no-op on hashes already + // in the on-disk INDEX) so calling on every reset is cheap. + mVUProgCache::SaveAllPrograms(mVU); + + // Single ownership lives in mVU.mvuContentMap. Free each program + // exactly once via the map iteration; per-PC deques and quick slots are + // non-owning references and just get cleared. + for (auto& entry : mVU.mvuContentMap) + { + microProgram* prog = entry.second; + mVUdeleteProg(mVU, prog); + } + mVU.mvuContentMap.clear(); + + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + if (!mVU.prog.prog[i]) + { + mVU.prog.prog[i] = new std::deque(); + continue; + } + mVU.prog.prog[i]->clear(); + mVU.prog.quick[i].block = NULL; + mVU.prog.quick[i].prog = NULL; + } + +#ifdef mVUcacheTrace + mVUCacheTraceResetWindow(mVU.index); +#endif +} + +// Free Allocated Resources +void mVUclose(microVU& mVU) +{ +#ifdef mVUcacheTrace + mVUCacheTraceDump(mVU, "shutdown"); +#endif + + // Final checkpoint of live programs to the on-disk cache before we let + // the contentMap go. Mirrors the mVUreset path; harmless if nothing + // new has been added since the last reset. + mVUProgCache::SaveAllPrograms(mVU); + mVUProgCache::Close(mVU); + + // Same ownership rule as mVUreset: free via contentMap, then + // drop the per-PC deque shells. + for (auto& entry : mVU.mvuContentMap) + { + microProgram* prog = entry.second; + mVUdeleteProg(mVU, prog); + } + mVU.mvuContentMap.clear(); + + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + if (!mVU.prog.prog[i]) + continue; + safe_delete(mVU.prog.prog[i]); + } +} + +// Clears Block Data in specified range +__fi void mVUclear(mV, u32 addr, u32 size) +{ +#ifdef mVUcacheTrace + mVUCacheTraceObserveClear(mVU, addr, size, /*wasRealClear=*/!mVU.prog.cleared); +#endif + + if (doWholeProgCompare) + { + // Whole-program compare — every program cares about every byte in + // microMem, so any overlap check is moot. Fall back to the original + // unconditional invalidate. + if (!mVU.prog.cleared) + { + mVU.prog.cleared = 1; + std::memset(&mVU.prog.lpState, 0, sizeof(mVU.prog.lpState)); + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + mVU.prog.quick[i].block = NULL; + mVU.prog.quick[i].prog = NULL; + } + } + return; + } + + // Range-aware path: only invalidate quick[i] whose cached program has a + // compiled range overlapping [addr, addr+size). Programs whose ranges are + // disjoint from the touched bytes stay quick-cached, skipping the per-PC + // deque walk on the next dispatch. + bool anyInvalidated = false; + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + const microProgram* p = mVU.prog.quick[i].prog; + if (!p) + continue; + if (mVUProgRangesOverlap(p, addr, size)) + { + mVU.prog.quick[i].block = NULL; + mVU.prog.quick[i].prog = NULL; + anyInvalidated = true; + } + } + + // lpState + cleared bookkeeping: only set cleared=1 / zero lpState when we + // actually invalidated something. Otherwise we leave the existing pipeline + // state intact — surviving quick.prog entries will re-enter with the same + // lpState they exited with, which remains valid because their compiled + // bytes weren't touched. + if (anyInvalidated && !mVU.prog.cleared) + { + mVU.prog.cleared = 1; + std::memset(&mVU.prog.lpState, 0, sizeof(mVU.prog.lpState)); + } +} + +//------------------------------------------------------------------ +// Micro VU - Private Functions +//------------------------------------------------------------------ + +__ri void mVUdeleteProg(microVU& mVU, microProgram*& prog) +{ + for (u32 i = 0; i < (mVU.progSize / 2); i++) + { + safe_delete(prog->block[i]); + } + safe_delete(prog->ranges); + mVUPersist::OnProgramDeleted(*prog); + safe_aligned_free(prog); +} + +__ri microProgram* mVUcreateProg(microVU& mVU, int startPC) +{ +#ifdef mVUcacheTrace + ++g_mVUCacheTrace[mVU.index & 1].programs_created; +#endif + + microProgram* prog = (microProgram*)_aligned_malloc(sizeof(microProgram), 64); + memset(prog, 0, sizeof(microProgram)); + prog->idx = mVU.prog.total++; + prog->ranges = new std::deque(); + prog->startPC = startPC; + prog->refcount = 0; // Caller increments when pushing into a per-PC deque. + // Record the creator's startPC (microMem byte offset) as the first + // observed entry. mVUsearchProg appends any additional PCs seen + // during the program's lifetime. + prog->observed.clear(); + prog->observed.record(static_cast(startPC) * 8u); + if(doWholeProgCompare) + mVUcacheProg(mVU, *prog); + + // Anchor the program's content identity from live microMem and register it + // in the per-VU contentMap. Done here (not in mVUcacheProg) so the hash is + // set even under !doWholeProgCompare where mVUcacheProg fires only later + // from mVUsetupRange. The hash stays stable for the program's lifetime — + // subsequent re-caches don't shift the contentMap key. + prog->contentHash = mVUcomputeProgramHash(mVU); + prog->contentHashValid = true; + mVUcontentMapInsert(mVU, prog); + + double cacheSize = (double)((uptr)mVU.prog.x86end - (uptr)mVU.prog.x86start); + double cacheUsed = ((double)((uptr)mVU.prog.x86ptr - (uptr)mVU.prog.x86start)) / (double)_1mb; + double cachePerc = ((double)((uptr)mVU.prog.x86ptr - (uptr)mVU.prog.x86start)) / cacheSize * 100; + ConsoleColors c = mVU.index ? Color_Orange : Color_Magenta; + DevCon.WriteLn(c, "microVU%d: Cached Prog = [%03d] [PC=%04x] [List=%02d] (Cache=%3.3f%%) [%3.1fmb]", + mVU.index, prog->idx, startPC * 8, mVU.prog.prog[startPC]->size() + 1, cachePerc, cacheUsed); + return prog; +} + +__ri void mVUcacheProg(microVU& mVU, microProgram& prog) +{ + if (!doWholeProgCompare) + { + auto cmpOffset = [&](void* x) { return (u8*)x + mVUrange.start; }; + memcpy(cmpOffset(prog.data), cmpOffset(mVU.regs().Micro), (mVUrange.end - mVUrange.start)); + } + else + { + if (!mVU.index) + memcpy(prog.data, mVU.regs().Micro, 0x1000); + else + memcpy(prog.data, mVU.regs().Micro, 0x4000); + } + mVUdumpProg(mVU, prog); + + // Do NOT recompute contentHash here. The hash is anchored at mVUcreateProg + // from live microMem and pinned for the program's lifetime so the contentMap + // key stays stable across mVUsetupRange-driven re-caches (range expansions + // don't change identity). +} + +u64 mVUrangesHash(microVU& mVU, microProgram& prog) +{ + union + { + u64 v64; + u32 v32[2]; + } hash = {0}; + + std::deque::const_iterator it(prog.ranges->begin()); + for (; it != prog.ranges->end(); ++it) + { + if ((it[0].start < 0) || (it[0].end < 0)) + { + DevCon.Error("microVU%d: Negative Range![%d][%d]", mVU.index, it[0].start, it[0].end); + } + for (int i = it[0].start / 4; i < it[0].end / 4; i++) + { + hash.v32[0] -= prog.data[i]; + hash.v32[1] ^= prog.data[i]; + } + } + return hash.v64; +} + +__fi bool mVUcmpProg(microVU& mVU, microProgram& prog) +{ + if (doWholeProgCompare) + { + if (memcmp((u8*)prog.data, mVU.regs().Micro, mVU.microMemSize)) + return false; + } + else + { + for (const auto& range : *prog.ranges) + { +#if defined(PCSX2_DEVBUILD) || defined(_DEBUG) + if ((range.start < 0) || (range.end < 0)) + DevCon.Error("microVU%d: Negative Range![%d][%d]", mVU.index, range.start, range.end); +#endif + auto cmpOffset = [&](void* x) { return (u8*)x + range.start; }; + + if (memcmp(cmpOffset(prog.data), cmpOffset(mVU.regs().Micro), (range.end - range.start))) + return false; + } + } + mVU.prog.cleared = 0; + mVU.prog.cur = &prog; + mVU.prog.isSame = doWholeProgCompare ? 1 : -1; + return true; +} + +// Searches for Cached Micro Program and sets prog.cur to it +_mVUt __fi void* mVUsearchProg(u32 startPC, uptr pState) +{ + microVU& mVU = mVUx; + microProgramQuick& quick = mVU.prog.quick[mVU.regs().start_pc / 8]; + microProgramList* list = mVU.prog.prog [mVU.regs().start_pc / 8]; + + if (!quick.prog) + { + // Cross-PC content-hash fast path. Hash the live microMem once; + // a contentMap hit reuses the existing microProgram across all + // startPCs that produce the same image, without paying the per-PC + // deque walk. Trusts the 128-bit xxh3 hash as the identity (no + // memcmp confirm — collision odds are astronomical and the on-disk + // cache uses the same key). + const XXH128_hash_t liveHash = mVUcomputeProgramHash(mVU); + auto cmIt = mVU.mvuContentMap.find(liveHash); + if (cmIt != mVU.mvuContentMap.end()) + { + microProgram* shared = cmIt->second; + mVUdequePushUnique(list, shared); + mVU.prog.cleared = 0; + mVU.prog.isSame = 1; + mVU.prog.cur = shared; + quick.prog = shared; + quick.block = shared->block[startPC / 8]; + // Record the dispatched entry on the resolved program. + // Idempotent on duplicates; bumps `observed.version` only + // when this PC is new for the program. + shared->observed.record(startPC); + if (quick.block == nullptr) + { + // First time this startPC is compiled for the shared program + // — drop through to mVUblockFetch to build the block. + void* entryPoint = mVUblockFetch(mVU, startPC, pState); + return entryPoint; + } + return mVUentryGet(mVU, quick.block, startPC, pState); + } + +#ifdef mVUcacheTrace + u64 walkIters = 0; +#endif + for (auto it = list->begin(); it != list->end(); ++it) + { +#ifdef mVUcacheTrace + ++walkIters; +#endif + bool b = mVUcmpProg(mVU, *it[0]); + + if (b) + { +#ifdef mVUcacheTrace + mVUCacheTraceObserveWalk(mVU.index, walkIters, /*matched=*/true, /*matchPos=*/walkIters - 1); +#endif + quick.block = it[0]->block[startPC / 8]; + quick.prog = it[0]; + list->erase(it); + list->push_front(quick.prog); + // Per-PC deque match resolved to this program; + // record the dispatched entry. + quick.prog->observed.record(startPC); + + if (quick.block == nullptr) + { + void* entryPoint = mVUblockFetch(mVU, startPC, pState); + return entryPoint; + } + return mVUentryGet(mVU, quick.block, startPC, pState); + } + } + +#ifdef mVUcacheTrace + mVUCacheTraceObserveWalk(mVU.index, walkIters, /*matched=*/false, /*matchPos=*/0); +#endif + // Full in-process miss (contentMap + per-PC deque) — this PC needs a + // program. Try hydrating the block graph from the on-disk cache + // before compiling from scratch. Placed after the deque walk so a + // range-equal program already in memory wins over creating a + // duplicate from disk. + mVUProgCache::ObserveDispatchHash(mVU, liveHash, startPC); + if (microProgram* hydrated = mVUProgCache::TryLoadProgram(mVU, liveHash)) + { + // Same install sequence as the contentMap-hit path above — + // HydrateProgram registered the program in the contentMap, so + // from here on it is indistinguishable from a shared program. + mVUdequePushUnique(list, hydrated); + mVU.prog.cleared = 0; + mVU.prog.isSame = 1; + mVU.prog.cur = hydrated; + quick.prog = hydrated; + quick.block = hydrated->block[startPC / 8]; + hydrated->observed.record(startPC); + if (quick.block == nullptr) + { + // The image carried no block for this entry PC — compile it + // into the hydrated program (the recorder attaches it to the + // rebuilt persist log as a growth chunk). + void* entryPoint = mVUblockFetch(mVU, startPC, pState); + return entryPoint; + } + return mVUentryGet(mVU, quick.block, startPC, pState); + } + + mVU.prog.cleared = 0; + mVU.prog.isSame = 1; + mVU.prog.cur = mVUcreateProg(mVU, mVU.regs().start_pc/8); + // createProg seeded `observed` with its own startPC; record + // the dispatcher's `startPC` too (idempotent if they match, + // which is the common case). + mVU.prog.cur->observed.record(startPC); + void* entryPoint = mVUblockFetch(mVU, startPC, pState); + quick.block = mVU.prog.cur->block[startPC/8]; + quick.prog = mVU.prog.cur; + // Count this deque insertion in the program's refcount. + // contentMap owns the program; per-PC deques are non-owning refs. + list->push_front(mVU.prog.cur); + ++mVU.prog.cur->refcount; + return entryPoint; + } + + mVU.prog.isSame = -1; + mVU.prog.cur = quick.prog; + quick.block = mVU.prog.cur->block[startPC / 8]; + // Quick-slot hit; record the dispatched entry on the resolved + // program (idempotent if already observed). + mVU.prog.cur->observed.record(startPC); + + if (quick.block == nullptr) + { + void* entryPoint = mVUblockFetch(mVU, startPC, pState); + return entryPoint; + } + return mVUentryGet(mVU, quick.block, startPC, pState); +} + +// Read-only fast path: returns the cached block entry on a pure cache hit, +// or nullptr if any compilation, allocation, or full-list comparison is +// required. When nullptr, the caller must open the code cache and call +// mVUsearchProg. Mirrors the bookkeeping mVUsearchProg's hot path performs +// (mVU.prog.cur, quick.block, isSame) so downstream consumers don't see +// stale state on hit. Saves ~12% of CPU thread time by skipping the +// per-dispatch MacroAssembler ctor/dtor + BeginCodeWrite/EndCodeWrite + +// FlushInstructionCache wrapper that mVUopenCodeCache/mVUcloseCodeCache pay. +_mVUt __fi void* mVUlookupProg(u32 startPC, uptr pState) +{ + microVU& mVU = mVUx; + microProgramQuick& quick = mVU.prog.quick[mVU.regs().start_pc / 8]; + + if (!quick.prog) + return nullptr; + + microBlockManager* block = quick.prog->block[startPC / 8]; + if (!block) + return nullptr; + + microBlock* pBlock = block->search(mVU, (microRegInfo*)pState); + if (!pBlock) + return nullptr; + + mVU.prog.isSame = -1; + mVU.prog.cur = quick.prog; + quick.block = block; + // Fast-path also resolves to a program; record the dispatched + // entry (idempotent on duplicates). + quick.prog->observed.record(startPC); + return pBlock->hostEntry; +} + +//------------------------------------------------------------------ +// Execution Functions +//------------------------------------------------------------------ + +_mVUt void* mVUexecute(u32 startPC, u32 cycles) +{ + microVU& mVU = mVUx; + u32 vuLimit = vuIndex ? 0x3ff8 : 0xff8; + if (startPC > vuLimit + 7) + { + DevCon.Warning("microVU%x Warning: startPC = 0x%x, cycles = 0x%x", vuIndex, startPC, cycles); + } + + mVU.cycles = cycles; + mVU.totalCycles = cycles; + +#ifdef PCSX2_RECOMPILER_TESTS + // Live-game capture probe — no-op unless PCSX2_VU_CAPTURE_DIR is set. + // Snapshots microcode + VU memory + entry register state so the + // program can be replayed in pcsx2-vurunner without booting a game. + vu_capture::MaybeCapture(static_cast(vuIndex), startPC & vuLimit, cycles, + mVU.regs().Micro, mVU.microMemSize, + mVU.regs().Mem, mVU.microMemSize, + mVU.regs()); +#endif + + const u32 maskedPC = startPC & vuLimit; + const uptr pState = (uptr)&mVU.prog.lpState; + + void* result = mVUlookupProg(maskedPC, pState); + if (!result) + { + mVUopenCodeCache(mVU); + result = mVUsearchProg(maskedPC, pState); + mVUcloseCodeCache(mVU); + } + + if (!result) + { + DevCon.Error("microVU%d: mVUexecute got NULL block! startPC=0x%04x", vuIndex, startPC); + } + else if ((u8*)result < mVU.prog.x86start || (u8*)result >= mVU.prog.x86end) + { + DevCon.Error("microVU%d: Block pointer %p OUTSIDE code cache [%p-%p]! startPC=0x%04x", + vuIndex, result, mVU.prog.x86start, mVU.prog.x86end, startPC); + result = nullptr; + } + + return result; +} + +_mVUt void mVUcleanUp() +{ + microVU& mVU = mVUx; + + // Cycle accounting (mVU.cycles update + regs().cycle bump) is emitted + // inline in the dispatcher exit stub — see mVUdispatcherAB; it is not + // repeated here. + + // x86ptr is updated in mVUexecute after compilation. + if ((mVU.prog.x86ptr < mVU.prog.x86start) || (mVU.prog.x86ptr >= mVU.prog.x86end)) + { + Console.WriteLn(vuIndex ? Color_Orange : Color_Magenta, "microVU%d: Program cache limit reached.", mVU.index); + mVUreset(mVU, false); + } + + if (!vuIndex || !THREAD_VU1) + { + u32 cycles_passed = std::min(mVU.cycles, 3000) * EmuConfig.Speedhacks.EECycleSkip; + if (cycles_passed > 0) + { + s64 vu0_offset = VU0.cycle - cpuRegs.cycle; + cpuRegs.cycle += cycles_passed; + + if (!vuIndex) + VU0.cycle = cpuRegs.cycle + vu0_offset; + else + VU0.cycle += cycles_passed; + } + } + mVU.profiler.Print(); +} + +void* mVUexecuteVU0(u32 startPC, u32 cycles) { return mVUexecute<0>(startPC, cycles); } +void* mVUexecuteVU1(u32 startPC, u32 cycles) { return mVUexecute<1>(startPC, cycles); } +void mVUcleanUpVU0() { mVUcleanUp<0>(); } +void mVUcleanUpVU1() { mVUcleanUp<1>(); } + +// Non-template entry points callable from the dispatcher BL. They +// mirror mVUexecute's hot-path body (cycle field set + maskedPC + +// cache lookup) without the slow-path compile fallback. Return +// nullptr on miss; the dispatcher then BLs mVUexecuteVUx for the +// full slow path (which re-runs the cycle set + maskedPC harmlessly +// and then mVUopenCodeCache + mVUsearchProg). Cache hits skip the +// slow-path BL entirely. +// +// Skipping the bounds check that mVUexecute does on its result is +// safe: mVUlookupProg always returns either nullptr or a +// pBlock->hostEntry from a block emitted into the post-dispatcher +// region of the same code cache. The bounds check in mVUexecute +// only fires when the slow-path mVUsearchProg returns an +// out-of-cache pointer; that path still runs through mVUexecute +// here unchanged. +void* mVUlookupProg_VU0(u32 startPC, u32 cycles) +{ + microVU0.cycles = cycles; + microVU0.totalCycles = cycles; + const u32 maskedPC = startPC & 0xff8; + return mVUlookupProg<0>(maskedPC, (uptr)µVU0.prog.lpState); +} +void* mVUlookupProg_VU1(u32 startPC, u32 cycles) +{ + microVU1.cycles = cycles; + microVU1.totalCycles = cycles; + const u32 maskedPC = startPC & 0x3ff8; + return mVUlookupProg<1>(maskedPC, (uptr)µVU1.prog.lpState); +} + +#ifdef PCSX2_RECOMPILER_TESTS +// Exposed for the vu_capture replay harness (VuReplay::DumpJitAsm) so it +// doesn't have to include microVU-arm64.h (which has __fi defs that can't +// safely be cross-TU'd). Returns the program-cache range currently in use +// by the named VU. +namespace vu_capture_internal +{ + void GetCompiledRange(int vu_index, const u8** out_start, const u8** out_end) + { + const microVU& mVU = (vu_index == 0) ? microVU0 : microVU1; + *out_start = mVU.prog.x86start; + *out_end = mVU.prog.x86ptr; + } +} +#endif + +//------------------------------------------------------------------ +// Block Fetch / Compile +//------------------------------------------------------------------ + +void* mVUblockFetch(microVU& mVU, u32 startPC, uptr pState) +{ + pxAssert((startPC & 7) == 0); + pxAssert(startPC <= mVU.microMemSize - 8); + startPC &= mVU.microMemSize - 8; + + blockCreate(startPC / 8); + return mVUentryGet(mVU, mVUblocks[startPC / 8], startPC, pState); +} + +//------------------------------------------------------------------ +// recMicroVU0 / recMicroVU1 +//------------------------------------------------------------------ + +recMicroVU0 CpuMicroVU0; +recMicroVU1 CpuMicroVU1; + +recMicroVU0::recMicroVU0() { m_Idx = 0; IsInterpreter = false; } +recMicroVU1::recMicroVU1() { m_Idx = 1; IsInterpreter = false; } + +void recMicroVU0::Reserve() +{ + mVUinit(microVU0, 0); +} +void recMicroVU1::Reserve() +{ + mVUinit(microVU1, 1); + vu1Thread.Open(); +} + +void recMicroVU0::Shutdown() +{ + mVUclose(microVU0); +} +void recMicroVU1::Shutdown() +{ + if (vu1Thread.IsOpen()) + vu1Thread.WaitVU(); + mVUclose(microVU1); +} + +void recMicroVU0::Reset() +{ + mVUreset(microVU0, true); +} + +void recMicroVU0::Step() +{ +} + +void recMicroVU1::Reset() +{ + vu1Thread.WaitVU(); + vu1Thread.Get_MTVUChanges(); + mVUreset(microVU1, true); +} + +void recMicroVU0::SetStartPC(u32 startPC) +{ + VU0.start_pc = startPC; +} + +void recMicroVU0::Execute(u32 cycles) +{ + VU0.flags &= ~VUFLAG_MFLAGSET; + + if (!(VU0.VI[REG_VPU_STAT].UL & 1)) + return; + VU0.VI[REG_TPC].UL <<= 3; + + ((mVUrecCall)microVU0.startFunct)(VU0.VI[REG_TPC].UL, cycles); + VU0.VI[REG_TPC].UL >>= 3; + if (microVU0.regs().flags & 0x4) + { + microVU0.regs().flags &= ~0x4; + hwIntcIrq(6); + } +} + +void recMicroVU1::SetStartPC(u32 startPC) +{ + VU1.start_pc = startPC; +} + +void recMicroVU1::Step() +{ +} + +void recMicroVU1::Execute(u32 cycles) +{ + if (!THREAD_VU1) + { + if (!(VU0.VI[REG_VPU_STAT].UL & 0x100)) + return; + } + VU1.VI[REG_TPC].UL <<= 3; +#ifdef PCSX2_RECOMPILER_TESTS + vu1_trace::Entry* trace = vu1_trace::g_enabled.load(std::memory_order_relaxed) + ? vu1_trace::begin('r', VU1.VI[REG_TPC].UL, cycles) + : nullptr; +#endif + ((mVUrecCall)microVU1.startFunct)(VU1.VI[REG_TPC].UL, cycles); + VU1.VI[REG_TPC].UL >>= 3; +#ifdef PCSX2_RECOMPILER_TESTS + vu1_trace::finish(trace); +#endif + + if (microVU1.regs().flags & 0x4 && !THREAD_VU1) + { + microVU1.regs().flags &= ~0x4; + hwIntcIrq(7); + } +} + +void recMicroVU0::Clear(u32 addr, u32 size) +{ + mVUclear(microVU0, addr, size); +} +void recMicroVU1::Clear(u32 addr, u32 size) +{ + mVUclear(microVU1, addr, size); +} + +void recMicroVU1::ResumeXGkick() +{ + if (!(VU0.VI[REG_VPU_STAT].UL & 0x100)) + return; + ((mVUrecCallXG)microVU1.startFunctXG)(); +} + +//------------------------------------------------------------------ +// COP2 Macro-Mode State Helpers +//------------------------------------------------------------------ +// Ports x86 setupMacroOp / endMacroOp's microVU0-state work (microVU_Macro.inl +// lines 26, 33-36, 42-58, 84, 102-103). Lives here because microVU0 + its +// regAlloc/prog/code/cop2 fields are only fully visible inside microVU-arm64.cpp +// (microVU_Lower-arm64.inl is #include'd from microVU-arm64.h with file-local +// static emitters — including them in iCOP2-arm64.cpp would require pulling +// in the full header context). +// +// setupMacroOp_arm64 / endMacroOp_arm64 in iCOP2-arm64.cpp call into these +// after the existing sync + flag denorm/norm work; mode bits and eeinstInfo +// (g_pCurInstInfo->info) come from the caller. + +void mVUmacroSetupCOP2State(int mode, u32 eeinstInfo) +{ + microVU0.regAlloc->reset(true); + microVU0.cop2 = 1; + microVU0.prog.IRinfo.curPC = 0; + microVU0.code = cpuRegs.code; + std::memset(µVU0.prog.IRinfo.info[0], 0, sizeof(microVU0.prog.IRinfo.info[0])); + + if ((mode & 0x08) && (!CHECK_VU_FLAGHACK || (eeinstInfo & EEINST_COP2_CLIP_FLAG))) + { + microVU0.prog.IRinfo.info[0].cFlag.write = 0xff; + microVU0.prog.IRinfo.info[0].cFlag.lastWrite = 0xff; + } + if ((mode & 0x10) && (!CHECK_VU_FLAGHACK || (eeinstInfo & EEINST_COP2_STATUS_FLAG))) + { + microVU0.prog.IRinfo.info[0].sFlag.doFlag = true; + microVU0.prog.IRinfo.info[0].sFlag.doNonSticky = true; + microVU0.prog.IRinfo.info[0].sFlag.write = 0; + microVU0.prog.IRinfo.info[0].sFlag.lastWrite = 0; + } + if ((mode & 0x10) && (!CHECK_VU_FLAGHACK || (eeinstInfo & EEINST_COP2_MAC_FLAG))) + { + microVU0.prog.IRinfo.info[0].mFlag.doFlag = true; + microVU0.prog.IRinfo.info[0].mFlag.write = 0xff; + } +} + +void mVUmacroEndCOP2State() +{ + // regAlloc writebacks happened inside the per-op adapter (while x19 still + // held &VU0). The only cleanup needed here is clearing the map state. + microVU0.cop2 = 0; + microVU0.regAlloc->reset(false); +} + +// COP2 macro-mode emit adapters. The 12 mVU_* emitters in microVU_Lower-arm64.inl +// are file-static (the .inl is #include'd here), so iR5900Misc-arm64.cpp can't +// take their address. Each adapter runs the standard pass1+pass2 dispatch x86 +// uses in REC_COP2_mVU0 (microVU_Macro.inl:127-133): when mode bit 0x04 is set, +// run pass1 (analyze) first, then pass2 (codegen) unless the analysis flagged +// the op as NOP; otherwise run pass2 directly. +// +// gprVUState (x19) bridging: the mVU emitters address VURegs via x19 = &VU0, +// but the EE recompiler pins x19 to RFASTMEMBASE for the whole block. We +// rebase x19 = RVU0 (x24, which the EE rec already loaded with &VU0) before +// the mVU emit, then reload x19 from vtlbdata.fastmem_base afterward so any +// subsequent fastmem ldr/str in the EE block keeps working. +static void mVUmacroEmitPrologue() +{ + // Evict the EE register cache before the mVU emit runs. The mVU regAlloc + // allocates VI into host x14/x15/x26-x28 and VF into Q0-Q27, all of which + // overlap the EE allocator's pool — and the EE recompiler keeps GPR/NEON + // values cached across instruction boundaries (recompileNextInstruction + // only _clearNeeded's, it does not write back). With no cross-allocator + // coordination on arm64 (clearRegCOP2/clearGPRCOP2 are stubs) the mVU emit + // would silently clobber a live EE GPR/NEON value. Free+writeback all EE + // allocations here so the mVU emit gets a clean slate; subsequent EE code + // reloads from cpuRegs as needed. This is at most the cost of the old + // REC_COP2_INTERP path (which full-flushed via recCall). Pinned regs + // (x19/x20/x24/x25) are outside the allocatable pool and untouched. + _freeArm64GPRregs(); + _freeNEONregs(); + + armAsm->Mov(gprVUState, RVU0); +} + +static void mVUmacroEmitEpilogue() +{ + if (CHECK_FASTMEM) + { + armMoveAddressToReg(RSCRATCHADDR, &vtlb_private::vtlbdata.fastmem_base); + armAsm->Ldr(RFASTMEMBASE, a64::MemOperand(RSCRATCHADDR)); + } +} + +#define MVU_MACRO_EMIT_ADAPTER(opname) \ + void mVUmacroEmit_##opname(int mode) \ + { \ + mVUmacroEmitPrologue(); \ + if (mode & 0x04) \ + { \ + mVU_##opname(microVU0, 0); \ + if (!microVU0.prog.IRinfo.info[0].lOp.isNOP) \ + mVU_##opname(microVU0, 1); \ + } \ + else \ + { \ + mVU_##opname(microVU0, 1); \ + } \ + /* Writebacks MUST happen while x19 still holds &VU0 — the \ + * epilogue restores x19 to RFASTMEMBASE and any later store \ + * via mVUstateMem would land in fastmem garbage. \ + * Arm64 regAlloc has no x86-style cross-op VI preservation, \ + * so flushAll vs flushPartialForCOP2 is correctness-required.*/ \ + microVU0.regAlloc->flushAll(true); \ + mVUmacroEmitEpilogue(); \ + } + +MVU_MACRO_EMIT_ADAPTER(LQI) +MVU_MACRO_EMIT_ADAPTER(SQI) +MVU_MACRO_EMIT_ADAPTER(LQD) +MVU_MACRO_EMIT_ADAPTER(SQD) +MVU_MACRO_EMIT_ADAPTER(MTIR) +MVU_MACRO_EMIT_ADAPTER(MFIR) +MVU_MACRO_EMIT_ADAPTER(ILWR) +MVU_MACRO_EMIT_ADAPTER(ISWR) +MVU_MACRO_EMIT_ADAPTER(RNEXT) +MVU_MACRO_EMIT_ADAPTER(RGET) +MVU_MACRO_EMIT_ADAPTER(RINIT) +MVU_MACRO_EMIT_ADAPTER(RXOR) + +#undef MVU_MACRO_EMIT_ADAPTER + +//------------------------------------------------------------------ +// On-disk program cache implementation — single-TU inclusion to share the +// mVU header context. See microVU_ProgCache-arm64.inl for the rationale. +//------------------------------------------------------------------ +#include "microVU_ProgCache-arm64.inl" + +//------------------------------------------------------------------ +// Persisted-JIT relocation recorder + block-graph serializer — same +// single-TU inclusion pattern (needs mVUcreateProg / mVUcomputeProgramHash +// and the TU-local mVUopenCodeCache / mVUcloseCodeCache). +//------------------------------------------------------------------ +#include "microVU_Persist-arm64.inl" + +//------------------------------------------------------------------ +// Save State +//------------------------------------------------------------------ + +bool SaveStateBase::vuJITFreeze() +{ + if (IsSaving()) + vu1Thread.WaitVU(); + + Freeze(microVU0.prog.lpState); + Freeze(microVU1.prog.lpState); + return IsOkay(); +} diff --git a/pcsx2/arm64/microVU-arm64.h b/pcsx2/arm64/microVU-arm64.h new file mode 100644 index 0000000000..acc604cce9 --- /dev/null +++ b/pcsx2/arm64/microVU-arm64.h @@ -0,0 +1,708 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//#define mVUlogProg // Dumps MicroPrograms to \logs\*.html +//#define mVUprofileProg // Shows opcode statistics in console + +#include +#include +#include +#include +#include +#include +#include "Common.h" +#include "VU.h" +#include "MTVU.h" +#include "GS.h" +#include "Gif_Unit.h" +#include "iR5900-arm64.h" +#include "R5900OpcodeTables.h" +#include "common/Perf.h" + +#include "microVU_Misc-arm64.h" +#include "MvuObservedEntries.h" +#include "microVU_Persist-arm64.h" + +#ifndef XXH_versionNumber +#define XXH_STATIC_LINKING_ONLY 1 +#define XXH_INLINE_ALL 1 +#include "xxhash.h" +#endif + +// Bump on any change to the arm64 mVU emitter that invalidates previously-cached +// (in-process or on-disk) program identity. Mixed into every contentHash so +// stale artifacts can't be reused after a codegen shape change. +// +// History: +// 1 — initial options sentinel (codegen constexprs + clamp modes +// + speedhacks + FPCR). +// 2 — mixed in helper-table layout hash so the on-disk cache +// invalidates atomically when the helper ABI shape changes. +// 3 — dropped helperTableLayoutHash from the options sentinel +// (sentinel layout shrank; every contentHash changes). +static constexpr u32 kMvuCompilerAbiVersion = 3; + +// Hash/equality functors for XXH128_hash_t — let std::unordered_map +// work without a wrapping struct. low64 already carries the well-mixed half of +// the xxh3 output, so XORing in high64 is plenty for the bucket index; the map +// resolves collisions by ::operator== on the full 128 bits. +struct MvuContentHashHash +{ + size_t operator()(const XXH128_hash_t& h) const noexcept + { + return static_cast(h.low64 ^ h.high64); + } +}; + +struct MvuContentHashEq +{ + bool operator()(const XXH128_hash_t& a, const XXH128_hash_t& b) const noexcept + { + return a.low64 == b.low64 && a.high64 == b.high64; + } +}; + +// Forward declarations +class microBlockManager; + +//------------------------------------------------------------------ +// IR structures — platform-independent, kept in sync with x86/microVU_IR.h. +//------------------------------------------------------------------ + +struct regCycleInfo +{ + u8 x : 4; + u8 y : 4; + u8 z : 4; + u8 w : 4; +}; + +union alignas(16) microRegInfo +{ + struct + { + union + { + struct + { + u8 needExactMatch; + u8 flagInfo; + u8 q; + u8 p; + u8 xgkick; + u8 viBackUp; + u8 blockType; + u8 r; + }; + u64 quick64[1]; + u32 quick32[2]; + }; + + u32 xgkickcycles; + u8 unused; + u8 vi15v; + u16 vi15; + + struct + { + u8 VI[16]; + regCycleInfo VF[32]; + }; + }; + + u128 full128[96 / sizeof(u128)]; + u64 full64[96 / sizeof(u64)]; + u32 full32[96 / sizeof(u32)]; +}; + +static_assert(sizeof(microRegInfo) == 96, "microRegInfo was not 96 bytes"); + +struct microProgram; +struct MvuPersistLog; +struct microJumpCache +{ + microJumpCache() : prog(NULL), x86ptrStart(NULL), hostEntry(NULL) {} + microProgram* prog; + void* x86ptrStart; // Code entry point (name kept for struct compatibility) + // Generalised "host entry" pointer. For JIT-emitted blocks this + // mirrors x86ptrStart (the rec-cache slot the block was compiled + // into). The indirection is retained for the persisted-JIT program + // cache: hydrated blocks point hostEntry at the reloaded code + // without disturbing the rec-cache bookkeeping. The dispatcher and + // block-linking paths BR through hostEntry; x86ptrStart stays a + // debugging/perf-attribution hint. + void* hostEntry; +}; + +struct alignas(16) microBlock +{ + microRegInfo pState; + microRegInfo pStateEnd; + u8* x86ptrStart; // Code entry point (name kept for struct compatibility) + void* hostEntry; // see microJumpCache::hostEntry + microJumpCache* jumpCache; +}; + +struct microTempRegInfo +{ + regCycleInfo VF[2]; + u8 VFreg[2]; + u8 VI; + u8 VIreg; + u8 q; + u8 p; + u8 r; + u8 xgkick; +}; + +struct microVFreg +{ + u8 reg; + u8 x; + u8 y; + u8 z; + u8 w; +}; + +struct microVIreg +{ + u8 reg; + u8 used; +}; + +struct microConstInfo +{ + u8 isValid; + u32 regValue; +}; + +struct microUpperOp +{ + bool eBit; + bool iBit; + bool mBit; + bool tBit; + bool dBit; + microVFreg VF_write; + microVFreg VF_read[2]; +}; + +struct microLowerOp +{ + microVFreg VF_write; + microVFreg VF_read[2]; + microVIreg VI_write; + microVIreg VI_read[2]; + microConstInfo constJump; + u32 branch; + u32 kickcycles; + bool badBranch; + bool evilBranch; + bool isNOP; + bool isFSSET; + bool noWriteVF; + bool backupVI; + bool memReadIs; + bool memReadIt; + bool readFlags; + bool isMemWrite; + bool isKick; +}; + +struct microFlagInst +{ + bool doFlag; + bool doNonSticky; + u8 write; + u8 lastWrite; + u8 read; +}; + +struct microFlagCycles +{ + int xStatus[4]; + int xMac[4]; + int xClip[4]; + int cycles; +}; + +struct microOp +{ + u8 stall; + bool isBadOp; + bool isEOB; + bool isBdelay; + bool swapOps; + bool backupVF; + bool doXGKICK; + u32 XGKICKPC; + bool doDivFlag; + int readQ; + int writeQ; + int readP; + int writeP; + microFlagInst sFlag; + microFlagInst mFlag; + microFlagInst cFlag; + microUpperOp uOp; + microLowerOp lOp; +}; + +template +struct microIR +{ + microBlock block; + microBlock* pBlock; + microTempRegInfo regsTemp; + microOp info[pSize / 2]; + microConstInfo constReg[16]; + u8 branch; + u32 cycles; + u32 count; + u32 curPC; + u32 startPC; + u32 sFlagHack; +}; + +//------------------------------------------------------------------ +// Program/Block Management Structures (from x86/microVU.h) +//------------------------------------------------------------------ + +struct microBlockLink +{ + microBlock block; + microBlockLink* next; +}; + +struct microBlockLinkRef +{ + microBlock* pBlock; + u64 quick; +}; + +struct microRange +{ + s32 start; + s32 end; +}; + +#define mProgSize (0x4000 / 4) +struct microProgram +{ + u32 data [mProgSize]; + microBlockManager* block[mProgSize / 2]; + std::deque* ranges; + u32 startPC; + int idx; + // Content-keyed identity for in-process dedup and on-disk cache lookup. + // Computed in mVUcreateProg over the live microMem snapshot the + // program was created from, mixed with kMvuCompilerAbiVersion + the per-VU + // options sentinel + VU index. Stable across processes for a given VU + // program + config combo, so on-disk cache artifacts can be keyed by it. Set + // once at create and not updated thereafter — re-caches via mVUsetupRange + // don't shift identity (contentMap relies on a stable key). + XXH128_hash_t contentHash; + bool contentHashValid; + + // Count of mVU.prog.prog[] deque slots holding a (non-owning) pointer + // to this microProgram. Bumped on each push_front, decremented at eviction. + // The single owner is mVU.prog.contentMap[contentHash]; per-PC deques are + // non-owning references. Single-threaded per VU, so plain s32. + s32 refcount; + + // Set of microMem byte offsets the dispatcher has handed off into this + // program. Initialized at mVUcreateProg with the creator's startPC; + // mVUsearchProg accumulates additional entries seen during the program's + // lifetime. `version` lets callers detect "new entry since last compile" + // cheaply (compare vs the version snapshotted by the most recent + // SaveProgram). + MvuObservedEntries observed; + + // Recorded block-graph snapshot (chunks + fixups) for this program, built + // by the emit-time recorder in microVU_Persist-arm64.inl. Null when + // recording is disabled or nothing was recorded. Owned here; freed via + // mVUPersist::OnProgramDeleted from mVUdeleteProg. (microProgram is + // memset-constructed in mVUcreateProg, so a raw pointer is the right shape.) + MvuPersistLog* persist; +}; + +typedef std::deque microProgramList; + +struct microProgramQuick +{ + microBlockManager* block; + microProgram* prog; +}; + +struct microProgManager +{ + microIR IRinfo; + microProgramList* prog [mProgSize/2]; + microProgramQuick quick[mProgSize/2]; + microProgram* cur; + int total; + int isSame; + int cleared; + u32 curFrame; + u8* x86ptr; // Code cache write cursor (name kept for compat) + u8* x86start; // Start of rec-cache + u8* x86end; // Limit of rec-cache + microRegInfo lpState; +}; + +static const uint mVUcacheSafeZone = 3; // Safe-Zone (megabytes) + +//------------------------------------------------------------------ +// Profiler (shared, platform-independent) +//------------------------------------------------------------------ +#include "x86/microVU_Profiler.h" + +//------------------------------------------------------------------ +// Forward declarations for helpers used by regalloc +//------------------------------------------------------------------ +void mVUloadReg(const a64::VRegister& reg, const void* ptr, int xyzw); +void mVUloadReg(const a64::VRegister& reg, const a64::Register& base, int64_t off, int xyzw); +void mVUloadMem(const a64::VRegister& reg, const a64::Register& base, int xyzw); +void mVUsaveReg(const a64::VRegister& reg, const void* ptr, int xyzw, bool modXYZW); +void mVUsaveReg(const a64::VRegister& reg, const a64::Register& base, int64_t off, int xyzw, bool modXYZW); +void mVUmergeRegs(const a64::VRegister& dest, const a64::VRegister& src, int xyzw, bool modXYZW = false); +void mVUunpack_xyzw(const a64::VRegister& dstreg, const a64::VRegister& srcreg, int xyzw); + +//------------------------------------------------------------------ +// ARM64 Register Allocator +//------------------------------------------------------------------ +#include "microVU_IR-arm64.h" + +//------------------------------------------------------------------ +// microVU Main Structure +//------------------------------------------------------------------ + +struct microVU +{ + alignas(16) u32 statFlag[4]; + alignas(16) u32 macFlag [4]; + alignas(16) u32 clipFlag[4]; + alignas(16) u32 neonCTemp[4]; // Backup used in mVUclamp2() + alignas(16) u32 neonBackup[32][4]; // Backup for q0~q31 + + u32 index; + u32 cop2; + u32 vuMemSize; + u32 microMemSize; + u32 progSize; + u32 progMemMask; + u32 cacheSize; + + // Cached hash of every codegen-affecting build-time constexpr + runtime + // option (clamp modes, FPCRs, speedhacks, gamefixes). Rebuilt at mVUinit / + // mVUreset and mixed into every program's contentHash so configs that + // change emit (e.g. flipping a clamp bit) produce a distinct content key. + XXH128_hash_t optionsSentinel; + bool optionsSentinelValid; + + // Owning index keyed by content hash. Every live microProgram is + // referenced exactly once from here; per-startPC deques (prog.prog[]) and + // quick slots (prog.quick[]) hold non-owning pointers tracked via + // microProgram::refcount. Cross-startPC dedup happens by hash lookup: + // searches that hit this map short-circuit the per-PC deque walk and reuse + // the existing object across all startPCs whose microMem hashes to the + // same key. Lives on microVU (not microProgManager) because mVUinit memsets + // the prog manager — std::unordered_map can't survive that. + std::unordered_map mvuContentMap; + + microProgManager prog; + microProfiler profiler; + std::unique_ptr regAlloc; + // Persistent vixl MacroAssembler for the per-VU code cache. Constructed + // once per mVUreset over [prog.x86ptr, prog.x86end); cursor advances + // across all subsequent block compiles. Avoids per-dispatch MacroAssembler + // construction/teardown, which is measurable on in-order cores. + std::unique_ptr jitAsm; + std::FILE* logFile; + + u8* cache; + u8* startFunct; + u8* exitFunct; + u8* startFunctXG; + u8* exitFunctXG; + u8* waitMTVU; + u8* copyPLState; + // Per-VU SFLAGc + micro_flag tail helpers BL'd by mVUendProgram / + // mVUsetupBranch emit. See mVUGenerateEndProgramFlagsHelper in + // microVU-arm64.cpp — each exit thunk's inline shrinks from ~20 insns + // to one mov+bl pair. + u8* endProgramFlagsA; // non-Ebit exits (isEbit == 0 || isEbit == 3) + u8* endProgramFlagsB; // Ebit exits (isEbit && isEbit != 3) + u8* resumePtrXG; + u32 code; + u32 divFlag; + u32 VIbackup; + u32 VIxgkick; + u32 branch; + u32 badBranch; + u32 evilBranch; + u32 evilevilBranch; + u32 p; + u32 q; + u32 totalCycles; + s32 cycles; + + VURegs& regs() const { return ::vuRegs[index]; } + + __fi REG_VI& getVI(uint reg) const { return regs().VI[reg]; } + __fi VECTOR& getVF(uint reg) const { return regs().VF[reg]; } + __fi VIFregisters& getVifRegs() const + { + return (index && THREAD_VU1) ? vu1Thread.vifRegs : regs().GetVifRegs(); + } + + // Inline static-NEON 96-byte equality compare. Returns 0 if equal, non-zero + // otherwise. Layout-locked to microRegInfo (96 B / 16-byte aligned); see + // static_assert below. + __fi u32 compareState(microRegInfo* lhs, microRegInfo* rhs) const { + const u32* a = reinterpret_cast(lhs); + const u32* b = reinterpret_cast(rhs); + uint32x4_t c0 = vceqq_u32(vld1q_u32(a + 0), vld1q_u32(b + 0)); + uint32x4_t c1 = vceqq_u32(vld1q_u32(a + 4), vld1q_u32(b + 4)); + if (vminvq_u32(vandq_u32(c0, c1)) == 0) + return 1; + uint32x4_t c2 = vceqq_u32(vld1q_u32(a + 8), vld1q_u32(b + 8)); + uint32x4_t c3 = vceqq_u32(vld1q_u32(a + 12), vld1q_u32(b + 12)); + uint32x4_t c4 = vceqq_u32(vld1q_u32(a + 16), vld1q_u32(b + 16)); + uint32x4_t c5 = vceqq_u32(vld1q_u32(a + 20), vld1q_u32(b + 20)); + uint32x4_t a23 = vandq_u32(c2, c3); + uint32x4_t a45 = vandq_u32(c4, c5); + return (vminvq_u32(vandq_u32(a23, a45)) == 0) ? 1 : 0; + } +}; + +//------------------------------------------------------------------ +// Block Manager (from x86/microVU.h — platform-independent) +//------------------------------------------------------------------ + +class microBlockManager +{ +private: + microBlockLink *qBlockList, *qBlockEnd; + microBlockLink *fBlockList, *fBlockEnd; + std::vector quickLookup; + int qListI, fListI; + +public: + inline int getFullListCount() const { return fListI; } + microBlockManager() + { + qListI = fListI = 0; + qBlockEnd = qBlockList = nullptr; + fBlockEnd = fBlockList = nullptr; + } + ~microBlockManager() { reset(); } + void reset() + { + for (microBlockLink* linkI = qBlockList; linkI != nullptr;) + { + microBlockLink* freeI = linkI; + safe_delete_array(linkI->block.jumpCache); + linkI = linkI->next; + _aligned_free(freeI); + } + for (microBlockLink* linkI = fBlockList; linkI != nullptr;) + { + microBlockLink* freeI = linkI; + safe_delete_array(linkI->block.jumpCache); + linkI = linkI->next; + _aligned_free(freeI); + } + qListI = fListI = 0; + qBlockEnd = qBlockList = nullptr; + fBlockEnd = fBlockList = nullptr; + quickLookup.clear(); + }; + microBlock* add(microVU& mVU, microBlock* pBlock) + { + microBlock* thisBlock = search(mVU, &pBlock->pState); + if (!thisBlock) + { + u8 fullCmp = pBlock->pState.needExactMatch; + if (fullCmp) + fListI++; + else + qListI++; + + microBlockLink*& blockList = fullCmp ? fBlockList : qBlockList; + microBlockLink*& blockEnd = fullCmp ? fBlockEnd : qBlockEnd; + microBlockLink* newBlock = (microBlockLink*)_aligned_malloc(sizeof(microBlockLink), 32); + newBlock->block.jumpCache = nullptr; + newBlock->next = nullptr; + + if (blockEnd) + { + blockEnd->next = newBlock; + blockEnd = newBlock; + } + else + { + blockEnd = blockList = newBlock; + } + + std::memcpy(&newBlock->block, pBlock, sizeof(microBlock)); + thisBlock = &newBlock->block; + + quickLookup.push_back({&newBlock->block, pBlock->pState.quick64[0]}); + } + return thisBlock; + } + __ri microBlock* search(microVU& mVU, microRegInfo* pState) + { + if (pState->needExactMatch) + { + microBlockLink* prevI = nullptr; + for (microBlockLink* linkI = fBlockList; linkI != nullptr; prevI = linkI, linkI = linkI->next) + { + if (mVU.compareState(pState, &linkI->block.pState) == 0) + { + if (linkI != fBlockList) + { + prevI->next = linkI->next; + linkI->next = fBlockList; + fBlockList = linkI; + } + return &linkI->block; + } + } + } + else + { + const u64 quick64 = pState->quick64[0]; + for (const microBlockLinkRef& ref : quickLookup) + { + if (mVUsFlagHack) + { + if ((ref.quick & ~0x0C04) != (quick64 & ~0x0C04)) continue; + } + else if (ref.quick != quick64) continue; + + if (doConstProp && (ref.pBlock->pState.vi15 != pState->vi15)) continue; + if (doConstProp && (ref.pBlock->pState.vi15v != pState->vi15v)) continue; + return ref.pBlock; + } + } + return nullptr; + } + void printInfo(int pc, bool printQuick) + { + int listI = printQuick ? qListI : fListI; + if (listI < 7) + return; + microBlockLink* linkI = printQuick ? qBlockList : fBlockList; + for (int i = 0; i <= listI; i++) + { + u32 viCRC = 0, vfCRC = 0, crc = 0, z = sizeof(microRegInfo) / 4; + for (u32 j = 0; j < 4; j++) viCRC -= ((u32*)linkI->block.pState.VI)[j]; + for (u32 j = 0; j < 32; j++) vfCRC -= linkI->block.pState.VF[j].x + (linkI->block.pState.VF[j].y << 8) + (linkI->block.pState.VF[j].z << 16) + (linkI->block.pState.VF[j].w << 24); + for (u32 j = 0; j < z; j++) crc -= ((u32*)&linkI->block.pState)[j]; + DevCon.WriteLn(Color_Green, + "[%04x][Block #%d][crc=%08x][q=%02d][p=%02d][xgkick=%d][vi15=%04x][vi15v=%d][viBackup=%02d]" + "[flags=%02x][exactMatch=%x][blockType=%d][viCRC=%08x][vfCRC=%08x]", + pc, i, crc, linkI->block.pState.q, + linkI->block.pState.p, linkI->block.pState.xgkick, linkI->block.pState.vi15, linkI->block.pState.vi15v, + linkI->block.pState.viBackUp, linkI->block.pState.flagInfo, linkI->block.pState.needExactMatch, + linkI->block.pState.blockType, viCRC, vfCRC); + linkI = linkI->next; + } + } +}; + +//------------------------------------------------------------------ +// Globals and Prototypes +//------------------------------------------------------------------ + +alignas(16) extern microVU microVU0; +alignas(16) extern microVU microVU1; + +extern void DumpVUState(u32 n, u32 pc); + +extern void mVUclear(mV, u32, u32); +extern void mVUreset(microVU& mVU, bool resetReserve); +extern void* mVUblockFetch(microVU& mVU, u32 startPC, uptr pState); +extern void* mVUcompile(microVU& mVU, u32 startPC, uptr pState); +_mVUt extern void* mVUcompileJIT(u32 startPC, uptr ptr); + +extern void mVUcleanUpVU0(); +extern void mVUcleanUpVU1(); +mVUop(mVUopU); +mVUop(mVUopL); + +extern void mVUcacheProg(microVU& mVU, microProgram& prog); +extern void mVUdeleteProg(microVU& mVU, microProgram*& prog); +_mVUt extern void* mVUsearchProg(u32 startPC, uptr pState); +extern void* mVUexecuteVU0(u32 startPC, u32 cycles); +extern void* mVUexecuteVU1(u32 startPC, u32 cycles); + +// Non-template lookup-only entry points called from the dispatcher's inline +// fast path. Mirror mVUexecute's hot-path body (cycle field set + maskedPC +// + mVUlookupProg) but return nullptr on cache miss instead of falling +// through to compile; the dispatcher then BLs mVUexecuteVUx for the slow +// path. +extern void* mVUlookupProg_VU0(u32 startPC, u32 cycles); +extern void* mVUlookupProg_VU1(u32 startPC, u32 cycles); + +// Content-hash plumbing (xxhash3-128 program identity). +// - mVUbuildOptionsSentinel populates mVU.optionsSentinel from the current +// codegen-affecting config snapshot. Call at init / reset. +// - mVUcomputeProgramHash returns the 128-bit content hash for a freshly +// cached program image. Called from mVUcacheProg. +extern void mVUbuildOptionsSentinel(microVU& mVU); +extern XXH128_hash_t mVUcomputeProgramHash(microVU& mVU); + +typedef void (*mVUrecCall)(u32, u32); +typedef void (*mVUrecCallXG)(void); + +// Out-of-line definition — needs complete microVU type and globals +inline void microRegAlloc::writeVIBackup(const a64::Register& reg) +{ + microVU& mVU = (index ? microVU1 : microVU0); + armStorePtr(reg.W(), &mVU.VIbackup); +} + +template +void makeUnique(T& v) +{ + v.erase(unique(v.begin(), v.end()), v.end()); +} + +template +void sortVector(T& v) +{ + sort(v.begin(), v.end()); +} + +// Block entry-point lookup — if not found, compile directly (do NOT call mVUblockFetch — that causes infinite recursion) +__fi void* mVUentryGet(microVU& mVU, microBlockManager* block, u32 startPC, uptr pState) +{ + microBlock* pBlock = block->search(mVU, (microRegInfo*)pState); + if (pBlock) + return pBlock->hostEntry; + return mVUcompile(mVU, startPC, pState); +} + +//------------------------------------------------------------------ +// ARM64 helper .inl files + shared analysis/tables +//------------------------------------------------------------------ +#include "microVU_Clamp-arm64.inl" +#include "microVU_Misc-arm64.inl" +#include "microVU_Alloc-arm64.inl" +#include "microVU_Flags-arm64.inl" +#include "x86/microVU_Analyze.inl" +// Forward declarations for stubs defined in Branch .inl but referenced by Lower .inl +static void mVUdivSet(mV); +static void mVU_XGKICK_DELAY(mV); +static void mVU_XGKICK_SYNC(mV, bool); + +#include "microVU_Upper-arm64.inl" +#include "microVU_Lower-arm64.inl" +#include "x86/microVU_Tables.inl" +#include "microVU_Branch-arm64.inl" +#include "microVU_Compile-arm64.inl" diff --git a/pcsx2/arm64/microVU_Alloc-arm64.inl b/pcsx2/arm64/microVU_Alloc-arm64.inl new file mode 100644 index 0000000000..606f433204 --- /dev/null +++ b/pcsx2/arm64/microVU_Alloc-arm64.inl @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// Micro VU - Pass 2 Functions (ARM64) +//------------------------------------------------------------------ + +//------------------------------------------------------------------ +// Flag Allocators +//------------------------------------------------------------------ + +__fi static const a64::Register& getFlagReg(uint fInst) +{ + static const a64::Register* const gprFlags[4] = {&gprF0, &gprF1, &gprF2, &gprF3}; + pxAssert(fInst < 4); + return *gprFlags[fInst]; +} + +__fi void mVUallocSFLAGa(const a64::Register& reg, int fInstance) +{ + armAsm->Mov(reg.W(), getFlagReg(fInstance)); +} + +__fi void mVUallocSFLAGb(const a64::Register& reg, int fInstance) +{ + armAsm->Mov(getFlagReg(fInstance), reg.W()); +} + +// Normalize Status Flag (denormalized → standard format) +// Extracts Z, S, ZS, SS bits and shifts DS/DI/OS/US/D/I/O/U bits +__ri void mVUallocSFLAGc(const a64::Register& reg, const a64::Register& regT, int fInstance) +{ + mVUallocSFLAGa(regT, fInstance); + armAsm->Mov(reg.W(), 0); + + // Caller commonly passes reg=gprT1 (=w9). A Csel-based approach + // (`Orr(w9, reg, imm); Csel(reg, w9, reg, ne)`) mutates reg in-place + // when reg==w9 — producing unconditional sets for all four bits and a + // default STATUS_FLAG of 0xc3 at end-of-program. Use test + forward + // branch + or so the or is genuinely conditional and no scratch + // register is needed. + auto setBit = [&](int bitTest, int bitSet) { + armAsm->Tst(regT.W(), bitTest); + a64::Label skip; + armAsm->B(&skip, a64::eq); + armAsm->Orr(reg.W(), reg.W(), bitSet); + armAsm->Bind(&skip); + }; + + setBit(0x0f00, 0x0001); // Z bit + setBit(0xf000, 0x0002); // S bit + setBit(0x000f, 0x0040); // ZS bit + setBit(0x00f0, 0x0080); // SS bit + + // DS/DI/OS/US/D/I/O/U bits: (regT & 0xffff0000) >> 14 + armAsm->And(regT.W(), regT.W(), 0xffff0000u); + armAsm->Lsr(regT.W(), regT.W(), 14); + armAsm->Orr(reg.W(), reg.W(), regT.W()); +} + +// Denormalize Status Flag (standard → denormalized format) +__ri void mVUallocSFLAGd(u32* memAddr, const a64::Register& reg = a64::w9, + const a64::Register& tmp1 = a64::w10, const a64::Register& tmp2 = a64::w11) +{ + armMoveAddressToReg(a64::x8, memAddr); + armAsm->Ldr(tmp2.W(), a64::MemOperand(a64::x8)); + + // reg = (tmp2 >> 3) & 0x18 + armAsm->Lsr(reg.W(), tmp2.W(), 3); + armAsm->And(reg.W(), reg.W(), 0x18); + + // tmp1 = (tmp2 << 11) & 0x1800 + armAsm->Lsl(tmp1.W(), tmp2.W(), 11); + armAsm->And(tmp1.W(), tmp1.W(), 0x1800); + armAsm->Orr(reg.W(), reg.W(), tmp1.W()); + + // tmp2 = (tmp2 << 14) & 0x3cf0000 + armAsm->Lsl(tmp2.W(), tmp2.W(), 14); + armAsm->And(tmp2.W(), tmp2.W(), 0x3cf0000); + armAsm->Orr(reg.W(), reg.W(), tmp2.W()); +} + +//------------------------------------------------------------------ +// MAC/Clip Flag Allocators +//------------------------------------------------------------------ + +__fi void mVUallocMFLAGa(mV, const a64::Register& reg, int fInstance) +{ + armAsm->Ldrh(reg.W(), mVUmacFlagMem(fInstance)); +} + +__fi void mVUallocMFLAGb(mV, const a64::Register& reg, int fInstance) +{ + if (fInstance < 4) + armAsm->Str(reg.W(), mVUmacFlagMem(fInstance)); + else + armAsm->Str(reg.W(), mVUstateMem(offsetof(VURegs, VI) + REG_MAC_FLAG * sizeof(REG_VI))); +} + +__fi void mVUallocCFLAGa(mV, const a64::Register& reg, int fInstance) +{ + if (fInstance < 4) + armAsm->Ldr(reg.W(), mVUclipFlagMem(fInstance)); + else + armAsm->Ldr(reg.W(), mVUstateMem(offsetof(VURegs, VI) + REG_CLIP_FLAG * sizeof(REG_VI))); +} + +__fi void mVUallocCFLAGb(mV, const a64::Register& reg, int fInstance) +{ + if (fInstance < 4) + armAsm->Str(reg.W(), mVUclipFlagMem(fInstance)); + else + armAsm->Str(reg.W(), mVUstateMem(offsetof(VURegs, VI) + REG_CLIP_FLAG * sizeof(REG_VI))); +} + +//------------------------------------------------------------------ +// P/Q Reg Allocators +//------------------------------------------------------------------ + +// Get P register value from qmmPQ (lane 2 or 3 based on readP instance) +__fi void getPreg(mV, const a64::VRegister& reg) +{ + // qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, [3]=pending_p + int lane = 2 + mVUinfo.readP; + mVUunpack_xyzw(reg, qmmPQ, lane); +} + +// Get Q register value from qmmPQ (lane 0 or 1 based on qInstance) +__fi void getQreg(const a64::VRegister& reg, int qInstance) +{ + mVUunpack_xyzw(reg, qmmPQ, qInstance); +} + +// Write Q register value back into qmmPQ +__ri void writeQreg(const a64::VRegister& reg, int qInstance) +{ + // Insert scalar from reg lane 0 into qmmPQ at qInstance lane + armAsm->Ins(qmmPQ.V4S(), qInstance, reg.V4S(), 0); +} + +// VI Backup (writeVIBackup) is defined in microVU_IR-arm64.h diff --git a/pcsx2/arm64/microVU_Branch-arm64.inl b/pcsx2/arm64/microVU_Branch-arm64.inl new file mode 100644 index 0000000000..4e88349d63 --- /dev/null +++ b/pcsx2/arm64/microVU_Branch-arm64.inl @@ -0,0 +1,1087 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +extern void mVUincCycles(microVU& mVU, int x); +extern void* mVUcompile(microVU& mVU, u32 startPC, uptr pState); + +void mVU0clearlpStateJIT() { if (!microVU0.prog.cleared) std::memset(µVU0.prog.lpState, 0, sizeof(microVU0.prog.lpState)); } +void mVU1clearlpStateJIT() { if (!microVU1.prog.cleared) std::memset(µVU1.prog.lpState, 0, sizeof(microVU1.prog.lpState)); } + +__fi int getLastFlagInst(microRegInfo& pState, int* xFlag, int flagType, int isEbit) +{ + if (isEbit) + return findFlagInst(xFlag, 0x7fffffff); + if (pState.needExactMatch & (1 << flagType)) + return 3; + return (((pState.flagInfo >> (2 * flagType + 2)) & 3) - 1) & 3; +} + +//------------------------------------------------------------------ +// mVUDTendProgram — D/T-bit end program variant +//------------------------------------------------------------------ + +void mVUDTendProgram(mV, microFlagCycles* mFC, int isEbit) +{ + int fStatus = getLastFlagInst(mVUpBlock->pState, mFC->xStatus, 0, isEbit); + int fMac = getLastFlagInst(mVUpBlock->pState, mFC->xMac, 1, isEbit); + int fClip = getLastFlagInst(mVUpBlock->pState, mFC->xClip, 2, isEbit); + int qInst = 0, pInst = 0; + microBlock stateBackup; + memcpy(&stateBackup, &mVUregs, sizeof(mVUregs)); + + mVU.regAlloc->flushAll(); + + if (isEbit) + { + mVUincCycles(mVU, 100); + mVUcycles -= 100; + qInst = mVU.q; + pInst = mVU.p; + mVUregs.xgkickcycles = 0; + + if (mVUinfo.doDivFlag) + { + sFLAG.doFlag = true; + sFLAG.write = fStatus; + mVUdivSet(mVU); + } + // Run any pending XGKick providing we've reached its PC. + if (mVUinfo.doXGKICK && xPC >= mVUinfo.XGKICKPC) + { + mVU_XGKICK_DELAY(mVU); + } + if (isVU1 && CHECK_XGKICKHACK) + { + mVUlow.kickcycles = 99; + mVU_XGKICK_SYNC(mVU, true); + } + + if (!isVU1) + armEmitCall((void*)mVU0clearlpStateJIT); + else + armEmitCall((void*)mVU1clearlpStateJIT); + } + + // Save P/Q regs from qmmPQ. qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, + // [3]=pending_p. Lane-0 stores go through Str-S [gprVUState, #imm12] since + // the S form refers to the lower 32 bits of the V register. Ext-by-4 is a + // full 4-lane left rotate (NOT an involution), so its inverse is Ext-by-12, + // NOT another Ext-by-4 — the qInst==1 "swap back" below MUST use Ext12. + if (qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 4); + armAsm->Str(a64::SRegister(qmmPQ.GetCode()), + mVUstateMem(offsetof(VURegs, VI) + REG_Q * sizeof(REG_VI))); + if (qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 12); // Swap back (inverse of Ext4) + else + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 4); + armAsm->Str(a64::SRegister(qmmPQ.GetCode()), + mVUstateMem(offsetof(VURegs, pending_q))); + if (!qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 12); // Restore + + if (isVU1) + { + // pInst rotation: when set, lanes 2/3 hold (pending_p, P) instead of + // (P, pending_p). Mirror x86 mVUendProgram by swapping the St1 lane + // indices rather than emitting a physical lane swap. + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_P * sizeof(REG_VI)); + armAsm->St1(qmmPQ.V4S(), pInst ? 3 : 2, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_p)); + armAsm->St1(qmmPQ.V4S(), pInst ? 2 : 3, a64::MemOperand(a64::x8)); + } + + // MAC/CLIP store (per-callsite — fMac/fClip vary by emit-time fInstance). + mVUallocMFLAGa(mVU, gprT1, fMac); + mVUallocCFLAGa(mVU, gprT2, fClip); + armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_MAC_FLAG * sizeof(REG_VI))); + armAsm->Str(gprT2, mVUstateMem(offsetof(VURegs, VI) + REG_CLIP_FLAG * sizeof(REG_VI))); + + // SFLAGc + micro_flag tail factored into per-VU helpers; see + // mVUGenerateEndProgramFlagsHelper. + armAsm->Mov(gprT3, getFlagReg(fStatus)); + armEmitCall(isEbit ? mVU.endProgramFlagsB : mVU.endProgramFlagsA); + + if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) + { + armAsm->Str(a64::wzr, mVUstateMem(offsetof(VURegs, nextBlockCycles))); + } + + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + if (isEbit) + { + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->And(a64::w9, a64::w9, isVU1 ? ~0x100u : ~0x001u); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + } + } + + if (isEbit != 2) + { + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUTBit); + armEmitJmp(mVU.exitFunct); + } + + memcpy(&mVUregs, &stateBackup, sizeof(mVUregs)); +} + +//------------------------------------------------------------------ +// mVUendProgram — Main end-of-program handler +//------------------------------------------------------------------ + +void mVUendProgram(mV, microFlagCycles* mFC, int isEbit) +{ + int fStatus = getLastFlagInst(mVUpBlock->pState, mFC->xStatus, 0, isEbit && isEbit != 3); + int fMac = getLastFlagInst(mVUpBlock->pState, mFC->xMac, 1, isEbit && isEbit != 3); + int fClip = getLastFlagInst(mVUpBlock->pState, mFC->xClip, 2, isEbit && isEbit != 3); + int qInst = 0, pInst = 0; + microBlock stateBackup; + memcpy(&stateBackup, &mVUregs, sizeof(mVUregs)); + + // x86 (microVU_Branch.inl) splits this: TDwritebackAll() for the non-E-bit / + // isEbit==3 case (write back without clearing, to preserve mappings for + // downstream reuse) and flushAll() for the E-bit case. On arm64 nothing after + // this flush reuses the regAlloc VF/VI mappings — the P/Q saves below go + // through qmmPQ and the MAC/CLIP/status stores use scratch temps — so the + // clear-vs-no-clear distinction is unobservable and one flushAll() covers + // both. (arm64 regAlloc has no TDwritebackAll; flushAll(false) would be the + // no-clear equivalent if a downstream consumer ever appears here.) + mVU.regAlloc->flushAll(); + + if (isEbit && isEbit != 3) + { + std::memset(&mVUinfo, 0, sizeof(mVUinfo)); + std::memset(&mVUregsTemp, 0, sizeof(mVUregsTemp)); + mVUincCycles(mVU, 100); + mVUcycles -= 100; + qInst = mVU.q; + pInst = mVU.p; + mVUregs.xgkickcycles = 0; + + if (mVUinfo.doDivFlag) + { + sFLAG.doFlag = true; + sFLAG.write = fStatus; + mVUdivSet(mVU); + } + if (mVUinfo.doXGKICK) + { + mVU_XGKICK_DELAY(mVU); + } + if (isVU1 && CHECK_XGKICKHACK) + { + mVUlow.kickcycles = 99; + mVU_XGKICK_SYNC(mVU, true); + } + + if (!isVU1) + armEmitCall((void*)mVU0clearlpStateJIT); + else + armEmitCall((void*)mVU1clearlpStateJIT); + } + + // Save P/Q regs from qmmPQ. qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, + // [3]=pending_p. Lane-0 stores go through Str-S [gprVUState, #imm12] since + // the S form refers to the lower 32 bits of the V register. Ext-by-4 is a + // full 4-lane left rotate (NOT an involution), so its inverse is Ext-by-12, + // NOT another Ext-by-4 — the qInst==1 "swap back" below MUST use Ext12. + if (qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 4); + armAsm->Str(a64::SRegister(qmmPQ.GetCode()), + mVUstateMem(offsetof(VURegs, VI) + REG_Q * sizeof(REG_VI))); + if (qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 12); + else + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 4); + armAsm->Str(a64::SRegister(qmmPQ.GetCode()), + mVUstateMem(offsetof(VURegs, pending_q))); + if (!qInst) + armAsm->Ext(qmmPQ.V16B(), qmmPQ.V16B(), qmmPQ.V16B(), 12); + + if (isVU1) + { + // pInst rotation: when set, lanes 2/3 hold (pending_p, P) instead of + // (P, pending_p). Mirror x86 mVUendProgram by swapping the St1 lane + // indices rather than emitting a physical lane swap. + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_P * sizeof(REG_VI)); + armAsm->St1(qmmPQ.V4S(), pInst ? 3 : 2, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_p)); + armAsm->St1(qmmPQ.V4S(), pInst ? 2 : 3, a64::MemOperand(a64::x8)); + } + + // MAC/CLIP store (per-callsite — fMac/fClip vary by emit-time fInstance). + mVUallocMFLAGa(mVU, gprT1, fMac); + mVUallocCFLAGa(mVU, gprT2, fClip); + armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_MAC_FLAG * sizeof(REG_VI))); + armAsm->Str(gprT2, mVUstateMem(offsetof(VURegs, VI) + REG_CLIP_FLAG * sizeof(REG_VI))); + + // SFLAGc denormalization + micro_flag backup-or-broadcast tail factored + // into a per-VU BL-callable helper. Per-exit emit shrinks from ~20 insns + // to 2 (Mov + Bl). See mVUGenerateEndProgramFlagsHelper in + // pcsx2/arm64/microVU-arm64.cpp. + armAsm->Mov(gprT3, getFlagReg(fStatus)); + armEmitCall((!isEbit || isEbit == 3) ? mVU.endProgramFlagsA : mVU.endProgramFlagsB); + + // Save TPC + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + if (isEbit && isEbit != 3) + { + if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) + { + armAsm->Str(a64::wzr, mVUstateMem(offsetof(VURegs, nextBlockCycles))); + } + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->And(a64::w9, a64::w9, isVU1 ? ~0x100u : ~0x001u); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + } + } + else if (isEbit == 3) + { + if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) + { + armAsm->Str(a64::wzr, mVUstateMem(offsetof(VURegs, nextBlockCycles))); + } + } + + if (isEbit != 2 && isEbit != 3) + { + // lpState is established by the branch sites (normBranch/condBranch via + // pStateEnd), the M-bit path, and the cycle early-exit before control + // reaches here. mVUendProgram intentionally does NOT copy pipeline state + // (matching mVUendProgram in x86 microVU_Branch.inl), so a Q/P-pipeline + // countdown still carried by lpState is preserved for the next program. + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + } + + memcpy(&mVUregs, &stateBackup, sizeof(mVUregs)); +} + +//------------------------------------------------------------------ +// Branch Setup +//------------------------------------------------------------------ + +void mVUsetupBranch(mV, microFlagCycles& mFC) +{ + mVU.regAlloc->flushAll(); + mVUsetupFlags(mVU, mFC); + + // Shuffle P/Q regs since every block starts at instance #0. + // qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, [3]=pending_p. + // When mVU.q==1 the "current" Q is in pending_q's slot — swap lanes 0/1. + // When mVU.p==1, similarly swap lanes 2/3. + // Matches x86 mVUsetupBranch (xPSHUF.D(xmmPQ, xmmPQ, shufflePQ)). + if (mVU.q) + { + armAsm->Umov(gprT1.W(), qmmPQ.V4S(), 0); + armAsm->Ins(qmmPQ.V4S(), 0, qmmPQ.V4S(), 1); + armAsm->Ins(qmmPQ.V4S(), 1, gprT1.W()); + } + if (mVU.p) + { + armAsm->Umov(gprT1.W(), qmmPQ.V4S(), 2); + armAsm->Ins(qmmPQ.V4S(), 2, qmmPQ.V4S(), 3); + armAsm->Ins(qmmPQ.V4S(), 3, gprT1.W()); + } + mVU.p = 0; + mVU.q = 0; +} + +//------------------------------------------------------------------ +// normBranchCompile — Compile/link to a block at known PC +//------------------------------------------------------------------ + +void normBranchCompile(microVU& mVU, u32 branchPC) +{ + microBlock* pBlock; + blockCreate(branchPC / 8); + pBlock = mVUblocks[branchPC / 8]->search(mVU, (microRegInfo*)&mVUregs); + if (pBlock) + armEmitJmp(pBlock->hostEntry); + else + mVUcompile(mVU, branchPC, (uptr)&mVUregs); +} + +//------------------------------------------------------------------ +// normJumpCompile — Compile indirect jump (JR/JALR) +//------------------------------------------------------------------ + +void normJumpCompile(mV, microFlagCycles& mFC, bool isEvilJump) +{ + memcpy(&mVUpBlock->pStateEnd, &mVUregs, sizeof(microRegInfo)); + mVUsetupBranch(mVU, mFC); + mVUbackupRegs(mVU); + + if (!mVUpBlock->jumpCache) + mVUpBlock->jumpCache = new microJumpCache[mProgSize / 2]; + + if (isEvilJump) + { + armLoadPtr(RWARG1, &mVU.evilBranch); + armLoadPtr(gprT1, &mVU.evilevilBranch); + armStorePtr(gprT1, &mVU.evilBranch); + } + else + armLoadPtr(RWARG1, &mVU.branch); + + if (doJumpCaching) + armMoveAddressToReg(RXARG2, mVUpBlock); + else + armMoveAddressToReg(RXARG2, &mVUpBlock->pStateEnd); + + if (mVUup.eBit && isEvilJump) + { + mVUendProgram(mVU, &mFC, 2); + armAsm->Str(RWARG1, mVUstateMem(offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI))); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + } + + if (!mVU.index) + armEmitCall((void*)(void (*)())mVUcompileJIT<0>); + else + armEmitCall((void*)(void (*)())mVUcompileJIT<1>); + + mVUrestoreRegs(mVU); + + // Jump to returned code address (in x0). + // Guard against NULL: if compile failed, exit instead of crashing. + a64::Label validBlock; + armAsm->Cbnz(a64::x0, &validBlock); + armEmitJmp(mVU.exitFunct); + armAsm->Bind(&validBlock); + armAsm->Br(a64::x0); +} + +//------------------------------------------------------------------ +// ARM64 MacroAssembler helpers for runtime compilation +//------------------------------------------------------------------ + +// Per-mVUopenCodeCache state: binds the global armAsm to the persistent +// per-VU MacroAssembler and records where its cursor sat so mVUcloseCodeCache +// can compute *this* block's size from the cursor delta. The MA itself is +// constructed once per mVUreset over the whole post-dispatcher cache range. +static thread_local ptrdiff_t s_mVUblockStartOffset = 0; + +static void mVUopenCodeCache(microVU& mVU) +{ + // Nested call (same thread re-enters before its outer close): no-op. + if (armAsm) + return; + + pxAssert(mVU.jitAsm); // mVUreset must have built it. + + HostSys::BeginCodeWrite(); + // armAsmPtr MUST equal the MA's buffer base (= mVU.prog.x86start) for + // armGetCurrentCodePointer() to return the real write position: + // armGetCurrentCodePointer() = armAsmPtr + armAsm->GetCursorOffset() + // With the persistent MA the cursor is "bytes since x86start", so + // armAsmPtr must be x86start (NOT x86start + cursor_at_open). Setting + // armAsmPtr to the block-start address would double-count the cursor + // offset and shift every armEmitJmp displacement / recorded block-entry + // pointer by exactly s_mVUblockStartOffset bytes — landing the + // dispatcher's Br x0 in the literal pool / alignment nops below the + // real block. + armAsmPtr = mVU.prog.x86start; + armAsmCapacity = static_cast(mVU.prog.x86end - mVU.prog.x86start); + armConstantPool = nullptr; + armAsm = mVU.jitAsm.get(); + + // Align this block's start to 16 bytes inside the persistent buffer. + // With a single MA spanning the whole cache, the cursor must be walked + // forward rather than aligning the buffer pointer. Cheap (≤ 3 nops). + while (armAsm->GetCursorOffset() & 15) + armAsm->Nop(); + s_mVUblockStartOffset = armAsm->GetCursorOffset(); + + // Persisted-JIT recorder: everything emitted until the matching close is + // one contiguous, relocatable-as-a-unit chunk. No-op unless recording is + // enabled. (Nested opens returned above, so this attaches exactly once + // per episode, on the thread that owns this VU's emission.) + mVUPersist::BeginEpisode(mVU, armAsmPtr + s_mVUblockStartOffset); +} + +static void mVUcloseCodeCache(microVU& mVU) +{ + if (!armAsm) + return; + + // kFallThrough: emit any accumulated literal pool inline without a branch + // over it. Subsequent blocks emit immediately after; safe because every + // block ends with armEmitJmp / Ret, so control never falls through. + armAsm->FinalizeCode(vixl::aarch64::MacroAssembler::kFallThrough); + + const ptrdiff_t curOffset = armAsm->GetCursorOffset(); + const u32 codeSize = static_cast(curOffset - s_mVUblockStartOffset); + // armAsmPtr is the MA's buffer base (= x86start) so the actual block-start + // address is x86start + s_mVUblockStartOffset, NOT armAsmPtr. + u8* codeStart = armAsmPtr + s_mVUblockStartOffset; + + // Persisted-JIT recorder: finalize (or drop) the chunk. Runs after + // FinalizeCode so the captured bytes include any literal pool. + mVUPersist::EndEpisode(mVU, codeStart + codeSize); + + armAsm = nullptr; // unbind; do not delete (persistent) + HostSys::EndCodeWrite(); + if (codeSize > 0) + HostSys::FlushInstructionCache(codeStart, codeSize); + + mVU.prog.x86ptr = codeStart + codeSize; +} + +//------------------------------------------------------------------ +// mVUcompileJIT — Called by JR/JALR at runtime +//------------------------------------------------------------------ + +_mVUt void* mVUcompileJIT(u32 startPC, uptr ptr) +{ + microVU& mVU = mVUx; + + if (doJumpAsSameProgram) + { + if (doJumpCaching) + { + microBlock* pBlock = (microBlock*)ptr; + microJumpCache& jc = pBlock->jumpCache[startPC / 8]; + if (jc.prog && jc.prog == mVU.prog.quick[startPC / 8].prog) + return jc.hostEntry; + + mVUopenCodeCache(mVU); + void* v = mVUblockFetch(mVU, startPC, (uptr)&pBlock->pStateEnd); + mVUcloseCodeCache(mVU); + + jc.prog = mVU.prog.quick[startPC / 8].prog; + jc.x86ptrStart = v; + jc.hostEntry = v; + return v; + } + + mVUopenCodeCache(mVU); + void* v = mVUblockFetch(mVU, startPC, ptr); + mVUcloseCodeCache(mVU); + return v; + } + + mVU.regs().start_pc = startPC; + if (doJumpCaching) + { + microBlock* pBlock = (microBlock*)ptr; + microJumpCache& jc = pBlock->jumpCache[startPC / 8]; + if (jc.prog && jc.prog == mVU.prog.quick[startPC / 8].prog) + return jc.hostEntry; + + mVUopenCodeCache(mVU); + void* v = mVUsearchProg(startPC, (uptr)&pBlock->pStateEnd); + mVUcloseCodeCache(mVU); + + jc.prog = mVU.prog.quick[startPC / 8].prog; + jc.x86ptrStart = v; + jc.hostEntry = v; + return v; + } + else + { + mVUopenCodeCache(mVU); + void* v = mVUsearchProg(startPC, ptr); + mVUcloseCodeCache(mVU); + return v; + } +} + +//------------------------------------------------------------------ +// normBranch — Unconditional branch (B/BAL) +//------------------------------------------------------------------ + +void normBranch(mV, microFlagCycles& mFC) +{ + if (mVUup.dBit && doDBitHandling) + { + mVU.regAlloc->flushAll(false); + u32 tempPC = iPC; + + a64::Label noDBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->B(&noDBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x200 : 0x2); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + iPC = branchAddr(mVU) / 4; + mVUDTendProgram(mVU, &mFC, 1); + + armAsm->Bind(&noDBit); + iPC = tempPC; + } + if (mVUup.tBit) + { + mVU.regAlloc->flushAll(false); + u32 tempPC = iPC; + + a64::Label noTBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x800 : 0x8); + armAsm->B(&noTBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + iPC = branchAddr(mVU) / 4; + mVUDTendProgram(mVU, &mFC, 1); + + armAsm->Bind(&noTBit); + iPC = tempPC; + } + if (mVUup.mBit) + { + DevCon.Warning("M-Bit on normal branch, report if broken"); + u32 tempPC = iPC; + + memcpy(&mVUpBlock->pStateEnd, &mVUregs, sizeof(microRegInfo)); + armMoveAddressToReg(a64::x0, &mVUpBlock->pStateEnd); + armEmitCall(mVU.copyPLState); + + mVUsetupBranch(mVU, mFC); + mVUendProgram(mVU, &mFC, 3); + iPC = branchAddr(mVU) / 4; + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + iPC = tempPC; + } + if (mVUup.eBit) + { + if (mVUlow.badBranch) + DevCon.Warning("End on evil Unconditional branch! - Not implemented!"); + + iPC = branchAddr(mVU) / 4; + mVUendProgram(mVU, &mFC, 1); + return; + } + + // Normal unconditional branch + mVUsetupBranch(mVU, mFC); + normBranchCompile(mVU, branchAddr(mVU)); +} + +//------------------------------------------------------------------ +// condBranch — Conditional branch (IBEQ/IBNE/IBGEZ/IBGTZ/IBLEZ/IBLTZ) +//------------------------------------------------------------------ + +void condBranch(mV, microFlagCycles& mFC, a64::Condition cond) +{ + mVUsetupBranch(mVU, mFC); + + // T-bit, D-bit, M-bit conditional branches — match x86 condBranch(). Each + // branch bit tests the relevant FBRST flag (T/D), then if set raises INTC, + // ends the program, and exits to either the taken or not-taken target based + // on the branch condition. + + if (mVUup.tBit) + { + DevCon.Warning("T-Bit on branch, please report if broken"); + u32 tempPC = iPC; + + a64::Label noTBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x800 : 0x8); + armAsm->B(&noTBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + mVUDTendProgram(mVU, &mFC, 2); + armMoveAddressToReg(a64::x8, &mVU.branch); + armAsm->Ldrsh(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Cmp(a64::w9, 0); + + a64::Label tJMP; + armAsm->B(&tJMP, cond); + // Not taken: set TPC to PC after the delay slot + incPC(4); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUTBit); + armEmitJmp(mVU.exitFunct); + armAsm->Bind(&tJMP); + incPC(-4); + iPC = branchAddr(mVU) / 4; + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUTBit); + armEmitJmp(mVU.exitFunct); + + armAsm->Bind(&noTBit); + iPC = tempPC; + } + + if (mVUup.dBit && doDBitHandling) + { + u32 tempPC = iPC; + + a64::Label noDBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->B(&noDBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x200 : 0x2); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + mVUDTendProgram(mVU, &mFC, 2); + armMoveAddressToReg(a64::x8, &mVU.branch); + armAsm->Ldrsh(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Cmp(a64::w9, 0); + + a64::Label dJMP; + armAsm->B(&dJMP, cond); + incPC(4); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + armEmitJmp(mVU.exitFunct); + armAsm->Bind(&dJMP); + incPC(-4); + iPC = branchAddr(mVU) / 4; + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + armEmitJmp(mVU.exitFunct); + + armAsm->Bind(&noDBit); + iPC = tempPC; + } + + if (mVUup.mBit) + { + u32 tempPC = iPC; + + memcpy(&mVUpBlock->pStateEnd, &mVUregs, sizeof(microRegInfo)); + armMoveAddressToReg(a64::x0, &mVUpBlock->pStateEnd); + armEmitCall(mVU.copyPLState); + + mVUendProgram(mVU, &mFC, 3); + armMoveAddressToReg(a64::x8, &mVU.branch); + armAsm->Ldrsh(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Cmp(a64::w9, 0); + + a64::Label mJMP; + // x86 emits ForwardJump with (JccComparisonType)JMPcc — the TAKEN + // branch skips forward, so the inline path is NOT TAKEN. Match. + armAsm->B(&mJMP, cond); + incPC(4); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + armAsm->Bind(&mJMP); + incPC(-4); + iPC = branchAddr(mVU) / 4; + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + + iPC = tempPC; + } + + if (mVUup.eBit) + { + if (mVUlow.evilBranch) + DevCon.Warning("End on evil branch! - Not implemented!"); + + mVUendProgram(mVU, &mFC, 2); + + // Test branch condition. `mVU.branch` holds the signed comparison + // value (from IBLEZ/IBLTZ etc.) or XOR-result (IBEQ/IBNE). The x86 + // path uses a 16-bit memory cmp (xCMP ptr16[&mVU.branch], 0) which + // treats the value as signed s16. Match that here with Ldrsh so + // negative VI values (e.g. 0xFFFE = -2) trigger .le/.lt correctly. + armMoveAddressToReg(a64::x8, &mVU.branch); + armAsm->Ldrsh(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Cmp(a64::w9, 0); + + a64::Label taken; + incPC(3); + armAsm->B(&taken, cond); + // Not taken: set TPC to next instruction + incPC(1); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + armAsm->Bind(&taken); + incPC(-4); + + // Taken: set TPC to branch target + iPC = branchAddr(mVU) / 4; + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI)); + armAsm->Mov(a64::w9, xPC); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + return; + } + + // Normal conditional branch. See E-bit path above for why Ldrsh. + armMoveAddressToReg(a64::x8, &mVU.branch); + armAsm->Ldrsh(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Cmp(a64::w9, 0); + + incPC(3); + // Try to find cached block for not-taken path + microBlock* bBlock; + incPC2(1); + blockCreate(iPC / 2); + bBlock = mVUblocks[iPC / 2]->search(mVU, (microRegInfo*)&mVUregs); + incPC2(-1); + + if (bBlock) + { + // Not-taken block exists: emit conditional jump to it + a64::Condition invCond = a64::InvertCondition(cond); + armEmitCondBranch(invCond, bBlock->hostEntry); + incPC(-3); + normBranchCompile(mVU, branchAddr(mVU)); + } + else + { + // Neither path compiled yet: compile not-taken, then patch taken + a64::Label takenLabel; + armAsm->B(&takenLabel, cond); + + u32 bPC = iPC; + microRegInfo regBackup; + memcpy(®Backup, &mVUregs, sizeof(microRegInfo)); + + incPC2(1); + mVUcompile(mVU, xPC, (uptr)&mVUregs); + + iPC = bPC; + incPC(-3); + void* jumpAddr = mVUblockFetch(mVU, branchAddr(mVU), (uptr)®Backup); + armAsm->Bind(&takenLabel); + if (jumpAddr) + armEmitJmp(jumpAddr); + else + armEmitJmp(mVU.exitFunct); // Safety: exit if compile failed + } +} + +//------------------------------------------------------------------ +// normJump — Indirect jump (JR/JALR) +//------------------------------------------------------------------ + +void normJump(mV, microFlagCycles& mFC) +{ + if (mVUup.mBit) + DevCon.Warning("M-Bit on Jump! Please report if broken"); + + if (mVUlow.constJump.isValid) + { + if (mVUup.eBit) + { + iPC = (mVUlow.constJump.regValue * 2) & (mVU.progMemMask); + mVUendProgram(mVU, &mFC, 1); + return; + } + int jumpAddr = (mVUlow.constJump.regValue * 8) & (mVU.microMemSize - 8); + mVUsetupBranch(mVU, mFC); + normBranchCompile(mVU, jumpAddr); + return; + } + + if (mVUup.dBit && doDBitHandling) + { + mVU.regAlloc->flushAll(false); + a64::Label noDBit; + armMoveAddressToReg(a64::x8, (THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->B(&noDBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x200 : 0x2); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + mVUDTendProgram(mVU, &mFC, 2); + armLoadPtr(gprT1, &mVU.branch); + armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI))); + armEmitJmp(mVU.exitFunct); + + armAsm->Bind(&noDBit); + } + if (mVUup.tBit) + { + mVU.regAlloc->flushAll(false); + a64::Label noTBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) ? + (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x800 : 0x8); + armAsm->B(&noTBit, a64::eq); + + if (!mVU.index || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + mVUDTendProgram(mVU, &mFC, 2); + armLoadPtr(gprT1, &mVU.branch); + armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI))); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUTBit); + armEmitJmp(mVU.exitFunct); + + armAsm->Bind(&noTBit); + } + if (mVUup.eBit) + { + mVUendProgram(mVU, &mFC, 2); + armLoadPtr(gprT1, &mVU.branch); + armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_TPC * sizeof(REG_VI))); + if (mVU.index && THREAD_VU1) + armEmitCall((void*)mVUEBit); + armEmitJmp(mVU.exitFunct); + } + else + { + normJumpCompile(mVU, mFC, false); + } +} + +// Division flag transfer to status flag (ported from x86 mVUdivSet). +// If this instruction doesn't otherwise write the status flag, seed the write +// instance from lastWrite. Then clear the D/I division bits and OR in the +// latest division flags captured via mVU.divFlag. +static void mVUdivSet(mV) +{ + if (mVUinfo.doDivFlag) + { + const a64::Register& sReg = getFlagReg(sFLAG.write); + if (!sFLAG.doFlag) + armAsm->Mov(sReg, getFlagReg(sFLAG.lastWrite)); + // Clear D/I bits (18:19) before OR'ing new flags. AArch64 BIC immediate + // accepts 0x000C0000 (two consecutive 1s, ROR 14 in 32-bit element) as + // a valid logical-immediate, so this collapses Mov + And → single Bic. + armAsm->Bic(sReg.W(), sReg.W(), 0x000C0000u); + armMoveAddressToReg(a64::x8, &mVU.divFlag); + armAsm->Ldr(gprT1, a64::MemOperand(a64::x8)); + armAsm->Orr(sReg.W(), sReg.W(), gprT1); + } +} + +//------------------------------------------------------------------ +// XGKICK Support +//------------------------------------------------------------------ + +// C helper: perform the GIF transfer (called at runtime) +void mVU_XGKICK_(u32 addr) +{ + addr = (addr & 0x3ff) * 16; + u32 diff = 0x4000 - addr; + u32 size = gifUnit.GetGSPacketSize(GIF_PATH_1, vuRegs[1].Mem, addr, ~0u, true); + + if (size > diff) + { + gifUnit.gifPath[GIF_PATH_1].CopyGSPacketData(&vuRegs[1].Mem[addr], diff, true); + gifUnit.TransferGSPacketData(GIF_TRANS_XGKICK, &vuRegs[1].Mem[0], size - diff, true); + } + else + { + gifUnit.TransferGSPacketData(GIF_TRANS_XGKICK, &vuRegs[1].Mem[addr], size, true); + } +} + +// C helper: cycle-counted XGKICK transfer +void _vuXGKICKTransfermVU(bool flush) +{ + while (VU1.xgkickenable && (flush || VU1.xgkickcyclecount >= 2)) + { + u32 transfersize = 0; + + if (VU1.xgkicksizeremaining == 0) + { + u32 size = gifUnit.GetGSPacketSize(GIF_PATH_1, vuRegs[1].Mem, VU1.xgkickaddr, ~0u, flush); + VU1.xgkicksizeremaining = size & 0xFFFF; + VU1.xgkickendpacket = size >> 31; + VU1.xgkickdiff = 0x4000 - VU1.xgkickaddr; + + if (VU1.xgkicksizeremaining == 0) + { + VU1.xgkickenable = false; + break; + } + } + + if (!flush) + { + transfersize = std::min(VU1.xgkicksizeremaining, VU1.xgkickcyclecount * 8); + transfersize = std::min(transfersize, VU1.xgkickdiff); + } + else + { + transfersize = VU1.xgkicksizeremaining; + transfersize = std::min(transfersize, VU1.xgkickdiff); + } + + if (THREAD_VU1) + { + if (transfersize < VU1.xgkicksizeremaining) + gifUnit.gifPath[GIF_PATH_1].CopyGSPacketData(&VU1.Mem[VU1.xgkickaddr], transfersize, true); + else + gifUnit.TransferGSPacketData(GIF_TRANS_XGKICK, &vuRegs[1].Mem[VU1.xgkickaddr], transfersize, true); + } + else + { + gifUnit.TransferGSPacketData(GIF_TRANS_XGKICK, &vuRegs[1].Mem[VU1.xgkickaddr], transfersize, true); + } + + if (flush) + VU1.cycle += transfersize / 8; + + VU1.xgkickcyclecount -= transfersize / 8; + VU1.xgkickaddr = (VU1.xgkickaddr + transfersize) & 0x3FFF; + VU1.xgkicksizeremaining -= transfersize; + VU1.xgkickdiff = 0x4000 - VU1.xgkickaddr; + + if (VU1.xgkickendpacket && !VU1.xgkicksizeremaining) + VU1.xgkickenable = false; + } +} + +// JIT emitter: emit code to call mVU_XGKICK_ at runtime +static __fi void mVU_XGKICK_DELAY(mV) +{ + mVU.regAlloc->flushCallerSavedRegisters(); + mVUbackupRegs(mVU, true, true); + + // Load VIxgkick value as argument and call the C helper + armLoadPtr(RWARG1, &mVU.VIxgkick); + armEmitCall((void*)mVU_XGKICK_); + + mVUrestoreRegs(mVU, true, true); +} + +// JIT emitter: emit code for cycle-counted XGKICK sync +static __fi void mVU_XGKICK_SYNC(mV, bool flush) +{ + mVU.regAlloc->flushCallerSavedRegisters(); + + // Test if xgkickenable is set + a64::Label skipxgkick; + armMoveAddressToReg(a64::x8, &VU1.xgkickenable); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, 1); + armAsm->B(&skipxgkick, a64::eq); + + // Add kick cycles + armMoveAddressToReg(a64::x8, &VU1.xgkickcyclecount); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Add(a64::w9, a64::w9, mVUlow.kickcycles - 1); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + // Check if enough cycles accumulated + a64::Label needcycles; + armAsm->Cmp(a64::w9, 2); + armAsm->B(&needcycles, a64::lt); + + mVUbackupRegs(mVU, true, true); + armAsm->Mov(RWARG1, flush ? 1 : 0); + armEmitCall((void*)_vuXGKICKTransfermVU); + mVUrestoreRegs(mVU, true, true); + + armAsm->Bind(&needcycles); + + // Add the remaining 1 cycle + armMoveAddressToReg(a64::x8, &VU1.xgkickcyclecount); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Add(a64::w9, a64::w9, 1); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Bind(&skipxgkick); +} diff --git a/pcsx2/arm64/microVU_Clamp-arm64.inl b/pcsx2/arm64/microVU_Clamp-arm64.inl new file mode 100644 index 0000000000..2e6e3e12ee --- /dev/null +++ b/pcsx2/arm64/microVU_Clamp-arm64.inl @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// Micro VU - ARM64 NEON Clamp Functions +//------------------------------------------------------------------ + +// Result clamping: clamp to [minFloat, maxFloat]. +// Uses FMINNM/FMAXNM (number-preserving) so NaN inputs clamp to ±maxfloat, +// matching x86 SSE MINPS/MAXPS NaN-eating semantics. Plain FMIN/FMAX are +// IEEE-strict and propagate NaN — using them here lets NaN flow through +// matrix-multiply chains and corrupts vertex output. +void mVUclamp1(microVU& mVU, const a64::VRegister& reg, const a64::VRegister& regT1, int xyzw, bool bClampE = false) +{ + if (((!clampE && CHECK_VU_OVERFLOW(mVU.index)) || (clampE && bClampE)) && mVU.regAlloc->checkVFClamp(reg.GetCode())) + { + switch (xyzw) + { + case 1: case 2: case 4: case 8: + { + armAsm->Ldr(a64::VRegister(RQSCRATCH3.GetCode(), 32), mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Fminnm(a64::VRegister(reg.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), + a64::VRegister(RQSCRATCH3.GetCode(), 32)); + armAsm->Ldr(a64::VRegister(RQSCRATCH3.GetCode(), 32), mVUglobMem(&mVUglob.minvals[0])); + armAsm->Fmaxnm(a64::VRegister(reg.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), + a64::VRegister(RQSCRATCH3.GetCode(), 32)); + break; + } + default: + { + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Fminnm(reg.V4S(), reg.V4S(), RQSCRATCH3.V4S()); + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.minvals[0])); + armAsm->Fmaxnm(reg.V4S(), reg.V4S(), RQSCRATCH3.V4S()); + break; + } + } + } +} + +// Operand clamping with sign preservation. +// Uses integer SMIN/UMIN to preserve NaN sign bit. +void mVUclamp2(microVU& mVU, const a64::VRegister& reg, const a64::VRegister& regT1in, int xyzw, bool bClampE = false) +{ + if (((!clampE && CHECK_VU_SIGN_OVERFLOW(mVU.index)) || (clampE && bClampE && CHECK_VU_SIGN_OVERFLOW(mVU.index))) && mVU.regAlloc->checkVFClamp(reg.GetCode())) + { + // Integer min/max to preserve NaN sign + // SMIN.4S clamps the signed integer representation + // UMIN.4S clamps the unsigned integer representation + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Smin(reg.V4S(), reg.V4S(), RQSCRATCH3.V4S()); + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.minvals[0])); + armAsm->Umin(reg.V4S(), reg.V4S(), RQSCRATCH3.V4S()); + return; + } + else + { + mVUclamp1(mVU, reg, regT1in, xyzw, bClampE); + } +} + +// Operand clamping for every arithmetic op (only when extra overflow enabled) +void mVUclamp3(microVU& mVU, const a64::VRegister& reg, const a64::VRegister& regT1, int xyzw) +{ + if (clampE && mVU.regAlloc->checkVFClamp(reg.GetCode())) + mVUclamp2(mVU, reg, regT1, xyzw, true); +} + +// Result clamping for every arithmetic op (when extra overflow but not sign-preserving) +void mVUclamp4(microVU& mVU, const a64::VRegister& reg, const a64::VRegister& regT1, int xyzw) +{ + if (clampE && !CHECK_VU_SIGN_OVERFLOW(mVU.index) && mVU.regAlloc->checkVFClamp(reg.GetCode())) + mVUclamp1(mVU, reg, regT1, xyzw, true); +} diff --git a/pcsx2/arm64/microVU_Compile-arm64.inl b/pcsx2/arm64/microVU_Compile-arm64.inl new file mode 100644 index 0000000000..16b53d9f96 --- /dev/null +++ b/pcsx2/arm64/microVU_Compile-arm64.inl @@ -0,0 +1,1172 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Config.h" +#include "common/FileSystem.h" +#include "common/Path.h" + + +//------------------------------------------------------------------ +// Messages Called at Execution Time +//------------------------------------------------------------------ + +static inline void mVUbadOp0 (u32 prog, u32 pc) { Console.Error("microVU0 Warning: Bad opcode [%04x] [%03d]", pc, prog); } +static inline void mVUbadOp1 (u32 prog, u32 pc) { Console.Error("microVU1 Warning: Bad opcode [%04x] [%03d]", pc, prog); } + +//------------------------------------------------------------------ +// Program Range Checking +//------------------------------------------------------------------ + +__fi void mVUcheckIsSame(mV) +{ + if (mVU.prog.isSame == -1) + mVU.prog.isSame = !memcmp((u8*)mVUcurProg.data, mVU.regs().Micro, mVU.microMemSize); + if (mVU.prog.isSame == 0) + { + mVUcacheProg(mVU, *mVU.prog.cur); + mVU.prog.isSame = 1; + } +} + +void mVUsetupRange(microVU& mVU, s32 pc, bool isStartPC) +{ + std::deque*& ranges = mVUcurProg.ranges; + if (pc > (s64)mVU.microMemSize) + { + Console.Error("microVU%d: PC outside of VU memory PC=0x%04x", mVU.index, pc); + pxFail("microVU: PC out of VU memory"); + } + + const s32 cur_pc = (!isStartPC && mVUrange.start > pc && pc == 0) ? mVU.microMemSize : pc; + + if (isStartPC) + { + for (auto it = ranges->begin(); it != ranges->end(); ++it) + { + if ((cur_pc >= it->start) && (cur_pc <= it->end)) + { + if (it->start != it->end) + { + microRange mRange = {it->start, it->end}; + ranges->erase(it); + ranges->push_front(mRange); + return; + } + } + } + } + else if (mVUrange.end >= cur_pc) + return; + + if (doWholeProgCompare) + mVUcheckIsSame(mVU); + + if (isStartPC) + { + microRange mRange = {cur_pc, -1}; + ranges->push_front(mRange); + return; + } + + if (mVUrange.start <= cur_pc) + { + mVUrange.end = cur_pc; + s32 rStart = mVUrange.start; + s32 rEnd = mVUrange.end; + for (auto it = ranges->begin() + 1; it != ranges->end();) + { + if (((it->start >= rStart) && (it->start <= rEnd)) || + ((it->end >= rStart) && (it->end <= rEnd))) + { + mVUrange.start = rStart = std::min(it->start, rStart); + mVUrange.end = rEnd = std::max(it->end, rEnd); + it = ranges->erase(it); + } + else + it++; + } + } + else + { + mVUrange.end = mVU.microMemSize; + microRange mRange = {0, cur_pc}; + ranges->push_front(mRange); + } + + if (!doWholeProgCompare) + mVUcacheProg(mVU, *mVU.prog.cur); +} + +//------------------------------------------------------------------ +// Pipeline State Helpers (platform-independent) +//------------------------------------------------------------------ + +__fi u8 optimizeReg(u8 rState) { return (rState == 1) ? 0 : rState; } +__fi u8 calcCycles(u8 reg, u8 x) { return ((reg > x) ? (reg - x) : 0); } +__fi u8 tCycles(u8 dest, u8 src) { return std::max(dest, src); } +__fi void incP(mV) { mVU.p ^= 1; } +__fi void incQ(mV) { mVU.q ^= 1; } + +// Optimizes the end pipeline state — collapses cycles-remaining==1 to 0, since +// mVU's block loop auto-decrements at entry so 1 is equivalent to 0. Without +// this, pipeline-state-hashed blocks get distinct variants for every cycle +// delta, exploding the block cache. Ported verbatim from x86 microVU_Compile.inl. +void mVUoptimizePipeState(mV) +{ + for (int i = 0; i < 32; i++) + { + mVUregs.VF[i].x = optimizeReg(mVUregs.VF[i].x); + mVUregs.VF[i].y = optimizeReg(mVUregs.VF[i].y); + mVUregs.VF[i].z = optimizeReg(mVUregs.VF[i].z); + mVUregs.VF[i].w = optimizeReg(mVUregs.VF[i].w); + } + for (int i = 0; i < 16; i++) + { + mVUregs.VI[i] = optimizeReg(mVUregs.VI[i]); + } + if (mVUregs.q) { mVUregs.q = optimizeReg(mVUregs.q); if (!mVUregs.q) { incQ(mVU); } } + if (mVUregs.p) { mVUregs.p = optimizeReg(mVUregs.p); if (!mVUregs.p) { incP(mVU); } } + mVUregs.r = 0; // No stalls on R-reg — safe to discard. +} + +// Advance pipeline cycles by x instructions. Ported verbatim from x86. +void mVUincCycles(mV, int x) +{ + mVUcycles += x; + // VF[0] is a constant (0,0,0,1) — skip. + for (int z = 31; z > 0; z--) + { + mVUregs.VF[z].x = calcCycles(mVUregs.VF[z].x, x); + mVUregs.VF[z].y = calcCycles(mVUregs.VF[z].y, x); + mVUregs.VF[z].z = calcCycles(mVUregs.VF[z].z, x); + mVUregs.VF[z].w = calcCycles(mVUregs.VF[z].w, x); + } + // VI[0] is constant (0) — skip. + for (int z = 15; z > 0; z--) + { + mVUregs.VI[z] = calcCycles(mVUregs.VI[z], x); + } + if (mVUregs.q) + { + if (mVUregs.q > 4) + { + mVUregs.q = calcCycles(mVUregs.q, x); + if (mVUregs.q <= 4) + mVUinfo.doDivFlag = 1; + } + else + { + mVUregs.q = calcCycles(mVUregs.q, x); + } + if (!mVUregs.q) + incQ(mVU); + } + if (mVUregs.p) + { + mVUregs.p = calcCycles(mVUregs.p, x); + if (!mVUregs.p || mVUregsTemp.p) + incP(mVU); + } + if (mVUregs.xgkick) + { + mVUregs.xgkick = calcCycles(mVUregs.xgkick, x); + if (!mVUregs.xgkick) + { + mVUinfo.doXGKICK = 1; + mVUinfo.XGKICKPC = xPC; + } + } + mVUregs.r = calcCycles(mVUregs.r, x); +} + +// Helper: set xVar to 1 if VFreg1 and VFreg2 reference the same VF reg and +// any of the X/Y/Z/W components are touched by both. Ported from x86. +static __fi void cmpVFregs(microVFreg& VFreg1, microVFreg& VFreg2, bool& xVar) +{ + if (VFreg1.reg == VFreg2.reg) + { + if ((VFreg1.x && VFreg2.x) || (VFreg1.y && VFreg2.y) + || (VFreg1.z && VFreg2.z) || (VFreg1.w && VFreg2.w)) + { + xVar = 1; + } + } +} + +void mVUsetCycles(mV) +{ + mVUincCycles(mVU, mVUstall); + + // If upper Op && lower Op write to same VF reg: either make Lower skip its + // VF write (noWriteVF) or mark it a NOP entirely when Lower has no other + // side effects. + if ((mVUregsTemp.VFreg[0] == mVUregsTemp.VFreg[1]) && mVUregsTemp.VFreg[0]) + { + if (mVUregsTemp.r || mVUregsTemp.VI) + mVUlow.noWriteVF = true; + else + mVUlow.isNOP = true; + } + + // If Lower reads a VF reg that Upper writes, Upper's semantic output must + // be visible to Lower → run Lower first (swapOps). + if ((mVUlow.VF_read[0].reg || mVUlow.VF_read[1].reg) && mVUup.VF_write.reg) + { + cmpVFregs(mVUup.VF_write, mVUlow.VF_read[0], mVUinfo.swapOps); + cmpVFregs(mVUup.VF_write, mVUlow.VF_read[1], mVUinfo.swapOps); + } + + // If swapOps is set AND Upper also reads a VF reg that Lower writes, + // snapshot the VF reg before Lower runs so Upper sees pre-Lower + // state (backupVF). + if (mVUinfo.swapOps && ((mVUup.VF_read[0].reg || mVUup.VF_read[1].reg) && mVUlow.VF_write.reg)) + { + cmpVFregs(mVUlow.VF_write, mVUup.VF_read[0], mVUinfo.backupVF); + cmpVFregs(mVUlow.VF_write, mVUup.VF_read[1], mVUinfo.backupVF); + } + + mVUregs.VF[mVUregsTemp.VFreg[0]].x = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].x, mVUregsTemp.VF[0].x); + mVUregs.VF[mVUregsTemp.VFreg[0]].y = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].y, mVUregsTemp.VF[0].y); + mVUregs.VF[mVUregsTemp.VFreg[0]].z = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].z, mVUregsTemp.VF[0].z); + mVUregs.VF[mVUregsTemp.VFreg[0]].w = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].w, mVUregsTemp.VF[0].w); + + mVUregs.VF[mVUregsTemp.VFreg[1]].x = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].x, mVUregsTemp.VF[1].x); + mVUregs.VF[mVUregsTemp.VFreg[1]].y = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].y, mVUregsTemp.VF[1].y); + mVUregs.VF[mVUregsTemp.VFreg[1]].z = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].z, mVUregsTemp.VF[1].z); + mVUregs.VF[mVUregsTemp.VFreg[1]].w = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].w, mVUregsTemp.VF[1].w); + + mVUregs.VI[mVUregsTemp.VIreg] = tCycles(mVUregs.VI[mVUregsTemp.VIreg], mVUregsTemp.VI); + + mVUregs.q = tCycles(mVUregs.q, mVUregsTemp.q); + mVUregs.p = tCycles(mVUregs.p, mVUregsTemp.p); + mVUregs.r = tCycles(mVUregs.r, mVUregsTemp.r); + mVUregs.xgkick = tCycles(mVUregs.xgkick, mVUregsTemp.xgkick); + memset(&mVUregsTemp, 0, sizeof(mVUregsTemp)); +} + +//------------------------------------------------------------------ +// Flag-Pass Analysis (ported from x86 microVU_Flags.inl) +//------------------------------------------------------------------ +// Scans forward through instructions to determine which pipeline flags +// (sFlag/mFlag/cFlag) the next block reads in its first ~4 instructions. +// Sets mVUregs.needExactMatch bits (1/2/4) so block lookup can require +// an exact pipeline-state match for correctness. + +#define shortBranchPass() \ + { \ + if ((branch == 3) || (branch == 4)) /* Branches */ \ + { \ + _mVUflagPass(mVU, aBranchAddr, sCount + found, found, v); \ + if (branch == 3) /* Non-conditional Branch */ \ + break; \ + branch = 0; \ + } \ + else if (branch == 5) /* JR/JARL */ \ + { \ + if (sCount + found < 4) \ + mVUregs.needExactMatch |= 7; \ + break; \ + } \ + else /* E-Bit End */ \ + break; \ + } + +// Scan instructions at startPC and check if they read any pipeline flags. +// Uses pass4 (recPass=3) on each Upper/Lower op to accumulate needExactMatch bits. +void _mVUflagPass(mV, u32 startPC, u32 sCount, u32 found, std::vector& v) +{ + for (u32 i = 0; i < v.size(); i++) + { + if (v[i] == startPC) + return; // Prevent infinite recursion + } + v.push_back(startPC); + + int oldPC = iPC; + int oldBranch = mVUbranch; + int aBranchAddr = 0; + iPC = startPC / 4; + mVUbranch = 0; + for (int branch = 0; sCount < 4; sCount += found) + { + mVUregs.needExactMatch &= 7; + incPC(1); + mVUopU(mVU, 3); + found |= (mVUregs.needExactMatch & 8) >> 3; + mVUregs.needExactMatch &= 7; + if (curI & _Ebit_) + { + branch = 1; + } + if (curI & _Tbit_) + { + branch = 6; + } + if ((curI & _Dbit_) && doDBitHandling) + { + branch = 6; + } + if (!(curI & _Ibit_)) + { + incPC(-1); + mVUopL(mVU, 3); + incPC(1); + } + + if (branch >= 2) + { + shortBranchPass(); + } + else if (branch == 1) + { + branch = 2; + } + if (mVUbranch) + { + branch = ((mVUbranch > 8) ? (5) : ((mVUbranch < 3) ? 3 : 4)); + incPC(-1); + aBranchAddr = branchAddr(mVU); + incPC(1); + mVUbranch = 0; + } + incPC(1); + if ((mVUregs.needExactMatch & 7) == 7) + break; + } + iPC = oldPC; + mVUbranch = oldBranch; + mVUregs.needExactMatch &= 7; + setCode(); +} + +void mVUflagPass(mV, u32 startPC, u32 sCount = 0, u32 found = 0) +{ + std::vector v; + _mVUflagPass(mVU, startPC, sCount, found, v); +} + +// Checks if the first ~4 instructions of the successor block(s) read flags, +// and sets needExactMatch bits accordingly so block lookup requires exact state. +void mVUsetFlagInfo(mV) +{ + if (noFlagOpts) + { + mVUregs.needExactMatch = 0x7; + mVUregs.flagInfo = 0x0; + return; + } + if (mVUbranch <= 2) // B/BAL + { + incPC(-1); + mVUflagPass(mVU, branchAddr(mVU)); + incPC(1); + + mVUregs.needExactMatch &= 0x7; + } + else if (mVUbranch <= 8) // Conditional Branch + { + incPC(-1); // Branch Taken + mVUflagPass(mVU, branchAddr(mVU)); + int backupFlagInfo = mVUregs.needExactMatch; + mVUregs.needExactMatch = 0; + + incPC(4); // Branch Not Taken + mVUflagPass(mVU, xPC); + incPC(-3); + + mVUregs.needExactMatch |= backupFlagInfo; + mVUregs.needExactMatch &= 0x7; + } + else // JR/JALR + { + if (!doConstProp || !mVUlow.constJump.isValid) + { + mVUregs.needExactMatch |= 0x7; + } + else + { + mVUflagPass(mVU, (mVUlow.constJump.regValue * 8) & (mVU.microMemSize - 8)); + } + mVUregs.needExactMatch &= 0x7; + } +} + +//------------------------------------------------------------------ +// Cycle Test (emits code to check remaining cycles) +//------------------------------------------------------------------ + +// Test remaining cycles; if insufficient, save block state via copyPLState + +// mVUendProgram(0) and exit to the dispatcher. Otherwise deduct cycles and +// continue into the block. Ported from x86 microVU_Compile.inl:449. +// The copyPLState + mVUendProgram(0) on early-exit is required so that +// a cycle-timeout block has its pipeline state saved; without it, the block +// manager would see stale pState on re-entry and create a new variant. +static void mVUtestCycles(mV, microFlagCycles& mFC) +{ + iPC = mVUstartPC; + + if (isVU0 && EmuConfig.Speedhacks.EECycleRate != 0 && (!EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Speedhacks.EECycleRate < 0)) + { + switch (std::min(static_cast(EmuConfig.Speedhacks.EECycleRate), static_cast(mVUcycles))) + { + case -3: mVUcycles *= 2.0f; break; + case -2: mVUcycles *= 1.6666667f; break; + case -1: mVUcycles *= 1.3333333f; break; + case 1: mVUcycles /= 1.3f; break; + case 2: mVUcycles /= 1.8f; break; + case 3: mVUcycles /= 3.0f; break; + default: break; + } + } + + armMoveAddressToReg(a64::x8, &mVU.cycles); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + if (EmuConfig.Gamefixes.VUSyncHack) + armAsm->Subs(a64::w9, a64::w9, mVUcycles); + else + armAsm->Subs(a64::w9, a64::w9, 1); + + // If (cycles - check) is non-negative, there is budget — skip the early exit. + a64::Label skip; + armAsm->B(&skip, a64::pl); // pl = N clear = non-negative + + // Early exit path: save pipeline state then exit via mVUendProgram(0). + armMoveAddressToReg(a64::x0, &mVUpBlock->pState); + armEmitCall(mVU.copyPLState); + if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) + { + armAsm->Mov(a64::w9, mVUcycles); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, nextBlockCycles))); + } + mVUendProgram(mVU, &mFC, 0); + + armAsm->Bind(&skip); + + // Budget remains — deduct block cycles from mVU.cycles and fall through + // into the block body. x8 still holds &mVU.cycles from the first + // materialization above; the early-exit path that clobbers it tail-calls + // mVUendProgram and never reaches here, so x8 is safe to reuse directly. + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Sub(a64::w9, a64::w9, mVUcycles); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); +} + +//------------------------------------------------------------------ +// Execute VU Instruction (Upper + Lower) +//------------------------------------------------------------------ + +// Pre-populate NEON/GPR caches with VF/VI registers the next few ops will +// read. Ported from pcsx2/x86/microVU_Compile.inl:603-690. Runs once at +// the start of pass 2; iterates forward through mVUinfo until caches are +// nearly full, or an XGKICK / branch is encountered. Purely an +// optimization — skips pre-loaded regs on allocReg so subsequent ops +// reuse the cached data instead of re-loading from memory. +static void mvuPreloadRegisters(microVU& mVU, u32 endCount) +{ + static constexpr const int REQUIRED_FREE_NEON = 3; + static constexpr const int REQUIRED_FREE_GPRS = 1; + + u32 vfs_loaded = 0; + u32 vis_loaded = 0; + + for (int reg = 0; reg < mVU.regAlloc->getNeonCount(); reg++) + { + const int vf = mVU.regAlloc->getRegVF(reg); + if (vf >= 0) + vfs_loaded |= (1u << vf); + } + for (int reg = 0; reg < mVU.regAlloc->getGPRCount(); reg++) + { + const int vi = mVU.regAlloc->getRegVI(reg); + if (vi >= 0) + vis_loaded |= (1u << vi); + } + + const u32 orig_pc = iPC; + const u32 orig_code = mVU.code; + int free_regs = mVU.regAlloc->getFreeNeonCount(); + int free_gprs = mVU.regAlloc->getFreeGPRCount(); + + auto preloadVF = [&mVU, &vfs_loaded, &free_regs](u8 reg) + { + if (free_regs <= REQUIRED_FREE_NEON || reg == 0 || (vfs_loaded & (1u << reg)) != 0) + return; + mVU.regAlloc->clearNeeded(mVU.regAlloc->allocReg(reg)); + vfs_loaded |= (1u << reg); + free_regs--; + }; + + auto preloadVI = [&mVU, &vis_loaded, &free_gprs](u8 reg) + { + if (free_gprs <= REQUIRED_FREE_GPRS || reg == 0 || (vis_loaded & (1u << reg)) != 0) + return; + mVU.regAlloc->clearNeeded(mVU.regAlloc->allocGPR(reg)); + vis_loaded |= (1u << reg); + free_gprs--; + }; + + auto canPreload = [&free_regs, &free_gprs]() { + return (free_regs >= REQUIRED_FREE_NEON || free_gprs >= REQUIRED_FREE_GPRS); + }; + + for (u32 x = 0; x < endCount && canPreload(); x++) + { + incPC(1); + + const microOp* info = &mVUinfo; + if (info->doXGKICK) + break; + + for (u32 i = 0; i < 2; i++) + { + preloadVF(info->uOp.VF_read[i].reg); + preloadVF(info->lOp.VF_read[i].reg); + if (info->lOp.VI_read[i].used) + preloadVI(info->lOp.VI_read[i].reg); + } + + const microVFreg& uvfr = info->uOp.VF_write; + if (uvfr.reg != 0 && (!uvfr.x || !uvfr.y || !uvfr.z || !uvfr.w)) + preloadVF(uvfr.reg); + + const microVFreg& lvfr = info->lOp.VF_write; + if (lvfr.reg != 0 && (!lvfr.x || !lvfr.y || !lvfr.z || !lvfr.w)) + preloadVF(lvfr.reg); + + if (info->lOp.branch) + break; + } + + iPC = orig_pc; + mVU.code = orig_code; +} + +__ri void doUpperOp(mV) { mVUopU(mVU, 1); } +__ri void doLowerOp(mV) { incPC(-1); mVUopL(mVU, 1); incPC(1); } +__ri void flushRegs(mV) { if (!doRegAlloc) mVU.regAlloc->flushAll(); } + +void doIbit(mV) +{ + if (mVUup.iBit) + { + incPC(-1); + u32 tempI = curI; + if (CHECK_VU_OVERFLOW(mVU.index) && ((curI & 0x7fffffff) >= 0x7f800000)) + tempI = (0x80000000 & curI) | 0x7f7fffff; + + armAsm->Mov(a64::w9, tempI); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, VI) + REG_I * sizeof(REG_VI))); + incPC(1); + } +} + +// Ported from x86 microVU_Compile.inl:doSwapOp — runs Lower before Upper, and +// when Upper reads a VF reg Lower writes, snapshots the pre-Lower VF value via +// an XOR-swap so Upper observes the original value. +static void doSwapOp(mV) +{ + if (mVUinfo.backupVF && !mVUlow.noWriteVF) + { + DevCon.WriteLn(Color_Green, "microVU%d: Backing Up VF Reg [%04x]", getIndex, xPC); + + // Alloc t1 = current value of Lower's VF_write reg (pre-Lower). + const a64::VRegister t1 = mVU.regAlloc->allocReg(mVUlow.VF_write.reg); + const a64::VRegister t2 = mVU.regAlloc->allocReg(); + armAsm->Mov(t2.V16B(), t1.V16B()); // t2 = pre-Lower value + mVU.regAlloc->clearNeeded(t1); + + mVUopL(mVU, 1); // Lower writes new value to VF_write + + // XOR-swap: t2 gets new value, VF_write reg (via t3) gets old value, + // so Upper sees the pre-Lower state. + const a64::VRegister t3 = mVU.regAlloc->allocReg(mVUlow.VF_write.reg, mVUlow.VF_write.reg, 0xf, false); + armAsm->Eor(t2.V16B(), t2.V16B(), t3.V16B()); + armAsm->Eor(t3.V16B(), t3.V16B(), t2.V16B()); + armAsm->Eor(t2.V16B(), t2.V16B(), t3.V16B()); + mVU.regAlloc->clearNeeded(t3); + + incPC(1); + doUpperOp(mVU); // Upper reads VF_write reg with old value + + // Write the new value (held in t2) back to VF_write reg. + const a64::VRegister t4 = mVU.regAlloc->allocReg(-1, mVUlow.VF_write.reg, 0xf); + armAsm->Mov(t4.V16B(), t2.V16B()); + mVU.regAlloc->clearNeeded(t4); + mVU.regAlloc->clearNeeded(t2); + } + else + { + mVUopL(mVU, 1); + incPC(1); + flushRegs(mVU); + doUpperOp(mVU); + } +} + +// Runtime D-bit handler: if VU0/VU1 FBRST has the D-interrupt bit set, raise +// VPU_STAT and INTCINTERRUPT flags, end the program, otherwise fall through. +// Mirrors x86 microVU_Compile.inl:560-576. +static void mVUDoDBit(microVU& mVU, microFlagCycles* mFC) +{ + // Flush regalloc before the conditional skip — mVUDTendProgram's internal + // flushAll emits the stores INSIDE the branch, so the silent path would + // otherwise drop dirty regs (the lower op of the D-bit pair). Same pattern + // as the branch-side D-bit handler in microVU_Branch-arm64.inl:540. + mVU.regAlloc->flushAll(false); + + a64::Label noDBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) + ? (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->B(&noDBit, a64::eq); + + if (!isVU1 || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x200 : 0x2); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + + incPC(1); + mVUDTendProgram(mVU, mFC, 1); + incPC(-1); + + armAsm->Bind(&noDBit); +} + +// Runtime T-bit handler. Same pattern as mVUDoDBit but tests the T bit. +// Mirrors x86 microVU_Compile.inl:578-595. +static void mVUDoTBit(microVU& mVU, microFlagCycles* mFC) +{ + // Flush regalloc before the conditional skip — mVUDTendProgram's internal + // flushAll emits the stores INSIDE the branch, so the silent path would + // otherwise drop dirty regs (the lower op of the T-bit pair). Same pattern + // as the branch-side T-bit handler in microVU_Branch-arm64.inl:569. + mVU.regAlloc->flushAll(false); + + a64::Label noTBit; + armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1) + ? (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Tst(a64::w9, isVU1 ? 0x800 : 0x8); + armAsm->B(&noTBit, a64::eq); + + if (!isVU1 || !THREAD_VU1) + { + armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x400 : 0x4); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + + incPC(1); + mVUDTendProgram(mVU, mFC, 1); + incPC(-1); + + armAsm->Bind(&noTBit); +} + +void mVUexecuteInstruction(mV) +{ + if (mVUlow.isNOP) + { + incPC(1); + doUpperOp(mVU); + flushRegs(mVU); + doIbit(mVU); + } + else if (!mVUinfo.swapOps) + { + incPC(1); + doUpperOp(mVU); + flushRegs(mVU); + doLowerOp(mVU); + } + else + { + doSwapOp(mVU); + } + flushRegs(mVU); +} + +//------------------------------------------------------------------ +// Init helpers +//------------------------------------------------------------------ + +__fi void startLoop(mV) +{ + memset(&mVUinfo, 0, sizeof(mVUinfo)); + memset(&mVUregsTemp, 0, sizeof(mVUregsTemp)); +} + +__fi void mVUinitConstValues(microVU& mVU) +{ + for (int i = 0; i < 16; i++) + { + mVUconstReg[i].isValid = 0; + mVUconstReg[i].regValue = 0; + } + mVUconstReg[15].isValid = mVUregs.vi15v; + mVUconstReg[15].regValue = mVUregs.vi15v ? mVUregs.vi15 : 0; +} + +__fi void mVUinitFirstPass(mV, uptr pState, u8* thisPtr) +{ + mVUstartPC = iPC; + mVUbranch = 0; + mVUcount = 0; + mVUcycles = 0; + mVU.p = 0; + mVU.q = 0; + + if ((uptr)&mVUregs != pState) + memcpy((u8*)&mVUregs, (u8*)pState, sizeof(microRegInfo)); + if ((uptr)&mVU.prog.lpState != pState) + memcpy((u8*)&mVU.prog.lpState, (u8*)pState, sizeof(microRegInfo)); + + mVUblock.x86ptrStart = thisPtr; + // hostEntry mirrors x86ptrStart: the JIT stores its code-cache slot in both. + // The indirection is kept so an alternate code source can repoint hostEntry + // at a prepared block without disturbing the code-cache slot tracking. + mVUblock.hostEntry = thisPtr; + + // Create block manager if needed, then add this block. Both conditions are + // invariant violations — the caller (mVUcompile) unconditionally proceeds + // into the first-pass loop and dereferences mVUpBlock, so a bare return + // here would just defer a NULL/stale deref into UB. Fail fast instead. + pxAssertRel(mVU.prog.cur, "microVU: mVUinitFirstPass with NULL mVU.prog.cur"); + blockCreate(mVUstartPC / 2); + mVUpBlock = mVUblocks[mVUstartPC / 2]->add(mVU, &mVUblock); + pxAssertRel(mVUpBlock, "microVU: mVUpBlock NULL after blockManager::add"); + // Register this block (manager copy + host entry) with the VU program-cache + // recorder so the emitted code can be persisted and reloaded across runs. + mVUPersist::OnBlockCompiled(mVU, mVUpBlock, thisPtr, mVUstartPC * 4); + mVUregs.needExactMatch = (mVUpBlock->pState.blockType) ? 7 : 0; + mVUregs.blockType = 0; + mVUregs.viBackUp = 0; + mVUregs.flagInfo = 0; // Must be cleared each compile: mVUsetFlags OR-updates + // flagInfo at end of compile, so stale bits accumulate + // across blocks, making every compile hash to a new + // pipeline state and create a new variant. + mVUsFlagHack = CHECK_VU_FLAGHACK; + + mVUinitConstValues(mVU); +} + +__fi void mVUcheckBadOp(mV) +{ + if (mVUinfo.isBadOp && mVU.code != 0x8000033c) + { + mVUinfo.isEOB = true; + DevCon.Warning("microVU Warning: Block contains an illegal opcode..."); + } +} + +__fi void eBitPass1(mV, int& branch) +{ + if (mVUregs.blockType != 1) + { + branch = 1; + mVUup.eBit = true; + } +} + +__fi void branchWarning(mV) +{ + incPC(-2); + if (mVUup.eBit && mVUbranch) + { + incPC(2); + mVUlow.isNOP = true; + } + else + incPC(2); + + if (mVUinfo.isBdelay && !mVUlow.evilBranch) + { + if (mVUlow.VI_write.reg && mVUlow.VI_write.used && !mVUlow.readFlags) + { + mVUlow.backupVI = true; + mVUregs.viBackUp = mVUlow.VI_write.reg; + } + } +} + +__ri void eBitWarning(mV) +{ + incPC(2); + if (curI & _Ebit_) + mVUregs.blockType = 1; + incPC(-2); +} + +void mVUdebugPrintBlocks(mV, bool isEndPC) {} + +//------------------------------------------------------------------ +// Main Compile Function +//------------------------------------------------------------------ + +void* mVUcompile(microVU& mVU, u32 startPC, uptr pState) +{ + microFlagCycles mFC; + // armAsm is managed by mVUexecute/mVUcompileJIT — must be active here. + pxAssert(armAsm); + u8* thisPtr = armGetCurrentCodePointer(); + + const u32 endCount = (((microRegInfo*)pState)->blockType) ? 1 : (mVU.microMemSize / 8); + + // === First Pass (Analysis) === + iPC = startPC / 4; + mVUsetupRange(mVU, startPC, 1); + mVU.regAlloc->reset(false); + mVUinitFirstPass(mVU, pState, thisPtr); + mVUbranch = 0; + + for (int branch = 0; mVUcount < endCount;) + { + incPC(1); + startLoop(mVU); + mVUincCycles(mVU, 1); + mVUopU(mVU, 0); // Upper analysis + mVUcheckBadOp(mVU); + + if (curI & _Ebit_) + { + eBitPass1(mVU, branch); + // VU0 end of program MAC results can be read by COP2, so best to + // make sure the last instance is valid. Needed for State of Emergency 2 + // and Driving Emotion Type-S (mirrors x86 microVU_Compile.inl:711-717). + if (isVU0) + mVUregs.needExactMatch |= 7; + } + + // M-bit: VU0 sync point with EE. If the previous instruction was also + // M-bit, skip — no need to re-sync. Mirrors x86 microVU_Compile.inl:720-735. + if ((curI & _Mbit_) && isVU0) + { + if (xPC > 0) + { + incPC(-2); + if (!(curI & _Mbit_)) + { + incPC(2); + mVUup.mBit = true; + } + else + { + incPC(2); + } + } + else + { + mVUup.mBit = true; + } + } + + if (curI & _Ibit_) + { + mVUlow.isNOP = true; + mVUup.iBit = true; + if (EmuConfig.Gamefixes.IbitHack) + { + mVUsetupRange(mVU, xPC, false); + if (branch < 2) + mVUsetupRange(mVU, xPC + 4, true); + } + } + else + { + incPC(-1); + if (EmuConfig.Gamefixes.IbitHack) + { + // Ignore IADDI/IADDIU/ISUBU/ILW/ISW/LQ/SQ on the lower slot when + // IbitHack is active. Matches x86 microVU_Compile.inl:751-765. + const u32 upper = (mVU.code >> 25); + if (upper == 0x1 || upper == 0x0 || upper == 0x4 || upper == 0x5 + || upper == 0x8 || upper == 0x9 + || (upper == 0x40 && (mVU.code & 0x3F) == 0x32)) + { + incPC(1); + mVUsetupRange(mVU, xPC, false); + if (branch < 2) + mVUsetupRange(mVU, xPC + 2, true); + incPC(-1); + } + } + mVUopL(mVU, 0); + incPC(1); + } + + if (curI & _Dbit_) { mVUup.dBit = true; } + if (curI & _Tbit_) { mVUup.tBit = true; } + mVUsetCycles(mVU); + + if (!mVUlow.isKick) + { + mVUregs.xgkickcycles += 1 + mVUstall; + if (mVUlow.isMemWrite) { mVUlow.kickcycles = mVUregs.xgkickcycles; mVUregs.xgkickcycles = 0; } + } + else + { + mVUregs.xgkickcycles = 1; + mVUlow.kickcycles = 0; + } + + mVUinfo.readQ = mVU.q; + mVUinfo.writeQ = !mVU.q; + mVUinfo.readP = mVU.p && isVU1; + mVUinfo.writeP = !mVU.p && isVU1; + mVUcount++; + + if (branch >= 2) + { + mVUinfo.isEOB = true; + if (branch == 3) + mVUinfo.isBdelay = true; + branchWarning(mVU); + if (mVUregs.xgkickcycles) + { + mVUlow.kickcycles = mVUregs.xgkickcycles; + mVUregs.xgkickcycles = 0; + } + break; + } + else if (branch == 1) + { + branch = 2; + } + + if (mVUbranch) { mVUsetFlagInfo(mVU); eBitWarning(mVU); branch = 3; mVUbranch = 0; } + + if (mVUup.mBit && !branch && !mVUup.eBit) + { + mVUregs.needExactMatch |= 7; + if (mVUregs.xgkickcycles) + { + mVUlow.kickcycles = mVUregs.xgkickcycles; + mVUregs.xgkickcycles = 0; + } + break; + } + + if (mVUinfo.isEOB) + { + if (mVUregs.xgkickcycles) + { + mVUlow.kickcycles = mVUregs.xgkickcycles; + mVUregs.xgkickcycles = 0; + } + break; + } + + incPC(1); + } + + mVUregs.vi15 = 0; + mVUregs.vi15v = 0; + mVUsetFlags(mVU, mFC); + mVUoptimizePipeState(mVU); + mVUtestCycles(mVU, mFC); + + // === Second Pass (Codegen) === + iPC = mVUstartPC; + setCode(); + mVUbranch = 0; + u32 x = 0; + + mvuPreloadRegisters(mVU, endCount); + + for (; x < endCount; x++) + { + if (mVUinfo.isEOB) { x = 0xffff; } + + // M-bit: signal the EE-visible M-flag so VU0 micro-mode can break/sync + // to the EE (VU0.cpp gates the M-bit Break on VURegs.flags & MFLAGSET). + // Mirrors x86 microVU_Compile.inl:890-893; VURegs.flags is always + // memory-resident, so no regalloc flush is needed (same as x86's + // direct memory xOR). Matches the VUFLAG_INTCINTERRUPT D/T-bit pattern. + if (mVUup.mBit) + { + armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + armAsm->Orr(a64::w9, a64::w9, VUFLAG_MFLAGSET); + armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags))); + } + + if (isVU1 && mVUlow.kickcycles && CHECK_XGKICKHACK) + mVU_XGKICK_SYNC(mVU, false); + + mVUexecuteInstruction(mVU); + +#ifdef PCSX2_RECOMPILER_TESTS + // Per-op state-snapshot hook (test builds only). When enabled, flush all + // dirty allocator state to vuRegs[N] memory and emit a brk whose imm16 + // encodes the op index; a test harness's SIGTRAP handler captures + // vuRegs[N] for that index and skips the brk. Release builds emit no + // per-op probe into the block. + if (mvu_divtrace::g_enabled.load(std::memory_order_relaxed)) + { + mvu_divtrace::OpMeta meta{}; + meta.op_idx = static_cast(mvu_divtrace::g_meta.size()); + meta.microvu_pc = xPC; + // Raw 64-bit microvu instruction (lower word + upper word) at xPC. + std::memcpy(&meta.opcode, &mVU.regs().Micro[xPC], sizeof(u32)); + meta.host_lo = armGetCurrentCodePointer(); + meta.alloc = mVU.regAlloc->snapshotMaps(); + + mVU.regAlloc->flushAll(true); + + // Flush qmmPQ (host-resident Q/P pipeline) to vuRegs.VI[REG_Q]/[REG_P] + // + pending_q/pending_p so vi22/vi23 are meaningful at the brk. + // The current lane is not known at codegen-of-op-N (mVU.q is + // the post-analyze final value, not the per-op value), so dump both: + // VI[REG_Q] := qmmPQ[0], pending_q := qmmPQ[1]. The driver compares + // JIT and interp Q as a multiset {VI[Q], pending_q} to tolerate the + // JIT/interp lane-index disagreement. + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_Q * sizeof(REG_VI)); + armAsm->St1(qmmPQ.V4S(), 0, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_q)); + armAsm->St1(qmmPQ.V4S(), 1, a64::MemOperand(a64::x8)); + if (isVU1) + { + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_P * sizeof(REG_VI)); + armAsm->St1(qmmPQ.V4S(), 2, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_p)); + armAsm->St1(qmmPQ.V4S(), 3, a64::MemOperand(a64::x8)); + } + + armAsm->Brk(meta.op_idx); + meta.host_hi = armGetCurrentCodePointer(); + mvu_divtrace::g_meta.push_back(meta); + } +#endif + + // T/D/M-bit per-instruction handling (excluding branch delay slots; + // those are handled after the branch itself emits). Mirrors x86 + // microVU_Compile.inl:901-923. + if (!mVUinfo.isBdelay && !mVUlow.branch) + { + if (mVUup.tBit) + { + mVUDoTBit(mVU, &mFC); + } + else if (mVUup.dBit && doDBitHandling) + { + mVUDoDBit(mVU, &mFC); + } + else if (mVUup.mBit && !mVUup.eBit && !mVUinfo.isEOB) + { + // Flags must be exact: Gungrave does FCAND/FMAND with M-bit + // back-to-back. setupBranch sorts flag instances. + mVUsetupBranch(mVU, mFC); + // Emit a runtime snapshot of the current pipeline state into + // mVU.prog.lpState. Matches x86's xMOV(ptr32[lpS], cpS[0]) + // loop: the values are compile-time constants (from mVUregs) + // baked into the emitted store instructions. + { + const u32* cpS = reinterpret_cast(&mVUregs); + const size_t nWords = (sizeof(microRegInfo) - 4) / 4; + armMoveAddressToReg(a64::x8, &mVU.prog.lpState); + for (size_t i = 0; i < nWords; i++) + { + armAsm->Mov(a64::w9, cpS[i]); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8, static_cast(i * 4))); + } + } + incPC(2); + mVUsetupRange(mVU, xPC, false); + // VUSyncHack: clear nextBlockCycles (mVUendProgram only emits + // this for the E-bit/isEbit!=0 paths, so the M-bit case must + // store it explicitly). Mirrors x86 microVU_Compile.inl:926-927. + if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack) + armAsm->Str(a64::wzr, mVUstateMem(offsetof(VURegs, nextBlockCycles))); + // endProgram + normBranchCompile run at iPC+2 so the saved TPC is + // the continuation PC (not the already-executed M-bit instruction) + // and the continuation block is compiled/linked into the cache. + // incPC(-2) is deferred until after, matching x86:928-930. + mVUendProgram(mVU, &mFC, 0); + normBranchCompile(mVU, xPC); + incPC(-2); + goto perf_and_return; + } + } + + if (mVUinfo.doXGKICK) + mVU_XGKICK_DELAY(mVU); + + if (isEvilBlock) + { + mVUsetupRange(mVU, xPC + 8, false); + normJumpCompile(mVU, mFC, true); + goto perf_and_return; + } + else if (!mVUinfo.isBdelay) + { + if ((xPC + 8) == mVU.microMemSize) + { + mVUsetupRange(mVU, xPC + 8, false); + mVUsetupRange(mVU, 0, 1); + } + incPC(1); + } + else + { + incPC(1); + mVUsetupRange(mVU, xPC, false); + incPC(-4); // Go back to branch opcode + + switch (mVUlow.branch) + { + case 1: case 2: // B/BAL + normBranch(mVU, mFC); + goto perf_and_return; + case 9: case 10: // JR/JALR + normJump(mVU, mFC); + goto perf_and_return; + case 3: // IBEQ + condBranch(mVU, mFC, a64::eq); + goto perf_and_return; + case 4: // IBGEZ + condBranch(mVU, mFC, a64::ge); + goto perf_and_return; + case 5: // IBGTZ + condBranch(mVU, mFC, a64::gt); + goto perf_and_return; + case 6: // IBLEQ + condBranch(mVU, mFC, a64::le); + goto perf_and_return; + case 7: // IBLTZ + condBranch(mVU, mFC, a64::lt); + goto perf_and_return; + case 8: // IBNE + condBranch(mVU, mFC, a64::ne); + goto perf_and_return; + } + } + } + + // E-bit end + mVUsetupRange(mVU, xPC, false); + mVUendProgram(mVU, &mFC, 1); + +perf_and_return: + { + u8* endPtr = armGetCurrentCodePointer(); + if (mVU.index) + Perf::vu1.RegisterPC(thisPtr, static_cast(endPtr - thisPtr), startPC); + else + Perf::vu0.RegisterPC(thisPtr, static_cast(endPtr - thisPtr), startPC); + } + return thisPtr; +} diff --git a/pcsx2/arm64/microVU_Flags-arm64.inl b/pcsx2/arm64/microVU_Flags-arm64.inl new file mode 100644 index 0000000000..f91e71838c --- /dev/null +++ b/pcsx2/arm64/microVU_Flags-arm64.inl @@ -0,0 +1,409 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// mVUupdateFlags() - ARM64 NEON flag extraction +//------------------------------------------------------------------ +// Uses NEON CMLT/FCMEQ + lane extraction in place of x86 MOVMSKPS. + +#define AND_XYZW ((_XYZW_SS && modXYZW) ? (1) : (mFLAG.doFlag ? (_X_Y_Z_W) : (flipMask[_X_Y_Z_W]))) +#define ADD_XYZW ((_XYZW_SS && modXYZW) ? (_X ? 3 : (_Y ? 2 : (_Z ? 1 : 0))) : 0) +#define SHIFT_XYZW(gprReg) \ + do { \ + if (_XYZW_SS && modXYZW && !_W) \ + armAsm->Lsl(gprReg, gprReg, ADD_XYZW); \ + } while (0) + +static void mVUupdateFlags(mV, const a64::VRegister& reg, + const a64::VRegister& regT1in = a64::NoVReg, + const a64::VRegister& regT2in = a64::NoVReg, + bool modXYZW = true) +{ + const a64::Register& mReg = gprT1; + const a64::Register& sReg = getFlagReg(sFLAG.write); + static const u16 flipMask[16] = {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}; + + if (!sFLAG.doFlag && !mFLAG.doFlag) + return; + + // Allocate temp NEON reg if not provided + bool regT1b = regT1in.IsNone(); + a64::VRegister regT1 = regT1b ? mVU.regAlloc->allocReg() : regT1in; + + // The x86 path shuffles WZYX→XYZW via PSHUFD 0x1B when updating MAC flag (not in + // single-scalar mode) so MOVMSKPS produces [W,Z,Y,X] in bits [0:3], + // matching PS2 MAC flag order. ARM64 achieves the same bit layout + // by passing reverse=true to armEmitPackLaneBits, which picks the + // {8,4,2,1} weight vector so lane3→bit0, lane0→bit3. + const bool macPath = mFLAG.doFlag && !(_XYZW_SS && modXYZW); + + if (sFLAG.doFlag) + { + mVUallocSFLAGa(sReg, sFLAG.lastWrite); + if (sFLAG.doNonSticky) + armAsm->And(sReg.W(), sReg.W(), 0xfffc00ffu); + } + + //--------- Extract sign bits (negative lanes) → bits [7:4] --------- + + armAsm->Cmlt(regT1.V4S(), reg.V4S(), 0); // All-1s where negative + armEmitPackLaneBits(mReg.W(), regT1, RQSCRATCH3, macPath); + + armAsm->And(mReg.W(), mReg.W(), AND_XYZW); + armAsm->Lsl(mReg.W(), mReg.W(), 4); + + //--------- Extract zero bits (zero lanes) → bits [3:0] --------- + + armAsm->Fcmeq(regT1.V4S(), reg.V4S(), 0.0); + armEmitPackLaneBits(gprT2, regT1, RQSCRATCH3, macPath); + + armAsm->And(gprT2, gprT2, AND_XYZW); + armAsm->Orr(mReg.W(), mReg.W(), gprT2); + + //--------- Write back flags --------- + + if (mFLAG.doFlag) + { + SHIFT_XYZW(mReg.W()); + mVUallocMFLAGb(mVU, mReg, mFLAG.write); + } + + if (sFLAG.doFlag) + { + armAsm->And(a64::w12, mReg.W(), 0xFF); + armAsm->Orr(sReg.W(), sReg.W(), a64::w12); + if (sFLAG.doNonSticky) + { + armAsm->Lsl(a64::w12, a64::w12, 8); + armAsm->Orr(sReg.W(), sReg.W(), a64::w12); + } + } + + if (regT1b) + mVU.regAlloc->clearNeeded(regT1); +} + +//------------------------------------------------------------------ +// Flag Cycling — ARM64 NEON implementation of microVU_Flags.inl logic +//------------------------------------------------------------------ +// Pure-logic analysis and NEON codegen for pipeline flag instance +// tracking. Status flags live in four callee-saved GPRs (gprF0-F3); +// MAC/clip flags live as 4x 32-bit lanes in mVU.macFlag/clipFlag. + +static int findFlagInst(int* fFlag, int cycles) +{ + int j = 0, jValue = -1; + for (int i = 0; i < 4; i++) + { + if ((fFlag[i] <= cycles) && (fFlag[i] > jValue)) + { + j = i; + jValue = fFlag[i]; + } + } + return j; +} + +// Setup last 4 instances of Status/Mac/Clip flags (for accurate block linking). +// Returns number of distinct flag instances. +static int sortFlag(int* fFlag, int* bFlag, int cycles) +{ + int lFlag = -5; + int x = 0; + for (int i = 0; i < 4; i++) + { + bFlag[i] = findFlagInst(fFlag, cycles); + if (lFlag != bFlag[i]) + x++; + lFlag = bFlag[i]; + cycles++; + } + return x; +} + +// Retained for parity with x86 microVU_Flags.inl::sortFullFlag; the arm64 flag +// path does not currently call it (hence [[maybe_unused]]). +[[maybe_unused]] static void sortFullFlag(int* fFlag, int* bFlag) +{ + int m = std::max(std::max(fFlag[0], fFlag[1]), std::max(fFlag[2], fFlag[3])); + for (int i = 0; i < 4; i++) + { + int t = 3 - (m - fFlag[i]); + bFlag[i] = (t < 0) ? 0 : t + 1; + } +} + +// Optimizes out unneeded status flag updates (safely done when there is an FSSET opcode). +static __fi void mVUstatusFlagOp(mV) +{ + int curPC = iPC; + int i = mVUcount; + bool runLoop = true; + + if (sFLAG.doFlag) + { + sFLAG.doNonSticky = true; + } + else + { + for (; i > 0; i--) + { + incPC2(-2); + if (sFLAG.doNonSticky) + { + runLoop = false; + break; + } + else if (sFLAG.doFlag) + { + sFLAG.doNonSticky = true; + break; + } + } + } + if (runLoop) + { + for (; i > 0; i--) + { + incPC2(-2); + + if (sFLAG.doNonSticky) + break; + + sFLAG.doFlag = false; + } + } + iPC = curPC; + DevCon.WriteLn(Color_Green, "microVU%d: FSSET Optimization", getIndex); +} + +#define sFlagCond (sFLAG.doFlag || mVUlow.isFSSET || mVUinfo.doDivFlag) +#define sHackCond (mVUsFlagHack && !sFLAG.doNonSticky) + +// Note: Flag handling is 'very' complex; requires full knowledge of microVU recs. +static __fi void mVUsetFlags(mV, microFlagCycles& mFC) +{ + int endPC = iPC; + u32 aCount = 0; // Amount of instructions needed to get valid mac flag instances for block linking + + // Ensure last ~4+ instructions update mac/status flags (if next block's first 4 read them) + for (int i = mVUcount; i > 0; i--, aCount++) + { + if (sFLAG.doFlag) + { + if (__Mac) + mFLAG.doFlag = true; + + if (__Status) + sFLAG.doNonSticky = true; + + if (aCount >= 3) + break; + } + incPC2(-2); + } + + // Status/Mac Flags setup + int xS = 0, xM = 0, xC = 0; + + for (int i = 0; i < 4; i++) + { + mFC.xStatus[i] = i; + mFC.xMac [i] = i; + mFC.xClip [i] = i; + } + + if (!(mVUpBlock->pState.needExactMatch & 1)) + { + xS = (mVUpBlock->pState.flagInfo >> 2) & 3; + mFC.xStatus[0] = -1; + mFC.xStatus[1] = -1; + mFC.xStatus[2] = -1; + mFC.xStatus[3] = -1; + mFC.xStatus[(xS - 1) & 3] = 0; + } + + if (!(mVUpBlock->pState.needExactMatch & 2)) + { + mFC.xMac[0] = -1; + mFC.xMac[1] = -1; + mFC.xMac[2] = -1; + mFC.xMac[3] = -1; + } + + if (!(mVUpBlock->pState.needExactMatch & 4)) + { + xC = (mVUpBlock->pState.flagInfo >> 6) & 3; + mFC.xClip[0] = -1; + mFC.xClip[1] = -1; + mFC.xClip[2] = -1; + mFC.xClip[3] = -1; + mFC.xClip[(xC - 1) & 3] = 0; + } + + mFC.cycles = 0; + u32 xCount = mVUcount; + iPC = mVUstartPC; + for (mVUcount = 0; mVUcount < xCount; mVUcount++) + { + if (mVUlow.isFSSET && !noFlagOpts) + { + if (__Status) + { + if ((xCount - mVUcount) > aCount) + mVUstatusFlagOp(mVU); + } + else + mVUstatusFlagOp(mVU); + } + mFC.cycles += mVUstall; + + sFLAG.read = doSFlagInsts ? findFlagInst(mFC.xStatus, mFC.cycles) : 0; + mFLAG.read = doMFlagInsts ? findFlagInst(mFC.xMac, mFC.cycles) : 0; + cFLAG.read = doCFlagInsts ? findFlagInst(mFC.xClip, mFC.cycles) : 0; + + sFLAG.write = doSFlagInsts ? xS : 0; + mFLAG.write = doMFlagInsts ? xM : 0; + cFLAG.write = doCFlagInsts ? xC : 0; + + sFLAG.lastWrite = doSFlagInsts ? (xS - 1) & 3 : 0; + mFLAG.lastWrite = doMFlagInsts ? (xM - 1) & 3 : 0; + cFLAG.lastWrite = doCFlagInsts ? (xC - 1) & 3 : 0; + + if (sHackCond) + sFLAG.doFlag = false; + + if (sFLAG.doFlag) + { + if (noFlagOpts) + { + sFLAG.doNonSticky = true; + mFLAG.doFlag = true; + } + } + + if (sFlagCond) + { + mFC.xStatus[xS] = mFC.cycles + 4; + xS = (xS + 1) & 3; + } + + if (mFLAG.doFlag) + { + mFC.xMac[xM] = mFC.cycles + 4; + xM = (xM + 1) & 3; + } + + if (cFLAG.doFlag) + { + mFC.xClip[xC] = mFC.cycles + 4; + xC = (xC + 1) & 3; + } + + mFC.cycles++; + incPC2(2); + } + + mVUregs.flagInfo |= ((__Status) ? 0 : (xS << 2)); + mVUregs.flagInfo |= (xM << 4); + mVUregs.flagInfo |= ((__Clip) ? 0 : (xC << 6)); + iPC = endPC; +} + +#define getFlagReg2(x) ((bStatus[0] == x) ? getFlagReg(x) : gprT1) +#define getFlagReg3(x) ((gFlag == x) ? gprT1 : getFlagReg(x)) +#define getFlagReg4(x) ((gFlag == x) ? gprT1 : gprT2) + +// Emit NEON lane-shuffle equivalent to x86's SHUF.PS xmm, xmm, pattern. +// bFlag[i] names the source lane that should end up in dest lane i. +// Clobbers one temp NEON register. +static __fi void mVUshuffleFlagVec(a64::VRegister vec, const int* bFlag, a64::VRegister tmp) +{ + // If already identity, nothing to do. + if (bFlag[0] == 0 && bFlag[1] == 1 && bFlag[2] == 2 && bFlag[3] == 3) + return; + // Copy source so we can read lanes before overwriting them. + armAsm->Mov(tmp.V16B(), vec.V16B()); + for (int i = 0; i < 4; i++) + { + if (bFlag[i] != i) + armAsm->Ins(vec.V4S(), i, tmp.V4S(), bFlag[i]); + } +} + +// Recompiles code for proper flags on block linkings (equivalent to x86's mVUsetupFlags). +static __fi void mVUsetupFlags(mV, microFlagCycles& mFC) +{ + if (mVUregs.flagInfo & 1) + { + if (mVUregs.needExactMatch) + DevCon.Error("mVU ERROR!!!"); + } + + if (doSFlagInsts && __Status) + { + int bStatus[4]; + int sortRegs = sortFlag(mFC.xStatus, bStatus, mFC.cycles); + // Note: vixl does NOT elide a W-register self-move (kDontDiscardForSameWReg) — + // Mov(Wd, Wd) emits a real ORR because it clears bits 63:32 of the X reg. So + // in the all-same-instance case each Mov below is emitted (a cheap no-op ORR), + // not optimized away. + if (sortRegs == 1) + { + armAsm->Mov(gprF0, getFlagReg(bStatus[0])); + armAsm->Mov(gprF1, getFlagReg(bStatus[1])); + armAsm->Mov(gprF2, getFlagReg(bStatus[2])); + armAsm->Mov(gprF3, getFlagReg(bStatus[3])); + } + else if (sortRegs == 2) + { + armAsm->Mov(gprT1, getFlagReg (bStatus[3])); + armAsm->Mov(gprF0, getFlagReg (bStatus[0])); + armAsm->Mov(gprF1, getFlagReg2(bStatus[1])); + armAsm->Mov(gprF2, getFlagReg2(bStatus[2])); + armAsm->Mov(gprF3, gprT1); + } + else if (sortRegs == 3) + { + int gFlag = (bStatus[0] == bStatus[1]) ? bStatus[2] : bStatus[1]; + armAsm->Mov(gprT1, getFlagReg (gFlag)); + armAsm->Mov(gprT2, getFlagReg (bStatus[3])); + armAsm->Mov(gprF0, getFlagReg (bStatus[0])); + armAsm->Mov(gprF1, getFlagReg3(bStatus[1])); + armAsm->Mov(gprF2, getFlagReg4(bStatus[2])); + armAsm->Mov(gprF3, gprT2); + } + else + { + // All four are distinct — need an extra temp. Use gprT3 (w11) which + // is scratch in the ABI (not in VI pool). + armAsm->Mov(gprT1, getFlagReg(bStatus[0])); + armAsm->Mov(gprT2, getFlagReg(bStatus[1])); + armAsm->Mov(gprT3, getFlagReg(bStatus[2])); + armAsm->Mov(gprF3, getFlagReg(bStatus[3])); + armAsm->Mov(gprF0, gprT1); + armAsm->Mov(gprF1, gprT2); + armAsm->Mov(gprF2, gprT3); + } + } + + if (doMFlagInsts && __Mac) + { + int bMac[4]; + sortFlag(mFC.xMac, bMac, mFC.cycles); + armAsm->Ldr(qmmT1, a64::MemOperand(gprMVUFlag)); + mVUshuffleFlagVec(qmmT1, bMac, qmmT2); + armAsm->Str(qmmT1, a64::MemOperand(gprMVUFlag)); + } + + if (doCFlagInsts && __Clip) + { + int bClip[4]; + sortFlag(mFC.xClip, bClip, mFC.cycles); + armAsm->Ldr(qmmT2, a64::MemOperand(gprMVUFlag, 16)); + mVUshuffleFlagVec(qmmT2, bClip, qmmT1); + armAsm->Str(qmmT2, a64::MemOperand(gprMVUFlag, 16)); + } +} diff --git a/pcsx2/arm64/microVU_IR-arm64.h b/pcsx2/arm64/microVU_IR-arm64.h new file mode 100644 index 0000000000..0c7a71306e --- /dev/null +++ b/pcsx2/arm64/microVU_IR-arm64.h @@ -0,0 +1,868 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "microVU_Divtrace.h" + +//#define MVURALOG(...) fprintf(stderr, __VA_ARGS__) +#define MVURALOG(...) + +//------------------------------------------------------------------ +// ARM64 NEON/GPR Register Maps +//------------------------------------------------------------------ + +struct microMapNEON +{ + int VFreg; // VF Reg Number Stored (-1=Temp, 0=VF0, 1-31=VF, 32=ACC, 33=I reg) + int xyzw; // xyzw to write back (0 = clean/fully cached, nonzero = dirty partial) + int count; // LRU counter + bool isNeeded; // Locked for current instruction + bool isZero; // Loaded from VF0, no clamping needed +}; + +struct microMapGPR +{ + int VIreg; // VI reg number (-1=unused, 0-15=VI regs) + int count; // LRU counter + bool isNeeded; // Locked for current instruction + bool dirty; // Modified, needs writeback + bool isZeroExtended; // 16-bit value zero-extended to 32-bit + bool usable; // Available for allocation (not reserved) +}; + +//------------------------------------------------------------------ +// ARM64 Register Pools +//------------------------------------------------------------------ + +// NEON allocatable: Q0-Q27 (28 registers). Q28=PQ, Q29-Q31=scratch. +static const int neonAllocTotal = 28; + +// GPR allocatable for VI: x14-x15 (2) + x26-x28 (3) = 5. +// x0-x7=args/scratch, x8=RXSCRATCH, x9-x13=scratch, x16-x17=vixl, +// x18=platform, x19=gprVUState, x20-x23=gprF0-F3, x24=gprMVUFlag, +// x25=gprMVUglob, x29=fp, x30=lr, sp=stack. +static const int gprAllocCount = 32; // Total GPR IDs (unusable ones are marked in the map) + +//------------------------------------------------------------------ +// ARM64 microRegAlloc +//------------------------------------------------------------------ + +class microRegAlloc +{ +protected: + std::array neonMap; + std::array gprMap; + + int counter; + int index; // VU0 or VU1 + + VURegs& regs() const { return ::vuRegs[index]; } + + // Load I register (immediate) into NEON reg + __ri void loadIreg(const a64::VRegister& reg, int xyzw) + { + // If REG_I is cached in a VI GPR slot, transfer via GPR→NEON to + // pick up any pending writes that haven't been flushed. Matches + // x86 loadIreg. + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable && gprMap[i].VIreg == REG_I) + { + // MOV into lane 0, zero upper 96 bits (equivalent to + // x86 xMOVDZX — 32-bit reg into xmm with zero-extension). + armAsm->Fmov(a64::SRegister(reg.GetCode()), armWRegister(i)); + if (!_XYZWss(xyzw)) + armAsm->Dup(reg.V4S(), reg.V4S(), 0); + return; + } + } + + armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), + mVUstateMem(offsetof(VURegs, VI) + REG_I * sizeof(REG_VI))); + if (!_XYZWss(xyzw)) + armAsm->Dup(reg.V4S(), reg.V4S(), 0); // Broadcast to all lanes + } + + // Find least-recently-used NEON reg (recursive, for eviction) + int findFreeNeonRec(int startIdx) + { + for (int i = startIdx; i < neonAllocTotal; i++) + { + if (!neonMap[i].isNeeded) + { + int x = findFreeNeonRec(i + 1); + if (x == -1) + return i; + return (neonMap[i].count < neonMap[x].count) ? i : x; + } + } + return -1; + } + + int findFreeNeon(int vfreg) + { + // Prefer unoccupied temp regs + for (int i = 0; i < neonAllocTotal; i++) + { + if (!neonMap[i].isNeeded && neonMap[i].VFreg < 0) + return i; + } + // Evict LRU + int x = findFreeNeonRec(0); + pxAssertMsg(x >= 0, "microVU NEON register allocation failure!"); + return x; + } + + int findFreeGPRRec(int startIdx) + { + for (int i = startIdx; i < gprAllocCount; i++) + { + if (gprMap[i].usable && !gprMap[i].isNeeded) + { + int x = findFreeGPRRec(i + 1); + if (x == -1) + return i; + return (gprMap[i].count < gprMap[x].count) ? i : x; + } + } + return -1; + } + + int findFreeGPR(int vireg) + { + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable && !gprMap[i].isNeeded && gprMap[i].VIreg < 0) + return i; + } + int x = findFreeGPRRec(0); + pxAssertMsg(x >= 0, "microVU GPR register allocation failure!"); + return x; + } + + // Write back a dirty NEON reg to VF memory. + // + // Matches x86 writeBackReg semantics: + // - Full-vector writes (xyzw == 0xF): cache is still valid after the + // store, mark clean (xyzw=0). Later reads reuse the reg. + // - Partial writes (xyzw in {0x1..0xE}): the register may be SS-shuffled + // (single-lane case from allocReg's SS path put Y/Z/W in lane 0), or + // have natural layout but stale lanes. Either way the register's + // contents no longer reliably match memory, so invalidate the cache + // entry entirely after the store. Subsequent reads will re-load from + // the fresh memory values. + // + // Pass modXYZW=true to mVUsaveReg so it writes lane 0 (the SS-shuffled + // value) for single-lane partial writes, matching x86 writeBackReg. + void writeBackNeon(int regIdx, bool clearEntry = false) + { + microMapNEON& entry = neonMap[regIdx]; + if (entry.VFreg > 0 && entry.xyzw != 0) // VFreg 0 is read-only + { + const a64::VRegister qreg = armQRegister(regIdx); + const int64_t off = (entry.VFreg == 32) ? offsetof(VURegs, ACC) : + (entry.VFreg == 33) ? offsetof(VURegs, VI) + REG_I * sizeof(REG_VI) : + offsetof(VURegs, VF) + entry.VFreg * sizeof(VECTOR); + if (entry.xyzw == 0xF) + { + armAsm->Str(qreg, mVUstateMem(off)); + if (clearEntry) + clearNeon(regIdx); + else + entry.xyzw = 0; // Cache still valid, memory matches. + return; + } + // Partial write — must invalidate after store regardless of + // clearEntry, otherwise later cached reads pick up a reg with + // stale / shuffled lanes and skip the re-load. + mVUsaveReg(qreg, gprVUState, off, entry.xyzw, true); + clearNeon(regIdx); + return; + } + if (clearEntry) + clearNeon(regIdx); + } + + // Write back a dirty GPR to VI memory + void writeBackGPR(int regIdx, bool clearEntry = false) + { + microMapGPR& entry = gprMap[regIdx]; + if (entry.VIreg > 0 && entry.dirty) + { + // Store 16-bit value (VI regs are 16-bit in memory) + const a64::Register wreg = armWRegister(regIdx); + armAsm->Strh(wreg, mVUstateMem(offsetof(VURegs, VI) + entry.VIreg * sizeof(REG_VI))); + entry.dirty = false; + } + if (clearEntry) + clearGPR(regIdx); + } + + void clearNeon(int regIdx) + { + neonMap[regIdx].VFreg = -1; + neonMap[regIdx].xyzw = 0; + neonMap[regIdx].count = 0; + neonMap[regIdx].isNeeded = false; + neonMap[regIdx].isZero = false; + } + + void clearGPR(int regIdx) + { + gprMap[regIdx].VIreg = -1; + gprMap[regIdx].count = 0; + gprMap[regIdx].isNeeded = false; + gprMap[regIdx].dirty = false; + gprMap[regIdx].isZeroExtended = false; + } + +public: + microRegAlloc(int _index) + { + index = _index; + + // Mark usable GPRs for VI allocation. + // CRITICAL: gprT1(w9), gprT2(w10), gprT3(w11) are scratch registers + // used by codegen (address computation, temps). They MUST NOT be in + // the VI allocation pool or they'll be clobbered. + // Also exclude w12-w13 which are used as scratch in flag extraction. + gprMap.fill({-1, 0, false, false, false, false}); + for (int i = 14; i <= 15; i++) + gprMap[i].usable = true; // x14-x15: caller-saved, safe for VI + for (int i = 26; i <= 28; i++) + gprMap[i].usable = true; // x26-x28: callee-saved, VI cache + // x24 = gprMVUFlag (pinned to &mVU.macFlag[0]), + // x25 = gprMVUglob (pinned to &mVUglob). + // Both pinned by mVUdispatcherAB; excluded from VI allocation. + + reset(false); + } + + // cop2mode is unused here: x86 toggles fastmem-base/text-pointer GPR + // usability per cop2 mode, but the arm64 allocator pins those base + // registers outside the allocatable set, so the mode does not change the + // reset. + void reset(bool cop2mode = false) + { + for (int i = 0; i < neonAllocTotal; i++) + clearNeon(i); + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable) + clearGPR(i); + } + counter = 0; + } + + //------------------------------------------------------------------ + // VF Register Allocation (NEON Q registers) + //------------------------------------------------------------------ + + // Emit the NEON equivalent of x86's PSHUF.D(dst, src, imm) used in the + // clone-write path for single-scalar VF ops. Moves src's lane `srcLane` + // into dst's lane 0. Other dst lanes are don't-care (SS ops only read + // lane 0). dst and src may alias. + __fi void emitSSShuffle(const a64::VRegister& dst, const a64::VRegister& src, int srcLane) + { + // Compare physical register numbers, not the parameter-reference + // addresses: dst/src bind to distinct caller locals even when they name + // the same NEON register (the clone path), so &dst != &src would emit a + // redundant self-Mov. GetCode() detects the true self-alias. + if (srcLane == 0) + { + if (dst.GetCode() != src.GetCode()) + armAsm->Mov(dst.V16B(), src.V16B()); + return; + } + if (dst.GetCode() != src.GetCode()) + armAsm->Mov(dst.V16B(), src.V16B()); + armAsm->Ext(dst.V16B(), dst.V16B(), dst.V16B(), srcLane * 4); + } + + // Allocate a NEON register for VF access. Ported from x86 allocReg — + // preserves cache-validity and clone-write SS-shuffle semantics. + // + // vfLoadReg: VF to load from (-1=temp, 0-31=VF, 32=ACC, 33=I) + // vfWriteReg: VF to write to (-1=none/read-only, 0-31, 32=ACC, 33=I) + // xyzw: components touched (mask: X=8, Y=4, Z=2, W=1; 0=full-read; + // 0xF=full-write). For SS writes a single bit is set. + // cloneWrite: if true, clone cached reg so caller's write doesn't + // stomp the cached value. + const a64::VRegister allocReg(int vfLoadReg = -1, int vfWriteReg = -1, int xyzw = 0, bool cloneWrite = true) + { + counter++; + + // Search for cached copy. + if (vfLoadReg >= 0) + { + for (int i = 0; i < neonAllocTotal; i++) + { + microMapNEON& mapI = neonMap[i]; + // Cache is only valid when: + // - Reg was not modified (xyzw == 0), OR + // - Reg had ALL vectors modified (xyzw == 0xF) and it's not VF0. + // Partial-dirty caches are NOT reused; the partial writes live + // in-register but memory still reflects the pre-partial value. + if (mapI.VFreg == vfLoadReg + && (!mapI.xyzw + || (mapI.VFreg != 0 && mapI.xyzw == 0xF))) + { + int z = i; + if (vfWriteReg >= 0) + { + const a64::VRegister qmmI = armQRegister(i); + if (cloneWrite) + { + z = findFreeNeon(vfWriteReg); + const a64::VRegister qmmZ = armQRegister(z); + writeBackNeon(z); + + if (xyzw == 4) + emitSSShuffle(qmmZ, qmmI, 1); // Y to lane 0 + else if (xyzw == 2) + emitSSShuffle(qmmZ, qmmI, 2); // Z to lane 0 + else if (xyzw == 1) + emitSSShuffle(qmmZ, qmmI, 3); // W to lane 0 + else if (z != i) + armAsm->Mov(qmmZ.V16B(), qmmI.V16B()); + + mapI.count = counter; // Reg i was used, so update counter. + } + else + { + if ((vfLoadReg != vfWriteReg) || (xyzw != 0xF)) + writeBackNeon(i); + + if (xyzw == 4) + emitSSShuffle(qmmI, qmmI, 1); + else if (xyzw == 2) + emitSSShuffle(qmmI, qmmI, 2); + else if (xyzw == 1) + emitSSShuffle(qmmI, qmmI, 3); + } + neonMap[z].VFreg = vfWriteReg; + neonMap[z].xyzw = xyzw; + neonMap[z].isZero = (vfLoadReg == 0); + } + neonMap[z].count = counter; + neonMap[z].isNeeded = true; + return armQRegister(z); + } + } + } + + // Not cached — allocate a fresh slot. + int x = findFreeNeon(vfWriteReg >= 0 ? vfWriteReg : vfLoadReg); + const a64::VRegister qmmX = armQRegister(x); + writeBackNeon(x); + + if (vfWriteReg >= 0) // Reg will be modified (partial reg loading allowed) + { + if ((vfLoadReg == 0) && !(xyzw & 1)) + { + // Writing to a partial fresh slot based on VF0 with X masked out. + // x86 issues PXOR to zero the reg; lane 0 won't be read by the op. + armAsm->Eor(qmmX.V16B(), qmmX.V16B(), qmmX.V16B()); + } + else if (vfLoadReg == 33) // I register + { + loadIreg(qmmX, xyzw); + } + else if (vfLoadReg == 32) + { + mVUloadReg(qmmX, gprVUState, offsetof(VURegs, ACC), xyzw); + } + else if (vfLoadReg >= 0) + { + mVUloadReg(qmmX, gprVUState, offsetof(VURegs, VF) + vfLoadReg * sizeof(VECTOR), xyzw); + } + + neonMap[x].VFreg = vfWriteReg; + neonMap[x].xyzw = xyzw; + } + else // Reg will not be modified (always load the full reg so it can be cached) + { + if (vfLoadReg == 33) + { + loadIreg(qmmX, 0xF); + } + else if (vfLoadReg == 32) + { + armAsm->Ldr(qmmX, mVUstateMem(offsetof(VURegs, ACC))); + } + else if (vfLoadReg >= 0) + { + armAsm->Ldr(qmmX, mVUstateMem(offsetof(VURegs, VF) + vfLoadReg * sizeof(VECTOR))); + } + + neonMap[x].VFreg = vfLoadReg; + neonMap[x].xyzw = 0; + } + neonMap[x].isZero = (vfLoadReg == 0); + neonMap[x].count = counter; + neonMap[x].isNeeded = true; + return qmmX; + } + + //------------------------------------------------------------------ + // VI Register Allocation (ARM64 W registers) + //------------------------------------------------------------------ + + // Flush and un-cache any existing GPR slot that claims VIreg == targetVI. + // Ported from the x86 allocGPR duplicate-flush path. If the slot is still + // in use by an active allocGPR, unbind it (clear VIreg/dirty/isZeroExtended) while + // leaving isNeeded intact so findFreeGPR won't hand the physical reg to + // another allocation. Otherwise fully clear the slot. + void unbindAnyVIAllocations(int targetVI, bool& backup) + { + if (targetVI < 0) + return; + for (int i = 0; i < gprAllocCount; i++) + { + microMapGPR& mapI = gprMap[i]; + if (!mapI.usable || mapI.VIreg != targetVI) + continue; + + if (backup) + { + writeVIBackup(armWRegister(i)); + backup = false; + } + + if (mapI.isNeeded) + { + // Still held by a live allocation — flush to memory but keep + // the physical reg bound to its caller. + writeBackGPR(i, false); + mapI.VIreg = -1; + mapI.dirty = false; + mapI.isZeroExtended = false; + } + else + { + // No one is using this slot — fully release it. + writeBackGPR(i, false); + clearGPR(i); + } + + // Invariant: only one slot can be bound to a given VIreg. + for (int j = i + 1; j < gprAllocCount; j++) + pxAssert(gprMap[j].VIreg != targetVI); + break; + } + } + + const a64::Register allocGPR(int viLoadReg = -1, int viWriteReg = -1, bool backup = false, bool zext_if_dirty = false) + { + counter++; + + // Writing zero? Return a zeroed register + if (viWriteReg == 0) + { + int idx = findFreeGPR(0); + writeBackGPR(idx); + clearGPR(idx); + armAsm->Mov(armWRegister(idx), 0); + gprMap[idx].VIreg = 0; + gprMap[idx].isNeeded = true; + gprMap[idx].isZeroExtended = true; + return armWRegister(idx); + } + + // Search for cached copy + if (viLoadReg >= 0) + { + for (int i = 0; i < gprAllocCount; i++) + { + if (!gprMap[i].usable) + continue; + if (gprMap[i].VIreg == viLoadReg) + { + // Bump count on the Is cache slot before anything can + // steal it via findFreeGPR (matches x86 ordering). + gprMap[i].count = counter; + + if (viWriteReg >= 0) + { + if (viLoadReg != viWriteReg) + { + // Clone-write: allocate a NEW slot for viWriteReg, + // copy the Is value into it, leave the Is cache + // entry untouched. Matches x86 allocGPR. + unbindAnyVIAllocations(viWriteReg, backup); + int x = findFreeGPR(viWriteReg); + writeBackGPR(x); + + if (backup && gprMap[x].VIreg != viWriteReg) + { + armAsm->Ldrh(armWRegister(x), + mVUstateMem(offsetof(VURegs, VI) + viWriteReg * sizeof(REG_VI))); + writeVIBackup(armWRegister(x)); + backup = false; + } + + armAsm->Mov(armWRegister(x).W(), armWRegister(i).W()); + + gprMap[x].isZeroExtended = zext_if_dirty; + // Swap so `i` names the new slot — matches x86's + // std::swap(x, i) trick. The Is slot (now named x) + // intentionally does NOT get isNeeded=true; only + // the write slot the caller will clearNeeded. + std::swap(x, i); + } + else + { + // In-place read-modify-write: no longer zext. + gprMap[i].isZeroExtended = false; + } + + gprMap[i].VIreg = viWriteReg; + gprMap[i].dirty = true; + } + else if (zext_if_dirty && !gprMap[i].isZeroExtended) + { + // Mirror x86 allocGPR's zero-extend path. Caller (e.g. mVU_ISW) + // needs 32-bit-clean storage of a 16-bit VI value. If the + // cached slot was last written by IADDIU/IADD/IOR/etc., its + // top 16 bits are dirty and an unmasked Str(W) would push + // garbage into the GIFtag's PRIM/NREG bits. + armAsm->Uxth(armWRegister(i), armWRegister(i)); + gprMap[i].isZeroExtended = true; + } + + gprMap[i].isNeeded = true; + + if (backup) + writeVIBackup(armWRegister(i)); + return armWRegister(i); + } + } + } + + // Not cached — allocate new. Flush any duplicate binding of viWriteReg first. + if (viWriteReg >= 0) + unbindAnyVIAllocations(viWriteReg, backup); + + int idx = findFreeGPR(viWriteReg >= 0 ? viWriteReg : viLoadReg); + writeBackGPR(idx); + + gprMap[idx].count = counter; + gprMap[idx].isNeeded = true; + + if (viLoadReg >= 0 && viLoadReg != 0) + { + // Load VI from memory (16-bit zero-extended) + armAsm->Ldrh(armWRegister(idx), + mVUstateMem(offsetof(VURegs, VI) + viLoadReg * sizeof(REG_VI))); + gprMap[idx].isZeroExtended = true; + } + else if (viLoadReg == 0) + { + armAsm->Mov(armWRegister(idx), 0); + gprMap[idx].isZeroExtended = true; + } + + gprMap[idx].VIreg = (viWriteReg >= 0) ? viWriteReg : ((viLoadReg >= 0) ? viLoadReg : -1); + gprMap[idx].dirty = (viWriteReg >= 0); + + if (backup) + { + // viWriteReg wasn't already in any GPR slot (so unbindAny didn't + // back it up). For write-only allocations (viLoadReg < 0, e.g. + // MTIR), the freshly-allocated `idx` register is uninitialised at + // this point — backing it up would store garbage to mVU.VIbackup + // and the following IBxxx branch would read garbage. + // + // Load viWriteReg's CURRENT (pre-write) value from VI memory first, + // then back it up. Mirrors x86 allocGPR. + if (viLoadReg < 0 && viWriteReg > 0) + { + armAsm->Ldrh(armWRegister(idx), + mVUstateMem(offsetof(VURegs, VI) + viWriteReg * sizeof(REG_VI))); + } + writeVIBackup(armWRegister(idx)); + } + + return armWRegister(idx); + } + + //------------------------------------------------------------------ + // Clear / Flush + //------------------------------------------------------------------ + + // Mark a NEON slot as no-longer-needed after the op that allocated it is + // done. Matches x86 clearNeeded: when the cleared + // slot was written to (xyzw != 0), we must either merge the partial + // write into another cached copy of the same VFreg, or flush it to + // memory — otherwise the partial write is stuck in a cache slot that + // subsequent cache searches will skip (xyzw mismatch in the cache- + // validity check in allocReg), causing fresh-loads to read stale memory + // and lose the write entirely. + // + // Full writes (xyzw == 0xF) invalidate other cached copies (they hold + // the complete current state). + // Partial writes (xyzw in {0x1..0xE}) try to merge into another copy; + // if no other copy exists, writeback flushes to memory. + void clearNeeded(const a64::VRegister& reg) + { + const int idx = reg.GetCode(); + if (idx >= neonAllocTotal) + return; + + microMapNEON& clear = neonMap[idx]; + clear.isNeeded = false; + + if (!clear.xyzw) // Read-only slot, nothing to do. + return; + + if (clear.VFreg <= 0) // Temp or VF0: just drop the slot. + { + clearNeon(idx); + return; + } + + // Modified VFreg: handle merge / invalidate of other cached copies. + int mergeState = 0; // 0: full-write, invalidate others + // 1: partial-write, haven't merged yet + // 2: partial-write, merged into another slot + if (clear.xyzw < 0xF) + mergeState = 1; + + for (int i = 0; i < neonAllocTotal; i++) + { + if (i == idx) + continue; + microMapNEON& mapI = neonMap[i]; + if (mapI.VFreg != clear.VFreg) + continue; + + if (mergeState == 1) + { + // First other cached copy found — merge our partial write + // into it. The merged reg now holds the complete state, so + // mark it as fully valid (xyzw=0xF). We'll invalidate our + // own slot below. + mVUmergeRegs(armQRegister(i), reg, clear.xyzw, /*modXYZW=*/true); + mapI.xyzw = 0xF; + mapI.count = counter; + mergeState = 2; + } + else + { + // Full-write path OR we already merged into another slot: + // invalidate this copy (our slot, or the merged copy, now + // holds the authoritative state). + clearNeon(i); + } + } + + if (mergeState == 2) + { + // Partial write was merged into another slot — this slot is no longer needed. + clearNeon(idx); + } + else if (mergeState == 1) + { + // No other cached copy to merge into — flush to memory. The + // writeBack invalidates the cache entry for partial writes. + writeBackNeon(idx); + } + // else mergeState == 0 (full write): cache is still valid after + // other copies were invalidated. Keep it cached; writeback + // to memory happens at eviction or flushAll time. + } + + void clearNeeded(const a64::Register& reg) + { + const int idx = reg.GetCode(); + if (idx < gprAllocCount && gprMap[idx].usable) + gprMap[idx].isNeeded = false; + } + + void writeBackReg(const a64::VRegister& reg, bool invalidate = true) + { + const int idx = reg.GetCode(); + if (idx < neonAllocTotal) + writeBackNeon(idx, invalidate); + } + + void writeBackReg(const a64::Register& reg, bool clearDirty = true) + { + const int idx = reg.GetCode(); + if (idx < gprAllocCount && gprMap[idx].usable) + writeBackGPR(idx, clearDirty); + } + + void flushAll(bool clearState = true) + { + for (int i = 0; i < neonAllocTotal; i++) + { + writeBackNeon(i, clearState); + } + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable) + writeBackGPR(i, clearState); + } + } + + // Snapshot allocator state for vudivtrace meta records. Captured at JIT + // compile time, BEFORE the divtrace flushAll, so the report shows what + // the allocator was holding at the moment of each microvu instruction. + mvu_divtrace::AllocSnapshot snapshotMaps() const + { + mvu_divtrace::AllocSnapshot s{}; + static_assert(neonAllocTotal == mvu_divtrace::kNeonSlots, + "divtrace neon slot count mismatch"); + static_assert(gprAllocCount == mvu_divtrace::kGprSlots, + "divtrace gpr slot count mismatch"); + for (int i = 0; i < neonAllocTotal; i++) + { + s.neon[i] = {neonMap[i].VFreg, neonMap[i].xyzw, neonMap[i].count, + neonMap[i].isNeeded, neonMap[i].isZero}; + } + for (int i = 0; i < gprAllocCount; i++) + { + s.gpr[i] = {gprMap[i].VIreg, gprMap[i].count, gprMap[i].isNeeded, + gprMap[i].dirty, gprMap[i].isZeroExtended, gprMap[i].usable}; + } + return s; + } + + void flushCallerSavedRegisters(bool clearNeededFlag = false) + { + // Flush NEON caller-saved: Q0-Q7, Q16-Q27 + for (int i = 0; i < 8; i++) + writeBackNeon(i, true); + for (int i = 16; i < neonAllocTotal; i++) + writeBackNeon(i, true); + + // Flush GPR caller-saved: x9-x15 + for (int i = 9; i <= 15; i++) + { + if (gprMap[i].usable) + writeBackGPR(i, true); + } + } + + void flushPartialForCOP2() + { + // For COP2 transition: write back dirty regs, keep clean caches + for (int i = 0; i < neonAllocTotal; i++) + { + if (neonMap[i].VFreg < 0) // Temp + clearNeon(i); + else if (neonMap[i].xyzw != 0 && neonMap[i].xyzw != 0xF) // Partial dirty + writeBackNeon(i, true); + } + } + + //------------------------------------------------------------------ + // Query + //------------------------------------------------------------------ + + bool checkCachedReg(int regId) const + { + return (regId < neonAllocTotal && neonMap[regId].VFreg >= 0); + } + + bool checkCachedGPR(int regId) const + { + return (regId < gprAllocCount && gprMap[regId].usable && + (gprMap[regId].VIreg >= 0 || gprMap[regId].isNeeded)); + } + + bool hasRegVF(int vfreg) const + { + for (int i = 0; i < neonAllocTotal; i++) + if (neonMap[i].VFreg == vfreg) return true; + return false; + } + + bool hasRegVI(int vireg) const + { + for (int i = 0; i < gprAllocCount; i++) + if (gprMap[i].usable && gprMap[i].VIreg == vireg) return true; + return false; + } + + // Helpers used by mvuPreloadRegisters. Match the x86 register-count + // accessors. + int getNeonCount() const { return neonAllocTotal + 1; } + + int getFreeNeonCount() const + { + int count = 0; + for (int i = 0; i < neonAllocTotal; i++) + { + if (!neonMap[i].isNeeded && neonMap[i].VFreg < 0) + count++; + } + return count; + } + + int getRegVF(int i) const + { + return (i < neonAllocTotal) ? neonMap[i].VFreg : -1; + } + + int getGPRCount() const { return gprAllocCount; } + + int getFreeGPRCount() const + { + int count = 0; + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable && !gprMap[i].isNeeded && gprMap[i].VIreg < 0) + count++; + } + return count; + } + + int getRegVI(int i) const + { + return (i < gprAllocCount && gprMap[i].usable) ? gprMap[i].VIreg : -1; + } + + // Move VI value into a specific GPR (for address computation etc.) + void moveVIToGPR(const a64::Register& dstReg, int vi, bool signext = false) + { + // Check if cached + for (int i = 0; i < gprAllocCount; i++) + { + if (gprMap[i].usable && gprMap[i].VIreg == vi) + { + if (signext) + armAsm->Sxth(dstReg.W(), armWRegister(i)); + else if (static_cast(dstReg.GetCode()) != i) + armAsm->Mov(dstReg.W(), armWRegister(i)); + return; + } + } + + // Not cached — load from memory + const a64::MemOperand src = mVUstateMem(offsetof(VURegs, VI) + vi * sizeof(REG_VI)); + if (signext) + armAsm->Ldrsh(dstReg.W(), src); + else + armAsm->Ldrh(dstReg.W(), src); + } + + // Defined out-of-line after microVU struct is complete + void writeVIBackup(const a64::Register& reg); + + // Check if a VF register needs clamping (skip VF0 and I-reg) + bool checkVFClamp(int regId) const + { + if (regId >= neonAllocTotal) + return true; + if ((neonMap[regId].VFreg == 33 && !EmuConfig.Gamefixes.IbitHack) || neonMap[regId].isZero) + return false; + return true; + } + + // COP2 stubs + void clearRegCOP2(int hostreg) {} + void clearGPRCOP2(int hostreg) {} +}; diff --git a/pcsx2/arm64/microVU_Lower-arm64.inl b/pcsx2/arm64/microVU_Lower-arm64.inl new file mode 100644 index 0000000000..8bf8327ddc --- /dev/null +++ b/pcsx2/arm64/microVU_Lower-arm64.inl @@ -0,0 +1,2280 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// Micro VU - ARM64 Lower Instructions (Full Codegen) +//------------------------------------------------------------------ +// pass1: Platform-independent analysis from microVU_Analyze.inl +// pass2: ARM64 NEON/GPR codegen +// pass3: Logging +// pass4: Flag exact-match hints where needed +//------------------------------------------------------------------ + +//------------------------------------------------------------------ +// DIV/SQRT/RSQRT +//------------------------------------------------------------------ + +mVUop(mVU_DIV) +{ + pass1 { mVUanalyzeFDIV(mVU, _Fs_, _Fsf_, _Ft_, _Ftf_, 7); } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, (1 << (3 - _Ftf_))); + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::SRegister sFt(Ft.GetCode()); + const a64::SRegister sFs(Fs.GetCode()); + + // Test if Ft is zero (NaN takes the not-zero branch — matches Fcmeq path). + armAsm->Fcmp(sFt, 0.0); + a64::Label ftNotZero, divDone; + armAsm->B(&ftNotZero, a64::ne); // Skip if Ft != 0 + + // Ft is zero -- check Fs + armAsm->Fcmp(sFs, 0.0); + a64::Label fsNotZero; + armAsm->B(&fsNotZero, a64::ne); // Skip if Fs != 0 + + // 0/0 => Invalid + armAsm->Mov(gprT1.W(), divI); + armStorePtr(gprT1, &mVU.divFlag); + a64::Label afterDivFlag; + armAsm->B(&afterDivFlag); + + armAsm->Bind(&fsNotZero); + // Non-zero / 0 => Divide by zero + armAsm->Mov(gprT1.W(), divD); + armStorePtr(gprT1, &mVU.divFlag); + + armAsm->Bind(&afterDivFlag); + // Result = +/- fmax: sign(Fs) XOR sign(Ft), magnitude = fmax + armAsm->Eor(Fs.V16B(), Fs.V16B(), Ft.V16B()); + armAsm->Ldr(t1, mVUglobMem(&mVUglob.signbit[0])); + armAsm->And(Fs.V16B(), Fs.V16B(), t1.V16B()); + armAsm->Ldr(t1, mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Orr(Fs.V16B(), Fs.V16B(), t1.V16B()); + a64::Label skipNormalDiv; + armAsm->B(&skipNormalDiv); + + armAsm->Bind(&ftNotZero); + // Normal division + armAsm->Mov(gprT1.W(), 0); + armStorePtr(gprT1, &mVU.divFlag); + armAsm->Fdiv(sFs, sFs, sFt); + mVUclamp1(mVU, Fs, t1, 8, true); + + armAsm->Bind(&skipNormalDiv); + + writeQreg(Fs, mVUinfo.writeQ); + + if (mVU.cop2) + { + armAsm->Bic(gprF0, gprF0, 0xc0000); + armLoadPtr(gprT1, &mVU.divFlag); + armAsm->Orr(gprF0, gprF0, gprT1.W()); + } + + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(t1); + mVU.profiler.EmitOp(opDIV); + } + pass3 { mVUlog("DIV Q, vf%02d%s, vf%02d%s", _Fs_, _Fsf_String, _Ft_, _Ftf_String); } +} + +mVUop(mVU_SQRT) +{ + pass1 { mVUanalyzeFDIV(mVU, 0, 0, _Ft_, _Ftf_, 7); } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, (1 << (3 - _Ftf_))); + const a64::SRegister sFt(Ft.GetCode()); + + // Clear divFlag + armAsm->Mov(gprT1.W(), 0); + armStorePtr(gprT1, &mVU.divFlag); + + // Check for negative: if sign bit set, set I flag and make positive + armAsm->Umov(gprT1.W(), Ft.V4S(), 0); + armAsm->Tst(gprT1.W(), 0x80000000u); + a64::Label notNeg; + armAsm->B(¬Neg, a64::eq); + armAsm->Mov(gprT1.W(), divI); + armStorePtr(gprT1, &mVU.divFlag); + armAsm->Ldr(RQSCRATCH, mVUglobMem(&mVUglob.absclip[0])); + armAsm->And(Ft.V16B(), Ft.V16B(), RQSCRATCH.V16B()); + armAsm->Bind(¬Neg); + + // Clamp infinity. Fminnm (number-preserving) so positive-NaN inputs + // clamp to +maxfloat instead of propagating into Fsqrt — matches + // mVUclamp1's semantics (see microVU_Clamp-arm64.inl). + if (CHECK_VU_OVERFLOW(mVU.index)) + { + armAsm->Ldr(RQSCRATCH, mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Fminnm(sFt, sFt, a64::SRegister(RQSCRATCH.GetCode())); + } + + armAsm->Fsqrt(sFt, sFt); + writeQreg(Ft, mVUinfo.writeQ); + + if (mVU.cop2) + { + armAsm->Bic(gprF0, gprF0, 0xc0000); + armLoadPtr(gprT1, &mVU.divFlag); + armAsm->Orr(gprF0, gprF0, gprT1.W()); + } + + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opSQRT); + } + pass3 { mVUlog("SQRT Q, vf%02d%s", _Ft_, _Ftf_String); } +} + +mVUop(mVU_RSQRT) +{ + pass1 { mVUanalyzeFDIV(mVU, _Fs_, _Fsf_, _Ft_, _Ftf_, 13); } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, (1 << (3 - _Ftf_))); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::SRegister sFs(Fs.GetCode()); + const a64::SRegister sFt(Ft.GetCode()); + + // Clear divFlag + armAsm->Mov(gprT1.W(), 0); + armStorePtr(gprT1, &mVU.divFlag); + + // Check for negative Ft: if sign bit set, set I flag and make positive + armAsm->Umov(gprT1.W(), Ft.V4S(), 0); + armAsm->Tst(gprT1.W(), 0x80000000u); + a64::Label notNeg; + armAsm->B(¬Neg, a64::eq); + armAsm->Mov(gprT1.W(), divI); + armStorePtr(gprT1, &mVU.divFlag); + armAsm->Ldr(RQSCRATCH, mVUglobMem(&mVUglob.absclip[0])); + armAsm->And(Ft.V16B(), Ft.V16B(), RQSCRATCH.V16B()); + armAsm->Bind(¬Neg); + + armAsm->Fsqrt(sFt, sFt); + + // Test if sqrt(Ft) is zero (NaN takes the not-zero branch — matches Fcmeq path). + armAsm->Fcmp(sFt, 0.0); + a64::Label sqrtNotZero, rsqrtDone; + armAsm->B(&sqrtNotZero, a64::ne); + + // sqrt(Ft) is zero -- check Fs + armAsm->Fcmp(sFs, 0.0); + a64::Label fsNotZero2; + armAsm->B(&fsNotZero2, a64::ne); + + // 0/0 => Invalid + armAsm->Mov(gprT1.W(), divI); + armStorePtr(gprT1, &mVU.divFlag); + a64::Label afterFlag2; + armAsm->B(&afterFlag2); + + armAsm->Bind(&fsNotZero2); + armAsm->Mov(gprT1.W(), divD); + armStorePtr(gprT1, &mVU.divFlag); + + armAsm->Bind(&afterFlag2); + // Result = sign(Fs) | fmax + armAsm->Ldr(t1, mVUglobMem(&mVUglob.signbit[0])); + armAsm->And(Fs.V16B(), Fs.V16B(), t1.V16B()); + armAsm->Ldr(t1, mVUglobMem(&mVUglob.maxvals[0])); + armAsm->Orr(Fs.V16B(), Fs.V16B(), t1.V16B()); + armAsm->B(&rsqrtDone); + + armAsm->Bind(&sqrtNotZero); + armAsm->Fdiv(sFs, sFs, sFt); + mVUclamp1(mVU, Fs, t1, 8, true); + + armAsm->Bind(&rsqrtDone); + writeQreg(Fs, mVUinfo.writeQ); + + if (mVU.cop2) + { + armAsm->Bic(gprF0, gprF0, 0xc0000); + armLoadPtr(gprT1, &mVU.divFlag); + armAsm->Orr(gprF0, gprF0, gprT1.W()); + } + + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(t1); + mVU.profiler.EmitOp(opRSQRT); + } + pass3 { mVUlog("RSQRT Q, vf%02d%s, vf%02d%s", _Fs_, _Fsf_String, _Ft_, _Ftf_String); } +} + +//------------------------------------------------------------------ +// EATAN/EEXP/ELENG/ERCPR/ERLENG/ERSADD/ERSQRT/ESADD/ESIN/ESQRT/ESUM +//------------------------------------------------------------------ +// EFU ops are VU1-only extended math. Ported from x86 microVU_Lower.inl. +// qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, [3]=pending_p. +// +// x86 uses PSHUFD to bring the P-write lane to position 0 for xMUL.SS/xADD.SS +// (which preserve upper lanes), then flips back. On ARM64, scalar FP ops +// (FMUL Sd,Sn,Sm etc.) ZERO the upper 96 bits of the destination V register. +// So we can't accumulate directly into qmmPQ — we'd destroy Q/pending_q/pending_p. +// Instead we accumulate in a scratch VRegister (`pq`) and Ins into qmmPQ at +// the end. Target lane is 2 (P) when writeP=false, 3 (pending_p) when true. + +// NEON equivalent of SSE_DIVSS (scalar divide with clamping) +static __fi void NEON_DIVSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, 0x8); + mVUclamp3(mVU, from, RQSCRATCH3, 0x8); + armAsm->Fdiv(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), + a64::SRegister(from.GetCode())); + mVUclamp4(mVU, to, RQSCRATCH3, 0x8); +} + +// Scalar lane-0 multiply by 32-bit float constant at addr (raw xMUL.SS equiv, no clamping). +// Uses RQSCRATCH internally so `to` may alias any caller-owned scratch. +static __fi void mVUmulSSConst(const a64::VRegister& to, const void* addr) +{ + armAsm->Ldr(a64::SRegister(RQSCRATCH.GetCode()), mVUglobMem(addr)); + armAsm->Fmul(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), + a64::SRegister(RQSCRATCH.GetCode())); +} + +// Scalar lane-0 add of 32-bit float constant at addr (raw xADD.SS equiv, no clamping). +static __fi void mVUaddSSConst(const a64::VRegister& to, const void* addr) +{ + armAsm->Ldr(a64::SRegister(RQSCRATCH.GetCode()), mVUglobMem(addr)); + armAsm->Fadd(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), + a64::SRegister(RQSCRATCH.GetCode())); +} + +// Scalar lane-0 subtract 32-bit float constant at addr (raw xSUB.SS equiv, no clamping). +static __fi void mVUsubSSConst(const a64::VRegister& to, const void* addr) +{ + armAsm->Ldr(a64::SRegister(RQSCRATCH.GetCode()), mVUglobMem(addr)); + armAsm->Fsub(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), + a64::SRegister(RQSCRATCH.GetCode())); +} + +// xMOVAPS reg,reg — full 128-bit copy. +static __fi void mVUmovAPSReg(const a64::VRegister& dst, const a64::VRegister& src) +{ + armAsm->Mov(dst.V16B(), src.V16B()); +} + +// Copy src lane 0 into qmmPQ at the P-write target lane (2 or 3). +static __fi void mVUwritePQresult(const a64::VRegister& src, bool writeP) +{ + const int targetLane = writeP ? 3 : 2; + armAsm->Ins(qmmPQ.V4S(), targetLane, src.V4S(), 0); +} + +// sumXYZ: dst[0] = Fs.x*Fs.x + Fs.y*Fs.y + Fs.z*Fs.z. Trashes Fs. +// Matches x86 DPPS 0x71 + MOVSS semantics. AArch64 NEON has no FADDV +// across-vector reduction for floats, so fall back to two FADDP passes +// (the standard pattern). Skip the final Ins when dst == Fs (single +// caller, ESUM, passes the same register for both). +static __fi void mVU_sumXYZ_arm(const a64::VRegister& dst, const a64::VRegister& Fs) +{ + armAsm->Fmul(Fs.V4S(), Fs.V4S(), Fs.V4S()); + armAsm->Ins(Fs.V4S(), 3, a64::wzr); // zero W lane + armAsm->Faddp(Fs.V4S(), Fs.V4S(), Fs.V4S()); + armAsm->Faddp(Fs.V4S(), Fs.V4S(), Fs.V4S()); + if (dst.GetCode() != Fs.GetCode()) + armAsm->Ins(dst.V4S(), 0, Fs.V4S(), 0); +} + +// EATAN polynomial helper: pq[0] += Fs^(2*n+1) * T_n (Taylor-like series). +// Matches the x86 EATANhelper macro. +// All scalar math operates on lane 0 of `pq` (a scratch, NOT qmmPQ). +#define EATANhelper_arm(addr) \ + do { \ + NEON_MULSS(mVU, t2, Fs); \ + NEON_MULSS(mVU, t2, Fs); \ + mVUmovAPSReg(t1, t2); \ + mVUmulSSConst(t1, (addr)); \ + NEON_ADDSS(mVU, pq, t1); \ + } while (0) + +static __fi void mVU_EATAN_arm(mV, const a64::VRegister& pq, const a64::VRegister& Fs, + const a64::VRegister& t1, const a64::VRegister& t2) +{ + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); + mVUmulSSConst(pq, &mVUglob.T1[0]); + mVUmovAPSReg(t2, Fs); + EATANhelper_arm(&mVUglob.T2[0]); + EATANhelper_arm(&mVUglob.T3[0]); + EATANhelper_arm(&mVUglob.T4[0]); + EATANhelper_arm(&mVUglob.T5[0]); + EATANhelper_arm(&mVUglob.T6[0]); + EATANhelper_arm(&mVUglob.T7[0]); + EATANhelper_arm(&mVUglob.T8[0]); + mVUaddSSConst(pq, &mVUglob.Pi4[0]); +} + +mVUop(mVU_EATAN) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 54); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); // scratch P accumulator + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + // pq[0] = Fs[0] + 1; Fs[0] -= 1; Fs = (Fs-1)/(Fs+1) + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); + mVUsubSSConst(Fs, &mVUglob.one[0]); + mVUaddSSConst(pq, &mVUglob.one[0]); + NEON_DIVSS(mVU, Fs, pq); + mVU_EATAN_arm(mVU, pq, Fs, t1, t2); + mVUwritePQresult(pq, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); + mVU.profiler.EmitOp(opEATAN); + } + pass3 { mVUlog("EATAN P"); } +} + +mVUop(mVU_EATANxy) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 54); + } + pass2 + { + const a64::VRegister& t1 = mVU.regAlloc->allocReg(_Fs_, 0, 0xf); + const a64::VRegister& Fs = mVU.regAlloc->allocReg(); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + // x86: PSHUFD(Fs, t1, 0x01) broadcasts t1.y — only lane 0 is read later. + armAsm->Ins(Fs.V4S(), 0, t1.V4S(), 1); + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); // pq[0] = Fs[0] (= VF.y) + NEON_SUBSS(mVU, Fs, t1); // Fs[0] = y - x + NEON_ADDSS(mVU, t1, pq); // t1[0] = x + y + NEON_DIVSS(mVU, Fs, t1); // Fs[0] = (y-x)/(y+x) + mVU_EATAN_arm(mVU, pq, Fs, t1, t2); + mVUwritePQresult(pq, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); + mVU.profiler.EmitOp(opEATANxy); + } + pass3 { mVUlog("EATANxy P"); } +} + +mVUop(mVU_EATANxz) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 54); + } + pass2 + { + const a64::VRegister& t1 = mVU.regAlloc->allocReg(_Fs_, 0, 0xf); + const a64::VRegister& Fs = mVU.regAlloc->allocReg(); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + armAsm->Ins(Fs.V4S(), 0, t1.V4S(), 2); // Fs[0] = VF.z + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); + NEON_SUBSS(mVU, Fs, t1); // z - x + NEON_ADDSS(mVU, t1, pq); // z + x + NEON_DIVSS(mVU, Fs, t1); + mVU_EATAN_arm(mVU, pq, Fs, t1, t2); + mVUwritePQresult(pq, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); + mVU.profiler.EmitOp(opEATANxz); + } + pass3 { mVUlog("EATANxz P"); } +} + +// EEXP polynomial helper: pq[0] += (Fs^n) * E_n. +// Matches the x86 eexpHelper macro. +#define eexpHelper_arm(addr) \ + do { \ + NEON_MULSS(mVU, t2, Fs); \ + mVUmovAPSReg(t1, t2); \ + mVUmulSSConst(t1, (addr)); \ + NEON_ADDSS(mVU, pq, t1); \ + } while (0) + +mVUop(mVU_EEXP) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 44); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); // pq = Fs + mVUmulSSConst(pq, &mVUglob.E1[0]); // pq *= E1 + mVUaddSSConst(pq, &mVUglob.one[0]); // pq += 1 + mVUmovAPSReg(t1, Fs); + NEON_MULSS(mVU, t1, Fs); // t1 = Fs^2 + mVUmovAPSReg(t2, t1); // t2 = Fs^2 + mVUmulSSConst(t1, &mVUglob.E2[0]); + NEON_ADDSS(mVU, pq, t1); + eexpHelper_arm(&mVUglob.E3[0]); + eexpHelper_arm(&mVUglob.E4[0]); + eexpHelper_arm(&mVUglob.E5[0]); + NEON_MULSS(mVU, t2, Fs); + mVUmulSSConst(t2, &mVUglob.E6[0]); + NEON_ADDSS(mVU, pq, t2); + NEON_MULSS(mVU, pq, pq); + NEON_MULSS(mVU, pq, pq); + // pq[0] = 1 / pq[0]^4 + armAsm->Ldr(a64::SRegister(t2.GetCode()), mVUglobMem(&mVUglob.one[0])); + NEON_DIVSS(mVU, t2, pq); + mVUwritePQresult(t2, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); + mVU.profiler.EmitOp(opEEXP); + } + pass3 { mVUlog("EEXP P"); } +} + +mVUop(mVU_ELENG) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 18); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + mVU_sumXYZ_arm(pq, Fs); + armAsm->Fsqrt(a64::SRegister(pq.GetCode()), a64::SRegister(pq.GetCode())); + mVUwritePQresult(pq, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.profiler.EmitOp(opELENG); + } + pass3 { mVUlog("ELENG P"); } +} + +mVUop(mVU_ERCPR) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 12); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + // Fs is reused after pq is filled (Fs[0] := 1.0, then 1/pq[0]). Guard + // the bound-vs-scratch allocator invariant in debug builds. + pxAssert(Fs.GetCode() != pq.GetCode()); + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); // pq[0] = Fs[0] + armAsm->Ldr(a64::SRegister(Fs.GetCode()), mVUglobMem(&mVUglob.one[0])); // Fs[0] = 1.0 + NEON_DIVSS(mVU, Fs, pq); // Fs[0] = 1 / pq[0] + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.profiler.EmitOp(opERCPR); + } + pass3 { mVUlog("ERCPR P"); } +} + +mVUop(mVU_ERLENG) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 24); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + // pq is filled from Fs (sumXYZ) then Fs[0] := 1.0 — Fs and pq must + // be different NEON registers. mVU_sumXYZ_arm squares Fs in-place. + pxAssert(Fs.GetCode() != pq.GetCode()); + mVU_sumXYZ_arm(pq, Fs); + armAsm->Fsqrt(a64::SRegister(pq.GetCode()), a64::SRegister(pq.GetCode())); + armAsm->Ldr(a64::SRegister(Fs.GetCode()), mVUglobMem(&mVUglob.one[0])); + NEON_DIVSS(mVU, Fs, pq); + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.profiler.EmitOp(opERLENG); + } + pass3 { mVUlog("ERLENG P"); } +} + +mVUop(mVU_ERSADD) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 18); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + pxAssert(Fs.GetCode() != pq.GetCode()); + mVU_sumXYZ_arm(pq, Fs); + armAsm->Ldr(a64::SRegister(Fs.GetCode()), mVUglobMem(&mVUglob.one[0])); + NEON_DIVSS(mVU, Fs, pq); + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.profiler.EmitOp(opERSADD); + } + pass3 { mVUlog("ERSADD P"); } +} + +mVUop(mVU_ERSQRT) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 18); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + pxAssert(Fs.GetCode() != pq.GetCode()); + armAsm->Ldr(RQSCRATCH, mVUglobMem(&mVUglob.absclip[0])); + armAsm->And(Fs.V16B(), Fs.V16B(), RQSCRATCH.V16B()); + armAsm->Fsqrt(a64::SRegister(pq.GetCode()), a64::SRegister(Fs.GetCode())); + armAsm->Ldr(a64::SRegister(Fs.GetCode()), mVUglobMem(&mVUglob.one[0])); + NEON_DIVSS(mVU, Fs, pq); + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.profiler.EmitOp(opERSQRT); + } + pass3 { mVUlog("ERSQRT P"); } +} + +mVUop(mVU_ESADD) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 11); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + mVU_sumXYZ_arm(Fs, Fs); + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opESADD); + } + pass3 { mVUlog("ESADD P"); } +} + +mVUop(mVU_ESIN) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 29); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::VRegister& pq = mVU.regAlloc->allocReg(); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + // pq = X + armAsm->Ins(pq.V4S(), 0, Fs.V4S(), 0); + NEON_MULSS(mVU, Fs, Fs); // Fs = X^2 + mVUmovAPSReg(t1, Fs); // t1 = X^2 + NEON_MULSS(mVU, Fs, pq); // Fs = X^3 + mVUmovAPSReg(t2, Fs); // t2 = X^3 + mVUmulSSConst(Fs, &mVUglob.S2[0]); // Fs = s2 * X^3 + NEON_ADDSS(mVU, pq, Fs); // pq = X + s2*X^3 + + NEON_MULSS(mVU, t2, t1); // t2 = X^5 + mVUmovAPSReg(Fs, t2); + mVUmulSSConst(Fs, &mVUglob.S3[0]); // Fs = s3*X^5 + NEON_ADDSS(mVU, pq, Fs); + + NEON_MULSS(mVU, t2, t1); // t2 = X^7 + mVUmovAPSReg(Fs, t2); + mVUmulSSConst(Fs, &mVUglob.S4[0]); // Fs = s4*X^7 + NEON_ADDSS(mVU, pq, Fs); + + NEON_MULSS(mVU, t2, t1); // t2 = X^9 + mVUmulSSConst(t2, &mVUglob.S5[0]); // t2 = s5*X^9 + NEON_ADDSS(mVU, pq, t2); + + mVUwritePQresult(pq, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(pq); + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); + mVU.profiler.EmitOp(opESIN); + } + pass3 { mVUlog("ESIN P"); } +} + +mVUop(mVU_ESQRT) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU1(mVU, _Fs_, _Fsf_, 12); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + armAsm->Ldr(RQSCRATCH, mVUglobMem(&mVUglob.absclip[0])); + armAsm->And(Fs.V16B(), Fs.V16B(), RQSCRATCH.V16B()); + armAsm->Fsqrt(a64::SRegister(Fs.GetCode()), a64::SRegister(Fs.GetCode())); + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opESQRT); + } + pass3 { mVUlog("ESQRT P"); } +} + +mVUop(mVU_ESUM) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeEFU2(mVU, _Fs_, 12); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + // x86: PSHUFD(t1, Fs, 0x1b) reverses lanes: t1 = [Fs[3], Fs[2], Fs[1], Fs[0]] + armAsm->Rev64(t1.V4S(), Fs.V4S()); // t1 = [Fs[1], Fs[0], Fs[3], Fs[2]] + armAsm->Ext(t1.V16B(), t1.V16B(), t1.V16B(), 8); // rotate: [Fs[3], Fs[2], Fs[1], Fs[0]] + NEON_ADDPS(mVU, Fs, t1); // Fs = Fs + reverse(Fs) — lane 0 holds (x+w) + // x86: PSHUFD(t1, Fs, 0x01) — only lane 0 used: t1[0] = Fs[1] (= y+z) + armAsm->Ins(t1.V4S(), 0, Fs.V4S(), 1); + NEON_ADDSS(mVU, Fs, t1); // Fs[0] = x+y+z+w + mVUwritePQresult(Fs, mVUinfo.writeP); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(t1); + mVU.profiler.EmitOp(opESUM); + } + pass3 { mVUlog("ESUM P"); } +} + +//------------------------------------------------------------------ +// FCAND/FCEQ/FCGET/FCOR/FCSET +//------------------------------------------------------------------ + +mVUop(mVU_FCAND) +{ + pass1 { mVUanalyzeCflag(mVU, 1); } + pass2 + { + const a64::Register& dst = mVU.regAlloc->allocGPR(-1, 1, mVUlow.backupVI); + mVUallocCFLAGa(mVU, dst, cFLAG.read); + // vi01 = ((clip & imm24) != 0) ? 1 : 0. Use Tst+Cset. + if (_Imm24_) + { + armAsm->Tst(dst.W(), _Imm24_); + armAsm->Cset(dst.W(), a64::ne); + } + else + { + armAsm->Mov(dst.W(), 0); + } + mVU.regAlloc->clearNeeded(dst); + mVU.profiler.EmitOp(opFCAND); + } + pass3 { mVUlog("FCAND vi01, $%x", _Imm24_); } + pass4 { mVUregs.needExactMatch |= 4; } +} + +mVUop(mVU_FCEQ) +{ + pass1 { mVUanalyzeCflag(mVU, 1); } + pass2 + { + const a64::Register& dst = mVU.regAlloc->allocGPR(-1, 1, mVUlow.backupVI); + mVUallocCFLAGa(mVU, dst, cFLAG.read); + armAsm->Mov(gprT1.W(), _Imm24_); + armAsm->Eor(dst.W(), dst.W(), gprT1.W()); + armAsm->Sub(dst.W(), dst.W(), 1); + armAsm->Lsr(dst.W(), dst.W(), 31); + mVU.regAlloc->clearNeeded(dst); + mVU.profiler.EmitOp(opFCEQ); + } + pass3 { mVUlog("FCEQ vi01, $%x", _Imm24_); } + pass4 { mVUregs.needExactMatch |= 4; } +} + +mVUop(mVU_FCGET) +{ + pass1 { mVUanalyzeCflag(mVU, _It_); } + pass2 + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + mVUallocCFLAGa(mVU, regT, cFLAG.read); + armAsm->And(regT.W(), regT.W(), 0xfff); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opFCGET); + } + pass3 { mVUlog("FCGET vi%02d", _Ft_); } + pass4 { mVUregs.needExactMatch |= 4; } +} + +mVUop(mVU_FCOR) +{ + pass1 { mVUanalyzeCflag(mVU, 1); } + pass2 + { + const a64::Register& dst = mVU.regAlloc->allocGPR(-1, 1, mVUlow.backupVI); + mVUallocCFLAGa(mVU, dst, cFLAG.read); + armAsm->Mov(gprT1.W(), _Imm24_); + armAsm->Orr(dst.W(), dst.W(), gprT1.W()); + armAsm->Add(dst.W(), dst.W(), 1); + armAsm->Lsr(dst.W(), dst.W(), 24); + mVU.regAlloc->clearNeeded(dst); + mVU.profiler.EmitOp(opFCOR); + } + pass3 { mVUlog("FCOR vi01, $%x", _Imm24_); } + pass4 { mVUregs.needExactMatch |= 4; } +} + +mVUop(mVU_FCSET) +{ + pass1 { cFLAG.doFlag = true; } + pass2 + { + armAsm->Mov(gprT1.W(), _Imm24_); + mVUallocCFLAGb(mVU, gprT1, cFLAG.write); + mVU.profiler.EmitOp(opFCSET); + } + pass3 { mVUlog("FCSET $%x", _Imm24_); } +} + +//------------------------------------------------------------------ +// FMAND/FMEQ/FMOR +//------------------------------------------------------------------ + +mVUop(mVU_FMAND) +{ + pass1 { mVUanalyzeMflag(mVU, _Is_, _It_); } + pass2 + { + mVUallocMFLAGa(mVU, gprT1, mFLAG.read); + const a64::Register& regT = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + armAsm->And(regT.W(), regT.W(), gprT1.W()); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opFMAND); + } + pass3 { mVUlog("FMAND vi%02d, vi%02d", _Ft_, _Fs_); } + pass4 { mVUregs.needExactMatch |= 2; } +} + +mVUop(mVU_FMEQ) +{ + pass1 { mVUanalyzeMflag(mVU, _Is_, _It_); } + pass2 + { + mVUallocMFLAGa(mVU, gprT1, mFLAG.read); + const a64::Register& regT = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + armAsm->Eor(regT.W(), regT.W(), gprT1.W()); + armAsm->Sub(regT.W(), regT.W(), 1); + armAsm->Lsr(regT.W(), regT.W(), 31); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opFMEQ); + } + pass3 { mVUlog("FMEQ vi%02d, vi%02d", _Ft_, _Fs_); } + pass4 { mVUregs.needExactMatch |= 2; } +} + +mVUop(mVU_FMOR) +{ + pass1 { mVUanalyzeMflag(mVU, _Is_, _It_); } + pass2 + { + mVUallocMFLAGa(mVU, gprT1, mFLAG.read); + const a64::Register& regT = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + armAsm->Orr(regT.W(), regT.W(), gprT1.W()); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opFMOR); + } + pass3 { mVUlog("FMOR vi%02d, vi%02d", _Ft_, _Fs_); } + pass4 { mVUregs.needExactMatch |= 2; } +} + +//------------------------------------------------------------------ +// FSAND/FSEQ/FSOR/FSSET +//------------------------------------------------------------------ + +mVUop(mVU_FSAND) +{ + pass1 { mVUanalyzeSflag(mVU, _It_); } + pass2 + { + if (_Imm12_ & 0x0c30) DevCon.WriteLn(Color_Green, "mVU_FSAND: Checking I/D/IS/DS Flags"); + if (_Imm12_ & 0x030c) DevCon.WriteLn(Color_Green, "mVU_FSAND: Checking U/O/US/OS Flags"); + const a64::Register& reg = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + mVUallocSFLAGc(reg, gprT1, sFLAG.read); + if (_Imm12_) + { + armAsm->Mov(gprT1.W(), _Imm12_); + armAsm->And(reg.W(), reg.W(), gprT1.W()); + } + else + { + armAsm->Mov(reg.W(), 0); + } + mVU.regAlloc->clearNeeded(reg); + mVU.profiler.EmitOp(opFSAND); + } + pass3 { mVUlog("FSAND vi%02d, $%x", _Ft_, _Imm12_); } + pass4 { mVUregs.needExactMatch |= 1; } +} + +mVUop(mVU_FSOR) +{ + pass1 { mVUanalyzeSflag(mVU, _It_); } + pass2 + { + const a64::Register& reg = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + mVUallocSFLAGc(reg, gprT2, sFLAG.read); + if (_Imm12_) + { + armAsm->Mov(gprT1.W(), _Imm12_); + armAsm->Orr(reg.W(), reg.W(), gprT1.W()); + } + mVU.regAlloc->clearNeeded(reg); + mVU.profiler.EmitOp(opFSOR); + } + pass3 { mVUlog("FSOR vi%02d, $%x", _Ft_, _Imm12_); } + pass4 { mVUregs.needExactMatch |= 1; } +} + +mVUop(mVU_FSEQ) +{ + pass1 { mVUanalyzeSflag(mVU, _It_); } + pass2 + { + if (_Imm12_ & 0x0c30) DevCon.WriteLn(Color_Green, "mVU_FSEQ: Checking I/D/IS/DS Flags"); + if (_Imm12_ & 0x030c) DevCon.WriteLn(Color_Green, "mVU_FSEQ: Checking U/O/US/OS Flags"); + + // Use mVUallocSFLAGc which normalizes the flag, then compare with immediate + const a64::Register& reg = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + mVUallocSFLAGc(reg, gprT1, sFLAG.read); + if (_Imm12_) + { + armAsm->Mov(gprT1.W(), _Imm12_); + armAsm->Eor(reg.W(), reg.W(), gprT1.W()); + } + armAsm->Sub(reg.W(), reg.W(), 1); + armAsm->Lsr(reg.W(), reg.W(), 31); + mVU.regAlloc->clearNeeded(reg); + mVU.profiler.EmitOp(opFSEQ); + } + pass3 { mVUlog("FSEQ vi%02d, $%x", _Ft_, _Imm12_); } + pass4 { mVUregs.needExactMatch |= 1; } +} + +mVUop(mVU_FSSET) +{ + pass1 { mVUanalyzeFSSET(mVU); } + pass2 + { + // Build denormalized sticky bits from _Imm12_ + int imm = 0; + if (_Imm12_ & 0x0040) imm |= 0x000000f; // ZS + if (_Imm12_ & 0x0080) imm |= 0x00000f0; // SS + if (_Imm12_ & 0x0100) imm |= 0x0400000; // US + if (_Imm12_ & 0x0200) imm |= 0x0800000; // OS + if (_Imm12_ & 0x0400) imm |= 0x1000000; // IS + if (_Imm12_ & 0x0800) imm |= 0x2000000; // DS + + if (!(sFLAG.doFlag || mVUinfo.doDivFlag)) + { + mVUallocSFLAGa(getFlagReg(sFLAG.write), sFLAG.lastWrite); + } + armAsm->Mov(gprT1.W(), 0xfff00u); + armAsm->And(getFlagReg(sFLAG.write), getFlagReg(sFLAG.write), gprT1.W()); + if (imm) + { + armAsm->Mov(gprT1.W(), imm); + armAsm->Orr(getFlagReg(sFLAG.write), getFlagReg(sFLAG.write), gprT1.W()); + } + mVU.profiler.EmitOp(opFSSET); + } + pass3 { mVUlog("FSSET $%x", _Imm12_); } +} + +//------------------------------------------------------------------ +// IADD/IADDI/IADDIU/IAND/IOR/ISUB/ISUBIU +//------------------------------------------------------------------ + +mVUop(mVU_IADD) +{ + pass1 { mVUanalyzeIALU1(mVU, _Id_, _Is_, _It_); } + pass2 + { + if (_Is_ == 0 || _It_ == 0) + { + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_ ? _Is_ : _It_, -1); + const a64::Register& regD = mVU.regAlloc->allocGPR(-1, _Id_, mVUlow.backupVI); + armAsm->Mov(regD.W(), regS.W()); + mVU.regAlloc->clearNeeded(regD); + mVU.regAlloc->clearNeeded(regS); + } + else + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1); + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Id_, mVUlow.backupVI); + armAsm->Add(regS.W(), regS.W(), regT.W()); + mVU.regAlloc->clearNeeded(regS); + mVU.regAlloc->clearNeeded(regT); + } + mVU.profiler.EmitOp(opIADD); + } + pass3 { mVUlog("IADD vi%02d, vi%02d, vi%02d", _Fd_, _Fs_, _Ft_); } +} + +mVUop(mVU_IADDI) +{ + pass1 { mVUanalyzeIADDI(mVU, _Is_, _It_, _Imm5_); } + pass2 + { + if (_Is_ == 0) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm5_ != 0) + armAsm->Mov(regT.W(), (u32)(s32)_Imm5_); + else + armAsm->Mov(regT.W(), 0); + } + else + { + // IbitHack: reconstruct signed Imm5 from the live opcode word at + // runtime. _Imm5_ takes bit 10 of curI as sign and bits 9:6 as + // magnitude (matching the _Imm5_ macro), so emit the + // sbfx+and+bfxil idiom. + armLoadPtr(gprT1, &curI); + armAsm->Sbfx(regT.W(), gprT1.W(), 10, 1); + armAsm->And(regT.W(), regT.W(), 0xfff0); + armAsm->Bfxil(regT.W(), gprT1.W(), 6, 4); + } + mVU.regAlloc->clearNeeded(regT); + } + else + { + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm5_ != 0) + { + s16 imm = _Imm5_; + if (imm >= 0) + armAsm->Add(regS.W(), regS.W(), (u32)imm); + else + armAsm->Sub(regS.W(), regS.W(), (u32)(-imm)); + } + } + else + { + armLoadPtr(gprT1, &curI); + armAsm->Sbfx(gprT2.W(), gprT1.W(), 10, 1); + armAsm->And(gprT2.W(), gprT2.W(), 0xfff0); + armAsm->Bfxil(gprT2.W(), gprT1.W(), 6, 4); + armAsm->Add(regS.W(), regS.W(), gprT2.W()); + } + mVU.regAlloc->clearNeeded(regS); + } + mVU.profiler.EmitOp(opIADDI); + } + pass3 { mVUlog("IADDI vi%02d, vi%02d, %d", _Ft_, _Fs_, _Imm5_); } +} + +mVUop(mVU_IADDIU) +{ + pass1 { mVUanalyzeIADDI(mVU, _Is_, _It_, _Imm15_); } + pass2 + { + if (_Is_ == 0) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm15_ != 0) + armAsm->Mov(regT.W(), _Imm15_); + else + armAsm->Mov(regT.W(), 0); + } + else + { + // IbitHack: the game patches the I-bit immediate field in micro + // memory between block runs without invalidating the JIT cache, so + // the JIT must reconstruct Imm15 from the live opcode word. + // Imm15 = ((curI >> 21) & 0xf) << 11 | (curI & 0x7ff), expressed + // via ubfx+and+bfxil. + armLoadPtr(gprT1, &curI); + armAsm->Ubfx(regT.W(), gprT1.W(), 10, 22); + armAsm->And(regT.W(), regT.W(), 0x7800); + armAsm->Bfxil(regT.W(), gprT1.W(), 0, 11); + } + mVU.regAlloc->clearNeeded(regT); + } + else + { + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm15_ != 0) + { + armAsm->Mov(gprT1.W(), _Imm15_); + armAsm->Add(regS.W(), regS.W(), gprT1.W()); + } + } + else + { + armLoadPtr(gprT1, &curI); + armAsm->Ubfx(gprT2.W(), gprT1.W(), 10, 22); + armAsm->And(gprT2.W(), gprT2.W(), 0x7800); + armAsm->Bfxil(gprT2.W(), gprT1.W(), 0, 11); + armAsm->Add(regS.W(), regS.W(), gprT2.W()); + } + mVU.regAlloc->clearNeeded(regS); + } + mVU.profiler.EmitOp(opIADDIU); + } + pass3 { mVUlog("IADDIU vi%02d, vi%02d, %d", _Ft_, _Fs_, _Imm15_); } +} + +mVUop(mVU_IAND) +{ + pass1 { mVUanalyzeIALU1(mVU, _Id_, _Is_, _It_); } + pass2 + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1); + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Id_, mVUlow.backupVI); + if (_It_ != _Is_) + armAsm->And(regS.W(), regS.W(), regT.W()); + mVU.regAlloc->clearNeeded(regS); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opIAND); + } + pass3 { mVUlog("IAND vi%02d, vi%02d, vi%02d", _Fd_, _Fs_, _Ft_); } +} + +mVUop(mVU_IOR) +{ + pass1 { mVUanalyzeIALU1(mVU, _Id_, _Is_, _It_); } + pass2 + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1); + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Id_, mVUlow.backupVI); + if (_It_ != _Is_) + armAsm->Orr(regS.W(), regS.W(), regT.W()); + mVU.regAlloc->clearNeeded(regS); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opIOR); + } + pass3 { mVUlog("IOR vi%02d, vi%02d, vi%02d", _Fd_, _Fs_, _Ft_); } +} + +mVUop(mVU_ISUB) +{ + pass1 { mVUanalyzeIALU1(mVU, _Id_, _Is_, _It_); } + pass2 + { + if (_It_ != _Is_) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1); + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Id_, mVUlow.backupVI); + armAsm->Sub(regS.W(), regS.W(), regT.W()); + mVU.regAlloc->clearNeeded(regS); + mVU.regAlloc->clearNeeded(regT); + } + else + { + const a64::Register& regD = mVU.regAlloc->allocGPR(-1, _Id_, mVUlow.backupVI); + armAsm->Mov(regD.W(), 0); + mVU.regAlloc->clearNeeded(regD); + } + mVU.profiler.EmitOp(opISUB); + } + pass3 { mVUlog("ISUB vi%02d, vi%02d, vi%02d", _Fd_, _Fs_, _Ft_); } +} + +mVUop(mVU_ISUBIU) +{ + pass1 { mVUanalyzeIALU2(mVU, _Is_, _It_); } + pass2 + { + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _It_, mVUlow.backupVI); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm15_ != 0) + { + armAsm->Mov(gprT1.W(), _Imm15_); + armAsm->Sub(regS.W(), regS.W(), gprT1.W()); + } + } + else + { + armLoadPtr(gprT1, &curI); + armAsm->Ubfx(gprT2.W(), gprT1.W(), 10, 22); + armAsm->And(gprT2.W(), gprT2.W(), 0x7800); + armAsm->Bfxil(gprT2.W(), gprT1.W(), 0, 11); + armAsm->Sub(regS.W(), regS.W(), gprT2.W()); + } + mVU.regAlloc->clearNeeded(regS); + mVU.profiler.EmitOp(opISUBIU); + } + pass3 { mVUlog("ISUBIU vi%02d, vi%02d, %d", _Ft_, _Fs_, _Imm15_); } +} + +//------------------------------------------------------------------ +// MFIR/MFP/MOVE/MR32/MTIR +//------------------------------------------------------------------ + +mVUop(mVU_MFIR) +{ + pass1 + { + if (!_Ft_) + { + mVUlow.isNOP = true; + } + analyzeVIreg1(mVU, _Is_, mVUlow.VI_read[0]); + analyzeReg2 (mVU, _Ft_, mVUlow.VF_write, 1); + } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + if (_Is_ != 0) + { + // Load VI[Is] sign-extended to 32-bit, then broadcast + mVU.regAlloc->moveVIToGPR(gprT1, _Is_, true); // sign-extend + armAsm->Dup(Ft.V4S(), gprT1.W()); + } + else + { + armAsm->Movi(Ft.V16B(), 0); + } + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opMFIR); + } + pass3 { mVUlog("MFIR.%s vf%02d, vi%02d", _XYZW_String, _Ft_, _Fs_); } +} + +mVUop(mVU_MFP) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeMFP(mVU, _Ft_); + } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + getPreg(mVU, Ft); + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opMFP); + } + pass3 { mVUlog("MFP.%s vf%02d, P", _XYZW_String, _Ft_); } +} + +mVUop(mVU_MOVE) +{ + pass1 { mVUanalyzeMOVE(mVU, _Fs_, _Ft_); } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, _Ft_, _X_Y_Z_W); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opMOVE); + } + pass3 { mVUlog("MOVE.%s vf%02d, vf%02d", _XYZW_String, _Ft_, _Fs_); } +} + +mVUop(mVU_MR32) +{ + pass1 { mVUanalyzeMR32(mVU, _Fs_, _Ft_); } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_); + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + if (_XYZW_SS) + { + // Single component: pick the rotated lane + // MR32 rotates XYZW -> YZWX + // If writing X, read Y (lane 1); Y->Z (lane 2); Z->W (lane 3); W->X (lane 0) + mVUunpack_xyzw(Ft, Fs, (_X ? 1 : (_Y ? 2 : (_Z ? 3 : 0)))); + } + else + { + // EXT #4 rotates left by 1 lane: [X,Y,Z,W] -> [Y,Z,W,X] + armAsm->Ext(Ft.V16B(), Fs.V16B(), Fs.V16B(), 4); + } + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opMR32); + } + pass3 { mVUlog("MR32.%s vf%02d, vf%02d", _XYZW_String, _Ft_, _Fs_); } +} + +mVUop(mVU_MTIR) +{ + pass1 + { + if (!_It_) + mVUlow.isNOP = true; + + analyzeReg5(mVU, _Fs_, _Fsf_, mVUlow.VF_read[0]); + analyzeVIreg2(mVU, _It_, mVUlow.VI_write, 1); + } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armAsm->Umov(regT.W(), Fs.V4S(), 0); + mVU.regAlloc->clearNeeded(regT); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opMTIR); + } + pass3 { mVUlog("MTIR vi%02d, vf%02d%s", _Ft_, _Fs_, _Fsf_String); } +} + +//------------------------------------------------------------------ +// ILW/ILWR +//------------------------------------------------------------------ + +mVUop(mVU_ILW) +{ + pass1 + { + if (!_It_) + mVUlow.isNOP = true; + + analyzeVIreg1(mVU, _Is_, mVUlow.VI_read[0]); + analyzeVIreg2(mVU, _It_, mVUlow.VI_write, 4); + } + pass2 + { + // Compute address: (VI[Is] + Imm11) wrapped, then byte offset + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } + } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at + // runtime via sbfx+bfxil. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + + // Add lane offset for the selected component + armAsm->Add(gprT1.W(), gprT1.W(), offsetSS); + + // Add VU memory base + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + // Load 16-bit value from memory + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armAsm->Ldrh(regT.W(), a64::MemOperand(gprT1q)); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opILW); + } + pass3 { mVUlog("ILW.%s vi%02d, vi%02d + %d", _XYZW_String, _Ft_, _Fs_, _Imm11_); } +} + +mVUop(mVU_ILWR) +{ + pass1 + { + if (!_It_) + mVUlow.isNOP = true; + + analyzeVIreg1(mVU, _Is_, mVUlow.VI_read[0]); + analyzeVIreg2(mVU, _It_, mVUlow.VI_write, 4); + } + pass2 + { + if (_Is_) + { + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + mVUaddrFix(mVU, gprT1); + } + else + { + armAsm->Mov(gprT1.W(), 0); + } + + armAsm->Add(gprT1.W(), gprT1.W(), offsetSS); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armAsm->Ldrh(regT.W(), a64::MemOperand(gprT1q)); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opILWR); + } + pass3 { mVUlog("ILWR.%s vi%02d, vi%02d", _XYZW_String, _Ft_, _Fs_); } +} + +//------------------------------------------------------------------ +// ISW/ISWR +//------------------------------------------------------------------ + +mVUop(mVU_ISW) +{ + pass1 + { + mVUlow.isMemWrite = true; + analyzeVIreg1(mVU, _Is_, mVUlow.VI_read[0]); + analyzeVIreg1(mVU, _It_, mVUlow.VI_read[1]); + } + pass2 + { + // Compute address + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } + } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + // Load VI[It] value (zero-extended to 32-bit) and store to selected lanes + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1, false, true); + if (_X) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 0)); + if (_Y) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 4)); + if (_Z) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 8)); + if (_W) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 12)); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opISW); + } + pass3 { mVUlog("ISW.%s vi%02d, vi%02d + %d", _XYZW_String, _Ft_, _Fs_, _Imm11_); } +} + +mVUop(mVU_ISWR) +{ + pass1 + { + mVUlow.isMemWrite = true; + analyzeVIreg1(mVU, _Is_, mVUlow.VI_read[0]); + analyzeVIreg1(mVU, _It_, mVUlow.VI_read[1]); + } + pass2 + { + if (_Is_) + { + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + mVUaddrFix(mVU, gprT1); + } + else + { + armAsm->Mov(gprT1.W(), 0); + } + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1, false, true); + if (_X) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 0)); + if (_Y) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 4)); + if (_Z) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 8)); + if (_W) armAsm->Str(regT.W(), a64::MemOperand(gprT1q, 12)); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opISWR); + } + pass3 { mVUlog("ISWR.%s vi%02d, vi%02d", _XYZW_String, _Ft_, _Fs_); } +} + +//------------------------------------------------------------------ +// LQ/LQD/LQI +//------------------------------------------------------------------ + +mVUop(mVU_LQ) +{ + pass1 { mVUanalyzeLQ(mVU, _Ft_, _Is_, false); } + pass2 + { + // Compute address: (VI[Is] + Imm11) wrapped + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } + } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + mVUloadMem(Ft, gprT1q, _X_Y_Z_W); + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opLQ); + } + pass3 { mVUlog("LQ.%s vf%02d, vi%02d + %d", _XYZW_String, _Ft_, _Fs_, _Imm11_); } +} + +mVUop(mVU_LQD) +{ + pass1 { mVUanalyzeLQ(mVU, _Ft_, _Is_, true); } + pass2 + { + if (_Is_ || isVU0) + { + // Pre-decrement VI[Is] + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Is_, mVUlow.backupVI); + armAsm->Sub(regS.W(), regS.W(), 1); + armAsm->Sxth(gprT1.W(), regS.W()); + mVU.regAlloc->clearNeeded(regS); + mVUaddrFix(mVU, gprT1); + } + else + { + // _Is_ == 0 and !isVU0: use fixed address (end of micro mem - 8) + armAsm->Mov(gprT1.W(), 0xffff & (mVU.microMemSize - 8)); + } + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + if (!mVUlow.noWriteVF) + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + mVUloadMem(Ft, gprT1q, _X_Y_Z_W); + mVU.regAlloc->clearNeeded(Ft); + } + mVU.profiler.EmitOp(opLQD); + } + pass3 { mVUlog("LQD.%s vf%02d, --vi%02d", _XYZW_String, _Ft_, _Is_); } +} + +mVUop(mVU_LQI) +{ + pass1 { mVUanalyzeLQ(mVU, _Ft_, _Is_, true); } + pass2 + { + if (_Is_) + { + // Post-increment: read current value, then increment + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, _Is_, mVUlow.backupVI); + armAsm->Sxth(gprT1.W(), regS.W()); + armAsm->Add(regS.W(), regS.W(), 1); + mVU.regAlloc->clearNeeded(regS); + mVUaddrFix(mVU, gprT1); + } + else + { + armAsm->Mov(gprT1.W(), 0); + } + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + if (!mVUlow.noWriteVF) + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + mVUloadMem(Ft, gprT1q, _X_Y_Z_W); + mVU.regAlloc->clearNeeded(Ft); + } + mVU.profiler.EmitOp(opLQI); + } + pass3 { mVUlog("LQI.%s vf%02d, vi%02d++", _XYZW_String, _Ft_, _Fs_); } +} + +//------------------------------------------------------------------ +// SQ/SQD/SQI +//------------------------------------------------------------------ + +mVUop(mVU_SQ) +{ + pass1 { mVUanalyzeSQ(mVU, _Fs_, _It_, false); } + pass2 + { + // Compute address from VI[It] + Imm11 + mVU.regAlloc->moveVIToGPR(gprT1, _It_); + if (!EmuConfig.Gamefixes.IbitHack) + { + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } + } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, -1, _X_Y_Z_W); + if (_X_Y_Z_W == 0xf) + { + armAsm->Str(Fs, a64::MemOperand(gprT1q)); + } + else + { + // Partial store: load existing, merge, store + armAsm->Ldr(RQSCRATCH, a64::MemOperand(gprT1q)); + mVUmergeRegs(RQSCRATCH, Fs, _X_Y_Z_W, false); + armAsm->Str(RQSCRATCH, a64::MemOperand(gprT1q)); + } + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opSQ); + } + pass3 { mVUlog("SQ.%s vf%02d, vi%02d + %d", _XYZW_String, _Fs_, _Ft_, _Imm11_); } +} + +mVUop(mVU_SQD) +{ + pass1 { mVUanalyzeSQ(mVU, _Fs_, _It_, true); } + pass2 + { + if (_It_ || isVU0) + { + // Pre-decrement VI[It] + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, _It_, mVUlow.backupVI); + armAsm->Sub(regT.W(), regT.W(), 1); + armAsm->Uxth(gprT1.W(), regT.W()); + mVU.regAlloc->clearNeeded(regT); + mVUaddrFix(mVU, gprT1); + } + else + { + armAsm->Mov(gprT1.W(), 0xffff & (mVU.microMemSize - 8)); + } + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, -1, _X_Y_Z_W); + if (_X_Y_Z_W == 0xf) + { + armAsm->Str(Fs, a64::MemOperand(gprT1q)); + } + else + { + armAsm->Ldr(RQSCRATCH, a64::MemOperand(gprT1q)); + mVUmergeRegs(RQSCRATCH, Fs, _X_Y_Z_W, false); + armAsm->Str(RQSCRATCH, a64::MemOperand(gprT1q)); + } + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opSQD); + } + pass3 { mVUlog("SQD.%s vf%02d, --vi%02d", _XYZW_String, _Fs_, _Ft_); } +} + +mVUop(mVU_SQI) +{ + pass1 { mVUanalyzeSQ(mVU, _Fs_, _It_, true); } + pass2 + { + if (_It_) + { + // Post-increment: read current, then increment + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, _It_, mVUlow.backupVI); + armAsm->Uxth(gprT1.W(), regT.W()); + armAsm->Add(regT.W(), regT.W(), 1); + mVU.regAlloc->clearNeeded(regT); + mVUaddrFix(mVU, gprT1); + } + else + { + armAsm->Mov(gprT1.W(), 0); + } + + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, -1, _X_Y_Z_W); + if (_X_Y_Z_W == 0xf) + { + armAsm->Str(Fs, a64::MemOperand(gprT1q)); + } + else + { + armAsm->Ldr(RQSCRATCH, a64::MemOperand(gprT1q)); + mVUmergeRegs(RQSCRATCH, Fs, _X_Y_Z_W, false); + armAsm->Str(RQSCRATCH, a64::MemOperand(gprT1q)); + } + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opSQI); + } + pass3 { mVUlog("SQI.%s vf%02d, vi%02d++", _XYZW_String, _Fs_, _Ft_); } +} + +//------------------------------------------------------------------ +// RINIT/RGET/RNEXT/RXOR +//------------------------------------------------------------------ + +mVUop(mVU_RINIT) +{ + pass1 { mVUanalyzeR1(mVU, _Fs_, _Fsf_); } + pass2 + { + if (_Fs_ || (_Fsf_ == 3)) + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + armAsm->Umov(gprT1.W(), Fs.V4S(), 0); + armAsm->And(gprT1.W(), gprT1.W(), 0x007fffff); + armAsm->Orr(gprT1.W(), gprT1.W(), 0x3f800000); + armStorePtr(gprT1, Rmem); + mVU.regAlloc->clearNeeded(Fs); + } + else + { + armAsm->Mov(gprT1.W(), 0x3f800000); + armStorePtr(gprT1, Rmem); + } + mVU.profiler.EmitOp(opRINIT); + } + pass3 { mVUlog("RINIT R, vf%02d%s", _Fs_, _Fsf_String); } +} + +static __fi void mVU_RGET_(mV, const a64::Register& Rreg) +{ + if (!mVUlow.noWriteVF) + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); + armAsm->Dup(Ft.V4S(), Rreg.W()); + mVU.regAlloc->clearNeeded(Ft); + } +} + +mVUop(mVU_RGET) +{ + pass1 { mVUanalyzeR2(mVU, _Ft_, true); } + pass2 + { + armLoadPtr(gprT1, Rmem); + mVU_RGET_(mVU, gprT1); + mVU.profiler.EmitOp(opRGET); + } + pass3 { mVUlog("RGET.%s vf%02d, R", _XYZW_String, _Ft_); } +} + +mVUop(mVU_RNEXT) +{ + pass1 { mVUanalyzeR2(mVU, _Ft_, false); } + pass2 + { + // LFSR step: new = (R << 1) with bit 0 = R[4] XOR R[22], then mantissa + // mask + IEEE single 1.0-exponent. AArch64 fuses bit4/bit22 extract via + // EOR-with-shift: bit 4 of (R EOR (R LSR 18)) is exactly R[4] XOR R[22]. + const a64::Register& temp3 = mVU.regAlloc->allocGPR(); + armLoadPtr(temp3, Rmem); + armAsm->Eor(gprT1.W(), temp3.W(), a64::Operand(temp3.W(), a64::LSR, 18)); + armAsm->Lsl(temp3.W(), temp3.W(), 1); + armAsm->Bfxil(temp3.W(), gprT1.W(), 4, 1); + armAsm->And(temp3.W(), temp3.W(), 0x007fffff); + armAsm->Orr(temp3.W(), temp3.W(), 0x3f800000); + armStorePtr(temp3, Rmem); + mVU_RGET_(mVU, temp3); + mVU.regAlloc->clearNeeded(temp3); + mVU.profiler.EmitOp(opRNEXT); + } + pass3 { mVUlog("RNEXT.%s vf%02d, R", _XYZW_String, _Ft_); } +} + +mVUop(mVU_RXOR) +{ + pass1 { mVUanalyzeR1(mVU, _Fs_, _Fsf_); } + pass2 + { + if (_Fs_ || (_Fsf_ == 3)) + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, (1 << (3 - _Fsf_))); + armAsm->Umov(gprT1.W(), Fs.V4S(), 0); + armAsm->And(gprT1.W(), gprT1.W(), 0x7fffff); + armLoadPtr(gprT2, Rmem); + armAsm->Eor(gprT2.W(), gprT2.W(), gprT1.W()); + armStorePtr(gprT2, Rmem); + mVU.regAlloc->clearNeeded(Fs); + } + mVU.profiler.EmitOp(opRXOR); + } + pass3 { mVUlog("RXOR R, vf%02d%s", _Fs_, _Fsf_String); } +} + +//------------------------------------------------------------------ +// WaitP/WaitQ +//------------------------------------------------------------------ + +mVUop(mVU_WAITP) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUstall = std::max(mVUstall, (u8)((mVUregs.p) ? (mVUregs.p - 1) : 0)); + } + pass2 { mVU.profiler.EmitOp(opWAITP); } + pass3 { mVUlog("WAITP"); } +} + +mVUop(mVU_WAITQ) +{ + pass1 { mVUstall = std::max(mVUstall, mVUregs.q); } + pass2 { mVU.profiler.EmitOp(opWAITQ); } + pass3 { mVUlog("WAITQ"); } +} + +//------------------------------------------------------------------ +// XTOP/XITOP +//------------------------------------------------------------------ + +mVUop(mVU_XTOP) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + + if (!_It_) + mVUlow.isNOP = true; + + analyzeVIreg2(mVU, _It_, mVUlow.VI_write, 1); + } + pass2 + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armMoveAddressToReg(a64::x8, &mVU.getVifRegs().top); + armAsm->Ldrh(regT.W(), a64::MemOperand(a64::x8)); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opXTOP); + } + pass3 { mVUlog("XTOP vi%02d", _Ft_); } +} + +mVUop(mVU_XITOP) +{ + pass1 + { + if (!_It_) + mVUlow.isNOP = true; + + analyzeVIreg2(mVU, _It_, mVUlow.VI_write, 1); + } + pass2 + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armMoveAddressToReg(a64::x8, &mVU.getVifRegs().itop); + armAsm->Ldrh(regT.W(), a64::MemOperand(a64::x8)); + armAsm->And(regT.W(), regT.W(), isVU1 ? 0x3ff : 0xff); + mVU.regAlloc->clearNeeded(regT); + mVU.profiler.EmitOp(opXITOP); + } + pass3 { mVUlog("XITOP vi%02d", _Ft_); } +} + +//------------------------------------------------------------------ +// XGkick +//------------------------------------------------------------------ + +mVUop(mVU_XGKICK) +{ + pass1 + { + if (isVU0) + { + mVUlow.isNOP = true; + return; + } + mVUanalyzeXGkick(mVU, _Is_, 1); + } + pass2 + { + if (CHECK_XGKICKHACK) + { + mVUlow.kickcycles = 99; + mVU_XGKICK_SYNC(mVU, true); + mVUlow.kickcycles = 0; + } + if (mVUinfo.doXGKICK) + { + mVU_XGKICK_DELAY(mVU); + mVUinfo.doXGKICK = false; + } + + const a64::Register& regS = mVU.regAlloc->allocGPR(_Is_, -1); + if (!CHECK_XGKICKHACK) + { + armStorePtr(regS, &mVU.VIxgkick); + } + else + { + // Gamefix XgKickHack — set up VU1 xgkick state registers so + // _vuXGKICKTransfermVU's loop can run cycle-by-cycle. + // Mirrors the x86 mVU_XGKICK XgKickHack path. + armMoveAddressToReg(a64::x8, &VU1.xgkickenable); + armAsm->Mov(a64::w9, 1); + armAsm->Str(a64::w9, a64::MemOperand(a64::x8)); + + armMoveAddressToReg(a64::x8, &VU1.xgkickendpacket); + armAsm->Str(a64::wzr, a64::MemOperand(a64::x8)); + armMoveAddressToReg(a64::x8, &VU1.xgkicksizeremaining); + armAsm->Str(a64::wzr, a64::MemOperand(a64::x8)); + armMoveAddressToReg(a64::x8, &VU1.xgkickcyclecount); + armAsm->Str(a64::wzr, a64::MemOperand(a64::x8)); + + // xgkicklastcycle = totalCycles - cycles + VU1.cycle + armMoveAddressToReg(a64::x8, &mVU.totalCycles); + armAsm->Ldr(gprT2.W(), a64::MemOperand(a64::x8)); + armMoveAddressToReg(a64::x8, &mVU.cycles); + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Sub(gprT2.W(), gprT2.W(), a64::w9); + armMoveAddressToReg(a64::x8, &VU1.cycle); + // VU1.cycle is u32 — narrow load, otherwise we splice 4 bytes of + // neighbouring VU1 state into the carry path of the 64-bit add. + armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + armAsm->Add(gprT2q, gprT2q, a64::x9); + armMoveAddressToReg(a64::x8, &VU1.xgkicklastcycle); + armAsm->Str(gprT2.W(), a64::MemOperand(a64::x8)); + + // xgkickaddr = (regS & 0x3FF) << 4 + armAsm->Mov(gprT1.W(), regS); + armAsm->And(gprT1.W(), gprT1.W(), 0x3FF); + armAsm->Lsl(gprT1.W(), gprT1.W(), 4); + armMoveAddressToReg(a64::x8, &VU1.xgkickaddr); + armAsm->Str(gprT1.W(), a64::MemOperand(a64::x8)); + } + mVU.regAlloc->clearNeeded(regS); + mVU.profiler.EmitOp(opXGKICK); + } + pass3 { mVUlog("XGKICK vi%02d", _Fs_); } +} + +//------------------------------------------------------------------ +// Branches/Jumps +//------------------------------------------------------------------ + +// Branch setup helper -- runs across all passes to set branch metadata. +// Mirrors x86/microVU_Lower.inl setBranchA. +void setBranchA(mP, int x, int _x_) +{ + bool isBranchDelaySlot = false; + + incPC(-2); + if (mVUlow.branch) + isBranchDelaySlot = true; + incPC(2); + + pass1 + { + if (_Imm11_ == 1 && !_x_ && !isBranchDelaySlot) + { + DevCon.WriteLn(Color_Green, "microVU%d: Branch Optimization", mVU.index); + mVUlow.isNOP = true; + return; + } + mVUbranch = x; + mVUlow.branch = x; + } + pass2 { if (_Imm11_ == 1 && !_x_ && !isBranchDelaySlot) { return; } mVUbranch = x; } + pass3 { mVUbranch = x; } + pass4 { if (_Imm11_ == 1 && !_x_ && !isBranchDelaySlot) { return; } mVUbranch = x; } +} + +mVUop(mVU_B) +{ + setBranchA(mX, 1, 0); + pass1 { mVUanalyzeNormBranch(mVU, 0, false); } + pass2 + { + if (mVUlow.badBranch) { armAsm->Mov(gprT1.W(), branchAddr(mVU)); armStorePtr(gprT1, &mVU.badBranch); } + if (mVUlow.evilBranch) { + armAsm->Mov(gprT1.W(), branchAddr(mVU)); + if (isEvilBlock) armStorePtr(gprT1, &mVU.evilevilBranch); + else armStorePtr(gprT1, &mVU.evilBranch); + } + mVU.profiler.EmitOp(opB); + } + pass3 { mVUlog("B [%04x]", branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_BAL) +{ + setBranchA(mX, 2, _It_); + pass1 { mVUanalyzeNormBranch(mVU, _It_, true); } + pass2 + { + if (!mVUlow.evilBranch) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armAsm->Mov(regT.W(), bSaveAddr); + mVU.regAlloc->clearNeeded(regT); + } + else + { + incPC(-2); + DevCon.Warning("Linking BAL from %s branch taken/not taken target! - If game broken report to PCSX2 Team", branchSTR[mVUlow.branch & 0xf]); + incPC(2); + + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + if (isEvilBlock) + armLoadPtr(regT, &mVU.evilBranch); + else + armLoadPtr(regT, &mVU.badBranch); + + armAsm->Add(regT.W(), regT.W(), 8); + armAsm->Lsr(regT.W(), regT.W(), 3); + mVU.regAlloc->clearNeeded(regT); + } + + if (mVUlow.badBranch) { armAsm->Mov(gprT1.W(), branchAddr(mVU)); armStorePtr(gprT1, &mVU.badBranch); } + if (mVUlow.evilBranch) { + armAsm->Mov(gprT1.W(), branchAddr(mVU)); + if (isEvilBlock) armStorePtr(gprT1, &mVU.evilevilBranch); + else armStorePtr(gprT1, &mVU.evilBranch); + } + mVU.profiler.EmitOp(opBAL); + } + pass3 { mVUlog("BAL vi%02d [%04x]", _Ft_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBEQ) +{ + setBranchA(mX, 3, 0); + pass1 { mVUanalyzeCondBranch2(mVU, _Is_, _It_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + + if (mVUlow.memReadIt) + { + armLoadPtr(gprT2, &mVU.VIbackup); + armAsm->Eor(gprT1.W(), gprT1.W(), gprT2.W()); + } + else + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_); + armAsm->Eor(gprT1.W(), gprT1.W(), regT.W()); + mVU.regAlloc->clearNeeded(regT); + } + + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBEQ); + } + pass3 { mVUlog("IBEQ vi%02d, vi%02d [%04x]", _Ft_, _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBGEZ) +{ + setBranchA(mX, 4, 0); + pass1 { mVUanalyzeCondBranch1(mVU, _Is_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_, true); // sign-extend for comparison + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBGEZ); + } + pass3 { mVUlog("IBGEZ vi%02d [%04x]", _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBGTZ) +{ + setBranchA(mX, 5, 0); + pass1 { mVUanalyzeCondBranch1(mVU, _Is_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_, true); + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBGTZ); + } + pass3 { mVUlog("IBGTZ vi%02d [%04x]", _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBLEZ) +{ + setBranchA(mX, 6, 0); + pass1 { mVUanalyzeCondBranch1(mVU, _Is_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_, true); + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBLEZ); + } + pass3 { mVUlog("IBLEZ vi%02d [%04x]", _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBLTZ) +{ + setBranchA(mX, 7, 0); + pass1 { mVUanalyzeCondBranch1(mVU, _Is_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_, true); + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBLTZ); + } + pass3 { mVUlog("IBLTZ vi%02d [%04x]", _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +mVUop(mVU_IBNE) +{ + setBranchA(mX, 8, 0); + pass1 { mVUanalyzeCondBranch2(mVU, _Is_, _It_); } + pass2 + { + if (mVUlow.memReadIs) + armLoadPtr(gprT1, &mVU.VIbackup); + else + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + + if (mVUlow.memReadIt) + { + armLoadPtr(gprT2, &mVU.VIbackup); + armAsm->Eor(gprT1.W(), gprT1.W(), gprT2.W()); + } + else + { + const a64::Register& regT = mVU.regAlloc->allocGPR(_It_); + armAsm->Eor(gprT1.W(), gprT1.W(), regT.W()); + mVU.regAlloc->clearNeeded(regT); + } + + if (!(isBadOrEvil)) + armStorePtr(gprT1, &mVU.branch); + mVU.profiler.EmitOp(opIBNE); + } + pass3 { mVUlog("IBNE vi%02d, vi%02d [%04x]", _Ft_, _Fs_, branchAddr(mVU), branchAddr(mVU)); } +} + +static void normJumpPass2(mV) +{ + if (!mVUlow.constJump.isValid || mVUlow.evilBranch) + { + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + armAsm->Lsl(gprT1.W(), gprT1.W(), 3); + armAsm->And(gprT1.W(), gprT1.W(), mVU.microMemSize - 8); + + if (!mVUlow.evilBranch) + { + armStorePtr(gprT1, &mVU.branch); + } + else + { + if (isEvilBlock) + armStorePtr(gprT1, &mVU.evilevilBranch); + else + armStorePtr(gprT1, &mVU.evilBranch); + } + + if (mVUlow.badBranch) + { + armStorePtr(gprT1, &mVU.badBranch); + } + } +} + +mVUop(mVU_JR) +{ + mVUbranch = 9; + pass1 { mVUanalyzeJump(mVU, _Is_, 0, false); } + pass2 + { + normJumpPass2(mVU); + mVU.profiler.EmitOp(opJR); + } + pass3 { mVUlog("JR [vi%02d]", _Fs_); } +} + +mVUop(mVU_JALR) +{ + mVUbranch = 10; + pass1 { mVUanalyzeJump(mVU, _Is_, _It_, 1); } + pass2 + { + normJumpPass2(mVU); + if (!mVUlow.evilBranch) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + armAsm->Mov(regT.W(), bSaveAddr); + mVU.regAlloc->clearNeeded(regT); + } + if (mVUlow.evilBranch) + { + const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); + if (isEvilBlock) + { + armLoadPtr(regT, &mVU.evilBranch); + armAsm->Add(regT.W(), regT.W(), 8); + armAsm->Lsr(regT.W(), regT.W(), 3); + } + else + { + incPC(-2); + DevCon.Warning("Linking JALR from %s branch taken/not taken target! - If game broken report to PCSX2 Team", branchSTR[mVUlow.branch & 0xf]); + incPC(2); + + armLoadPtr(regT, &mVU.badBranch); + armAsm->Add(regT.W(), regT.W(), 8); + armAsm->Lsr(regT.W(), regT.W(), 3); + } + mVU.regAlloc->clearNeeded(regT); + } + + mVU.profiler.EmitOp(opJALR); + } + pass3 { mVUlog("JALR vi%02d, [vi%02d]", _Ft_, _Fs_); } +} diff --git a/pcsx2/arm64/microVU_Misc-arm64.h b/pcsx2/arm64/microVU_Misc-arm64.h new file mode 100644 index 0000000000..464d0d6173 --- /dev/null +++ b/pcsx2/arm64/microVU_Misc-arm64.h @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "AsmHelpers.h" + +namespace a64 = vixl::aarch64; + +struct microVU; + +//------------------------------------------------------------------ +// Global Variables +//------------------------------------------------------------------ + +struct mVU_Globals +{ +#define __four(val) { val, val, val, val } + u32 absclip [4] = __four(0x7fffffff); + u32 signbit [4] = __four(0x80000000); + u32 minvals [4] = __four(0xff7fffff); + u32 maxvals [4] = __four(0x7f7fffff); + u32 exponent[4] = __four(0x7f800000); + u32 one [4] = __four(0x3f800000); + u32 Pi4 [4] = __four(0x3f490fdb); + u32 T1 [4] = __four(0x3f7ffff5); + u32 T5 [4] = __four(0xbeaaa61c); + u32 T2 [4] = __four(0x3e4c40a6); + u32 T3 [4] = __four(0xbe0e6c63); + u32 T4 [4] = __four(0x3dc577df); + u32 T6 [4] = __four(0xbd6501c4); + u32 T7 [4] = __four(0x3cb31652); + u32 T8 [4] = __four(0xbb84d7e7); + u32 S2 [4] = __four(0xbe2aaaa4); + u32 S3 [4] = __four(0x3c08873e); + u32 S4 [4] = __four(0xb94fb21f); + u32 S5 [4] = __four(0x362e9c14); + u32 E1 [4] = __four(0x3e7fffa8); + u32 E2 [4] = __four(0x3d0007f4); + u32 E3 [4] = __four(0x3b29d3ff); + u32 E4 [4] = __four(0x3933e553); + u32 E5 [4] = __four(0x36b63510); + u32 E6 [4] = __four(0x353961ac); + u32 I32MAXF [4] = __four(0x4effffff); + float FTOI_4 [4] = __four(16.0); + float FTOI_12 [4] = __four(4096.0); + float FTOI_15 [4] = __four(32768.0); + float ITOF_4 [4] = __four(0.0625f); + float ITOF_12 [4] = __four(0.000244140625); + float ITOF_15 [4] = __four(0.000030517578125); +#undef __four +}; + +alignas(32) static constexpr struct mVU_Globals mVUglob; + +static const uint _Ibit_ = 1 << 31; +static const uint _Ebit_ = 1 << 30; +static const uint _Mbit_ = 1 << 29; +static const uint _Dbit_ = 1 << 28; +static const uint _Tbit_ = 1 << 27; + +static const uint divI = 0x1040000; +static const uint divD = 0x2080000; + +static const char branchSTR[16][8] = { + "None", "B", "BAL", "IBEQ", + "IBGEZ", "IBGTZ", "IBLEZ", "IBLTZ", + "IBNE", "JR", "JALR", "N/A", + "N/A", "N/A", "N/A", "N/A" +}; + +//------------------------------------------------------------------ +// Opcode Decoding Macros (platform-independent) +//------------------------------------------------------------------ + +#define _Ft_ ((mVU.code >> 16) & 0x1F) +#define _Fs_ ((mVU.code >> 11) & 0x1F) +#define _Fd_ ((mVU.code >> 6) & 0x1F) + +#define _It_ ((mVU.code >> 16) & 0xF) +#define _Is_ ((mVU.code >> 11) & 0xF) +#define _Id_ ((mVU.code >> 6) & 0xF) + +#define _X ((mVU.code >> 24) & 0x1) +#define _Y ((mVU.code >> 23) & 0x1) +#define _Z ((mVU.code >> 22) & 0x1) +#define _W ((mVU.code >> 21) & 0x1) + +#define _cX ((cpuRegs.code >> 24) & 0x1) +#define _cY ((cpuRegs.code >> 23) & 0x1) +#define _cZ ((cpuRegs.code >> 22) & 0x1) +#define _cW ((cpuRegs.code >> 21) & 0x1) + +#define _X_Y_Z_W (((mVU.code >> 21) & 0xF)) +#define _cX_Y_Z_W (((cpuRegs.code >> 21) & 0xF)) +#define _cXYZW_SS (_cX + _cY + _cZ + _cW == 1) +#define _cXYZW_SS2 (_cXYZW_SS && (_cX_Y_Z_W != 8)) + +#define _XYZW_SS (_X + _Y + _Z + _W == 1) +#define _XYZW_SS2 (_XYZW_SS && (_X_Y_Z_W != 8)) +#define _XYZW_PS (_X_Y_Z_W == 0xf) +#define _XYZWss(x) ((x == 8) || (x == 4) || (x == 2) || (x == 1)) + +#define _bc_ (mVU.code & 0x3) +#define _bc_x ((mVU.code & 0x3) == 0) +#define _bc_y ((mVU.code & 0x3) == 1) +#define _bc_z ((mVU.code & 0x3) == 2) +#define _bc_w ((mVU.code & 0x3) == 3) + +#define _Fsf_ ((mVU.code >> 21) & 0x03) +#define _Ftf_ ((mVU.code >> 23) & 0x03) + +#define _Imm5_ ((s16) (((mVU.code & 0x400) ? 0xfff0 : 0) | ((mVU.code >> 6) & 0xf))) +#define _Imm11_ ((s32) ((mVU.code & 0x400) ? (0xfffffc00 | (mVU.code & 0x3ff)) : (mVU.code & 0x3ff))) +#define _Imm12_ ((u32)((((mVU.code >> 21) & 0x1) << 11) | (mVU.code & 0x7ff))) +#define _Imm15_ ((u32) (((mVU.code >> 10) & 0x7800) | (mVU.code & 0x7ff))) +#define _Imm24_ ((u32) (mVU.code & 0xffffff)) + +#define isCOP2 (mVU.cop2 != 0) +#define isVU1 (mVU.index != 0) +#define isVU0 (mVU.index == 0) +#define getIndex (isVU1 ? 1 : 0) +#define getVUmem(x) (((isVU1) ? (x & 0x3ff) : ((x >= 0x400) ? (x & 0x43f) : (x & 0xff))) * 16) +#define offsetSS ((_X) ? (0) : ((_Y) ? (4) : ((_Z) ? 8 : 12))) +#define offsetReg ((_X) ? (0) : ((_Y) ? (1) : ((_Z) ? 2 : 3))) + +//------------------------------------------------------------------ +// ARM64 Register Definitions +//------------------------------------------------------------------ + +// NEON scratch registers (matching existing AsmHelpers.h: Q29-Q31) +// qmmT1-qmmT7: allocatable VF cache / temp registers +#define qmmT1 a64::q0 +#define qmmT2 a64::q1 +#define qmmT3 a64::q2 +#define qmmT4 a64::q3 +#define qmmT5 a64::q4 +#define qmmT6 a64::q5 +#define qmmT7 a64::q6 + +// P/Q packed register (replaces x86 xmmPQ=xmm15) +#define qmmPQ a64::q28 + +// GPR scratch registers +#define gprT1 a64::w9 +#define gprT2 a64::w10 +#define gprT1q a64::x9 +#define gprT2q a64::x10 +#define gprT3 a64::w11 +#define gprT3q a64::x11 + +// Status flag instance registers (callee-saved) +#define gprF0 a64::w20 +#define gprF1 a64::w21 +#define gprF2 a64::w22 +#define gprF3 a64::w23 + +// VU state base pointer (callee-saved). Pinned at mVUdispatcherAB entry to +// `&mVU.regs()` (= &vuRegs[mVU.index], a static address per-VU). Stays live +// across all blocks of a single dispatch, including across armEmitCall to C +// helpers (callee-saved per AAPCS). Lets emit reach any VURegs field with a +// 1-insn `Ldr/Str dst, [gprVUState, #imm12]` instead of the 3-insn +// movz/movk/movk + Ldr that armLoadPtr emits for absolute addresses (data +// globals at 0xaaab_xxxx_xxxx live ~64TB from the JIT cache at 0xfffe_xxxx, +// so adrp+add never reaches and we always fall through to the 3-insn path). +#define gprVUState a64::x19 + +// MemOperand anchored at gprVUState — `mVU.regs()` field accessor. +// `off` is the byte offset within `VURegs`. Caller is responsible for +// matching the encoding's imm12 reach (Q=64K, X=32K, W=16K, H=8K). +__fi static a64::MemOperand mVUstateMem(int64_t off) +{ + return a64::MemOperand(gprVUState, off); +} + +// mVU shadow-flag base pointer (callee-saved). Pinned at mVUdispatcherAB +// entry to `&mVU.macFlag[0]`. The `microVU` struct lays out +// `statFlag[4]/macFlag[4]/clipFlag[4]/neonCTemp[4]/neonBackup[32][4]` as +// consecutive 16-byte-aligned arrays, so a single pin reaches all of them: +// +// &mVU.statFlag[0] = pin - 16 +// &mVU.macFlag[0] = pin +// &mVU.clipFlag[0] = pin + 16 +// &mVU.neonCTemp[0] = pin + 32 +// &mVU.neonBackup[N][0] = pin + 48 + N*16 (N=0..31, max +544) +// +// Every flag-touching FMAC reaches these globals with a single +// `Ldr/Str reg, [gprMVUFlag, #imm]` rather than the 3-insn +// movz/movk/movk + ldr sequence. The address is constant per-VU +// (microVU0/microVU1 are static globals) so the pin is set once at +// dispatch entry and survives all C-call paths (callee-saved per AAPCS). +#define gprMVUFlag a64::x24 + +// MemOperand anchored at gprMVUFlag. `off` is the byte offset from +// &mVU.macFlag[0] (signed; see layout above). Caller is responsible for +// matching the encoding's imm12 reach. +__fi static a64::MemOperand mVUmacFlagMem(int instance) +{ + return a64::MemOperand(gprMVUFlag, instance * 4); +} +__fi static a64::MemOperand mVUclipFlagMem(int instance) +{ + return a64::MemOperand(gprMVUFlag, 16 + instance * 4); +} +__fi static a64::MemOperand mVUneonBackupMem(int neonReg) +{ + return a64::MemOperand(gprMVUFlag, 48 + neonReg * 16); +} + +// mVUglob constants base pointer (callee-saved). Pinned at mVUdispatcherAB +// entry to `&mVUglob`. The mVU_Globals struct (~512 bytes of compile-time +// float constants — clamp limits, FTOI/ITOF scale factors, Taylor-series +// coefficients) is laid out as 16-byte-aligned u32[4]/float[4] arrays; +// every entry reachable via [gprMVUglob, #imm12] rather than the +// 3-insn movz/movk/movk + ldr sequence per constant load. +#define gprMVUglob a64::x25 + +// Return a MemOperand for a field within mVUglob, computed at JIT-emit +// time from the absolute pointer. Lets call sites keep writing +// `&mVUglob.X` (which the compiler folds to a constant offset) while the +// emit goes through the pinned base. +__fi static a64::MemOperand mVUglobMem(const void* addr) +{ + const u8* base = reinterpret_cast(&mVUglob); + const u8* p = reinterpret_cast(addr); + return a64::MemOperand(gprMVUglob, p - base); +} + +//------------------------------------------------------------------ +// Function/Template Macros (platform-independent) +//------------------------------------------------------------------ + +#define mP microVU& mVU, int recPass +#define mV microVU& mVU +#define mF int recPass +#define mX mVU, recPass + +typedef void Fntype_mVUrecInst(microVU& mVU, int recPass); +typedef Fntype_mVUrecInst* Fnptr_mVUrecInst; + +#define mVUx (vuIndex ? microVU1 : microVU0) +#define mVUop(opName) static void opName(mP) +#define _mVUt template + +// Define Passes +#define pass1 if (recPass == 0) // Analyze +#define pass2 if (recPass == 1) // Recompile +#define pass3 if (recPass == 2) // Logging +#define pass4 if (recPass == 3) // Flag stuff + +// Upper Opcode Cases +#define opCase1 if (opCase == 1) // Normal Opcodes +#define opCase2 if (opCase == 2) // BC Opcodes +#define opCase3 if (opCase == 3) // I Opcodes +#define opCase4 if (opCase == 4) // Q Opcodes + +//------------------------------------------------------------------ +// IR/Pipeline Macros (platform-independent) +//------------------------------------------------------------------ + +#define mVUcurProg mVU.prog.cur[0] +#define mVUblocks mVU.prog.cur->block +#define mVUir mVU.prog.IRinfo +#define mVUbranch mVU.prog.IRinfo.branch +#define mVUcycles mVU.prog.IRinfo.cycles +#define mVUcount mVU.prog.IRinfo.count +#define mVUpBlock mVU.prog.IRinfo.pBlock +#define mVUblock mVU.prog.IRinfo.block +#define mVUregs mVU.prog.IRinfo.block.pState +#define mVUregsTemp mVU.prog.IRinfo.regsTemp +#define iPC mVU.prog.IRinfo.curPC +#define mVUsFlagHack mVU.prog.IRinfo.sFlagHack +#define mVUconstReg mVU.prog.IRinfo.constReg +#define mVUstartPC mVU.prog.IRinfo.startPC +#define mVUinfo mVU.prog.IRinfo.info[iPC / 2] +#define mVUstall mVUinfo.stall +#define mVUup mVUinfo.uOp +#define mVUlow mVUinfo.lOp +#define sFLAG mVUinfo.sFlag +#define mFLAG mVUinfo.mFlag +#define cFLAG mVUinfo.cFlag +#define mVUrange (mVUcurProg.ranges[0])[0] +#define isEvilBlock (mVUpBlock->pState.blockType == 2) +#define isBadOrEvil (mVUlow.badBranch || mVUlow.evilBranch) +#define isConditional (mVUlow.branch > 2 && mVUlow.branch < 9) +#define xPC ((iPC / 2) * 8) +#define curI ((u32*)mVU.regs().Micro)[iPC] +#define setCode() { mVU.code = curI; } +#define bSaveAddr (((xPC + 16) & (mVU.microMemSize-8)) / 8) +#define shufflePQ (((mVU.p) ? 0xb0 : 0xe0) | ((mVU.q) ? 0x01 : 0x04)) +#define Rmem &mVU.regs().VI[REG_R].UL +#define aWrap(x, m) ((x > m) ? 0 : x) +#define shuffleSS(x) ((x == 1) ? (0x27) : ((x == 2) ? (0xc6) : ((x == 4) ? (0xe1) : (0xe4)))) +#define clampE CHECK_VU_EXTRA_OVERFLOW(mVU.index) +#define varPrint(x) DevCon.WriteLn(#x " = %d", (int)x) +#define islowerOP ((iPC & 1) == 0) + +#define blockCreate(addr) \ + { \ + if (!mVUblocks[addr]) \ + mVUblocks[addr] = new microBlockManager(); \ + } + +#define incPC(x) { iPC = ((iPC + (x)) & mVU.progMemMask); mVU.code = curI; } +#define incPC2(x) { iPC = ((iPC + (x)) & mVU.progMemMask); } + +// Flag Info +#define __Status (mVUregs.needExactMatch & 1) +#define __Mac (mVUregs.needExactMatch & 2) +#define __Clip (mVUregs.needExactMatch & 4) + +// Pass 3 Helper Macros (logging) +#define _Fsf_String ((_Fsf_ == 3) ? "w" : ((_Fsf_ == 2) ? "z" : ((_Fsf_ == 1) ? "y" : "x"))) +#define _Ftf_String ((_Ftf_ == 3) ? "w" : ((_Ftf_ == 2) ? "z" : ((_Ftf_ == 1) ? "y" : "x"))) +#define xyzwStr(x, s) (_X_Y_Z_W == x) ? s: +#define _XYZW_String (xyzwStr(1, "w") (xyzwStr(2, "z") (xyzwStr(3, "zw") (xyzwStr(4, "y") (xyzwStr(5, "yw") (xyzwStr(6, "yz") (xyzwStr(7, "yzw") (xyzwStr(8, "x") (xyzwStr(9, "xw") (xyzwStr(10, "xz") (xyzwStr(11, "xzw") (xyzwStr(12, "xy") (xyzwStr(13, "xyw") (xyzwStr(14, "xyz") "xyzw")))))))))))))) +#define _BC_String (_bc_x ? "x" : (_bc_y ? "y" : (_bc_z ? "z" : "w"))) +#define mVUlogFtFs() { mVUlog(".%s vf%02d, vf%02d", _XYZW_String, _Ft_, _Fs_); } +#define mVUlogFd() { mVUlog(".%s vf%02d, vf%02d", _XYZW_String, _Fd_, _Fs_); } +#define mVUlogACC() { mVUlog(".%s ACC, vf%02d", _XYZW_String, _Fs_); } +#define mVUlogFt() { mVUlog(", vf%02d", _Ft_); } +#define mVUlogBC() { mVUlog(", vf%02d%s", _Ft_, _BC_String); } +#define mVUlogI() { mVUlog(", I"); } +#define mVUlogQ() { mVUlog(", Q"); } +#define mVUlogCLIP() { mVUlog("w.xyz vf%02d, vf%02dw", _Fs_, _Ft_); } + +#ifdef mVUlogProg + #define mVUlog ((isVU1) ? __mVULog<1> : __mVULog<0>) + #define mVUdumpProg __mVUdumpProgram +#else + #define mVUlog(...) if (0) {} + #define mVUdumpProg(...) if (0) {} +#endif + +//------------------------------------------------------------------ +// Optimization / Debug Options (same as x86) +//------------------------------------------------------------------ + +static constexpr bool doRegAlloc = true; +static constexpr bool noFlagOpts = false; +static constexpr bool doSFlagInsts = true; +static constexpr bool doMFlagInsts = true; +static constexpr bool doCFlagInsts = true; +static constexpr bool doBranchInDelaySlot = true; +static constexpr bool doConstProp = false; +static constexpr bool doJumpCaching = true; +static constexpr bool doJumpAsSameProgram = false; +static constexpr bool doDBitHandling = false; +static constexpr bool doWholeProgCompare = false; + +// Status Flag Speed Hack +#define CHECK_VU_FLAGHACK (EmuConfig.Speedhacks.vuFlagHack) diff --git a/pcsx2/arm64/microVU_Misc-arm64.inl b/pcsx2/arm64/microVU_Misc-arm64.inl new file mode 100644 index 0000000000..7a8cb3c87c --- /dev/null +++ b/pcsx2/arm64/microVU_Misc-arm64.inl @@ -0,0 +1,356 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// Micro VU - NEON Reg Loading/Saving/Shuffling/Unpacking/Merging +//------------------------------------------------------------------ + +// Broadcast a single component to all 4 lanes +// xyzw: 0=X, 1=Y, 2=Z, 3=W +void mVUunpack_xyzw(const a64::VRegister& dstreg, const a64::VRegister& srcreg, int xyzw) +{ + // DUP Vd.4S, Vn.S[lane] — broadcast lane to all 4 slots + armAsm->Dup(dstreg.V4S(), srcreg.V4S(), xyzw); +} + +// Load VF register components from memory. +// xyzw bitmask: 8=X, 4=Y, 2=Z, 1=W. Single-component loads zero-extend. +// For full load (xyzw=0xF or non-single), loads all 128 bits. +void mVUloadReg(const a64::VRegister& reg, const void* ptr, int xyzw) +{ + switch (xyzw) + { + case 8: // X only — load 32-bit scalar, upper lanes zeroed + armLoadPtr(a64::VRegister(reg.GetCode(), 32), ptr); + break; + case 4: // Y only + armLoadPtr(a64::VRegister(reg.GetCode(), 32), (const u8*)ptr + 4); + break; + case 2: // Z only + armLoadPtr(a64::VRegister(reg.GetCode(), 32), (const u8*)ptr + 8); + break; + case 1: // W only + armLoadPtr(a64::VRegister(reg.GetCode(), 32), (const u8*)ptr + 12); + break; + default: // Full 128-bit load + armLoadPtr(reg, ptr); + break; + } +} + +// base+offset overload — for call sites where the address is reachable as +// `[base, #off]` (e.g. base=gprVUState, off within VURegs). Emits a single +// Ldr per partial lane, no scratch+materialize round-trip. The full-load +// case uses imm12 scaled by 16 (Q-reg) so reaches up to 64KB from base. +__fi void mVUloadReg(const a64::VRegister& reg, const a64::Register& base, int64_t off, int xyzw) +{ + switch (xyzw) + { + case 8: armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), a64::MemOperand(base, off)); break; + case 4: armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), a64::MemOperand(base, off + 4)); break; + case 2: armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), a64::MemOperand(base, off + 8)); break; + case 1: armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), a64::MemOperand(base, off + 12)); break; + default: armAsm->Ldr(reg, a64::MemOperand(base, off)); break; + } +} + +// Load from a runtime address (held in a 64-bit base register) into reg, +// following the regalloc lane convention enforced by writeBackNeon, which +// calls mVUsaveReg(modXYZW=true): +// - Single-lane partial (X/Y/Z/W): 32-bit load that zeroes upper lanes +// places the value in lane 0. mVUsaveReg's Y/Z/W cases with modXYZW=true +// write lane 0 back to the natural byte slot. (X is lane 0 either way.) +// - Multi-lane partial / full mask: full 16-byte load into natural lanes. +// Multi-lane mVUsaveReg cases store from natural lanes (modXYZW ignored). +// +// LQ/LQD/LQI must use this — using `Ldr Q, [base]` plus +// mVUmergeRegs(natural-lane) for partial writes leaves single-lane Y/Z/W +// values in their natural lane while the writeback reads lane 0, silently +// clobbering VU memory with whatever happened to be in the recycled +// Q register's lane 0. +__fi void mVUloadMem(const a64::VRegister& reg, const a64::Register& base, int xyzw) +{ + int offset; + switch (xyzw) + { + case 0x8: offset = 0; break; // X — naturally lane 0 + case 0x4: offset = 4; break; // Y — into lane 0 + case 0x2: offset = 8; break; // Z — into lane 0 + case 0x1: offset = 12; break; // W — into lane 0 + default: + armAsm->Ldr(reg, a64::MemOperand(base)); + return; + } + armAsm->Ldr(a64::VRegister(reg.GetCode(), 32), a64::MemOperand(base, offset)); +} + +// Store VF register components to memory with xyzw mask. +// Handles all 15 non-zero xyzw combinations. +// +// modXYZW semantics (match x86): +// - modXYZW == true : value is in lane 0 of `reg` (from SS-path shuffle +// in allocReg's clone-write). Write lane 0 to the +// target slot. writeBackNeon passes this path. +// - modXYZW == false : value is at the natural lane of `reg`. Write the +// natural lane to the target slot. Used by VIF path +// which loads data directly in natural positions. +// +// IMPORTANT: ARM64 ST1-single-lane has NO immediate-offset addressing form — +// VIXL's `St1(V, lane, MemOperand(base, imm))` silently drops `imm` in +// release builds (and asserts in debug). Partial stores here therefore +// advance `x8` with Add before each ST1 at a non-zero slot. `x8` is a +// scratch register (not RSCRATCHADDR), so clobbering it is fine. +// Internal helper — assumes x8 already holds the destination address. +// Both mVUsaveReg overloads route through this to share the case body. +static void mVUsaveRegAtX8(const a64::VRegister& reg, int xyzw, bool modXYZW); + +void mVUsaveReg(const a64::VRegister& reg, const void* ptr, int xyzw, bool modXYZW) +{ + armMoveAddressToReg(a64::x8, ptr); + mVUsaveRegAtX8(reg, xyzw, modXYZW); +} + +// base+offset overload — for call sites where the address is reachable as +// `[base, #off]` (e.g. base=gprVUState, off within VURegs). The full-store +// (xyzw=0xF) case bypasses x8 entirely and uses [base, #off] directly, +// saving an extra Add. Partial stores still need x8 because ST1-single-lane +// has no immediate-offset form on aarch64 — see notes on the absolute-ptr +// overload above. +void mVUsaveReg(const a64::VRegister& reg, const a64::Register& base, int64_t off, int xyzw, bool modXYZW) +{ + if (xyzw == 0xF) + { + armAsm->Str(reg, a64::MemOperand(base, off)); + return; + } + if (off != 0) + armAsm->Add(a64::x8, base, off); + else + armAsm->Mov(a64::x8, base); + mVUsaveRegAtX8(reg, xyzw, modXYZW); +} + +static void mVUsaveRegAtX8(const a64::VRegister& reg, int xyzw, bool modXYZW) +{ + switch (xyzw) + { + case 0xF: // XYZW — full store + armAsm->Str(reg, a64::MemOperand(a64::x8)); + break; + case 0x8: // X — always lane 0 (X is lane 0 natively) + armAsm->St1(reg.V4S(), 0, a64::MemOperand(a64::x8)); + break; + case 0x4: // Y + armAsm->Add(a64::x8, a64::x8, 4); + armAsm->St1(reg.V4S(), modXYZW ? 0 : 1, a64::MemOperand(a64::x8)); + break; + case 0x2: // Z + armAsm->Add(a64::x8, a64::x8, 8); + armAsm->St1(reg.V4S(), modXYZW ? 0 : 2, a64::MemOperand(a64::x8)); + break; + case 0x1: // W + armAsm->Add(a64::x8, a64::x8, 12); + armAsm->St1(reg.V4S(), modXYZW ? 0 : 3, a64::MemOperand(a64::x8)); + break; + case 0xC: // XY + armAsm->Str(a64::DRegister(reg.GetCode()), a64::MemOperand(a64::x8)); + break; + case 0x3: // ZW + armAsm->Add(a64::x8, a64::x8, 8); + armAsm->St1(reg.V2D(), 1, a64::MemOperand(a64::x8)); + break; + case 0xE: // XYZ — D-reg store (XY) post-indexes 8 bytes; Z lane store at [x8]. + armAsm->Str(a64::DRegister(reg.GetCode()), a64::MemOperand(a64::x8, 8, a64::PostIndex)); + armAsm->St1(reg.V4S(), 2, a64::MemOperand(a64::x8)); + break; + case 0x7: // YZW — Y lane post-indexes the V4S element size (4); ZW V2D at [x8]. + armAsm->Add(a64::x8, a64::x8, 4); + armAsm->St1(reg.V4S(), 1, a64::MemOperand(a64::x8, 4, a64::PostIndex)); + armAsm->St1(reg.V2D(), 1, a64::MemOperand(a64::x8)); + break; + case 0xD: // XYW — D-reg store (XY) post-indexes 12 bytes; W lane store at [x8]. + armAsm->Str(a64::DRegister(reg.GetCode()), a64::MemOperand(a64::x8, 12, a64::PostIndex)); + armAsm->St1(reg.V4S(), 3, a64::MemOperand(a64::x8)); + break; + case 0xB: // XZW + armAsm->St1(reg.V4S(), 0, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, a64::x8, 8); + armAsm->St1(reg.V2D(), 1, a64::MemOperand(a64::x8)); + break; + case 0xA: // XZ + armAsm->St1(reg.V4S(), 0, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, a64::x8, 8); + armAsm->St1(reg.V4S(), 2, a64::MemOperand(a64::x8)); + break; + case 0x9: // XW + armAsm->St1(reg.V4S(), 0, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, a64::x8, 12); + armAsm->St1(reg.V4S(), 3, a64::MemOperand(a64::x8)); + break; + case 0x6: // YZ — Y lane post-indexes the V4S element size (4); Z lane at [x8]. + armAsm->Add(a64::x8, a64::x8, 4); + armAsm->St1(reg.V4S(), 1, a64::MemOperand(a64::x8, 4, a64::PostIndex)); + armAsm->St1(reg.V4S(), 2, a64::MemOperand(a64::x8)); + break; + case 0x5: // YW + armAsm->Add(a64::x8, a64::x8, 4); + armAsm->St1(reg.V4S(), 1, a64::MemOperand(a64::x8)); + armAsm->Add(a64::x8, a64::x8, 8); // x8 now points at +12 (W) + armAsm->St1(reg.V4S(), 3, a64::MemOperand(a64::x8)); + break; + default: + break; + } +} + +// Merge selected components from src into dest. +// xyzw bitmask: 8=X, 4=Y, 2=Z, 1=W. +// On ARM64, use INS (single lane) or full MOV, or BIT/BIF for multi-lane blends. +void mVUmergeRegs(const a64::VRegister& dest, const a64::VRegister& src, int xyzw, bool modXYZW) +{ + xyzw &= 0xf; + if (dest.IsNone() || src.IsNone() || (dest.GetCode() == src.GetCode()) || xyzw == 0) + return; + + if (xyzw == 0xF) + { + // Full copy + if (dest.GetCode() != src.GetCode()) + armAsm->Mov(dest.V16B(), src.V16B()); + return; + } + + if (modXYZW) + { + // Source has the value in lane 0 — insert into target lane + switch (xyzw) + { + case 0x8: armAsm->Ins(dest.V4S(), 0, src.V4S(), 0); return; // X + case 0x4: armAsm->Ins(dest.V4S(), 1, src.V4S(), 0); return; // Y + case 0x2: armAsm->Ins(dest.V4S(), 2, src.V4S(), 0); return; // Z + case 0x1: armAsm->Ins(dest.V4S(), 3, src.V4S(), 0); return; // W + default: break; // Fall through to general case + } + } + + // Single-lane cases (non-modXYZW — value already in correct lane) + switch (xyzw) + { + case 0x8: // X only + armAsm->Ins(dest.V4S(), 0, src.V4S(), 0); + return; + case 0x4: // Y only + armAsm->Ins(dest.V4S(), 1, src.V4S(), 1); + return; + case 0x2: // Z only + armAsm->Ins(dest.V4S(), 2, src.V4S(), 2); + return; + case 0x1: // W only + armAsm->Ins(dest.V4S(), 3, src.V4S(), 3); + return; + default: + break; + } + + // Multi-lane blend: use individual lane inserts. + // Each INS is 1 cycle on most ARM64 cores, so 2-3 inserts is fine. + if (xyzw & 0x8) armAsm->Ins(dest.V4S(), 0, src.V4S(), 0); + if (xyzw & 0x4) armAsm->Ins(dest.V4S(), 1, src.V4S(), 1); + if (xyzw & 0x2) armAsm->Ins(dest.V4S(), 2, src.V4S(), 2); + if (xyzw & 0x1) armAsm->Ins(dest.V4S(), 3, src.V4S(), 3); +} + +//------------------------------------------------------------------ +// Micro VU - Backup/Restore Regs for C Calls +//------------------------------------------------------------------ + +// Flush all register allocations and save PQ before a C function call. +// The dispatcher already saved callee-saved regs, so once the regalloc is +// flushed the only state that must survive the call is PQ. toMemory/onlyNeeded +// are accepted for signature parity with x86 mVUbackupRegs and ignored here. +__fi void mVUbackupRegs(microVU& mVU, bool toMemory = false, bool onlyNeeded = false) +{ + mVU.regAlloc->flushAll(); + armAsm->Str(qmmPQ, mVUneonBackupMem(qmmPQ.GetCode())); +} + +__fi void mVUrestoreRegs(microVU& mVU, bool fromMemory = false, bool onlyNeeded = false) +{ + armAsm->Ldr(qmmPQ, mVUneonBackupMem(qmmPQ.GetCode())); +} + +//------------------------------------------------------------------ +// Micro VU - VU Memory Address Translation +//------------------------------------------------------------------ + +static inline u32 branchAddr(const mV) +{ + pxAssumeMsg(islowerOP, "MicroVU: Expected Lower OP code for valid branch addr."); + return ((((iPC + 2) + (_Imm11_ * 2)) & mVU.progMemMask) * 4); +} + +static void mVUwaitMTVU() +{ + if (IsDevBuild) + DevCon.WriteLn("microVU0: Waiting on VU1 thread to access VU1 regs!"); + vu1Thread.WaitVU(); +} + +static void mVUTBit() +{ + u32 old = vu1Thread.mtvuInterrupts.fetch_or(VU_Thread::InterruptFlagVUTBit, std::memory_order_release); + if (old & VU_Thread::InterruptFlagVUTBit) + DevCon.Warning("Old TBit not registered"); +} + +static void mVUEBit() +{ + vu1Thread.mtvuInterrupts.fetch_or(VU_Thread::InterruptFlagVUEBit, std::memory_order_release); +} + +// Transform VI register address to valid VU0/VU1 memory pointer offset. +// gprReg holds the VI value (address in VU quadwords). +// On exit, gprReg holds byte offset into VU memory. +__fi void mVUaddrFix(mV, const a64::Register& gprReg) +{ + if (isVU1) + { + // VU1: mask to 0x3FF quadwords, shift left 4 (x16 bytes) + armAsm->And(gprReg.W(), gprReg.W(), 0x3ff); + armAsm->Lsl(gprReg.W(), gprReg.W(), 4); + } + else + { + // VU0: if addr & 0x400, accessing VU1 register space + a64::Label notVU1Access, done; + + armAsm->Tst(gprReg.W(), 0x400); + armAsm->B(¬VU1Access, a64::eq); + + // Accessing VU1 regs from VU0 + if (THREAD_VU1) + { + // Need to wait for VU1 thread + armEmitCall((void*)mVU.waitMTVU); + } + armAsm->And(gprReg.W(), gprReg.W(), 0x3f); + // Add offset in u128 units (x86 mVUaddrFix uses pointer arithmetic so + // the delta is pre-scaled by sizeof(u128) = 16). The trailing Lsl by + // 4 below converts both the VI index and the offset to bytes in one + // shot. Casting to s64 first would produce a raw byte delta whose + // low bits get truncated by the (correctly 64-bit) shift. + const s64 vu1Offset = (u128*)VU1.VF - (u128*)VU0.Mem; + armAsm->Add(gprReg.X(), gprReg.X(), vu1Offset); + armAsm->B(&done); + + armAsm->Bind(¬VU1Access); + armAsm->And(gprReg.W(), gprReg.W(), 0xff); + + armAsm->Bind(&done); + // 64-bit Lsl: the VU0->VU1 path stashed a relative offset in the + // upper bits via Add(.X()) above; a 32-bit shift would zero them. + armAsm->Lsl(gprReg.X(), gprReg.X(), 4); + } +} diff --git a/pcsx2/arm64/microVU_Upper-arm64.inl b/pcsx2/arm64/microVU_Upper-arm64.inl new file mode 100644 index 0000000000..2147351342 --- /dev/null +++ b/pcsx2/arm64/microVU_Upper-arm64.inl @@ -0,0 +1,975 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +//------------------------------------------------------------------ +// Micro VU - ARM64 NEON Upper Instructions (FMAC pipeline) +//------------------------------------------------------------------ + +//------------------------------------------------------------------ +// NEON Arithmetic Functions +//------------------------------------------------------------------ + +static void NEON_ADDPS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W); + mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W); + armAsm->Fadd(to.V4S(), to.V4S(), from.V4S()); + mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W); +} + +static void NEON_SUBPS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W); + mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W); + armAsm->Fsub(to.V4S(), to.V4S(), from.V4S()); + mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W); +} + +static void NEON_MULPS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, _X_Y_Z_W); + mVUclamp3(mVU, from, RQSCRATCH3, _X_Y_Z_W); + armAsm->Fmul(to.V4S(), to.V4S(), from.V4S()); + mVUclamp4(mVU, to, RQSCRATCH3, _X_Y_Z_W); +} + +static void NEON_ADDSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, 0x8); + mVUclamp3(mVU, from, RQSCRATCH3, 0x8); + armAsm->Fadd(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode())); + mVUclamp4(mVU, to, RQSCRATCH3, 0x8); +} + +static void NEON_SUBSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, 0x8); + mVUclamp3(mVU, from, RQSCRATCH3, 0x8); + armAsm->Fsub(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode())); + mVUclamp4(mVU, to, RQSCRATCH3, 0x8); +} + +static void NEON_MULSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + mVUclamp3(mVU, to, RQSCRATCH3, 0x8); + mVUclamp3(mVU, from, RQSCRATCH3, 0x8); + armAsm->Fmul(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode())); + mVUclamp4(mVU, to, RQSCRATCH3, 0x8); +} + +// ADD2 variants — ADDi (opType 5). The PS form needs no special handling; the +// SS form implements the tri-ace VuAddSubHack when the gamefix is enabled. +static void NEON_ADD2PS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + NEON_ADDPS(mVU, to, from); +} + +// Port of x86 ADD_SS_TriAceHack (microVU_Misc.inl). Tri-ace games need ADDi to be +// bit-accurate: if the two operands' exponents differ by >= 25, the smaller one is +// flushed to a signed zero (sign bit kept, exponent+mantissa of lane 0 cleared — +// the x86 PAND against {0x80000000, ~0, ~0, ~0}) before the scalar add. Unclamped, +// matching x86. Without the gamefix this is a plain scalar add. +static void NEON_ADD2SS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + if (!CHECK_VUADDSUBHACK) + { + NEON_ADDSS(mVU, to, from); + return; + } + + // Exponent difference (from_exp - to_exp), bits 23..30 of each lane-0 word. + armAsm->Umov(gprT1.W(), to.V4S(), 0); + armAsm->Umov(gprT2.W(), from.V4S(), 0); + armAsm->Ubfx(gprT1.W(), gprT1.W(), 23, 8); + armAsm->Ubfx(gprT2.W(), gprT2.W(), 23, 8); + armAsm->Sub(gprT3.W(), gprT2.W(), gprT1.W()); + + a64::Label case_neg_big, case_end; + armAsm->Cmp(gprT3.W(), -25); + armAsm->B(&case_neg_big, a64::le); // from much smaller -> flush from + armAsm->Cmp(gprT3.W(), 25); + armAsm->B(&case_end, a64::lt); // within range -> no flush + + // to much smaller -> flush to: keep only the sign bit of lane 0. + armAsm->Umov(gprT1.W(), to.V4S(), 0); + armAsm->And(gprT1.W(), gprT1.W(), 0x80000000); + armAsm->Ins(to.V4S(), 0, gprT1.W()); + armAsm->B(&case_end); + + armAsm->Bind(&case_neg_big); + armAsm->Umov(gprT1.W(), from.V4S(), 0); + armAsm->And(gprT1.W(), gprT1.W(), 0x80000000); + armAsm->Ins(from.V4S(), 0, gprT1.W()); + + armAsm->Bind(&case_end); + armAsm->Fadd(a64::SRegister(to.GetCode()), a64::SRegister(to.GetCode()), a64::SRegister(from.GetCode())); +} + +//------------------------------------------------------------------ +// MAX/MIN — Integer comparison approach +//------------------------------------------------------------------ +// IEEE FMAX/FMIN have NaN propagation issues that don't match PS2 behavior. +// Use the same integer comparison trick as x86: convert float bit patterns +// so that the integer comparison order matches the float comparison order. +// For each lane: t = (val >> 31) ? (val ^ 0x7fffffff) : val +// Then CMGT.4S selects the correct operand. + +static void NEON_MAXPS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + + // Transform 'to' for integer comparison + armAsm->Sshr(t1.V4S(), to.V4S(), 31); // All sign bits replicated + armAsm->Ushr(t1.V4S(), t1.V4S(), 1); // 0x7fffffff where negative, 0 where positive + armAsm->Eor(t1.V16B(), t1.V16B(), to.V16B()); + + // Transform 'from' for integer comparison + armAsm->Sshr(t2.V4S(), from.V4S(), 31); + armAsm->Ushr(t2.V4S(), t2.V4S(), 1); + armAsm->Eor(t2.V16B(), t2.V16B(), from.V16B()); + + // MAX: select 'to' where t1 > t2, else 'from'. Bif (Bitwise Insert if + // False) writes 'from' into 'to' where the mask is 0 — fuses BSL+Mov. + armAsm->Cmgt(t1.V4S(), t1.V4S(), t2.V4S()); + armAsm->Bif(to.V16B(), from.V16B(), t1.V16B()); + + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); +} + +static void NEON_MINPS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + const a64::VRegister& t2 = mVU.regAlloc->allocReg(); + + // Transform 'to' for integer comparison + armAsm->Sshr(t1.V4S(), to.V4S(), 31); + armAsm->Ushr(t1.V4S(), t1.V4S(), 1); + armAsm->Eor(t1.V16B(), t1.V16B(), to.V16B()); + + // Transform 'from' for integer comparison + armAsm->Sshr(t2.V4S(), from.V4S(), 31); + armAsm->Ushr(t2.V4S(), t2.V4S(), 1); + armAsm->Eor(t2.V16B(), t2.V16B(), from.V16B()); + + // MIN: select 'to' where t2 > t1 (i.e., to < from), else 'from'. + // Bif fuses the BSL+Mov into a single insn. + armAsm->Cmgt(t2.V4S(), t2.V4S(), t1.V4S()); + armAsm->Bif(to.V16B(), from.V16B(), t2.V16B()); + + mVU.regAlloc->clearNeeded(t1); + mVU.regAlloc->clearNeeded(t2); +} + +static void NEON_MAXSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + + // Transform to[0] — read 'to' straight into the scratch; no copy needed (the + // in-place shift would only re-read what Sshr can read directly). + armAsm->Sshr(RQSCRATCH.V4S(), to.V4S(), 31); + armAsm->Ushr(RQSCRATCH.V4S(), RQSCRATCH.V4S(), 1); + armAsm->Eor(RQSCRATCH.V16B(), RQSCRATCH.V16B(), to.V16B()); + + // Transform from[0] — read 'from' straight into t1; no copy needed. + armAsm->Sshr(t1.V4S(), from.V4S(), 31); + armAsm->Ushr(t1.V4S(), t1.V4S(), 1); + armAsm->Eor(t1.V16B(), t1.V16B(), from.V16B()); + + // Compare lane 0 as integers: if to_xform > from_xform, keep to, else take from + armAsm->Cmgt(RQSCRATCH.V4S(), RQSCRATCH.V4S(), t1.V4S()); + // Use BSL: where mask=1 keep to, where mask=0 keep from + // Only lane 0 is relevant — write result into to[0] + armAsm->Bsl(RQSCRATCH.V16B(), to.V16B(), from.V16B()); + armAsm->Ins(to.V4S(), 0, RQSCRATCH.V4S(), 0); + + mVU.regAlloc->clearNeeded(t1); +} + +static void NEON_MINSS(mV, const a64::VRegister& to, const a64::VRegister& from) +{ + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + + // Transform to[0] — read 'to' straight into the scratch; no copy needed (the + // in-place shift would only re-read what Sshr can read directly). + armAsm->Sshr(RQSCRATCH.V4S(), to.V4S(), 31); + armAsm->Ushr(RQSCRATCH.V4S(), RQSCRATCH.V4S(), 1); + armAsm->Eor(RQSCRATCH.V16B(), RQSCRATCH.V16B(), to.V16B()); + + // Transform from[0] — read 'from' straight into t1; no copy needed. + armAsm->Sshr(t1.V4S(), from.V4S(), 31); + armAsm->Ushr(t1.V4S(), t1.V4S(), 1); + armAsm->Eor(t1.V16B(), t1.V16B(), from.V16B()); + + // MIN: where from_xform > to_xform (i.e., to is smaller), keep to + armAsm->Cmgt(t1.V4S(), t1.V4S(), RQSCRATCH.V4S()); + armAsm->Bsl(t1.V16B(), to.V16B(), from.V16B()); + armAsm->Ins(to.V4S(), 0, t1.V4S(), 0); + + mVU.regAlloc->clearNeeded(t1); +} + +//------------------------------------------------------------------ +// Function Pointer Tables +//------------------------------------------------------------------ +// opType: 0=ADD, 1=SUB, 2=MUL, 3=MAX, 4=MIN, 5=ADD2 + +typedef void (*NEONarithPS)(microVU&, const a64::VRegister&, const a64::VRegister&); + +static NEONarithPS const NEON_PS[] = { + NEON_ADDPS, // 0 + NEON_SUBPS, // 1 + NEON_MULPS, // 2 + NEON_MAXPS, // 3 + NEON_MINPS, // 4 + NEON_ADD2PS, // 5 +}; + +static NEONarithPS const NEON_SS[] = { + NEON_ADDSS, // 0 + NEON_SUBSS, // 1 + NEON_MULSS, // 2 + NEON_MAXSS, // 3 + NEON_MINSS, // 4 + NEON_ADD2SS, // 5 +}; + +//------------------------------------------------------------------ +// Single Scalar Lane Rotation +//------------------------------------------------------------------ +// For _XYZW_SS2 (single scalar, NOT X): rotate the target lane into +// lane 0 for scalar operations, then rotate back afterward. +// Uses EXT to rotate the 128-bit vector by N lanes. + +// Rotate vector so that lane 'offsetReg' moves to lane 0. +// offsetReg: 0=X(nop), 1=Y, 2=Z, 3=W +static void shuffleSSto0(const a64::VRegister& reg, int lane) +{ + if (lane == 0) return; + // EXT #(lane*4) rotates left by lane*4 bytes, bringing lane N to position 0 + armAsm->Ext(reg.V16B(), reg.V16B(), reg.V16B(), lane * 4); +} + +// Rotate vector back: undo the rotation done by shuffleSSto0. +static void shuffleSSfrom0(const a64::VRegister& reg, int lane) +{ + if (lane == 0) return; + // Rotate right by lane*4 bytes = rotate left by (16 - lane*4) + armAsm->Ext(reg.V16B(), reg.V16B(), reg.V16B(), (4 - lane) * 4); +} + +//------------------------------------------------------------------ +// Clamp Modes +//------------------------------------------------------------------ + +enum clampModes +{ + cFt = 0x01, // Clamp Ft / I-reg / Q-reg + cFs = 0x02, // Clamp Fs + cACC = 0x04, // Clamp ACC +}; + +//------------------------------------------------------------------ +// Logging Helper +//------------------------------------------------------------------ + +static void mVU_printOP(microVU& mVU, int opCase, microOpcode opEnum, bool isACC) +{ + mVUlog(microOpcodeName[opEnum]); + opCase1 { if (isACC) { mVUlogACC(); } else { mVUlogFd(); } mVUlogFt(); } + opCase2 { if (isACC) { mVUlogACC(); } else { mVUlogFd(); } mVUlogBC(); } + opCase3 { if (isACC) { mVUlogACC(); } else { mVUlogFd(); } mVUlogI(); } + opCase4 { if (isACC) { mVUlogACC(); } else { mVUlogFd(); } mVUlogQ(); } +} + +//------------------------------------------------------------------ +// Pass 1 Setup (Analysis — platform-independent) +//------------------------------------------------------------------ + +static void setupPass1(microVU& mVU, int opCase, bool isACC, bool noFlagUpdate) +{ + opCase1 { mVUanalyzeFMAC1(mVU, ((isACC) ? 0 : _Fd_), _Fs_, _Ft_); } + opCase2 { mVUanalyzeFMAC3(mVU, ((isACC) ? 0 : _Fd_), _Fs_, _Ft_); } + opCase3 { mVUanalyzeFMAC1(mVU, ((isACC) ? 0 : _Fd_), _Fs_, 0); } + opCase4 { mVUanalyzeFMAC1(mVU, ((isACC) ? 0 : _Fd_), _Fs_, 0); } + + if (noFlagUpdate) // Max/Min ops + sFLAG.doFlag = false; +} + +//------------------------------------------------------------------ +// Safe Subtraction — X minus X = 0 (avoids NaN from inf-inf) +//------------------------------------------------------------------ + +static bool doSafeSub(microVU& mVU, int opCase, int opType, bool isACC) +{ + opCase1 + { + if ((opType == 1) && (_Ft_ == _Fs_) && (opCase == 1)) + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(-1, isACC ? 32 : _Fd_, _X_Y_Z_W); + armAsm->Movi(Fs.V4S(), 0); // Set to positive zero + mVUupdateFlags(mVU, Fs); + mVU.regAlloc->clearNeeded(Fs); + return true; + } + } + return false; +} + +//------------------------------------------------------------------ +// Ft Register Setup for Normal/BC/I/Q Cases +//------------------------------------------------------------------ + +static void setupFtReg(microVU& mVU, a64::VRegister& Ft, a64::VRegister& tempFt, int opCase, int clampType) +{ + opCase1 + { + const bool willClamp = (clampE || ((clampType & cFt) && !clampE && (CHECK_VU_OVERFLOW(mVU.index) || CHECK_VU_SIGN_OVERFLOW(mVU.index)))); + + if (_XYZW_SS2) { Ft = mVU.regAlloc->allocReg(_Ft_, 0, _X_Y_Z_W); tempFt = Ft; } + else if (willClamp) { Ft = mVU.regAlloc->allocReg(_Ft_, 0, 0xf); tempFt = Ft; } + else { Ft = mVU.regAlloc->allocReg(_Ft_); tempFt = a64::NoVReg; } + } + opCase2 + { + tempFt = mVU.regAlloc->allocReg(_Ft_); + Ft = mVU.regAlloc->allocReg(); + mVUunpack_xyzw(Ft, tempFt, _bc_); + mVU.regAlloc->clearNeeded(tempFt); + tempFt = Ft; + } + opCase3 + { + Ft = mVU.regAlloc->allocReg(33, 0, _X_Y_Z_W); + tempFt = Ft; + } + opCase4 + { + if (!clampE && _XYZW_SS && !mVUinfo.readQ) + { + Ft = qmmPQ; + tempFt = a64::NoVReg; + } + else + { + Ft = mVU.regAlloc->allocReg(); + tempFt = Ft; + getQreg(Ft, mVUinfo.readQ); + } + } +} + +//------------------------------------------------------------------ +// mVU_FMACa — Normal FMAC Opcodes (ADD/SUB/MUL/MAX/MIN and ACC variants) +//------------------------------------------------------------------ + +static void mVU_FMACa(microVU& mVU, int recPass, int opCase, int opType, bool isACC, microOpcode opEnum, int clampType) +{ + pass1 { setupPass1(mVU, opCase, isACC, ((opType == 3) || (opType == 4))); } + pass2 + { + if (doSafeSub(mVU, opCase, opType, isACC)) + return; + + a64::VRegister Fs = a64::NoVReg; + a64::VRegister Ft = a64::NoVReg; + a64::VRegister ACC = a64::NoVReg; + a64::VRegister tempFt = a64::NoVReg; + + setupFtReg(mVU, Ft, tempFt, opCase, clampType); + + if (isACC) + { + Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + ACC = mVU.regAlloc->allocReg((_X_Y_Z_W == 0xf) ? -1 : 32, 32, 0xf, false); + if (_XYZW_SS2) + shuffleSSto0(ACC, offsetReg); // Rotate target lane to lane 0 + } + else + { + Fs = mVU.regAlloc->allocReg(_Fs_, _Fd_, _X_Y_Z_W); + } + + if (clampType & cFt) mVUclamp2(mVU, Ft, a64::NoVReg, _X_Y_Z_W); + if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W); + + if (_XYZW_SS) NEON_SS[opType](mVU, Fs, Ft); + else NEON_PS[opType](mVU, Fs, Ft); + + if (isACC) + { + if (_XYZW_SS) + armAsm->Ins(ACC.V4S(), 0, Fs.V4S(), 0); // MOVSS equivalent + else + mVUmergeRegs(ACC, Fs, _X_Y_Z_W); + mVUupdateFlags(mVU, ACC, Fs, tempFt); + if (_XYZW_SS2) + shuffleSSfrom0(ACC, offsetReg); // Rotate lane 0 back to original position + mVU.regAlloc->clearNeeded(ACC); + } + else if (opType < 3 || opType == 5) // Not Min/Max or is ADDi (opType 5) + { + mVUupdateFlags(mVU, Fs, tempFt); + } + + mVU.regAlloc->clearNeeded(Fs); // Always clear written reg first + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opEnum); + } + pass3 { mVU_printOP(mVU, opCase, opEnum, isACC); } + pass4 + { + if ((opType != 3) && (opType != 4)) + mVUregs.needExactMatch |= 8; + } +} + +//------------------------------------------------------------------ +// mVU_FMACb — MADDA/MSUBA Opcodes (MUL then ADD/SUB into ACC) +//------------------------------------------------------------------ + +static void mVU_FMACb(microVU& mVU, int recPass, int opCase, int opType, microOpcode opEnum, int clampType) +{ + pass1 { setupPass1(mVU, opCase, true, false); } + pass2 + { + a64::VRegister Fs = a64::NoVReg; + a64::VRegister Ft = a64::NoVReg; + a64::VRegister ACC = a64::NoVReg; + a64::VRegister tempFt = a64::NoVReg; + + setupFtReg(mVU, Ft, tempFt, opCase, clampType); + + Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + ACC = mVU.regAlloc->allocReg(32, 32, 0xf, false); + + if (_XYZW_SS2) + shuffleSSto0(ACC, offsetReg); // Rotate target lane to lane 0 + + if (clampType & cFt) mVUclamp2(mVU, Ft, a64::NoVReg, _X_Y_Z_W); + if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W); + + // Step 1: Multiply Fs * Ft + if (_XYZW_SS) NEON_SS[2](mVU, Fs, Ft); + else NEON_PS[2](mVU, Fs, Ft); + + // Step 2: ADD/SUB the product to/from ACC + if (_XYZW_SS || _X_Y_Z_W == 0xf) + { + if (_XYZW_SS) + { + // ACC is written back with a full 0xf mask, so its non-target + // lanes must survive this single-lane accumulate. AArch64 scalar + // FP writes ZERO the upper lanes of the dest V register (unlike + // x86 ADDSS/SUBSS, which preserve them — the x86 mVU relies on + // that). Accumulate on a scratch copy and merge only lane 0 back, + // mirroring the load+Ins pattern mVU_FMACa uses for its ACC. + const a64::VRegister& accSS = mVU.regAlloc->allocReg(); + armAsm->Mov(accSS.V16B(), ACC.V16B()); + NEON_SS[opType](mVU, accSS, Fs); + armAsm->Ins(ACC.V4S(), 0, accSS.V4S(), 0); + mVU.regAlloc->clearNeeded(accSS); + } + else + { + NEON_PS[opType](mVU, ACC, Fs); + } + mVUupdateFlags(mVU, ACC, Fs, tempFt); + if (_XYZW_SS && _X_Y_Z_W != 8) + shuffleSSfrom0(ACC, offsetReg); // Rotate back + } + else + { + const a64::VRegister& tempACC = mVU.regAlloc->allocReg(); + armAsm->Mov(tempACC.V16B(), ACC.V16B()); + NEON_PS[opType](mVU, tempACC, Fs); + mVUmergeRegs(ACC, tempACC, _X_Y_Z_W); + mVUupdateFlags(mVU, ACC, Fs, tempFt); + mVU.regAlloc->clearNeeded(tempACC); + } + + mVU.regAlloc->clearNeeded(ACC); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(Ft); + mVU.profiler.EmitOp(opEnum); + } + pass3 { mVU_printOP(mVU, opCase, opEnum, true); } + pass4 { mVUregs.needExactMatch |= 8; } +} + +//------------------------------------------------------------------ +// mVU_FMACc — MADD Opcodes (MUL then ADD from ACC into Fd) +//------------------------------------------------------------------ +// No FMA: separate MUL then ADD (matches PS2 rounding) + +static void mVU_FMACc(microVU& mVU, int recPass, int opCase, microOpcode opEnum, int clampType) +{ + pass1 { setupPass1(mVU, opCase, false, false); } + pass2 + { + a64::VRegister Fs = a64::NoVReg; + a64::VRegister Ft = a64::NoVReg; + a64::VRegister ACC = a64::NoVReg; + a64::VRegister tempFt = a64::NoVReg; + + setupFtReg(mVU, Ft, tempFt, opCase, clampType); + + ACC = mVU.regAlloc->allocReg(32); + Fs = mVU.regAlloc->allocReg(_Fs_, _Fd_, _X_Y_Z_W); + + if (_XYZW_SS2) + shuffleSSto0(ACC, offsetReg); // Rotate target lane to lane 0 + + if (clampType & cFt) mVUclamp2(mVU, Ft, a64::NoVReg, _X_Y_Z_W); + if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W); + if (clampType & cACC) mVUclamp2(mVU, ACC, a64::NoVReg, _X_Y_Z_W); + + // Step 1: Fs = Fs * Ft + // Step 2: Fs = Fs + ACC + if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft); NEON_SS[0](mVU, Fs, ACC); } + else { NEON_PS[2](mVU, Fs, Ft); NEON_PS[0](mVU, Fs, ACC); } + + if (_XYZW_SS2) + shuffleSSfrom0(ACC, offsetReg); // Rotate back + + mVUupdateFlags(mVU, Fs, tempFt); + + mVU.regAlloc->clearNeeded(Fs); // Always clear written reg first + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(ACC); + mVU.profiler.EmitOp(opEnum); + } + pass3 { mVU_printOP(mVU, opCase, opEnum, false); } + pass4 { mVUregs.needExactMatch |= 8; } +} + +//------------------------------------------------------------------ +// mVU_FMACd — MSUB Opcodes (ACC - Fs*Ft into Fd) +//------------------------------------------------------------------ + +static void mVU_FMACd(microVU& mVU, int recPass, int opCase, microOpcode opEnum, int clampType) +{ + pass1 { setupPass1(mVU, opCase, false, false); } + pass2 + { + a64::VRegister Fs = a64::NoVReg; + a64::VRegister Ft = a64::NoVReg; + a64::VRegister Fd = a64::NoVReg; + a64::VRegister tempFt = a64::NoVReg; + + setupFtReg(mVU, Ft, tempFt, opCase, clampType); + + Fs = mVU.regAlloc->allocReg(_Fs_, 0, _X_Y_Z_W); + Fd = mVU.regAlloc->allocReg(32, _Fd_, _X_Y_Z_W); + + if (clampType & cFt) mVUclamp2(mVU, Ft, a64::NoVReg, _X_Y_Z_W); + if (clampType & cFs) mVUclamp2(mVU, Fs, a64::NoVReg, _X_Y_Z_W); + if (clampType & cACC) mVUclamp2(mVU, Fd, a64::NoVReg, _X_Y_Z_W); + + // Step 1: Fs = Fs * Ft + // Step 2: Fd = Fd - Fs (Fd starts as ACC) + if (_XYZW_SS) { NEON_SS[2](mVU, Fs, Ft); NEON_SS[1](mVU, Fd, Fs); } + else { NEON_PS[2](mVU, Fs, Ft); NEON_PS[1](mVU, Fd, Fs); } + + mVUupdateFlags(mVU, Fd, Fs, tempFt); + + mVU.regAlloc->clearNeeded(Fd); // Always clear written reg first + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opEnum); + } + pass3 { mVU_printOP(mVU, opCase, opEnum, false); } + pass4 { mVUregs.needExactMatch |= 8; } +} + +//------------------------------------------------------------------ +// ABS Opcode +//------------------------------------------------------------------ + +mVUop(mVU_ABS) +{ + pass1 { mVUanalyzeFMAC2(mVU, _Fs_, _Ft_); } + pass2 + { + if (!_Ft_) + return; + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, _Ft_, _X_Y_Z_W, !((_Fs_ == _Ft_) && (_X_Y_Z_W == 0xf))); + // PS2 ABS clears the sign bit per lane. Fabs is the dedicated single + // insn for that on aarch64 and matches AND-with-0x7fffffff for every + // IEEE bit pattern (normal, denormal, Inf, NaN — sign cleared, payload + // preserved). + armAsm->Fabs(Fs.V4S(), Fs.V4S()); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opABS); + } + pass3 + { + mVUlog("ABS"); + mVUlogFtFs(); + } +} + +//------------------------------------------------------------------ +// OPMULA Opcode — Cross product multiply into ACC +//------------------------------------------------------------------ + +mVUop(mVU_OPMULA) +{ + pass1 { mVUanalyzeFMAC1(mVU, 0, _Fs_, _Ft_); } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, _X_Y_Z_W); + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 32, _X_Y_Z_W); + + // OPMULA: ACC.xyz = Fs.yzx * Ft.zxy + // Shuffle Fs: WXZY (0xC9) — puts Y,Z,X in positions 0,1,2 + // ARM64: Fs must be {Fs.y, Fs.z, Fs.x, Fs.w} + + // Fs shuffle: {Y, Z, X, W} — indices 1,2,0,3 + armAsm->Mov(RQSCRATCH.V16B(), Fs.V16B()); + armAsm->Ins(Fs.V4S(), 0, RQSCRATCH.V4S(), 1); // Fs[0] = Y + armAsm->Ins(Fs.V4S(), 1, RQSCRATCH.V4S(), 2); // Fs[1] = Z + armAsm->Ins(Fs.V4S(), 2, RQSCRATCH.V4S(), 0); // Fs[2] = X + // Fs[3] = W (unchanged) + + // Ft shuffle: {Z, X, Y, W} — indices 2,0,1,3 + armAsm->Mov(RQSCRATCH.V16B(), Ft.V16B()); + armAsm->Ins(Ft.V4S(), 0, RQSCRATCH.V4S(), 2); // Ft[0] = Z + armAsm->Ins(Ft.V4S(), 1, RQSCRATCH.V4S(), 0); // Ft[1] = X + armAsm->Ins(Ft.V4S(), 2, RQSCRATCH.V4S(), 1); // Ft[2] = Y + // Ft[3] = W (unchanged) + + NEON_MULPS(mVU, Fs, Ft); + mVU.regAlloc->clearNeeded(Ft); + mVUupdateFlags(mVU, Fs); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opOPMULA); + } + pass3 + { + mVUlog("OPMULA"); + mVUlogACC(); + mVUlogFt(); + } + pass4 { mVUregs.needExactMatch |= 8; } +} + +//------------------------------------------------------------------ +// OPMSUB Opcode — Cross product subtract from ACC +//------------------------------------------------------------------ + +mVUop(mVU_OPMSUB) +{ + pass1 { mVUanalyzeFMAC1(mVU, _Fd_, _Fs_, _Ft_); } + pass2 + { + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, 0xf); + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, 0xf); + const a64::VRegister& ACC = mVU.regAlloc->allocReg(32, _Fd_, _X_Y_Z_W); + + // Fs shuffle: {Y, Z, X, W} + armAsm->Mov(RQSCRATCH.V16B(), Fs.V16B()); + armAsm->Ins(Fs.V4S(), 0, RQSCRATCH.V4S(), 1); + armAsm->Ins(Fs.V4S(), 1, RQSCRATCH.V4S(), 2); + armAsm->Ins(Fs.V4S(), 2, RQSCRATCH.V4S(), 0); + + // Ft shuffle: {Z, X, Y, W} + armAsm->Mov(RQSCRATCH.V16B(), Ft.V16B()); + armAsm->Ins(Ft.V4S(), 0, RQSCRATCH.V4S(), 2); + armAsm->Ins(Ft.V4S(), 1, RQSCRATCH.V4S(), 0); + armAsm->Ins(Ft.V4S(), 2, RQSCRATCH.V4S(), 1); + + NEON_MULPS(mVU, Fs, Ft); + NEON_SUBPS(mVU, ACC, Fs); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(Ft); + mVUupdateFlags(mVU, ACC); + mVU.regAlloc->clearNeeded(ACC); + mVU.profiler.EmitOp(opOPMSUB); + } + pass3 + { + mVUlog("OPMSUB"); + mVUlogFd(); + mVUlogFt(); + } + pass4 { mVUregs.needExactMatch |= 8; } +} + +//------------------------------------------------------------------ +// FTOI0/4/12/15 — Float to Int conversion +//------------------------------------------------------------------ +// FCVTZS: converts float to signed int, truncating toward zero. +// For unrepresentable positive values, x86 CVTTPS2DQ returns 0x80000000 +// and then XORs with 0xffffffff to get 0x7fffffff. ARM64 FCVTZS +// saturates to INT_MAX (0x7fffffff) or INT_MIN (0x80000000) natively for +// finite overflow and ±Inf, so no correction XOR is needed there. +// +// NaN, however, diverges: ARM64 Fcvtzs(NaN)=0, but the interp's floatToInt +// (VUops.cpp) and x86 PCSX2 mVU both saturate a NaN by its sign bit — +// positive NaN → 0x7fffffff, negative NaN → 0x80000000 (exp 0xFF satisfies +// the interp's `>= 0x4f000000` saturation test). Feeding a NaN into FTOI0 +// would otherwise produce JIT=0 vs interp=0x7fffffff. NaN lanes are therefore +// patched to the sign-based INT saturation, matching the interp and x86 +// exactly (regression-pinned in vu_ftoi_saturation_tests.cpp). + +static void mVU_FTOIx(mP, const float* addr, microOpcode opEnum) +{ + pass1 { mVUanalyzeFMAC2(mVU, _Fs_, _Ft_); } + pass2 + { + if (!_Ft_) + return; + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, _Ft_, _X_Y_Z_W, !((_Fs_ == _Ft_) && (_X_Y_Z_W == 0xf))); + + if (addr) + { + // Scale by fixed-point multiplier before conversion + armAsm->Ldr(RQSCRATCH3, mVUglobMem(addr)); + armAsm->Fmul(Fs.V4S(), Fs.V4S(), RQSCRATCH3.V4S()); + } + + // NaN saturation correction (see header comment): build, BEFORE the + // convert clobbers Fs, a per-lane mask of which lanes are NaN and the + // sign-based saturation value for those lanes. + const a64::VRegister& notNan = mVU.regAlloc->allocReg(); // 0xffffffff where NOT NaN + const a64::VRegister& sat = mVU.regAlloc->allocReg(); // sign ? INT_MIN : INT_MAX + armAsm->Fcmeq(notNan.V4S(), Fs.V4S(), Fs.V4S()); // a==a is false only for NaN + armAsm->Sshr(sat.V4S(), Fs.V4S(), 31); // 0xffffffff if sign set, else 0 + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.absclip[0])); // 0x7fffffff (INT_MAX bits) + armAsm->Eor(sat.V16B(), sat.V16B(), RQSCRATCH3.V16B()); // +NaN→0x7fffffff, -NaN→0x80000000 + + // Convert float to signed int (truncating toward zero). Finite overflow + // and ±Inf saturate correctly here; NaN lanes become 0 and are fixed up. + armAsm->Fcvtzs(Fs.V4S(), Fs.V4S()); + + // Where notNan==0 (a NaN lane), replace the Fcvtzs 0 with the saturation + // value; non-NaN lanes keep the converted result. BIF: dst bit <- src bit + // where mask bit is 0. + armAsm->Bif(Fs.V16B(), sat.V16B(), notNan.V16B()); + + mVU.regAlloc->clearNeeded(notNan); + mVU.regAlloc->clearNeeded(sat); + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opEnum); + } + pass3 + { + mVUlog(microOpcodeName[opEnum]); + mVUlogFtFs(); + } +} + +//------------------------------------------------------------------ +// ITOF0/4/12/15 — Int to Float conversion +//------------------------------------------------------------------ + +static void mVU_ITOFx(mP, const float* addr, microOpcode opEnum) +{ + pass1 { mVUanalyzeFMAC2(mVU, _Fs_, _Ft_); } + pass2 + { + if (!_Ft_) + return; + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, _Ft_, _X_Y_Z_W, !((_Fs_ == _Ft_) && (_X_Y_Z_W == 0xf))); + + // Convert signed int to float + armAsm->Scvtf(Fs.V4S(), Fs.V4S()); + + if (addr) + { + // Scale by fixed-point divisor after conversion + armAsm->Ldr(RQSCRATCH3, mVUglobMem(addr)); + armAsm->Fmul(Fs.V4S(), Fs.V4S(), RQSCRATCH3.V4S()); + } + + mVU.regAlloc->clearNeeded(Fs); + mVU.profiler.EmitOp(opEnum); + } + pass3 + { + mVUlog(microOpcodeName[opEnum]); + mVUlogFtFs(); + } +} + +//------------------------------------------------------------------ +// CLIP Opcode — Clip flag computation +//------------------------------------------------------------------ + +mVUop(mVU_CLIP) +{ + pass1 { mVUanalyzeFMAC4(mVU, _Fs_, _Ft_); } + pass2 + { + const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, 0, 0xf); + const a64::VRegister& Ft = mVU.regAlloc->allocReg(_Ft_, 0, 0x1); + const a64::VRegister& t1 = mVU.regAlloc->allocReg(); + + // Broadcast Ft.w to all lanes + mVUunpack_xyzw(Ft, Ft, 0); + + // Get previous clip flag and shift left by 6 + mVUallocCFLAGa(mVU, gprT1, cFLAG.lastWrite); + armAsm->Lsl(gprT1.W(), gprT1.W(), 6); + + // Denormal check: if exponent is zero, treat as zero. Use the + // immediate-zero form of CMEQ to avoid materialising a zero vector. + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.exponent[0])); + armAsm->And(t1.V16B(), Fs.V16B(), RQSCRATCH3.V16B()); + armAsm->Cmeq(t1.V4S(), t1.V4S(), 0); // All 1s where denormal + armAsm->Bic(t1.V16B(), Fs.V16B(), t1.V16B()); // Zero out denormals + + // |Ft.w| (absolute value) + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.absclip[0])); + armAsm->And(Ft.V16B(), Ft.V16B(), RQSCRATCH3.V16B()); + + // Negate Fs (for -w comparison) + armAsm->Ldr(RQSCRATCH3, mVUglobMem(&mVUglob.signbit[0])); + armAsm->Eor(Fs.V16B(), t1.V16B(), RQSCRATCH3.V16B()); + + // t1 > Ft means +component > +w: bit set + armAsm->Cmgt(t1.V4S(), t1.V4S(), Ft.V4S()); // +x>+w, +y>+w, +z>+w, (ignored) + // Fs > Ft means -component > +w (i.e., component < -w): bit set + armAsm->Cmgt(Fs.V4S(), Fs.V4S(), Ft.V4S()); // -x>+w, -y>+w, -z>+w, (ignored) + + // Extract bits from the comparison results + // t1 lanes: [0]=+x>w, [1]=+y>w, [2]=+z>w + // Fs lanes: [0]=-x>w, [1]=-y>w, [2]=-z>w + // Required layout: bit0=+x>w, bit1=-x>w, bit2=+y>w, bit3=-y>w, bit4=+z>w, bit5=-z>w + + armAsm->Ushr(t1.V4S(), t1.V4S(), 31); + armAsm->Ushr(Fs.V4S(), Fs.V4S(), 31); + + // Build clip result in gprT2 + armAsm->Umov(gprT2.W(), t1.V4S(), 0); // +x > w → bit 0 + armAsm->Umov(a64::w12, Fs.V4S(), 0); // -x > w → bit 1 + armAsm->Orr(gprT2.W(), gprT2.W(), a64::Operand(a64::w12, a64::LSL, 1)); + + armAsm->Umov(a64::w12, t1.V4S(), 1); // +y > w → bit 2 + armAsm->Orr(gprT2.W(), gprT2.W(), a64::Operand(a64::w12, a64::LSL, 2)); + armAsm->Umov(a64::w12, Fs.V4S(), 1); // -y > w → bit 3 + armAsm->Orr(gprT2.W(), gprT2.W(), a64::Operand(a64::w12, a64::LSL, 3)); + + armAsm->Umov(a64::w12, t1.V4S(), 2); // +z > w → bit 4 + armAsm->Orr(gprT2.W(), gprT2.W(), a64::Operand(a64::w12, a64::LSL, 4)); + armAsm->Umov(a64::w12, Fs.V4S(), 2); // -z > w → bit 5 + armAsm->Orr(gprT2.W(), gprT2.W(), a64::Operand(a64::w12, a64::LSL, 5)); + + // Combine with shifted previous clip flag + armAsm->And(gprT2.W(), gprT2.W(), 0x3f); + armAsm->And(gprT1.W(), gprT1.W(), 0xffffff); + armAsm->Orr(gprT1.W(), gprT1.W(), gprT2.W()); + + mVUallocCFLAGb(mVU, gprT1, cFLAG.write); + mVU.regAlloc->clearNeeded(Fs); + mVU.regAlloc->clearNeeded(Ft); + mVU.regAlloc->clearNeeded(t1); + mVU.profiler.EmitOp(opCLIP); + } + pass3 + { + mVUlog("CLIP"); + mVUlogCLIP(); + } +} + +//------------------------------------------------------------------ +// Micro VU Micromode Upper Instructions — Opcode Dispatch +//------------------------------------------------------------------ + +mVUop(mVU_ADD) { mVU_FMACa(mVU, recPass, 1, 0, false, opADD, 0); } +mVUop(mVU_ADDi) { mVU_FMACa(mVU, recPass, 3, 5, false, opADDi, 0); } +mVUop(mVU_ADDq) { mVU_FMACa(mVU, recPass, 4, 0, false, opADDq, 0); } +mVUop(mVU_ADDx) { mVU_FMACa(mVU, recPass, 2, 0, false, opADDx, 0); } +mVUop(mVU_ADDy) { mVU_FMACa(mVU, recPass, 2, 0, false, opADDy, 0); } +mVUop(mVU_ADDz) { mVU_FMACa(mVU, recPass, 2, 0, false, opADDz, 0); } +mVUop(mVU_ADDw) { mVU_FMACa(mVU, recPass, 2, 0, false, opADDw, 0); } +mVUop(mVU_ADDA) { mVU_FMACa(mVU, recPass, 1, 0, true, opADDA, 0); } +mVUop(mVU_ADDAi) { mVU_FMACa(mVU, recPass, 3, 0, true, opADDAi, 0); } +mVUop(mVU_ADDAq) { mVU_FMACa(mVU, recPass, 4, 0, true, opADDAq, 0); } +mVUop(mVU_ADDAx) { mVU_FMACa(mVU, recPass, 2, 0, true, opADDAx, 0); } +mVUop(mVU_ADDAy) { mVU_FMACa(mVU, recPass, 2, 0, true, opADDAy, 0); } +mVUop(mVU_ADDAz) { mVU_FMACa(mVU, recPass, 2, 0, true, opADDAz, 0); } +mVUop(mVU_ADDAw) { mVU_FMACa(mVU, recPass, 2, 0, true, opADDAw, 0); } +mVUop(mVU_SUB) { mVU_FMACa(mVU, recPass, 1, 1, false, opSUB, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBi) { mVU_FMACa(mVU, recPass, 3, 1, false, opSUBi, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBq) { mVU_FMACa(mVU, recPass, 4, 1, false, opSUBq, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBx) { mVU_FMACa(mVU, recPass, 2, 1, false, opSUBx, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBy) { mVU_FMACa(mVU, recPass, 2, 1, false, opSUBy, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBz) { mVU_FMACa(mVU, recPass, 2, 1, false, opSUBz, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBw) { mVU_FMACa(mVU, recPass, 2, 1, false, opSUBw, (_XYZW_PS)?(cFs|cFt):0); } // Clamp (Kingdom Hearts I (VU0)) +mVUop(mVU_SUBA) { mVU_FMACa(mVU, recPass, 1, 1, true, opSUBA, 0); } +mVUop(mVU_SUBAi) { mVU_FMACa(mVU, recPass, 3, 1, true, opSUBAi, 0); } +mVUop(mVU_SUBAq) { mVU_FMACa(mVU, recPass, 4, 1, true, opSUBAq, 0); } +mVUop(mVU_SUBAx) { mVU_FMACa(mVU, recPass, 2, 1, true, opSUBAx, 0); } +mVUop(mVU_SUBAy) { mVU_FMACa(mVU, recPass, 2, 1, true, opSUBAy, 0); } +mVUop(mVU_SUBAz) { mVU_FMACa(mVU, recPass, 2, 1, true, opSUBAz, 0); } +mVUop(mVU_SUBAw) { mVU_FMACa(mVU, recPass, 2, 1, true, opSUBAw, 0); } +mVUop(mVU_MUL) { mVU_FMACa(mVU, recPass, 1, 2, false, opMUL, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULi) { mVU_FMACa(mVU, recPass, 3, 2, false, opMULi, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULq) { mVU_FMACa(mVU, recPass, 4, 2, false, opMULq, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULx) { mVU_FMACa(mVU, recPass, 2, 2, false, opMULx, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULy) { mVU_FMACa(mVU, recPass, 2, 2, false, opMULy, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULz) { mVU_FMACa(mVU, recPass, 2, 2, false, opMULz, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULw) { mVU_FMACa(mVU, recPass, 2, 2, false, opMULw, (_XYZW_PS)?(cFs|cFt):cFs); } // Clamp (TOTA, DoM, Ice Age (VU0)) +mVUop(mVU_MULA) { mVU_FMACa(mVU, recPass, 1, 2, true, opMULA, 0); } +mVUop(mVU_MULAi) { mVU_FMACa(mVU, recPass, 3, 2, true, opMULAi, 0); } +mVUop(mVU_MULAq) { mVU_FMACa(mVU, recPass, 4, 2, true, opMULAq, 0); } +mVUop(mVU_MULAx) { mVU_FMACa(mVU, recPass, 2, 2, true, opMULAx, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MULAy) { mVU_FMACa(mVU, recPass, 2, 2, true, opMULAy, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MULAz) { mVU_FMACa(mVU, recPass, 2, 2, true, opMULAz, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MULAw) { mVU_FMACa(mVU, recPass, 2, 2, true, opMULAw, (_XYZW_PS) ? (cFs | cFt) : cFs); } // Clamp (TOTA, DoM, ...) - Ft for Superman +mVUop(mVU_MADD) { mVU_FMACc(mVU, recPass, 1, opMADD, 0); } +mVUop(mVU_MADDi) { mVU_FMACc(mVU, recPass, 3, opMADDi, 0); } +mVUop(mVU_MADDq) { mVU_FMACc(mVU, recPass, 4, opMADDq, 0); } +mVUop(mVU_MADDx) { mVU_FMACc(mVU, recPass, 2, opMADDx, cFs); } // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDy) { mVU_FMACc(mVU, recPass, 2, opMADDy, cFs); } // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDz) { mVU_FMACc(mVU, recPass, 2, opMADDz, cFs); } // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDw) { mVU_FMACc(mVU, recPass, 2, opMADDw, (isCOP2)?(cACC|cFt|cFs):cFs);} // Clamp (ICO (COP2), TOTA, DoM) +mVUop(mVU_MADDA) { mVU_FMACb(mVU, recPass, 1, 0, opMADDA, 0); } +mVUop(mVU_MADDAi) { mVU_FMACb(mVU, recPass, 3, 0, opMADDAi, 0); } +mVUop(mVU_MADDAq) { mVU_FMACb(mVU, recPass, 4, 0, opMADDAq, 0); } +mVUop(mVU_MADDAx) { mVU_FMACb(mVU, recPass, 2, 0, opMADDAx, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDAy) { mVU_FMACb(mVU, recPass, 2, 0, opMADDAy, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDAz) { mVU_FMACb(mVU, recPass, 2, 0, opMADDAz, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MADDAw) { mVU_FMACb(mVU, recPass, 2, 0, opMADDAw, cFs);} // Clamp (TOTA, DoM, ...) +mVUop(mVU_MSUB) { mVU_FMACd(mVU, recPass, 1, opMSUB, (isCOP2) ? cFs : 0); } // Clamp (Superman) +mVUop(mVU_MSUBi) { mVU_FMACd(mVU, recPass, 3, opMSUBi, 0); } +mVUop(mVU_MSUBq) { mVU_FMACd(mVU, recPass, 4, opMSUBq, 0); } +mVUop(mVU_MSUBx) { mVU_FMACd(mVU, recPass, 2, opMSUBx, 0); } +mVUop(mVU_MSUBy) { mVU_FMACd(mVU, recPass, 2, opMSUBy, 0); } +mVUop(mVU_MSUBz) { mVU_FMACd(mVU, recPass, 2, opMSUBz, 0); } +mVUop(mVU_MSUBw) { mVU_FMACd(mVU, recPass, 2, opMSUBw, 0); } +mVUop(mVU_MSUBA) { mVU_FMACb(mVU, recPass, 1, 1, opMSUBA, 0); } +mVUop(mVU_MSUBAi) { mVU_FMACb(mVU, recPass, 3, 1, opMSUBAi, 0); } +mVUop(mVU_MSUBAq) { mVU_FMACb(mVU, recPass, 4, 1, opMSUBAq, 0); } +mVUop(mVU_MSUBAx) { mVU_FMACb(mVU, recPass, 2, 1, opMSUBAx, 0); } +mVUop(mVU_MSUBAy) { mVU_FMACb(mVU, recPass, 2, 1, opMSUBAy, 0); } +mVUop(mVU_MSUBAz) { mVU_FMACb(mVU, recPass, 2, 1, opMSUBAz, 0); } +mVUop(mVU_MSUBAw) { mVU_FMACb(mVU, recPass, 2, 1, opMSUBAw, 0); } +mVUop(mVU_MAX) { mVU_FMACa(mVU, recPass, 1, 3, false, opMAX, 0); } +mVUop(mVU_MAXi) { mVU_FMACa(mVU, recPass, 3, 3, false, opMAXi, 0); } +mVUop(mVU_MAXx) { mVU_FMACa(mVU, recPass, 2, 3, false, opMAXx, 0); } +mVUop(mVU_MAXy) { mVU_FMACa(mVU, recPass, 2, 3, false, opMAXy, 0); } +mVUop(mVU_MAXz) { mVU_FMACa(mVU, recPass, 2, 3, false, opMAXz, 0); } +mVUop(mVU_MAXw) { mVU_FMACa(mVU, recPass, 2, 3, false, opMAXw, 0); } +mVUop(mVU_MINI) { mVU_FMACa(mVU, recPass, 1, 4, false, opMINI, 0); } +mVUop(mVU_MINIi) { mVU_FMACa(mVU, recPass, 3, 4, false, opMINIi, 0); } +mVUop(mVU_MINIx) { mVU_FMACa(mVU, recPass, 2, 4, false, opMINIx, 0); } +mVUop(mVU_MINIy) { mVU_FMACa(mVU, recPass, 2, 4, false, opMINIy, 0); } +mVUop(mVU_MINIz) { mVU_FMACa(mVU, recPass, 2, 4, false, opMINIz, 0); } +mVUop(mVU_MINIw) { mVU_FMACa(mVU, recPass, 2, 4, false, opMINIw, 0); } +mVUop(mVU_FTOI0) { mVU_FTOIx(mX, NULL, opFTOI0); } +mVUop(mVU_FTOI4) { mVU_FTOIx(mX, mVUglob.FTOI_4, opFTOI4); } +mVUop(mVU_FTOI12) { mVU_FTOIx(mX, mVUglob.FTOI_12, opFTOI12); } +mVUop(mVU_FTOI15) { mVU_FTOIx(mX, mVUglob.FTOI_15, opFTOI15); } +mVUop(mVU_ITOF0) { mVU_ITOFx(mX, NULL, opITOF0); } +mVUop(mVU_ITOF4) { mVU_ITOFx(mX, mVUglob.ITOF_4, opITOF4); } +mVUop(mVU_ITOF12) { mVU_ITOFx(mX, mVUglob.ITOF_12, opITOF12); } +mVUop(mVU_ITOF15) { mVU_ITOFx(mX, mVUglob.ITOF_15, opITOF15); } +mVUop(mVU_NOP) { pass2 { mVU.profiler.EmitOp(opNOP); } pass3 { mVUlog("NOP"); } } From 3f01f39e9adda9173f93ecbfcd4abd9047c5310d Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 007/292] arm64: GS NEON vector and software-rasterizer codegen fixes Correct the NEON float->int conversion paths (GSVector round-to-nearest vs truncate), the SW prim/scanline arm64 codegen (g_const split, runtime-indexed LOD lane offset), and minor strict-build hygiene (unsigned fread check, [[fallthrough]]). Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/GS/GSLzma.cpp | 2 +- pcsx2/GS/Renderers/Common/GSTexture.cpp | 2 ++ pcsx2/GS/Renderers/SW/GSDrawScanlineCodeGenerator.arm64.cpp | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pcsx2/GS/GSLzma.cpp b/pcsx2/GS/GSLzma.cpp index 93efbae0d3..a55ddd9275 100644 --- a/pcsx2/GS/GSLzma.cpp +++ b/pcsx2/GS/GSLzma.cpp @@ -308,7 +308,7 @@ namespace MyFileInStream* fis = Z7_CONTAINER_FROM_VTBL(p, MyFileInStream, vt); const size_t size_to_read = *size; const auto bytes_read = std::fread(buf, 1, size_to_read, fis->fp); - *size = (bytes_read >= 0) ? bytes_read : 0; + *size = bytes_read; return (bytes_read == size_to_read) ? SZ_OK : SZ_ERROR_READ; }, .Seek = [](const ISeekInStream* p, Int64* pos, ESzSeek origin) -> SRes { diff --git a/pcsx2/GS/Renderers/Common/GSTexture.cpp b/pcsx2/GS/Renderers/Common/GSTexture.cpp index 1a115c1f6d..5fd972bdb2 100644 --- a/pcsx2/GS/Renderers/Common/GSTexture.cpp +++ b/pcsx2/GS/Renderers/Common/GSTexture.cpp @@ -67,6 +67,7 @@ const char* GSTexture::GetFormatName(Format format) { default: pxFailRel("Invalid texture format"); + [[fallthrough]]; case Format::Invalid: return "Invalid"; case Format::Color: return "Color"; case Format::ColorHQ: return "ColorHQ"; @@ -110,6 +111,7 @@ u32 GSTexture::GetCompressedBytesPerBlock(Format format) { default: pxFailRel("Invalid texture format"); + [[fallthrough]]; case Format::Invalid: return 1; // Invalid case Format::Color: return 4; // Color/RGBA8 case Format::ColorHQ: return 4; // ColorHQ/RGB10A2 diff --git a/pcsx2/GS/Renderers/SW/GSDrawScanlineCodeGenerator.arm64.cpp b/pcsx2/GS/Renderers/SW/GSDrawScanlineCodeGenerator.arm64.cpp index c1602e442f..737839ff2f 100644 --- a/pcsx2/GS/Renderers/SW/GSDrawScanlineCodeGenerator.arm64.cpp +++ b/pcsx2/GS/Renderers/SW/GSDrawScanlineCodeGenerator.arm64.cpp @@ -2268,7 +2268,11 @@ void GSDrawScanlineCodeGenerator::ReadTexelImplLoadTexLOD(const Register& addr, { pxAssert(addr.IsX()); pxAssert(m_sel.mmin); - armAsm->Ldr(addr.W(), m_sel.lcm ? _global(lod.i.U32[lod]) : _local(temp.lod.i.U32[lod])); + // Runtime-indexed U32 lane: hand-compute the offset since `lod` isn't constexpr. + const size_t lod_lane = static_cast(lod) * sizeof(u32); + armAsm->Ldr(addr.W(), m_sel.lcm + ? MemOperand(_globals, offsetof(GSScanlineGlobalData, lod.i.U32) + lod_lane) + : MemOperand(_locals, offsetof(GSScanlineLocalData, temp.lod.i.U32) + lod_lane)); if (mip_offset != 0) armAsm->Add(addr.W(), addr.W(), mip_offset); armAsm->Ldr(addr.X(), MemOperand(_global_tex0, addr, LSL, 3)); From ecff9140a5203ee9ff7ca25a67c34f869b80e96f Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 008/292] arm64: SPU2 mixer NEON paths ARM64 NEON implementations for the SPU2 mixer hot paths. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/SPU2/Mixer.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++ pcsx2/SPU2/defs.h | 15 +++++++++++++ 2 files changed, 68 insertions(+) diff --git a/pcsx2/SPU2/Mixer.cpp b/pcsx2/SPU2/Mixer.cpp index 6e3dabc451..ded43ca984 100644 --- a/pcsx2/SPU2/Mixer.cpp +++ b/pcsx2/SPU2/Mixer.cpp @@ -9,6 +9,10 @@ #include "common/Assertions.h" +#if defined(__aarch64__) +#include +#endif + // LOOP/END sets the ENDX bit and sets NAX to LSA, and the voice is muted if LOOP is not set // LOOP seems to only have any effect on the block with LOOP/END set, where it prevents muting the voice // (the documented requirement that every block in a loop has the LOOP bit set is nonsense according to tests) @@ -157,18 +161,50 @@ static __forceinline s32 ApplyVolume(s32 data, s32 volume) return (volume * data) >> 15; } +#if defined(__aarch64__) +// NEON helper: lanewise `(volume * data) >> 15` on s32x2. Bit-exact with the +// scalar code's `mul w*, w*` + `asr 15` — narrow the s64 product to s32 first +// (matching scalar's implicit s32 truncation) then arithmetic-shift right 15. +static __forceinline int32x2_t ApplyVolumeStereoNEON(int32x2_t data, int32x2_t volume) +{ + const int64x2_t prod = vmull_s32(data, volume); + const int32x2_t lo32 = vmovn_s64(prod); + return vshr_n_s32(lo32, 15); +} +#endif + static __forceinline StereoOut32 ApplyVolume(const StereoOut32& data, const V_VolumeLR& volume) { +#if defined(__aarch64__) + const int32x2_t d = vld1_s32(&data.Left); + const int32x2_t v = vld1_s32(&volume.Left); + StereoOut32 out; + vst1_s32(&out.Left, ApplyVolumeStereoNEON(d, v)); + return out; +#else return StereoOut32( ApplyVolume(data.Left, volume.Left), ApplyVolume(data.Right, volume.Right)); +#endif } static __forceinline StereoOut32 ApplyVolume(const StereoOut32& data, const V_VolumeSlideLR& volume) { +#if defined(__aarch64__) + // V_VolumeSlide is 12 bytes; .Value is the s32 at offset 8 (Reg_VOL u16 + pad, + // then u32 Counter, then s32 Value). Build {Left.Value, Right.Value} via two + // scalar loads — cheaper than a gather, no aliasing constraints. + static_assert(sizeof(V_VolumeSlide) == 12, "V_VolumeSlide layout assumed by NEON ApplyVolume"); + const int32x2_t d = vld1_s32(&data.Left); + const int32x2_t v = { volume.Left.Value, volume.Right.Value }; + StereoOut32 out; + vst1_s32(&out.Left, ApplyVolumeStereoNEON(d, v)); + return out; +#else return StereoOut32( ApplyVolume(data.Left, volume.Left.Value), ApplyVolume(data.Right, volume.Right.Value)); +#endif } static __forceinline void UpdateBlockHeader(V_Core& thiscore, uint voiceidx) @@ -424,6 +460,22 @@ static __forceinline void MixCoreVoices(VoiceMixSet& dest, const uint coreidx) { V_Core& thiscore(Cores[coreidx]); +#if defined(__aarch64__) + // dest is {Dry.L, Dry.R, Wet.L, Wet.R} = 4 contiguous s32, and each + // V_VoiceGates entry is the same {DryL, DryR, WetL, WetR} contiguous s32x4. + // Per voice: vval = {VVal.L, VVal.R, VVal.L, VVal.R}, accum += vval & gates. + // Bit-identical to the scalar version below. + int32x4_t accum = vld1q_s32(&dest.Dry.Left); + for (uint voiceidx = 0; voiceidx < V_Core::NumVoices; ++voiceidx) + { + const StereoOut32 VVal(MixVoice(coreidx, voiceidx)); + const int32x2_t lr = vld1_s32(&VVal.Left); + const int32x4_t vval = vcombine_s32(lr, lr); + const int32x4_t gate = vld1q_s32(&thiscore.VoiceGates[voiceidx].DryL); + accum = vaddq_s32(accum, vandq_s32(vval, gate)); + } + vst1q_s32(&dest.Dry.Left, accum); +#else for (uint voiceidx = 0; voiceidx < V_Core::NumVoices; ++voiceidx) { StereoOut32 VVal(MixVoice(coreidx, voiceidx)); @@ -435,6 +487,7 @@ static __forceinline void MixCoreVoices(VoiceMixSet& dest, const uint coreidx) dest.Wet.Left += VVal.Left & thiscore.VoiceGates[voiceidx].WetL; dest.Wet.Right += VVal.Right & thiscore.VoiceGates[voiceidx].WetR; } +#endif } static __forceinline StereoOut32 MixCore(const uint coreidx, const VoiceMixSet& inVoices, const StereoOut32& Input, const StereoOut32& Ext) diff --git a/pcsx2/SPU2/defs.h b/pcsx2/SPU2/defs.h index 3e240bc3b5..e517fefea3 100644 --- a/pcsx2/SPU2/defs.h +++ b/pcsx2/SPU2/defs.h @@ -9,6 +9,10 @@ #include #include +#if defined(__aarch64__) +#include +#endif + // -------------------------------------------------------------------------------------- // SPU2 Register Table LUT // -------------------------------------------------------------------------------------- @@ -83,7 +87,18 @@ static __forceinline s32 clamp_mix(s32 x) static __forceinline StereoOut32 clamp_mix(StereoOut32 sample) { +#if defined(__aarch64__) + // vmin/vmax on s32x2 — one cycle each on A53 vs scalar cmp+csel pair × 2. + const int32x2_t v = vld1_s32(&sample.Left); + const int32x2_t lo = vdup_n_s32(-0x8000); + const int32x2_t hi = vdup_n_s32(0x7fff); + const int32x2_t out = vmin_s32(vmax_s32(v, lo), hi); + StereoOut32 r; + vst1_s32(&r.Left, out); + return r; +#else return StereoOut32(clamp_mix(sample.Left), clamp_mix(sample.Right)); +#endif } struct V_VolumeLR From d1053226cdb6f4a1466c410e4deafc0c8a045ad0 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH 009/292] arm64: VIF unpack NEON and DMA transfer fixes ARM64 NEON VIF unpack dynarec plus the VIF stat narrowing-cast fix. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/Vif.cpp | 2 +- pcsx2/arm64/Vif_UnpackNEON.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pcsx2/Vif.cpp b/pcsx2/Vif.cpp index 8cf9d401d8..460876cd28 100644 --- a/pcsx2/Vif.cpp +++ b/pcsx2/Vif.cpp @@ -240,7 +240,7 @@ __fi void vif1STAT(u32 value) VIF_LOG("VIF1_STAT write32 0x%8.8x", value); /* Only FDR bit is writable, so mask the rest */ - if ((vif1Regs.stat.FDR) ^ ((tVIF_STAT&)value).FDR) + if ((vif1Regs.stat.FDR) ^ tVIF_STAT(value).FDR) { bool isStalled = false; // different so can't be stalled diff --git a/pcsx2/arm64/Vif_UnpackNEON.cpp b/pcsx2/arm64/Vif_UnpackNEON.cpp index 0be9824e4b..7c1fb80df0 100644 --- a/pcsx2/arm64/Vif_UnpackNEON.cpp +++ b/pcsx2/arm64/Vif_UnpackNEON.cpp @@ -291,8 +291,12 @@ void VifUnpackNEON_Base::xUPK_V4_5() const armAsm->Lsr(workGprW, workGprW, 8); // A armAsm->Lsl(workGprW, workGprW, 7); // A.0000000 armAsm->Ins(destReg.V4S(), 3, workGprW); // A|B|G|R - armAsm->Shl(destReg.V4S(), destReg.V4S(), 24); // can optimize to - armAsm->Ushr(destReg.V4S(), destReg.V4S(), 24); // single AND... + // Zero the upper 24 bits of each lane (lanes carry per-channel + // Lsr/Lsl residue from the unpack chain above). Shl+Ushr is 2 + // NEON insns; an AND with a pre-built mask is the same count + // (Movi #0xFF + And) so no win — keep the shift form. + armAsm->Shl(destReg.V4S(), destReg.V4S(), 24); + armAsm->Ushr(destReg.V4S(), destReg.V4S(), 24); } void VifUnpackNEON_Base::xUnpack(int upknum) const From 8fb919e6f72eb63f92e164520fdab2b0114b9744 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:56 -0700 Subject: [PATCH 010/292] arm64: in-process recompiler differential-test + diagnostics framework DiffJitVsInterp harness (EE/IOP/VU synthetic JIT-vs-interpreter tests) plus the shared capture/divergence-localizer infrastructure (vu_capture, ee_divtrace, microVU_Divtrace, VU1Trace) and the test hooks they install in interp/GIF/COP2 paths. All hook sites are guarded by PCSX2_RECOMPILER_TESTS and compile out of release builds. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/CMakeLists.txt | 12 + pcsx2/Gif_Unit.cpp | 8 + pcsx2/Gif_Unit.h | 40 + pcsx2/Interpreter.cpp | 24 + pcsx2/R5900OpcodeImpl.cpp | 32 +- pcsx2/VU0.cpp | 42 + pcsx2/VU0microInterp.cpp | 39 + pcsx2/VU1Trace.cpp | 353 +++++ pcsx2/VU1Trace.h | 136 ++ pcsx2/VU1microInterp.cpp | 54 + pcsx2/ee_divtrace.cpp | 147 ++ pcsx2/ee_divtrace.h | 126 ++ pcsx2/microVU_Divtrace.cpp | 217 +++ pcsx2/microVU_Divtrace.h | 148 ++ pcsx2/vu_capture.cpp | 343 +++++ pcsx2/vu_capture.h | 146 ++ pcsx2/x86/microVU_Execute.inl | 13 + tests/ctest/core/CMakeLists.txt | 7 + tests/ctest/core/recompilers/CMakeLists.txt | 90 ++ .../core/recompilers/ee_rec_alu64_tests.cpp | 50 + .../core/recompilers/ee_rec_alu_imm_tests.cpp | 273 ++++ .../core/recompilers/ee_rec_alu_tests.cpp | 416 ++++++ .../core/recompilers/ee_rec_branch_tests.cpp | 532 ++++++++ .../core/recompilers/ee_rec_cop0_tests.cpp | 289 ++++ .../ee_rec_fpu_full_mode_tests.cpp | 310 +++++ .../core/recompilers/ee_rec_fpu_tests.cpp | 1145 ++++++++++++++++ .../ee_rec_harness_validation_tests.cpp | 89 ++ .../recompilers/ee_rec_iop_handoff_tests.cpp | 765 +++++++++++ .../core/recompilers/ee_rec_jump_tests.cpp | 178 +++ .../recompilers/ee_rec_loadstore_tests.cpp | 579 ++++++++ .../ee_rec_mmi_coherence_tests.cpp | 965 +++++++++++++ .../recompilers/ee_rec_mmi_simd_tests.cpp | 482 +++++++ .../core/recompilers/ee_rec_mmi_tests.cpp | 764 +++++++++++ .../core/recompilers/ee_rec_move_tests.cpp | 276 ++++ .../core/recompilers/ee_rec_muldiv_tests.cpp | 469 +++++++ .../recompilers/ee_rec_multiblock_tests.cpp | 127 ++ .../core/recompilers/ee_rec_shift_tests.cpp | 291 ++++ .../core/recompilers/ee_rec_smc_tests.cpp | 153 +++ .../recompilers/ee_rec_timeout_loop_tests.cpp | 228 ++++ .../core/recompilers/ee_rec_traps_tests.cpp | 369 +++++ .../recompilers/ee_vu0_cfc2_ctc2_tests.cpp | 280 ++++ .../recompilers/ee_vu0_cop2_macro_tests.cpp | 1213 +++++++++++++++++ .../recompilers/ee_vu0_qmfc2_qmtc2_tests.cpp | 239 ++++ .../recompilers/ee_vu1_vif_dispatch_tests.cpp | 146 ++ .../recompilers/harness/EeRecTestHarness.cpp | 885 ++++++++++++ .../recompilers/harness/EeRecTestHarness.h | 346 +++++ .../recompilers/harness/JitTestHarness.cpp | 298 ++++ .../core/recompilers/harness/JitTestHarness.h | 199 +++ .../core/recompilers/harness/MipsEncode.h | 573 ++++++++ .../harness/RecompilerTestEnvironment.cpp | 232 ++++ .../harness/RecompilerTestEnvironment.h | 55 + .../recompilers/harness/StateSnapshot.cpp | 298 ++++ .../core/recompilers/harness/StateSnapshot.h | 76 ++ .../ctest/core/recompilers/harness/VuEncode.h | 484 +++++++ .../core/recompilers/harness/VuReplay.cpp | 354 +++++ .../ctest/core/recompilers/harness/VuReplay.h | 122 ++ .../core/recompilers/harness/VuSnapshot.cpp | 241 ++++ .../core/recompilers/harness/VuSnapshot.h | 97 ++ .../recompilers/harness/VuTestHarness.cpp | 466 +++++++ .../core/recompilers/harness/VuTestHarness.h | 159 +++ .../ctest/core/recompilers/iop_alu_tests.cpp | 350 +++++ .../core/recompilers/iop_branch_tests.cpp | 228 ++++ .../recompilers/iop_cop0_exception_tests.cpp | 108 ++ .../ctest/core/recompilers/iop_cop0_tests.cpp | 104 ++ .../core/recompilers/iop_jit_fuzz_tests.cpp | 156 +++ .../ctest/core/recompilers/iop_jump_tests.cpp | 70 + .../core/recompilers/iop_loadstore_tests.cpp | 248 ++++ .../recompilers/iop_memory_access_tests.cpp | 287 ++++ .../core/recompilers/iop_muldiv_tests.cpp | 187 +++ .../core/recompilers/iop_multiblock_tests.cpp | 213 +++ .../iop_regalloc_pressure_tests.cpp | 254 ++++ .../core/recompilers/iop_shift_tests.cpp | 234 ++++ .../ctest/core/recompilers/iop_smc_tests.cpp | 194 +++ tests/ctest/core/recompilers/main.cpp | 35 + .../core/recompilers/vu0_alu_lower_tests.cpp | 666 +++++++++ .../core/recompilers/vu0_alu_upper_tests.cpp | 496 +++++++ .../recompilers/vu0_branch_delay_tests.cpp | 438 ++++++ .../recompilers/vu0_clamp_modes_tests.cpp | 243 ++++ .../recompilers/vu0_e_d_t_m_bit_tests.cpp | 290 ++++ .../recompilers/vu0_flag_pipeline_tests.cpp | 326 +++++ .../vu0_harness_validation_tests.cpp | 134 ++ .../recompilers/vu0_integer_alu_tests.cpp | 440 ++++++ .../core/recompilers/vu0_q_pipeline_tests.cpp | 334 +++++ .../core/recompilers/vu1_alu_lower_tests.cpp | 475 +++++++ .../core/recompilers/vu1_alu_upper_tests.cpp | 442 ++++++ .../recompilers/vu1_efu_p_pipeline_tests.cpp | 283 ++++ .../core/recompilers/vu1_xgkick_tests.cpp | 232 ++++ .../recompilers/vu_capture_format_tests.cpp | 183 +++ .../recompilers/vu_ftoi_saturation_tests.cpp | 114 ++ .../recompilers/vu_madda_acc_lane_tests.cpp | 147 ++ .../core/recompilers/vu_replay_tests.cpp | 150 ++ 91 files changed, 25550 insertions(+), 1 deletion(-) create mode 100644 pcsx2/VU1Trace.cpp create mode 100644 pcsx2/VU1Trace.h create mode 100644 pcsx2/ee_divtrace.cpp create mode 100644 pcsx2/ee_divtrace.h create mode 100644 pcsx2/microVU_Divtrace.cpp create mode 100644 pcsx2/microVU_Divtrace.h create mode 100644 pcsx2/vu_capture.cpp create mode 100644 pcsx2/vu_capture.h create mode 100644 tests/ctest/core/recompilers/CMakeLists.txt create mode 100644 tests/ctest/core/recompilers/ee_rec_alu64_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_alu_imm_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_alu_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_branch_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_cop0_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_iop_handoff_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_jump_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_mmi_coherence_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_mmi_simd_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_mmi_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_move_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_muldiv_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_multiblock_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_shift_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_smc_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_timeout_loop_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_rec_traps_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_vu0_qmfc2_qmtc2_tests.cpp create mode 100644 tests/ctest/core/recompilers/ee_vu1_vif_dispatch_tests.cpp create mode 100644 tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp create mode 100644 tests/ctest/core/recompilers/harness/EeRecTestHarness.h create mode 100644 tests/ctest/core/recompilers/harness/JitTestHarness.cpp create mode 100644 tests/ctest/core/recompilers/harness/JitTestHarness.h create mode 100644 tests/ctest/core/recompilers/harness/MipsEncode.h create mode 100644 tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp create mode 100644 tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.h create mode 100644 tests/ctest/core/recompilers/harness/StateSnapshot.cpp create mode 100644 tests/ctest/core/recompilers/harness/StateSnapshot.h create mode 100644 tests/ctest/core/recompilers/harness/VuEncode.h create mode 100644 tests/ctest/core/recompilers/harness/VuReplay.cpp create mode 100644 tests/ctest/core/recompilers/harness/VuReplay.h create mode 100644 tests/ctest/core/recompilers/harness/VuSnapshot.cpp create mode 100644 tests/ctest/core/recompilers/harness/VuSnapshot.h create mode 100644 tests/ctest/core/recompilers/harness/VuTestHarness.cpp create mode 100644 tests/ctest/core/recompilers/harness/VuTestHarness.h create mode 100644 tests/ctest/core/recompilers/iop_alu_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_branch_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_cop0_exception_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_cop0_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_jit_fuzz_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_jump_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_loadstore_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_memory_access_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_muldiv_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_multiblock_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_regalloc_pressure_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_shift_tests.cpp create mode 100644 tests/ctest/core/recompilers/iop_smc_tests.cpp create mode 100644 tests/ctest/core/recompilers/main.cpp create mode 100644 tests/ctest/core/recompilers/vu0_alu_lower_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_alu_upper_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_clamp_modes_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_e_d_t_m_bit_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_flag_pipeline_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_harness_validation_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_integer_alu_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu1_alu_upper_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu1_efu_p_pipeline_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu1_xgkick_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu_capture_format_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu_ftoi_saturation_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu_madda_acc_lane_tests.cpp create mode 100644 tests/ctest/core/recompilers/vu_replay_tests.cpp diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 28fce32f29..d89df09c34 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -13,6 +13,10 @@ target_compile_features(PCSX2_FLAGS INTERFACE cxx_std_17) target_compile_definitions(PCSX2_FLAGS INTERFACE "${PCSX2_DEFS}") target_compile_options(PCSX2_FLAGS INTERFACE "${PCSX2_WARNINGS}") +if(ENABLE_RECOMPILER_TEST_HOOKS) + target_compile_definitions(PCSX2_FLAGS INTERFACE PCSX2_RECOMPILER_TESTS) +endif() + # Check that people use the good file if(NOT TOP_CMAKE_WAS_SOURCED) message(FATAL_ERROR " @@ -133,7 +137,11 @@ set(pcsx2Sources VU0microInterp.cpp VU1micro.cpp VU1microInterp.cpp + VU1Trace.cpp VUflags.cpp + vu_capture.cpp + microVU_Divtrace.cpp + ee_divtrace.cpp VUmicroMem.cpp VUops.cpp ) @@ -195,10 +203,14 @@ set(pcsx2Headers SupportURLs.h StateWrapper.h Vif_Dma.h + vu_capture.h + microVU_Divtrace.h + ee_divtrace.h Vif.h Vif_Unpack.h VMManager.h vtlb.h + VU1Trace.h VUflags.h VUmicro.h VUops.h) diff --git a/pcsx2/Gif_Unit.cpp b/pcsx2/Gif_Unit.cpp index eefdd51f60..44a0332bec 100644 --- a/pcsx2/Gif_Unit.cpp +++ b/pcsx2/Gif_Unit.cpp @@ -9,6 +9,14 @@ Gif_Unit gifUnit; +#ifdef PCSX2_RECOMPILER_TESTS +namespace gif_test_hooks +{ + std::vector* g_path1_sink = nullptr; + bool g_force_path1_busy = false; +} +#endif + // Returns true on stalling SIGNAL bool Gif_HandlerAD(u8* pMem) { diff --git a/pcsx2/Gif_Unit.h b/pcsx2/Gif_Unit.h index 6b6fde5d5a..7a337d6918 100644 --- a/pcsx2/Gif_Unit.h +++ b/pcsx2/Gif_Unit.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include "Gif.h" #include "Vif.h" #include "GS.h" @@ -525,6 +526,21 @@ struct Gif_Path } }; +#ifdef PCSX2_RECOMPILER_TESTS +namespace gif_test_hooks +{ + // When non-null, Gif_Unit::TransferGSPacketData(GIF_TRANS_XGKICK, ...) + // appends the packet bytes to *g_path1_sink and returns size — bypassing + // the path-1 ring buffer + MTGS::WaitGS() that asserts when no GS thread + // is running. VuTestHarness installs/clears this pointer. + extern std::vector* g_path1_sink; + + // When true, Gif_Unit::checkPaths(p1=true, ...) reports path 1 as busy. + // Used by EeVu1Vif's Mscalf-stall test to force the GIF-busy code path. + extern bool g_force_path1_busy; +} +#endif + struct Gif_Unit { Gif_Path gifPath[3]; @@ -619,6 +635,26 @@ struct Gif_Unit // If transfer cannot take place at this moment the return value is 0 u32 TransferGSPacketData(GIF_TRANSFER_TYPE tranType, u8* pMem, u32 size, bool aligned = false) { +#ifdef PCSX2_RECOMPILER_TESTS + if (gif_test_hooks::g_path1_sink && tranType == GIF_TRANS_XGKICK) + { + // Hard cap: GIF Path 1 ring is 16 KB (one VU memory). Anything + // larger means the JIT/helper miscalculated `size` (e.g. the + // EOP-bit at bit 31 leaked through to the byte count). Don't + // allocate the bogus span; record the anomaly + bail so the + // test fails loudly instead of OOM-ing the host. + if (size > 0x4000u) + { + Console.Error( + "[gif_test_hooks] PATH1 SINK ANOMALY: tranType=0x%x size=0x%x — capping at 0", + static_cast(tranType), size); + return 0; // Drop the transfer entirely; caller should treat as no-op. + } + gif_test_hooks::g_path1_sink->insert( + gif_test_hooks::g_path1_sink->end(), pMem, pMem + size); + return size; + } +#endif if (THREAD_VU1) { @@ -700,6 +736,10 @@ struct Gif_Unit // path is not finished (needs more data/processing for an EOP) __fi int checkPaths(bool p1, bool p2, bool p3, bool checkQ = false) { +#ifdef PCSX2_RECOMPILER_TESTS + if (gif_test_hooks::g_force_path1_busy && p1) + return 1; +#endif int ret = 0; ret |= (p1 && !gifPath[GIF_PATH_1].isDone()) << 0; ret |= (p2 && !gifPath[GIF_PATH_2].isDone()) << 1; diff --git a/pcsx2/Interpreter.cpp b/pcsx2/Interpreter.cpp index 70cd58d9f2..7385978aa2 100644 --- a/pcsx2/Interpreter.cpp +++ b/pcsx2/Interpreter.cpp @@ -6,6 +6,7 @@ #include "VMManager.h" #include "Elfheader.h" #include "Cache.h" +#include "ee_divtrace.h" #include "DebugTools/Breakpoints.h" @@ -26,6 +27,18 @@ static u32 intLastBranchTo; void intEventTest(); +// Charge raw block cycles for a syscall handler the interpreter +// SKIPPED (FlushCache/iFlushCache under g_skip_flushcache_syscall), mirroring the +// JIT's recSYSCALL `s_nBlockCycles += 5650`. cpuBlockCycles is the same 3-bit +// fixed-point accumulator as the JIT's s_nBlockCycles with identical scaling +// (intUpdateCPUCycles == scaleblockcycles_calculation), so adding the same raw +// constant keeps the cycle-derived hardware (EE timers) in lockstep across the +// skip. Gated entirely by the caller; no effect in production. +void intChargeSkippedHandlerCycles(u32 raw_block_cycles) +{ + cpuBlockCycles += raw_block_cycles; +} + void intUpdateCPUCycles() { const bool lowcycles = (cpuBlockCycles <= 40); @@ -214,6 +227,17 @@ static void execI() cpuBlockCycles += opcode.cycles * (2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1)); opcode.interpret(); + +#ifdef PCSX2_RECOMPILER_TESTS + // One sample per retired instruction (including branch delay slots, which also + // flow through execI). cpuRegs.pc now points at the next instruction to execute, + // so a sample with pc=X is "architectural state just before executing X" — the + // same point the JIT block hook captures for block entry X. Off unless + // ee_divtrace::g_enabled was set for this frame (single relaxed load otherwise). + // Test-hook only — release builds drop the per-instruction probe entirely. + if (ee_divtrace::g_enabled.load(std::memory_order_relaxed)) + ee_divtrace::RecordSample(cpuRegs.pc); +#endif } static __fi void _doBranch_shared(u32 tar) diff --git a/pcsx2/R5900OpcodeImpl.cpp b/pcsx2/R5900OpcodeImpl.cpp index fecebece99..f8fe369f79 100644 --- a/pcsx2/R5900OpcodeImpl.cpp +++ b/pcsx2/R5900OpcodeImpl.cpp @@ -13,6 +13,12 @@ #include "DebugTools/Breakpoints.h" #include "Host.h" #include "VMManager.h" +#include "ee_divtrace.h" + +// Defined in Interpreter.cpp — charges raw block cycles for a skipped syscall +// handler (FlushCache/iFlushCache skip mirror). Global scope so the namespaced +// SYSCALL() below resolves it via `::`. +extern void intChargeSkippedHandlerCycles(u32); #include "fmt/format.h" @@ -914,8 +920,32 @@ void SYSCALL() else call = cpuRegs.GPR.n.v1.UC[0]; - BIOS_LOG("Bios call: %s (%x)", R5900::bios[call], call); +#ifdef PCSX2_RECOMPILER_TESTS + // Mirror the JIT's recSYSCALL FlushCache/iFlushCache skip so + // the golden interp timeline stays bit-identical to the JIT across this + // ABI-benign divergence. A bare return (no pc-=4, no cpuException) makes the + // syscall a nop and execution continues at the next instruction — exactly + // what the JIT skip does. We also charge the same 5650 raw block cycles the + // JIT does (s_nBlockCycles += 5650), so cycle-derived hardware (EE timers) + // doesn't drift between the two timelines and surface as a phantom MMIO-read + // divergence. + // + // Invariant the bare return relies on: pc already points at the *next* + // instruction. This holds for every interpreter entry into SYSCALL() — + // execI() advances cpuRegs.pc by 4 before dispatching the handler, and + // branch-delay-slot instructions are dispatched through execI() too, so + // there is no path that reaches here with pc still on the syscall. + // Doubly inert in production: compiled out unless PCSX2_RECOMPILER_TESTS, + // and gated on a default-false bool only pcsx2-eerunner ever sets. + // See ee_divtrace.h / iR5900Misc-arm64.cpp recSYSCALL. + if (ee_divtrace::g_skip_flushcache_syscall && (call == 0x64 || call == 0x68)) + { + ::intChargeSkippedHandlerCycles(5650); + return; + } +#endif + BIOS_LOG("Bios call: %s (%x)", R5900::bios[call], call); switch (static_cast(call)) { diff --git a/pcsx2/VU0.cpp b/pcsx2/VU0.cpp index fb54b46883..edcc836625 100644 --- a/pcsx2/VU0.cpp +++ b/pcsx2/VU0.cpp @@ -27,6 +27,29 @@ using namespace R5900; +// ---- pcsx2-eerunner --vu0diff per-COP2-read capture hooks (DIAGNOSTIC) ---------- +// Null in production (zero overhead). pcsx2-eerunner installs a sink so that, with +// the EE pinned to interp in both passes, every EE-interpreter COP2 *read* (QMFC2 +// reads VF[fs], CFC2 reads VI[fs]) of a freshly-run VU0 program is recorded in +// execution order. Diffing the VU0-jit pass vs the VU0-interp pass read-streams +// pins the FIRST VU0 program output the micro JIT computes differently from the +// interpreter — a live, in-context VU0-jit-vs-interp value diff the offline +// capture-replay harness can't produce (no real EE<->VU0 interleave). op: +// 0=QMFC2(VF), 1=CFC2(VI). NOTE: single-arch jit-vs-interp; valid for an +// arithmetic value bug, but a pipeline/flag/cycle-instance divergence here is +// usually shared-with-x86 noise — +// confirm arch-specificity with an arm64-jit-vs-x86-jit diff before trusting it. +#ifdef PCSX2_RECOMPILER_TESTS +typedef void (*Cop2ReadHook)(u32 ee_pc, u32 op, u32 fs, const u32* lanes); +Cop2ReadHook g_cop2ReadHook = nullptr; + +// Companion: VU0 pipeline/flag state at the read (TPC=last micro PC, Q=DIV result, +// MAC/STATUS/CLIP flags). Lets the harness tell whether a divergent VF read is driven +// by a wrong Q (broadcast-scalar pipeline) or a flag-instance handoff. +typedef void (*Cop2StateHook)(u32 tpc, u32 q, u32 mac, u32 status, u32 clip); +Cop2StateHook g_cop2StateHook = nullptr; +#endif + void COP2_BC2() { Int_COP2BC2PrintTable[_Rt_]();} void COP2_SPECIAL() { _vu0FinishMicro(); Int_COP2SPECIAL1PrintTable[_Funct_]();} @@ -120,6 +143,15 @@ void QMFC2() { _vu0FinishMicro(); } +#ifdef PCSX2_RECOMPILER_TESTS + if (g_cop2ReadHook && g_cop2StateHook) // diagnostic hooks (recompiler test harness); null in production + { + g_cop2ReadHook(cpuRegs.pc, 0, _Fs_, VU0.VF[_Fs_].UL); + g_cop2StateHook(VU0.VI[REG_TPC].UL, VU0.VI[REG_Q].UL, VU0.VI[REG_MAC_FLAG].UL, + VU0.VI[REG_STATUS_FLAG].UL, VU0.VI[REG_CLIP_FLAG].UL); + } +#endif + if (_Rt_ == 0) return; cpuRegs.GPR.r[_Rt_].UD[0] = VU0.VF[_Fs_].UD[0]; cpuRegs.GPR.r[_Rt_].UD[1] = VU0.VF[_Fs_].UD[1]; @@ -144,6 +176,15 @@ void CFC2() { _vu0FinishMicro(); } +#ifdef PCSX2_RECOMPILER_TESTS + if (g_cop2ReadHook && g_cop2StateHook) // diagnostic hooks (recompiler test harness); null in production + { + g_cop2ReadHook(cpuRegs.pc, 1, _Fs_, &VU0.VI[_Fs_].UL); + g_cop2StateHook(VU0.VI[REG_TPC].UL, VU0.VI[REG_Q].UL, VU0.VI[REG_MAC_FLAG].UL, + VU0.VI[REG_STATUS_FLAG].UL, VU0.VI[REG_CLIP_FLAG].UL); + } +#endif + if (_Rt_ == 0) return; if (_Fs_ == REG_R) @@ -200,6 +241,7 @@ void CTC2() { break; case REG_CLIP_FLAG: VU0.clipflag = cpuRegs.GPR.r[_Rt_].UL[0]; + [[fallthrough]]; default: VU0.VI[_Fs_].UL = cpuRegs.GPR.r[_Rt_].UL[0]; break; diff --git a/pcsx2/VU0microInterp.cpp b/pcsx2/VU0microInterp.cpp index 9b7502802c..b7ea93da1d 100644 --- a/pcsx2/VU0microInterp.cpp +++ b/pcsx2/VU0microInterp.cpp @@ -4,7 +4,10 @@ #include "Common.h" #include "VUmicro.h" +#include "microVU_Divtrace.h" +#include "vu_capture.h" +#include #include extern void _vuFlushAll(VURegs* VU); @@ -252,6 +255,17 @@ void InterpVU0::Execute(u32 cycles) VU0.VI[REG_TPC].UL <<= 3; VU0.flags &= ~VUFLAG_MFLAGSET; + +#ifdef PCSX2_RECOMPILER_TESTS + // Live-game capture probe — mirror of the mVU JIT-side probe in mVUexecute + // (microVU-arm64.cpp) and the VU1 interp probe. Micro-mode VU0 programs + // only; COP2 macro-mode single ops don't route through here (same as JIT + // side). No-op unless PCSX2_VU_CAPTURE_DIR / PCSX2_VU_RANK_OUT is set. + vu_capture::MaybeCapture(0, VU0.VI[REG_TPC].UL & 0xff8, cycles, + (const u8*)VU0.Micro, VU0_PROGSIZE, + (const u8*)VU0.Mem, VU0_MEMSIZE, VU0); +#endif + u64 startcycles = VU0.cycle; while ((VU0.cycle - startcycles) < cycles) { @@ -268,7 +282,32 @@ void InterpVU0::Execute(u32 cycles) if (VU0.flags & VUFLAG_MFLAGSET) break; +#ifdef PCSX2_RECOMPILER_TESTS + // REG_TPC was shifted to byte-PC at function entry; already a byte addr. + const u32 dt_pre_xPC = VU0.VI[REG_TPC].UL; +#endif vu0Exec(&VU0); + +#ifdef PCSX2_RECOMPILER_TESTS + // vudivtrace: snapshot VU0 architectural state after each interp op. + // Test-hook-only; release builds drop the per-op probe entirely. + if (mvu_divtrace::g_enabled.load(std::memory_order_relaxed) + && mvu_divtrace::g_vu_index == 0 + && mvu_divtrace::g_interp_op_idx < mvu_divtrace::g_interp_fps.size()) + { + const u32 idx = mvu_divtrace::g_interp_op_idx; + mvu_divtrace::g_interp_fps[idx] = mvu_divtrace::FingerprintRegs(vuRegs[0]); + mvu_divtrace::g_interp_xpc[idx] = dt_pre_xPC; + if (idx >= mvu_divtrace::g_full_lo && idx < mvu_divtrace::g_full_hi) + { + auto& snap = mvu_divtrace::g_interp_snaps[idx - mvu_divtrace::g_full_lo]; + std::memcpy(&snap.regs, &vuRegs[0], sizeof(VURegs)); + snap.meta_idx = 0xFFFF; + snap.pre_xPC = dt_pre_xPC; + } + ++mvu_divtrace::g_interp_op_idx; + } +#endif } VU0.VI[REG_TPC].UL >>= 3; diff --git a/pcsx2/VU1Trace.cpp b/pcsx2/VU1Trace.cpp new file mode 100644 index 0000000000..f31d6bddfb --- /dev/null +++ b/pcsx2/VU1Trace.cpp @@ -0,0 +1,353 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "VU1Trace.h" + +#ifdef PCSX2_RECOMPILER_TESTS + +#include "Common.h" +#include "VUmicro.h" +#include "DebugTools/Debug.h" + +#include +#include +#include +#include + +namespace vu1_trace { + +Ring g_vu1_ring; + +// Master gate. begin() is hoisted behind this at every call site so the +// disabled path is a single relaxed load + predicted-not-taken branch. +std::atomic g_enabled{false}; + +// Disabled by default — set fields and flip `enabled` from gdb when +// debugging a specific divergence, then run until the tripwire fires +// to capture the N dispatches leading up to the bad state. +Tripwire g_vu1_tripwire = { + /*enabled*/ false, + /*start_pc*/ 0xFFFFFFFFu, // any + /*a_vf14_w*/ 0x00000000, /*a_vf15_w*/ 0x00000000, + /*b_vf14_w*/ 0x00000000, /*b_vf15_w*/ 0x00000000, +}; + +static void check_tripwire(const Entry* e) +{ + if (!g_vu1_tripwire.enabled) + return; + if (g_vu1_ring.frozen.load(std::memory_order_relaxed)) + return; + if (g_vu1_tripwire.start_pc != 0xFFFFFFFFu && e->start_pc != g_vu1_tripwire.start_pc) + return; + + const u32 vf14w = e->vf_out[14 * 4 + 3]; + const u32 vf15w = e->vf_out[15 * 4 + 3]; + + // Both pairs guarded by "set both to 0 to disable" so the default + // all-zero tripwire isn't a footgun if someone toggles `enabled` + // without filling values in. + const bool match_a = (g_vu1_tripwire.a_vf14_w || g_vu1_tripwire.a_vf15_w) && + (vf14w == g_vu1_tripwire.a_vf14_w && vf15w == g_vu1_tripwire.a_vf15_w); + const bool match_b = (g_vu1_tripwire.b_vf14_w || g_vu1_tripwire.b_vf15_w) && + (vf14w == g_vu1_tripwire.b_vf14_w && vf15w == g_vu1_tripwire.b_vf15_w); + + if (match_a || match_b) + { + g_vu1_ring.frozen.store(true, std::memory_order_relaxed); + std::fprintf(stderr, + "vu1_trace: TRIPWIRE FIRED at seq=%u start_pc=0x%04x mode=%c (vf14.w=%08x vf15.w=%08x) — ring frozen\n", + e->seq, e->start_pc, e->mode ? e->mode : '?', vf14w, vf15w); + } +} + +void reset() +{ + for (u32 i = 0; i < kRingSize; i++) + g_vu1_ring.entries[i].seq = 0; + g_vu1_ring.next_seq.store(0, std::memory_order_relaxed); + g_vu1_ring.frozen.store(false, std::memory_order_relaxed); +} + +static void snapshot_entry_state(Entry* e) +{ + std::memcpy(e->vf_in, &VU1.VF[0], sizeof(e->vf_in)); + for (int i = 0; i < 16; i++) + e->vi_in[i] = VU1.VI[i].UL; + std::memcpy(e->acc_in, &VU1.ACC, sizeof(e->acc_in)); + e->q_in = VU1.VI[REG_Q].UL; + e->mac_in = VU1.VI[REG_MAC_FLAG].UL; + e->clip_in = VU1.VI[REG_CLIP_FLAG].UL; + e->status_in = VU1.VI[REG_STATUS_FLAG].UL; + e->cycles_at_entry = VU1.cycle; + std::memcpy(e->mem_in, VU1.Mem, kDataMemCap); +} + +Entry* begin(char mode, u32 start_pc, u32 cycles) +{ + if (g_vu1_ring.frozen.load(std::memory_order_relaxed)) + return nullptr; + + // Single ticket → slot index and seq both derive from it. seq=0 is + // reserved for "empty slot", so seq = ticket + 1. + const u32 ticket = g_vu1_ring.next_seq.fetch_add(1, std::memory_order_relaxed); + const u32 idx = ticket % kRingSize; + Entry* e = &g_vu1_ring.entries[idx]; + + e->seq = ticket + 1; + e->mode = mode; + e->start_pc = start_pc; + e->end_pc = 0; + e->cycles_in = cycles; + e->cycles_at_exit = 0; + + // Copy microprogram bytes from VU1.Micro starting at start_pc. + const u32 pc_masked = start_pc & VU1_PROGMASK; + const u32 avail = (pc_masked < VU1_PROGSIZE) ? (VU1_PROGSIZE - pc_masked) : 0; + const u32 to_copy = avail < kProgramCap ? avail : kProgramCap; + if (to_copy) + std::memcpy(e->program, &VU1.Micro[pc_masked], to_copy); + if (to_copy < kProgramCap) + std::memset(e->program + to_copy, 0, kProgramCap - to_copy); + e->program_size = to_copy; + + snapshot_entry_state(e); + return e; +} + +void finish(Entry* e) +{ + if (!e) + return; + std::memcpy(e->vf_out, &VU1.VF[0], sizeof(e->vf_out)); + for (int i = 0; i < 16; i++) + e->vi_out[i] = VU1.VI[i].UL; + std::memcpy(e->acc_out, &VU1.ACC, sizeof(e->acc_out)); + e->q_out = VU1.VI[REG_Q].UL; + e->mac_out = VU1.VI[REG_MAC_FLAG].UL; + e->clip_out = VU1.VI[REG_CLIP_FLAG].UL; + e->status_out = VU1.VI[REG_STATUS_FLAG].UL; + e->end_pc = VU1.VI[REG_TPC].UL; + e->cycles_at_exit = VU1.cycle; + std::memcpy(e->mem_out, VU1.Mem, kDataMemCap); + check_tripwire(e); +} + +static void fp(FILE* f, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + std::vfprintf(f, fmt, ap); + va_end(ap); +} + +void dump_vu1_entry(FILE* f, const Entry* e, bool with_disasm) +{ + if (!e || e->seq == 0) + { + fp(f, "(empty entry)\n"); + return; + } + + fp(f, "=== seq=%u mode=%c start_pc=0x%04x end_pc=0x%04x cycles_in=%u cycles_used=%llu prog_bytes=%u ===\n", + e->seq, e->mode ? e->mode : '?', e->start_pc, e->end_pc, e->cycles_in, + (unsigned long long)(e->cycles_at_exit - e->cycles_at_entry), e->program_size); + + fp(f, "VF entry:\n"); + for (int i = 0; i < 32; i++) + { + const u32* v = &e->vf_in[i * 4]; + fp(f, " VF%02d: %08x %08x %08x %08x\n", i, v[0], v[1], v[2], v[3]); + } + fp(f, "VI entry: "); + for (int i = 0; i < 16; i++) + fp(f, "VI%02d=%08x%s", i, e->vi_in[i], (i & 3) == 3 ? "\n " : " "); + fp(f, "\n"); + fp(f, "ACC entry: %08x %08x %08x %08x Q=%08x MAC=%08x CLIP=%08x STATUS=%08x\n", + e->acc_in[0], e->acc_in[1], e->acc_in[2], e->acc_in[3], + e->q_in, e->mac_in, e->clip_in, e->status_in); + + fp(f, "MEM entry (first %u bytes / 16 qwords as x y z w):\n", kDataMemCap); + { + const u32* mem = reinterpret_cast(e->mem_in); + for (u32 i = 0; i < kDataMemCap / 16; i++) + fp(f, " m%02u: %08x %08x %08x %08x\n", i, + mem[i * 4 + 0], mem[i * 4 + 1], mem[i * 4 + 2], mem[i * 4 + 3]); + } + + fp(f, "VF exit:\n"); + for (int i = 0; i < 32; i++) + { + const u32* v = &e->vf_out[i * 4]; + fp(f, " VF%02d: %08x %08x %08x %08x\n", i, v[0], v[1], v[2], v[3]); + } + fp(f, "VI exit: "); + for (int i = 0; i < 16; i++) + fp(f, "VI%02d=%08x%s", i, e->vi_out[i], (i & 3) == 3 ? "\n " : " "); + fp(f, "\n"); + fp(f, "ACC exit: %08x %08x %08x %08x Q=%08x MAC=%08x CLIP=%08x STATUS=%08x\n", + e->acc_out[0], e->acc_out[1], e->acc_out[2], e->acc_out[3], + e->q_out, e->mac_out, e->clip_out, e->status_out); + + fp(f, "MEM exit (first %u bytes / 16 qwords; * = changed since entry):\n", kDataMemCap); + { + const u32* mem_in = reinterpret_cast(e->mem_in); + const u32* mem_out = reinterpret_cast(e->mem_out); + for (u32 i = 0; i < kDataMemCap / 16; i++) + { + const bool changed = + mem_in[i * 4 + 0] != mem_out[i * 4 + 0] || + mem_in[i * 4 + 1] != mem_out[i * 4 + 1] || + mem_in[i * 4 + 2] != mem_out[i * 4 + 2] || + mem_in[i * 4 + 3] != mem_out[i * 4 + 3]; + fp(f, " %cm%02u: %08x %08x %08x %08x\n", changed ? '*' : ' ', i, + mem_out[i * 4 + 0], mem_out[i * 4 + 1], mem_out[i * 4 + 2], mem_out[i * 4 + 3]); + } + } + + if (with_disasm && e->program_size >= 8) + { + fp(f, "Program disasm:\n"); + const u32* words = reinterpret_cast(e->program); + const u32 nwords = e->program_size / 4; + bool saw_ebit = false; + for (u32 i = 0; i + 1 < nwords; i += 2) + { + const u32 lower = words[i]; + const u32 upper = words[i + 1]; + const u32 pc = e->start_pc + i * 4; + const bool has_i = (upper & (1u << 31)) != 0; + const char ebit = (upper & (1u << 30)) ? 'E' : '-'; + const char mbit = (upper & (1u << 29)) ? 'M' : '-'; + const char dbit = (upper & (1u << 28)) ? 'D' : '-'; + const char tbit = (upper & (1u << 27)) ? 'T' : '-'; + const char ibit = has_i ? 'I' : '-'; + + // disVU1MicroUF/LF use a static buffer; copy each result before + // the next call clobbers it. + char ubuf[256], lbuf[256]; + const char* u = disVU1MicroUF(upper, pc + 4); + std::strncpy(ubuf, u ? u : "?", sizeof(ubuf) - 1); + ubuf[sizeof(ubuf) - 1] = 0; + + // I-bit: lower word is a 32-bit float literal loaded into VI[REG_I], + // not an opcode. Render the float instead of running the disasm. + if (has_i) + { + float fval; + std::memcpy(&fval, &lower, sizeof(fval)); + std::snprintf(lbuf, sizeof(lbuf), "I = %g (0x%08x)", (double)fval, lower); + } + else + { + const char* l = disVU1MicroLF(lower, pc); + std::strncpy(lbuf, l ? l : "?", sizeof(lbuf) - 1); + lbuf[sizeof(lbuf) - 1] = 0; + } + + fp(f, " %04x: %08x %08x [%c%c%c%c%c] U:%s L:%s\n", + pc, lower, upper, ibit, ebit, mbit, dbit, tbit, ubuf, lbuf); + + // The E-bit fires a two-step countdown (VU0microInterp.cpp: ebit=2 + // on the E-bit op, terminates after the *next* pair executes), so + // print one more pair after the E-bit op — otherwise the last + // visible op isn't the last executed one and the trace misleads. + if (saw_ebit) + break; + if (ebit == 'E') + saw_ebit = true; + } + } + fp(f, "\n"); +} + +void dump_vu1_trace(FILE* f, u32 last_n) +{ + if (last_n == 0 || last_n > kRingSize) + last_n = kRingSize; + + // Rank by seq so concurrent writes (MTGS / MTVU racing the dump) can't + // reorder the output. Iterate all slots, sort non-empty by seq desc, + // print the top last_n. + const u32 head = g_vu1_ring.next_seq.load(std::memory_order_relaxed); + + std::array idxs; + u32 nfilled = 0; + u32 nrec = 0, ninterp = 0; + for (u32 i = 0; i < kRingSize; i++) + { + const Entry& e = g_vu1_ring.entries[i]; + if (e.seq == 0) + continue; + idxs[nfilled++] = i; + if (e.mode == 'r') nrec++; + else if (e.mode == 'i') ninterp++; + } + + // Pick a filename if the caller didn't provide a FILE*. + bool owns_file = false; + if (!f) + { + const char* path; + if (nrec > 0 && ninterp == 0) + path = "/tmp/vu1_trace_jit.log"; + else if (ninterp > 0 && nrec == 0) + path = "/tmp/vu1_trace_interp.log"; + else if (nrec >= ninterp) + path = "/tmp/vu1_trace_jit.log"; + else + path = "/tmp/vu1_trace_interp.log"; + + f = std::fopen(path, "w"); + if (!f) + { + std::fprintf(stderr, "vu1_trace: failed to open %s\n", path); + return; + } + std::fprintf(stderr, "vu1_trace: writing %u entries (rec=%u interp=%u) to %s\n", + std::min(last_n, nfilled), nrec, ninterp, path); + owns_file = true; + } + + fp(f, "vu1_trace: head=%u last_n=%u rec=%u interp=%u frozen=%d\n", + head, last_n, nrec, ninterp, g_vu1_ring.frozen.load(std::memory_order_relaxed) ? 1 : 0); + + std::sort(idxs.begin(), idxs.begin() + nfilled, + [](u32 a, u32 b) { + return g_vu1_ring.entries[a].seq > g_vu1_ring.entries[b].seq; + }); + + const u32 n = std::min(last_n, nfilled); + for (u32 k = 0; k < n; k++) + dump_vu1_entry(f, &g_vu1_ring.entries[idxs[k]], /*with_disasm=*/true); + + std::fflush(f); + if (owns_file) + std::fclose(f); +} + +} // namespace vu1_trace + +// gdb-friendly no-arg wrapper. C linkage avoids name mangling and default-arg +// resolution issues with `call` from gdb. +extern "C" void dump_vu1_trace() +{ + vu1_trace::dump_vu1_trace(nullptr, 32); +} + +extern "C" void vu1_trace_reset() +{ + vu1_trace::reset(); +} + +extern "C" void vu1_trace_enable() +{ + vu1_trace::g_enabled.store(true, std::memory_order_relaxed); +} + +extern "C" void vu1_trace_disable() +{ + vu1_trace::g_enabled.store(false, std::memory_order_relaxed); +} + +#endif // PCSX2_RECOMPILER_TESTS diff --git a/pcsx2/VU1Trace.h b/pcsx2/VU1Trace.h new file mode 100644 index 0000000000..2e25788fab --- /dev/null +++ b/pcsx2/VU1Trace.h @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU1 microprogram trace ring buffer. +// +// Records each VU1 dispatch (rec or interp) with the full microprogram +// bytes, entry/exit register state, and cycle counts. Designed to be +// inspected from gdb during a stopped-frame debug session — pause the +// emulator while the frame of interest is rendering, then `call dump_vu1_trace()` +// to print the most recent dispatches with VU disassembly. + +#pragma once + +#ifdef PCSX2_RECOMPILER_TESTS + +#include "common/Pcsx2Defs.h" +#include +#include + +namespace vu1_trace { + +constexpr u32 kRingSize = 128; +constexpr u32 kProgramCap = 4096; // bytes; covers most microprograms +constexpr u32 kDataMemCap = 256; // bytes of VU1.Mem snapshotted (first 16 qwords) + +struct Entry +{ + u32 seq; // monotonic sequence number; 0 = empty slot + char mode; // 'r' = JIT rec, 'i' = interp, 0 = empty + char _pad[3]; + u32 start_pc; // input start PC (in bytes, post-shift) + u32 end_pc; // VI[REG_TPC] at exit (in bytes, post-shift) + u32 cycles_in; // input cycle budget + u64 cycles_at_entry; + u64 cycles_at_exit; + u32 program_size; // bytes copied into program[] + u8 program[kProgramCap]; + + // Entry state snapshot + u32 vf_in[32 * 4]; // VFs as u32 quads + u32 vi_in[16]; + u32 acc_in[4]; + u32 q_in; + u32 mac_in; + u32 clip_in; + u32 status_in; + u8 mem_in[kDataMemCap]; // first 256 bytes of VU1.Mem at entry + + // Exit state snapshot + u32 vf_out[32 * 4]; + u32 vi_out[16]; + u32 acc_out[4]; + u32 q_out; + u32 mac_out; + u32 clip_out; + u32 status_out; + u8 mem_out[kDataMemCap]; // first 256 bytes of VU1.Mem at exit +}; + +struct Ring +{ + Entry entries[kRingSize]; + // Single ticket counter — slot index and seq are both derived from it, + // so they can't drift apart under concurrent writes (MTGS / MTVU / EE). + std::atomic next_seq; + // Tripwire: once set, begin() returns nullptr so the ring stops + // overwriting itself. Lets the dump capture the N dispatches leading + // up to a known-bad state instead of the noise afterwards. + std::atomic frozen; +}; + +extern Ring g_vu1_ring; + +// Tripwire condition. When `enabled` is true, finish() freezes the ring +// after a dispatch whose start_pc + exit-VF state matches one of two +// (vf14.w, vf15.w) signature pairs — useful for capturing the N dispatches +// leading up to a divergence between two runs (e.g. JIT-broken signature +// in pair A, interp-correct signature in pair B; same binary catches +// either). Disabled by default; set the fields and flip `enabled` from +// gdb when debugging. +struct Tripwire +{ + bool enabled; + u32 start_pc; // match start_pc; 0xFFFFFFFF = any + // Pair A — set both to 0 to disable this pair. + u32 a_vf14_w; + u32 a_vf15_w; + // Pair B — set both to 0 to disable this pair. + u32 b_vf14_w; + u32 b_vf15_w; +}; +extern Tripwire g_vu1_tripwire; + +// Master gate. Default false — recording is ~5 KB of memcpy per dispatch, +// not free. Flip from gdb (`call vu1_trace_enable()`) when actively +// debugging a VU1 divergence; leave off in normal play and perf captures. +extern std::atomic g_enabled; + +// Open a new entry; snapshots VU1 state and copies program bytes. +// Returns nullptr if tracing should be skipped (ring frozen). +Entry* begin(char mode, u32 start_pc, u32 cycles); + +// Snapshot exit state into entry, then check tripwire. Safe to call with nullptr. +void finish(Entry* e); + +// Re-arm: zero all entries, reset seq, clear frozen flag. +void reset(); + +// gdb-callable. Print the last `last_n` entries from the ring. +// Pass f=nullptr to auto-write to /tmp/vu1_trace_jit.log or +// /tmp/vu1_trace_interp.log based on which mode dominates the ring. +// Pass stderr/stdout/your-own-FILE* to override. +void dump_vu1_trace(FILE* f = nullptr, u32 last_n = 32); + +} // namespace vu1_trace + +// True no-arg entry point — C linkage so gdb's `call` works without +// arg-resolving the C++ default args. Equivalent to dump_vu1_trace(nullptr, 32). +extern "C" void dump_vu1_trace(); + +// gdb-callable: re-arm the tripwire (clears ring + frozen flag). +extern "C" void vu1_trace_reset(); + +// gdb-callable: flip the master gate. Default-off; call enable() before +// running into the frame of interest, then dump_vu1_trace() once paused. +extern "C" void vu1_trace_enable(); +extern "C" void vu1_trace_disable(); + +namespace vu1_trace { + +// Print one entry with optional inline VU disasm. +void dump_vu1_entry(FILE* f, const Entry* e, bool with_disasm); + +} // namespace vu1_trace + +#endif // PCSX2_RECOMPILER_TESTS diff --git a/pcsx2/VU1microInterp.cpp b/pcsx2/VU1microInterp.cpp index fde3451226..4a9b0f3029 100644 --- a/pcsx2/VU1microInterp.cpp +++ b/pcsx2/VU1microInterp.cpp @@ -4,10 +4,14 @@ #include "Common.h" #include "VUmicro.h" +#include "VU1Trace.h" #include "GS.h" #include "Gif_Unit.h" #include "MTVU.h" +#include "microVU_Divtrace.h" +#include "vu_capture.h" +#include #include extern void _vuFlushAll(VURegs* VU); @@ -261,6 +265,24 @@ void InterpVU1::Execute(u32 cycles) const FPControlRegisterBackup fpcr_backup(EmuConfig.Cpu.VU1FPCR); VU1.VI[REG_TPC].UL <<= 3; +#ifdef PCSX2_RECOMPILER_TESTS + vu1_trace::Entry* trace = vu1_trace::g_enabled.load(std::memory_order_relaxed) + ? vu1_trace::begin('i', VU1.VI[REG_TPC].UL, cycles) + : nullptr; +#endif + +#ifdef PCSX2_RECOMPILER_TESTS + // Live-game capture probe — mirror of the mVU JIT-side probe in + // mVUexecute (microVU-arm64.cpp). Lets a full-interpreter boot still dump + // VU programs from games that crash under JIT before reaching VU-heavy + // code, so the captures can be replayed through pcsx2-vurunner --diff. + // No-op unless PCSX2_VU_CAPTURE_DIR / PCSX2_VU_RANK_OUT is set. REG_TPC is + // already byte-PC here (shifted at function entry above); mask to match the JIT key. + vu_capture::MaybeCapture(1, VU1.VI[REG_TPC].UL & 0x3ff8, cycles, + (const u8*)VU1.Micro, VU1_PROGSIZE, + (const u8*)VU1.Mem, VU1_MEMSIZE, VU1); +#endif + u64 startcycles = VU1.cycle; while ((VU1.cycle - startcycles) < cycles) @@ -274,8 +296,40 @@ void InterpVU1::Execute(u32 cycles) } break; } +#ifdef PCSX2_RECOMPILER_TESTS + // Capture xPC of the op we're ABOUT to execute, for divtrace alignment. + // REG_TPC was shifted to byte-PC at function entry, + // so it already holds the byte address — do NOT shift again. + const u32 dt_pre_xPC = VU1.VI[REG_TPC].UL; +#endif Step(); + +#ifdef PCSX2_RECOMPILER_TESTS + // vudivtrace: snapshot VU1 architectural state after each interp op + // so the driver can compare against the JIT's per-op snapshot stream. + // Test-hook-only; release builds drop the per-op probe entirely. + if (mvu_divtrace::g_enabled.load(std::memory_order_relaxed) + && mvu_divtrace::g_vu_index == 1 + && mvu_divtrace::g_interp_op_idx < mvu_divtrace::g_interp_fps.size()) + { + const u32 idx = mvu_divtrace::g_interp_op_idx; + mvu_divtrace::g_interp_fps[idx] = mvu_divtrace::FingerprintRegs(vuRegs[1]); + mvu_divtrace::g_interp_xpc[idx] = dt_pre_xPC; + if (idx >= mvu_divtrace::g_full_lo && idx < mvu_divtrace::g_full_hi) + { + auto& snap = mvu_divtrace::g_interp_snaps[idx - mvu_divtrace::g_full_lo]; + std::memcpy(&snap.regs, &vuRegs[1], sizeof(VURegs)); + snap.meta_idx = 0xFFFF; + snap.pre_xPC = dt_pre_xPC; + } + ++mvu_divtrace::g_interp_op_idx; + } +#endif } VU1.VI[REG_TPC].UL >>= 3; VU1.nextBlockCycles = (VU1.cycle - cpuRegs.cycle) + 1; + +#ifdef PCSX2_RECOMPILER_TESTS + vu1_trace::finish(trace); +#endif } diff --git a/pcsx2/ee_divtrace.cpp b/pcsx2/ee_divtrace.cpp new file mode 100644 index 0000000000..a5929effc4 --- /dev/null +++ b/pcsx2/ee_divtrace.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "ee_divtrace.h" + +#include "Memory.h" +#include "MemoryTypes.h" + +#include + +// Self-contained xxh3 (same guard pattern as GS/GSXXH.h) for the memory hash. +#ifndef XXH_versionNumber + #define XXH_STATIC_LINKING_ONLY 1 + #define XXH_INLINE_ALL 1 + #include +#endif + +namespace ee_divtrace +{ + std::atomic g_enabled{false}; + bool g_emit_block_hook = false; + bool g_fp_exclude = false; + bool g_skip_flushcache_syscall = false; + std::vector g_stream; + std::vector g_snaps; + u32 g_full_lo = 0; + u32 g_full_hi = 0; + + // FNV-1a style mix, matching microVU_Divtrace so the two diagnostics read + // the same way. + static inline u64 hash_mix(u64 h, u32 v) + { + h ^= static_cast(v); + h *= 1099511628211ull; + return h; + } + static inline u64 hash_mix64(u64 h, u64 v) + { + h = hash_mix(h, static_cast(v)); + h = hash_mix(h, static_cast(v >> 32)); + return h; + } + + u64 FingerprintCpu() + { + u64 h = 1469598103934665603ull; + // GPRs — full 128 bits each (.UD[0]/.UD[1]). + for (int i = 0; i < 32; ++i) + { + // Skip k0/k1 (26/27): the EE kernel exception handler loads EPC/Cause + // into these reserved scratch registers, so they inherit the same + // cycle-phase interrupt noise as EPC and linger into user code until + // overwritten; they reconverge by frame end. Excluding them from the + // alignment fingerprint lets the zoom walk through every interrupt + // handler. (The frame-level full-snapshot comparison still reports them.) + if (i == 26 || i == 27) + continue; + h = hash_mix64(h, cpuRegs.GPR.r[i].UD[0]); + h = hash_mix64(h, cpuRegs.GPR.r[i].UD[1]); + } + h = hash_mix64(h, cpuRegs.HI.UD[0]); + h = hash_mix64(h, cpuRegs.HI.UD[1]); + h = hash_mix64(h, cpuRegs.LO.UD[0]); + h = hash_mix64(h, cpuRegs.LO.UD[1]); + // CP0 — skip the dispatcher-driven counters: + // 1 Random, 9 Count, 11 Compare, plus 13 Cause + 14 EPC (interrupt + // TIMING: EPC/Cause IP bits are a pure function of cpuRegs.cycle phase, + // so JIT/interp take the same interrupt a few cycles apart in an idle + // poll loop; excluding them keeps the streams aligned across every + // interrupt instead of breaking at each one). + for (int i = 0; i < 32; ++i) + { + if (i == 1 || i == 9 || i == 11 || i == 13 || i == 14) + continue; + h = hash_mix(h, cpuRegs.CP0.r[i]); + } + // FPU register file + accumulator (control regs are excluded). Omitted + // when g_fp_exclude is set, so the alignment walk skips past FP-register + // divergences to hunt a non-FP (integer/control) divergence. + if (!g_fp_exclude) + { + for (int i = 0; i < 32; ++i) + h = hash_mix(h, fpuRegs.fpr[i].UL); + h = hash_mix(h, fpuRegs.ACC.UL); + } + h = hash_mix(h, cpuRegs.pc); + h = hash_mix(h, cpuRegs.sa); + return h; + } + + void RecordSample(u32 pc) + { + const size_t idx = g_stream.size(); + Sample s; + s.cycle = cpuRegs.cycle; + s.fp = FingerprintCpu(); + s.pc = pc; + s._pad = 0; + g_stream.push_back(s); + + if (idx >= g_full_lo && idx < g_full_hi) + { + FullSnap& fs = g_snaps[idx - g_full_lo]; + std::memcpy(&fs.cpu, &cpuRegs, sizeof(cpuRegisters)); + std::memcpy(&fs.fpu, &fpuRegs, sizeof(fpuRegisters)); + fs.cycle = cpuRegs.cycle; + fs.pc = pc; + fs._pad = 0; + } + } + + u64 HashMemory() + { + if (!eeMem) + return 0; + // Real PS2 main RAM is 32 MB (TotalRam is the 128 MB address-wrap span; + // only the first MainRam bytes are physical). Scratchpad is 16 KB. + u64 h = XXH3_64bits(eeMem->Main, Ps2MemSize::MainRam); + h = XXH3_64bits_withSeed(eeMem->Scratch, Ps2MemSize::Scratch, h); + return h; + } + + void Reset() + { + g_stream.clear(); + if (!g_snaps.empty()) + g_snaps.assign(g_snaps.size(), FullSnap{}); + } + + void ConfigureFullWindow(u32 lo, u32 len) + { + g_full_lo = lo; + g_full_hi = lo + len; + g_snaps.assign(len, FullSnap{}); + } + + void ReserveStream(size_t samples) + { + g_stream.reserve(samples); + } +} // namespace ee_divtrace + +extern "C" void ee_divtrace_jit_block_hook(u32 startpc) +{ + if (ee_divtrace::g_enabled.load(std::memory_order_relaxed)) + ee_divtrace::RecordSample(startpc); +} diff --git a/pcsx2/ee_divtrace.h b/pcsx2/ee_divtrace.h new file mode 100644 index 0000000000..49d402ad3d --- /dev/null +++ b/pcsx2/ee_divtrace.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +// ee_divtrace — EE (R5900) JIT-vs-interpreter divergence trace. +// +// Records a stream of architectural fingerprints from a deterministic +// full-system run so that an interpreter "golden" run and a JIT run, replayed +// from the same savestate, can be diffed OFFLINE (never live / in lockstep). +// The shared clock cpuRegs.cycle plus cpuRegs.pc is the alignment key between the two runs. +// +// Three-level funnel, all bounded in memory (pcsx2-eerunner drives it): +// 1. per-FRAME — runner hashes FingerprintCpu()+HashMemory() once per frame +// over the whole run; cheap; localizes the divergent frame. +// 2. per-OP — within the one divergent frame, the capture sites append a +// Sample (cycle, pc, fp) per executed op (interp, dense) and +// per block dispatch (JIT, sparse). Aligned by (cycle, pc). +// 3. full-snap — a small window (ConfigureFullWindow) around the first +// divergent op stores whole cpuRegisters+fpuRegisters for the +// register-level report. +// +// Capture sites: +// pcsx2/Interpreter.cpp — interp, after execI() (dense) +// pcsx2/arm64/iR5900-arm64.cpp — JIT, C-hook at the block dispatcher (sparse) +// +// fp hashes EXACTLY the fields DiffEe (harness/StateSnapshot.cpp) compares — +// GPR (both doublewords), HI/LO, CP0 except {1 Random, 9 Count, 11 Compare}, +// FPR, FPU ACC, pc, sa — so a fingerprint mismatch is a genuine architectural +// divergence, not dispatcher-bookkeeping noise. + +#include "common/Pcsx2Defs.h" +#include "R5900.h" + +#include +#include + +namespace ee_divtrace +{ + // One architectural sample, ~24 bytes. + struct Sample + { + u64 cycle; // cpuRegs.cycle at the sample + u64 fp; // FingerprintCpu() + u32 pc; // cpuRegs.pc at the sample (next instr / block entry) + u32 _pad; + }; + + // Full architectural snapshot — stored only inside the detail window. + struct FullSnap + { + cpuRegisters cpu; + fpuRegisters fpu; + u64 cycle; + u32 pc; + u32 _pad; + }; + + // Gates the per-op/per-block capture sites. Read on the hot path, so the + // site is `if (g_enabled.load(relaxed)) RecordSample();`. The runner sets + // it true only for the single frame being finely traced. + extern std::atomic g_enabled; + + // Current pass's sample stream (interp run, then JIT run — separate runs). + extern std::vector g_stream; + + // Detail-window full snapshots; g_snaps[idx - g_full_lo] for idx in window. + extern std::vector g_snaps; + extern u32 g_full_lo; + extern u32 g_full_hi; + + // When set, recRecompile emits a block-entry hook into every EE block + // prologue (pcsx2-eerunner triage build). Read at codegen time, so it must + // be set before the recompiler is initialized / reset. Default false → + // production recompiles emit nothing. Checked in pcsx2/arm64/iR5900-arm64.cpp. + extern bool g_emit_block_hook; + + // When set, FingerprintCpu() omits the FPU register file + ACC from the hash, + // so FP-register divergences do not break the alignment walk. Used to hunt a + // NON-FP (integer / control-flow) EE-jit divergence when the FP path is known + // benign (e.g. Burnout 3: the hang persists with the EE-FPU fully converged to + // the interp, so the cond_b off-by-0xC0 is an integer/pointer bug, and the + // pervasive 1-ULP div.s noise just masks it). The eerunner sets this from + // EERUNNER_NOFP; the Main.cpp diff helpers honor it too for consistent reports. + extern bool g_fp_exclude; + + // When set, the interpreter's SYSCALL handler skips FlushCache (0x64) / + // iFlushCache (0x68) — returning without raising the syscall exception — + // exactly as the JIT's recSYSCALL does when $v1 is a known const. This keeps + // the golden interp timeline in lockstep with the JIT across that ABI-benign + // divergence (the JIT skip leaves caller-saved v0/v1/at/t0/t1 + EPC and the + // BIOS exception-frame memory untouched; running the real handler in interp + // only would desync both register state AND the kernel stack, masking real + // codegen bugs downstream). Set by pcsx2-eerunner; default false → production + // interp runs the real handler. Read in pcsx2/R5900OpcodeImpl.cpp SYSCALL(). + extern bool g_skip_flushcache_syscall; + + // Hash live cpuRegs+fpuRegs over exactly DiffEe's compared field set. + u64 FingerprintCpu(); + + // Append one Sample (cycle, pc, fp) from live cpuRegs, plus a FullSnap when + // the new index is in the detail window. `pc` is passed explicitly because + // the JIT block hook knows its startpc at compile time but cpuRegs.pc is + // not reliable on a statically-linked entry. The caller (JIT site) must + // flush its pinned cycle register to cpuRegs.cycle first. + void RecordSample(u32 pc); + + // xxh3 of EE main RAM (32 MB) + scratchpad (16 KB) — frame memory fingerprint. + u64 HashMemory(); + + // Clear g_stream; zero the used portion of the detail window. + void Reset(); + + // Set the detail window to [lo, lo+len); (re)allocates g_snaps to `len`. + void ConfigureFullWindow(u32 lo, u32 len); + + // Reserve g_stream capacity (samples) up front so the fine pass doesn't + // reallocate mid-frame. Called by the runner before a traced frame. + void ReserveStream(size_t samples); +} // namespace ee_divtrace + +// C-callable JIT block-entry hook (plain symbol for armEmitCall). Emitted into +// every EE block prologue when ee_divtrace::g_emit_block_hook is set. Checks +// g_enabled and records a sample for block `startpc`. cpuRegs.cycle must +// already have been flushed from the pinned cycle register by the caller. +extern "C" void ee_divtrace_jit_block_hook(u32 startpc); diff --git a/pcsx2/microVU_Divtrace.cpp b/pcsx2/microVU_Divtrace.cpp new file mode 100644 index 0000000000..d05dfa6fa6 --- /dev/null +++ b/pcsx2/microVU_Divtrace.cpp @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "microVU_Divtrace.h" + +// Test-hook only: the whole TU is dead weight in a normal build. Every consumer +// (the arm64 JIT emit site, the VU0/VU1 interp sites, EnterMode via the replay +// driver) is already gated on PCSX2_RECOMPILER_TESTS. The header stays +// unconditional so microVU_IR-arm64.h::snapshotMaps() keeps the AllocSnapshot +// type. Mirrors VU1Trace.cpp. +#ifdef PCSX2_RECOMPILER_TESTS + +#include "VU.h" +#include "VUmicro.h" + +#include +#include +#include +#include +#include + +extern VURegs vuRegs[2]; + +namespace mvu_divtrace +{ + std::atomic g_enabled{false}; + int g_vu_index = 0; + std::vector g_meta; + std::vector g_jit_snaps; + std::vector g_interp_snaps; + std::atomic g_jit_snap_idx{0}; + u32 g_interp_op_idx = 0; + std::vector g_jit_fps; + std::vector g_interp_fps; + std::vector g_jit_xpc; + std::vector g_interp_xpc; + u32 g_full_lo = 0; + u32 g_full_hi = 0; + + // Default full-snapshot window length. Each StateSnap is ~3 KB; the + // two-pass driver overrides this to a small window around the first + // divergence, so this only matters for the single-pass path. + constexpr u32 kSnapCapacity = 65536; + + // Fingerprint stream capacity (executions). u64 fp + u32 xpc = 12 B/op, + // so 8 M ≈ 96 MB/side — covers multi-million-op vumain loops that overflow + // the full-snapshot stream. + constexpr u32 kFpCapacity = 8u * 1024u * 1024u; + + // FNV-style mix. + static inline u64 hash_mix(u64 h, u32 v) + { + h ^= static_cast(v); + h *= 1099511628211ull; + return h; + } + + static inline bool isFullWidthVi(int i) + { + return i == REG_R || i == REG_I || i == REG_Q || i == REG_P + || i == REG_STATUS_FLAG || i == REG_MAC_FLAG || i == REG_CLIP_FLAG + || i == REG_TPC || i == REG_FBRST || i == REG_VPU_STAT; + } + + u64 FingerprintRegs(const VURegs& r) + { + u64 h = 1469598103934665603ull; + for (int i = 0; i < 32; ++i) + { + h = hash_mix(h, r.VF[i].UL[0]); + h = hash_mix(h, r.VF[i].UL[1]); + h = hash_mix(h, r.VF[i].UL[2]); + h = hash_mix(h, r.VF[i].UL[3]); + } + h = hash_mix(h, r.ACC.UL[0]); + h = hash_mix(h, r.ACC.UL[1]); + h = hash_mix(h, r.ACC.UL[2]); + h = hash_mix(h, r.ACC.UL[3]); + // Skip the pipeline-state VI slots {16,17,18,22,23,26} (flag/Q/P/TPC), + // which carry timing-dependent noise, so a fingerprint mismatch is a + // real architectural divergence. + for (int i = 0; i < 32; ++i) + { + if (i == 16 || i == 17 || i == 18 || i == 22 || i == 23 || i == 26) + continue; + const u32 mask = isFullWidthVi(i) ? 0xFFFFFFFFu : 0x0000FFFFu; + h = hash_mix(h, r.VI[i].UL & mask); + } + return h; + } + + void ConfigureFullWindow(u32 lo, u32 len) + { + g_full_lo = lo; + g_full_hi = lo + len; + g_jit_snaps.assign(len, StateSnap{}); + g_interp_snaps.assign(len, StateSnap{}); + } + + void Reset() + { + g_meta.clear(); + const u32 prev_jit = g_jit_snap_idx.load(std::memory_order_relaxed); + for (u32 i = 0; i < prev_jit && i < g_jit_snaps.size(); ++i) + std::memset(&g_jit_snaps[i], 0, sizeof(StateSnap)); + for (u32 i = 0; i < g_interp_op_idx && i < g_interp_snaps.size(); ++i) + std::memset(&g_interp_snaps[i], 0, sizeof(StateSnap)); + g_jit_snap_idx.store(0, std::memory_order_relaxed); + g_interp_op_idx = 0; + } + + namespace + { + struct sigaction s_prev_handler{}; + bool s_installed = false; + + // brk #imm16 encoding (AArch64 BRK exception): 0xD4200000 | (imm16 << 5). + void SigtrapHandler(int /*signo*/, siginfo_t* /*info*/, void* ucontext_v) + { +#if defined(__aarch64__) + if (!g_enabled.load(std::memory_order_relaxed)) + { + // Not our trap — restore the disposition we saved at install + // time and re-raise, so the prior handler (a debugger's, or + // the default abort) actually gets it. Resetting to SIG_DFL + // here would silently drop any pre-existing SIGTRAP handler. + sigaction(SIGTRAP, &s_prev_handler, nullptr); + std::raise(SIGTRAP); + return; + } + auto* uc = static_cast(ucontext_v); + const u64 pc = uc->uc_mcontext.pc; + u32 insn = 0; + std::memcpy(&insn, reinterpret_cast(pc), sizeof(insn)); + // Validate it's actually a BRK (top 16 bits == 0xD420). + if ((insn & 0xFFE0001Fu) != 0xD4200000u) + { + std::fprintf(stderr, + "divtrace: SIGTRAP at pc=0x%lx but instruction 0x%08x is not BRK; aborting\n", + static_cast(pc), insn); + std::abort(); + } + const u16 brk_imm = static_cast((insn >> 5) & 0xFFFFu); + const u32 idx = g_jit_snap_idx.fetch_add(1, std::memory_order_relaxed); + if (idx >= g_jit_fps.size()) + { + std::fprintf(stderr, + "divtrace: jit fingerprint stream overflow (capacity=%zu); aborting\n", + g_jit_fps.size()); + std::abort(); + } + const u32 xpc = (brk_imm < g_meta.size()) + ? g_meta[brk_imm].microvu_pc + : 0xFFFFFFFFu; + g_jit_fps[idx] = FingerprintRegs(vuRegs[g_vu_index]); + g_jit_xpc[idx] = xpc; + // Full StateSnap only inside the detail window. + if (idx >= g_full_lo && idx < g_full_hi) + { + StateSnap& snap = g_jit_snaps[idx - g_full_lo]; + std::memcpy(&snap.regs, &vuRegs[g_vu_index], sizeof(VURegs)); + snap.meta_idx = brk_imm; + snap.pre_xPC = xpc; + } + uc->uc_mcontext.pc += 4; // skip the brk +#else + // divtrace SIGTRAP capture is aarch64-only: it relies on the JIT + // emitting brk instructions and on decoding them via mcontext.pc, + // which only exists on AArch64. On other arches fall back to the + // default SIGTRAP disposition so this TU still compiles. + (void)ucontext_v; + // Chain to the saved disposition rather than dropping it to + // SIG_DFL (see the aarch64 branch above). + sigaction(SIGTRAP, &s_prev_handler, nullptr); + std::raise(SIGTRAP); +#endif + } + } + + void EnterMode(int vu_index) + { + g_vu_index = vu_index; + Reset(); + // Fingerprint streams: large, always recorded — these are what scale + // to multi-million-op loops. + g_jit_fps.assign(kFpCapacity, 0); + g_interp_fps.assign(kFpCapacity, 0); + g_jit_xpc.assign(kFpCapacity, 0); + g_interp_xpc.assign(kFpCapacity, 0); + // Default full-snapshot window: full snaps for the first kSnapCapacity + // ops. The two-pass driver overrides this via ConfigureFullWindow + // between passes. + ConfigureFullWindow(0, kSnapCapacity); + if (!s_installed) + { + struct sigaction sa{}; + sa.sa_sigaction = &SigtrapHandler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + sigaction(SIGTRAP, &sa, &s_prev_handler); + s_installed = true; + } + g_enabled.store(true, std::memory_order_release); + } + + void ExitMode() + { + g_enabled.store(false, std::memory_order_release); + if (s_installed) + { + sigaction(SIGTRAP, &s_prev_handler, nullptr); + s_installed = false; + } + } +} // namespace mvu_divtrace + +#endif // PCSX2_RECOMPILER_TESTS diff --git a/pcsx2/microVU_Divtrace.h b/pcsx2/microVU_Divtrace.h new file mode 100644 index 0000000000..6497a24876 --- /dev/null +++ b/pcsx2/microVU_Divtrace.h @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +// Per-microVU-op state-snapshot diagnostic. +// +// When mvu_divtrace::g_enabled is true, both the arm64 microVU JIT and +// the interpreter snapshot vuRegs[g_vu_index] after every microVU +// instruction. A VU capture replay driver runs the same capture through +// both paths and reports the first divergent op, with full context +// (microcode decode, allocator state at compile time, host-code +// disassembly, surrounding-op window). +// +// Consumers: +// pcsx2/VU{0,1}microInterp.cpp — interp snapshot site (after Step) +// pcsx2/arm64/microVU_Compile-arm64.inl — JIT compile-time emit (flushAll + Brk) +// pcsx2/arm64/microVU_IR-arm64.h — allocator-state snapshot helper +// +// Snapshots are populated in two paths: +// - Interp: direct memcpy after each Step() +// - JIT: emitted code does flushAll() then `brk #op_idx`; the replay +// driver's SIGTRAP handler decodes the brk imm and memcpys vuRegs. + +#include "common/Pcsx2Defs.h" +#include "VU.h" + +#include +#include +#include + +namespace mvu_divtrace +{ + // Sized to match microVU_IR-arm64.h: neonAllocTotal=28, gprAllocCount=32. + // Kept here as plain ints rather than including the arm64 header, so the + // interp + non-arm64 builds compile without dragging in vixl. + constexpr int kNeonSlots = 28; + constexpr int kGprSlots = 32; + + struct AllocSnapshot + { + struct NeonSlot + { + int vfreg; // -1=temp/free, 0=VF0, 1-31=VF, 32=ACC, 33=I + int xyzw; // 0=clean, 0xF=fully dirty, partial=other + int count; // LRU + bool isNeeded; + bool isZero; + }; + struct GprSlot + { + int vireg; // -1=unused, 0-15=VI + int count; + bool isNeeded; + bool dirty; + bool isZeroExtended; + bool usable; + }; + std::array neon{}; + std::array gpr{}; + }; + + // Per-op metadata recorded at JIT compile time. + struct OpMeta + { + u16 op_idx; // matches brk imm16 + u32 microvu_pc; // microVU PC (byte offset; what xPC macro yields) + u32 opcode; // mVU.code raw 32-bit value + const u8* host_lo; // first host byte emitted for this op + const u8* host_hi; // first host byte after the brk + AllocSnapshot alloc; // allocator state immediately before flushAll+brk + }; + + // Per-op state snapshot — VURegs payload + bookkeeping. JIT and interp + // each append one entry per op-execution, so a loop body executed K + // times produces K consecutive entries (not one overwritten K times). + // Compare jit_snaps[i] vs interp_snaps[i] for op-aligned divergence. + struct StateSnap + { + VURegs regs; + u16 meta_idx; // JIT side: index into g_meta (= brk imm16). Interp: 0xFFFF. + u32 pre_xPC; // PC of the just-executed op (for xPC alignment cross-check). + }; + + // Globals. Definitions in microVU_Divtrace.cpp. + // + // Set by the replay driver before invoking the JIT or interp: + // 1. Reset() + // 2. g_vu_index = capture's vu_index + // 3. g_enabled = true + // 4. invoke JIT (populates g_meta + g_jit_snaps) + // 5. restore pre-state, run interp (populates g_interp_snaps) + // 6. g_enabled = false + // 7. compare g_jit_snaps[i] vs g_interp_snaps[i] + extern std::atomic g_enabled; + extern int g_vu_index; + extern std::vector g_meta; + extern std::vector g_jit_snaps; // windowed full snaps; idx-g_full_lo + extern std::vector g_interp_snaps; // windowed full snaps; idx-g_full_lo + extern std::atomic g_jit_snap_idx; // bumped by SIGTRAP handler + extern u32 g_interp_op_idx; // bumped by interp Step loop + + // Compact per-op fingerprint streams (one u64 hash + one xPC per executed + // op), keyed by the same execution counter as the full snaps. These scale + // to millions of ops (~12 B/op) where the 3 KB/op full StateSnap stream + // overflows at kSnapCapacity. The fingerprint hashes the architecturally + // meaningful state — VF (4 lanes raw), ACC (4 lanes raw), and VI[i] masked + // (16-bit unless full-width) for i outside the ignored set {16,17,18,22,23,26}. + // Those ignored VI registers are the pipeline-state slots (flag/Q/P/TPC) that + // carry timing-dependent noise, so masking them means a fingerprint mismatch + // is a genuine architectural divergence rather than pipeline noise. + extern std::vector g_jit_fps; + extern std::vector g_interp_fps; + extern std::vector g_jit_xpc; // pre_xPC per executed op + extern std::vector g_interp_xpc; + + // Full-StateSnap recording window [g_full_lo, g_full_lo+len). Writers + // store a full StateSnap into g_*_snaps[idx - g_full_lo] only when the + // execution index idx falls in the window; fingerprints are always + // recorded. Pass 1 sets a zero-length window (fingerprints only); pass 2 + // sets a small window around the first divergence for the detailed report. + extern u32 g_full_lo; + extern u32 g_full_hi; // == g_full_lo + g_jit_snaps.size() + + // Fingerprint one VURegs over its architecturally meaningful state (see the + // fingerprint-stream note above for the masked/ignored VI set). + u64 FingerprintRegs(const VURegs& r); + + // Set the full-snapshot window: g_full_lo=lo, snap buffers sized to `len` + // (cleared). Call between passes. Does not touch fingerprint streams. + void ConfigureFullWindow(u32 lo, u32 len); + + // Reset per-replay counters (and zero previously-written full snaps). + // Fingerprint streams are overwritten by index, so no explicit clear. + void Reset(); + + // Install/remove the SIGTRAP handler that snapshots vuRegs[g_vu_index] + // on each JIT-emitted brk and skips it. EnterMode also sets g_enabled + // and g_vu_index; ExitMode clears g_enabled and restores the prior + // SIGTRAP disposition. Safe to call multiple times. + // + // The handler decodes brk #imm16 from the trapping instruction, treats + // imm16 as the op index, and writes vuRegs[g_vu_index] into + // g_jit_snaps[op_idx]. Out-of-range op indices are reported and the + // process aborts (this would indicate a brk emit/handler mismatch). + void EnterMode(int vu_index); + void ExitMode(); +} // namespace mvu_divtrace diff --git a/pcsx2/vu_capture.cpp b/pcsx2/vu_capture.cpp new file mode 100644 index 0000000000..d95e403da0 --- /dev/null +++ b/pcsx2/vu_capture.cpp @@ -0,0 +1,343 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "vu_capture.h" + +#ifdef PCSX2_RECOMPILER_TESTS + +#include "VU.h" +#include "VUmicro.h" // VU0_PROGSIZE / VU1_PROGSIZE / VU0_MEMSIZE / VU1_MEMSIZE + +#include "common/Console.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace vu_capture +{ + namespace + { + // Single mutex covers all WriteToFile callers so concurrent VU0/VU1 + // dispatcher probes can't interleave bytes within one file. (Different + // files would be safe to write in parallel, but the cost of one-mutex + // is trivial and the simplicity is worth it.) + std::mutex& WriterMutex() + { + static std::mutex m; + return m; + } + + u32 ExpectedSizeFor(u8 vu_index) + { + return vu_index ? VU1_PROGSIZE : VU0_PROGSIZE; // PROG == MEM size + } + } // namespace + + bool WriteToFile(const std::string& path, const CaptureRecord& rec) + { + const u32 expected = ExpectedSizeFor(rec.vu_index); + if (rec.microcode.size() != expected || rec.vumem.size() != expected) + return false; + + std::lock_guard lock(WriterMutex()); + + std::FILE* f = std::fopen(path.c_str(), "wb"); + if (!f) + return false; + + FileHeader hdr{}; + std::memcpy(hdr.magic, kMagic, sizeof(hdr.magic)); + hdr.version = kVersion; + hdr.vu_index = rec.vu_index; + hdr.start_pc = rec.start_pc; + hdr.cycle_budget = rec.cycle_budget; + hdr.microcode_size = static_cast(rec.microcode.size()); + hdr.vumem_size = static_cast(rec.vumem.size()); + + bool ok = true; + ok &= (std::fwrite(&hdr, sizeof(hdr), 1, f) == 1); + ok &= (std::fwrite(rec.microcode.data(), 1, rec.microcode.size(), f) == rec.microcode.size()); + ok &= (std::fwrite(rec.vumem.data(), 1, rec.vumem.size(), f) == rec.vumem.size()); + ok &= (std::fwrite(&rec.state, sizeof(rec.state), 1, f) == 1); + + std::fclose(f); + return ok; + } + + bool ReadFromFile(const std::string& path, CaptureRecord& rec_out) + { + std::FILE* f = std::fopen(path.c_str(), "rb"); + if (!f) + return false; + + FileHeader hdr{}; + if (std::fread(&hdr, sizeof(hdr), 1, f) != 1) + { + std::fclose(f); + return false; + } + if (std::memcmp(hdr.magic, kMagic, sizeof(hdr.magic)) != 0 || hdr.version != kVersion) + { + std::fclose(f); + return false; + } + if (hdr.vu_index > 1) + { + std::fclose(f); + return false; + } + const u32 expected = ExpectedSizeFor(hdr.vu_index); + if (hdr.microcode_size != expected || hdr.vumem_size != expected) + { + std::fclose(f); + return false; + } + + rec_out.vu_index = hdr.vu_index; + rec_out.start_pc = hdr.start_pc; + rec_out.cycle_budget = hdr.cycle_budget; + rec_out.microcode.assign(hdr.microcode_size, 0); + rec_out.vumem.assign(hdr.vumem_size, 0); + + bool ok = true; + ok &= (std::fread(rec_out.microcode.data(), 1, hdr.microcode_size, f) == hdr.microcode_size); + ok &= (std::fread(rec_out.vumem.data(), 1, hdr.vumem_size, f) == hdr.vumem_size); + ok &= (std::fread(&rec_out.state, sizeof(rec_out.state), 1, f) == 1); + + std::fclose(f); + return ok; + } + + void SnapshotState(const VURegs& regs, CapturedState& out) + { + for (int i = 0; i < 32; ++i) + { + out.VF[i][0] = regs.VF[i].UL[0]; + out.VF[i][1] = regs.VF[i].UL[1]; + out.VF[i][2] = regs.VF[i].UL[2]; + out.VF[i][3] = regs.VF[i].UL[3]; + out.VI[i] = regs.VI[i].UL; + } + out.ACC[0] = regs.ACC.UL[0]; + out.ACC[1] = regs.ACC.UL[1]; + out.ACC[2] = regs.ACC.UL[2]; + out.ACC[3] = regs.ACC.UL[3]; + out.q = regs.q.UL; + out.p = regs.p.UL; + out.pending_q = regs.pending_q; + out.pending_p = regs.pending_p; + std::memcpy(out.micro_macflags, regs.micro_macflags, sizeof(out.micro_macflags)); + std::memcpy(out.micro_clipflags, regs.micro_clipflags, sizeof(out.micro_clipflags)); + std::memcpy(out.micro_statusflags, regs.micro_statusflags, sizeof(out.micro_statusflags)); + out.xgkickaddr = regs.xgkickaddr; + out.xgkickdiff = regs.xgkickdiff; + out.xgkicksizeremaining = regs.xgkicksizeremaining; + out.xgkicklastcycle = regs.xgkicklastcycle; + out.xgkickcyclecount = regs.xgkickcyclecount; + out.xgkickenable = regs.xgkickenable; + out.xgkickendpacket = regs.xgkickendpacket; + } + + void RestoreState(const CapturedState& state, VURegs& regs) + { + for (int i = 0; i < 32; ++i) + { + regs.VF[i].UL[0] = state.VF[i][0]; + regs.VF[i].UL[1] = state.VF[i][1]; + regs.VF[i].UL[2] = state.VF[i][2]; + regs.VF[i].UL[3] = state.VF[i][3]; + regs.VI[i].UL = state.VI[i]; + } + regs.ACC.UL[0] = state.ACC[0]; + regs.ACC.UL[1] = state.ACC[1]; + regs.ACC.UL[2] = state.ACC[2]; + regs.ACC.UL[3] = state.ACC[3]; + regs.q.UL = state.q; + regs.p.UL = state.p; + regs.pending_q = state.pending_q; + regs.pending_p = state.pending_p; + std::memcpy(regs.micro_macflags, state.micro_macflags, sizeof(state.micro_macflags)); + std::memcpy(regs.micro_clipflags, state.micro_clipflags, sizeof(state.micro_clipflags)); + std::memcpy(regs.micro_statusflags, state.micro_statusflags, sizeof(state.micro_statusflags)); + regs.xgkickaddr = state.xgkickaddr; + regs.xgkickdiff = state.xgkickdiff; + regs.xgkicksizeremaining = state.xgkicksizeremaining; + regs.xgkicklastcycle = state.xgkicklastcycle; + regs.xgkickcyclecount = state.xgkickcyclecount; + regs.xgkickenable = state.xgkickenable; + regs.xgkickendpacket = state.xgkickendpacket; + } + + // ---- Capture probe --------------------------------------------------- + + namespace + { + std::atomic g_active{false}; + bool g_capture_active = false; + bool g_rank_active = false; + std::string g_dir; + std::string g_rank_out; + u32 g_max_per_key = 32; + + std::mutex g_state_mutex; + // Capture-mode: per-key count of executions seen so far. Files are + // named with seq = slot index in [0, max), reused on replacement. + std::unordered_map g_count_seen; + // Rank-mode: total executions per (vu_index, start_pc). + std::unordered_map g_rank_counts; + std::mt19937_64 g_rng{0x5EEDu ^ static_cast(::getpid())}; + + void DumpRankReportAtExit() + { + std::lock_guard lock(g_state_mutex); + if (g_rank_counts.empty() || g_rank_out.empty()) + return; + std::FILE* f = std::fopen(g_rank_out.c_str(), "w"); + if (!f) + { + Console.Error("vu_capture: rank dump failed to open %s", g_rank_out.c_str()); + return; + } + + std::vector> sorted(g_rank_counts.begin(), g_rank_counts.end()); + std::sort(sorted.begin(), sorted.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + + std::fprintf(f, "# vu_capture rank report — pid %d\n", ::getpid()); + std::fprintf(f, "# %-3s %-10s %16s\n", "vu", "start_pc", "executions"); + for (const auto& [key, count] : sorted) + { + const u32 vu_index = static_cast(key >> 32); + const u32 start_pc = static_cast(key); + std::fprintf(f, " %-3u 0x%08X %16llu\n", + vu_index, start_pc, (unsigned long long)count); + } + std::fclose(f); + } + + void InitFromEnv() + { + const char* dir = std::getenv("PCSX2_VU_CAPTURE_DIR"); + const char* rank_out = std::getenv("PCSX2_VU_RANK_OUT"); + + if (dir && *dir) + { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + if (ec) + Console.Error("vu_capture: failed to create %s: %s", + dir, ec.message().c_str()); + else + { + g_dir = dir; + g_capture_active = true; + if (const char* m = std::getenv("PCSX2_VU_CAPTURE_MAX"); m && *m) + { + const long parsed = std::strtol(m, nullptr, 10); + if (parsed > 0 && parsed < (1 << 20)) + g_max_per_key = static_cast(parsed); + } + } + } + + if (rank_out && *rank_out) + { + g_rank_out = rank_out; + g_rank_active = true; + std::atexit(&DumpRankReportAtExit); + } + + if (g_capture_active || g_rank_active) + { + g_active.store(true, std::memory_order_relaxed); + Console.WriteLn("vu_capture: capture=%s rank=%s", + g_capture_active ? g_dir.c_str() : "off", + g_rank_active ? g_rank_out.c_str() : "off"); + } + } + + std::string MakeSlotPath(int vu_index, u32 start_pc, u32 seq) + { + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s/vu%d_pc%08X_seq%03u.vucap", + g_dir.c_str(), vu_index, start_pc, seq); + return std::string(buf); + } + } // namespace + + void MaybeCapture(int vu_index, u32 start_pc, u32 cycle_budget, + const u8* microcode_ptr, u32 microcode_size, + const u8* vumem_ptr, u32 vumem_size, + const VURegs& regs) + { + static std::once_flag init_once; + std::call_once(init_once, &InitFromEnv); + + if (!g_active.load(std::memory_order_relaxed)) [[likely]] + return; + + // Decide slot under the state lock; do the heavy I/O after releasing + // it so concurrent VU0 / VU1 captures don't serialize on the file + // write. (WriteToFile takes its own writer mutex internally.) + const u64 key = (static_cast(vu_index) << 32) | start_pc; + u32 slot = 0; + bool write_this = false; + { + std::lock_guard lock(g_state_mutex); + if (g_rank_active) + ++g_rank_counts[key]; + if (g_capture_active) + { + u32& seen = g_count_seen[key]; + if (seen < g_max_per_key) + { + slot = seen; + write_this = true; + } + else + { + // Standard reservoir replacement: pick j uniformly in + // [0, seen+1); if j < g_max_per_key, replace slot j. + std::uniform_int_distribution dist(0, seen); + const u64 j = dist(g_rng); + if (j < g_max_per_key) + { + slot = static_cast(j); + write_this = true; + } + } + ++seen; + } + } + + if (!write_this) + return; + + CaptureRecord rec; + rec.vu_index = static_cast(vu_index); + rec.start_pc = start_pc; + rec.cycle_budget = cycle_budget; + rec.microcode.assign(microcode_ptr, microcode_ptr + microcode_size); + rec.vumem.assign(vumem_ptr, vumem_ptr + vumem_size); + SnapshotState(regs, rec.state); + + const std::string path = MakeSlotPath(vu_index, start_pc, slot); + if (!WriteToFile(path, rec)) + Console.Error("vu_capture: write failed: %s", path.c_str()); + } + +} // namespace vu_capture + +#endif // PCSX2_RECOMPILER_TESTS diff --git a/pcsx2/vu_capture.h b/pcsx2/vu_capture.h new file mode 100644 index 0000000000..a0bcb808c4 --- /dev/null +++ b/pcsx2/vu_capture.h @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#ifdef PCSX2_RECOMPILER_TESTS + +#include "common/Pcsx2Defs.h" + +#include +#include +#include + +struct VURegs; + +// VU microprogram capture format. Used by the live capture probe in +// mVUexecute (vu_capture::MaybeCapture) to dump the microcode + entry +// register/memory state of executing VU programs to disk, and by +// pcsx2-vurunner / VuTestHarness::LoadFromFile to replay them in a tight +// codegen-iteration loop without having to boot a game. +// +// The whole module is gated by PCSX2_RECOMPILER_TESTS — release builds omit +// it entirely (zero symbol leakage, like gif_test_hooks). +namespace vu_capture +{ + // File magic 'PCSX2VUC' (no NUL — exactly 8 bytes). + inline constexpr char kMagic[8] = {'P', 'C', 'S', 'X', '2', 'V', 'U', 'C'}; + + // Bump on any on-disk layout change. Readers reject mismatches. + inline constexpr u32 kVersion = 1; + + // Architecturally-significant subset of VURegs serialized to disk. + // Mirrors the fields VuSnapshot.h treats as architectural; VURegs's + // pipeline-modeling and dispatcher-bookkeeping fields are omitted. + // + // Layout is fixed across builds (plain trivial types, no VECTOR / + // REG_VI) so a capture written today round-trips against a future + // CapturedState as long as kVersion stays the same. + #pragma pack(push, 1) + struct CapturedState + { + // VF[32] — 4 lanes × 32 regs, written as little-endian u32 lanes. + u32 VF[32][4]; + // VI[32] — full 32-bit value (only low 16 bits architectural for + // most regs; REG_R/Q/P/I/STATUS/MAC/CLIP/TPC use all 32 bits). + u32 VI[32]; + // Accumulator — 4 lanes. + u32 ACC[4]; + // Scalar pipeline stages (also live in VI[REG_Q]/VI[REG_P], but + // captured separately for round-trip safety). + u32 q; + u32 p; + // Interpreter staging scalars — microVU commits them at E-bit. + u32 pending_q; + u32 pending_p; + // microVU shadow flag pipelines. + u32 micro_macflags[4]; + u32 micro_clipflags[4]; + u32 micro_statusflags[4]; + // XGKICK state (VU1 only — populated regardless; all-zero for VU0). + u32 xgkickaddr; + u32 xgkickdiff; + u32 xgkicksizeremaining; + u64 xgkicklastcycle; + u32 xgkickcyclecount; + u32 xgkickenable; + u32 xgkickendpacket; + }; + #pragma pack(pop) + static_assert(sizeof(CapturedState) == 32 * 16 + 32 * 4 + 16 + 4 + 4 + 4 + 4 + 48 + 24 + 8, + "CapturedState layout drifted — bump kVersion if intentional"); + + // One captured execution of one VU microprogram. + struct CaptureRecord + { + u8 vu_index = 0; // 0 or 1 + u32 start_pc = 0; // entry microPC (byte offset into Micro) + u32 cycle_budget = 0; // cycles arg passed to mVUexecute + // Whole-program-memory snapshot. Size = VU0_PROGSIZE (4 KB) for + // vu_index 0, VU1_PROGSIZE (16 KB) for vu_index 1. + std::vector microcode; + // Whole VU data memory snapshot. Size = VU0_MEMSIZE (4 KB) for + // vu_index 0, VU1_MEMSIZE (16 KB) for vu_index 1. + std::vector vumem; + CapturedState state{}; + }; + + // On-disk header (packed; matches the bytes WriteToFile emits). + #pragma pack(push, 1) + struct FileHeader + { + char magic[8]; // kMagic + u32 version; // kVersion + u8 vu_index; + u8 _pad[3]; + u32 start_pc; + u32 cycle_budget; + u32 microcode_size; // payload bytes + u32 vumem_size; // payload bytes + }; + #pragma pack(pop) + static_assert(sizeof(FileHeader) == 32, "FileHeader must be exactly 32 bytes"); + + // Layout: [FileHeader][microcode bytes][vumem bytes][CapturedState bytes]. + // Returns false on I/O failure; the file is left in whatever state fwrite + // produced (callers are expected to write to a temp path then rename if + // atomicity matters). + bool WriteToFile(const std::string& path, const CaptureRecord& rec); + + // Reads a record. Returns false on I/O failure, magic mismatch, version + // mismatch, sane-size violation, or short read. On success, rec_out is + // fully populated; on failure, contents are unspecified. + bool ReadFromFile(const std::string& path, CaptureRecord& rec_out); + + // Pulls the architectural-only fields out of a live VURegs into a + // CapturedState. Called by the dispatcher probe; exposed for tests. + void SnapshotState(const VURegs& regs, CapturedState& out); + + // Writes the captured fields back to a live VURegs. Preserves the live + // Mem / Micro pointers and any pipeline-modeling fields not in the + // captured set. Called by VuTestHarness::LoadFromFile. + void RestoreState(const CapturedState& state, VURegs& regs); + + // Dispatcher capture probe. Called once per mVUexecute entry. First call + // reads PCSX2_VU_CAPTURE_DIR / PCSX2_VU_CAPTURE_MAX / PCSX2_VU_RANK_OUT + // env vars; if all three are unset the probe is permanently disabled + // and all subsequent calls are a single relaxed-atomic load + branch. + // + // Capture mode (PCSX2_VU_CAPTURE_DIR set): reservoir sampling per + // (vu_index, start_pc), keep up to PCSX2_VU_CAPTURE_MAX (default 32) + // captures per program as /vu_pc<8hex>_seq<3d>.vucap. + // + // Rank mode (PCSX2_VU_RANK_OUT set): maintain in-memory execution + // count per (vu_index, start_pc); on process exit (atexit handler), + // write sorted top-N to PCSX2_VU_RANK_OUT. Use this to discover which + // programs are hot before deciding what to capture in detail. + // + // The two modes can be active simultaneously. + void MaybeCapture(int vu_index, u32 start_pc, u32 cycle_budget, + const u8* microcode_ptr, u32 microcode_size, + const u8* vumem_ptr, u32 vumem_size, + const VURegs& regs); + +} // namespace vu_capture + +#endif // PCSX2_RECOMPILER_TESTS diff --git a/pcsx2/x86/microVU_Execute.inl b/pcsx2/x86/microVU_Execute.inl index 711e99093b..f3875fb3aa 100644 --- a/pcsx2/x86/microVU_Execute.inl +++ b/pcsx2/x86/microVU_Execute.inl @@ -6,6 +6,10 @@ #include "Config.h" #include "GS/MultiISA.h" +#ifdef PCSX2_RECOMPILER_TESTS +#include "vu_capture.h" +#endif + //------------------------------------------------------------------ // Dispatcher Functions //------------------------------------------------------------------ @@ -328,6 +332,15 @@ _mVUt void* mVUexecute(u32 startPC, u32 cycles) mVU.cycles = cycles; mVU.totalCycles = cycles; +#ifdef PCSX2_RECOMPILER_TESTS + // Records the microprogram plus the entry register/memory state at dispatch + // so the captured program can be replayed offline by the capture test tooling. + vu_capture::MaybeCapture(static_cast(vuIndex), startPC & vuLimit, cycles, + mVU.regs().Micro, mVU.microMemSize, + mVU.regs().Mem, mVU.microMemSize, + mVU.regs()); +#endif + xSetTextPtr(mVU.textPtr()); xSetPtr(mVU.prog.x86ptr); // Set x86ptr to where last program left off return mVUsearchProg(startPC & vuLimit, (uptr)&mVU.prog.lpState); // Find and set correct program diff --git a/tests/ctest/core/CMakeLists.txt b/tests/ctest/core/CMakeLists.txt index da86fe3dcd..d0bb5412c5 100644 --- a/tests/ctest/core/CMakeLists.txt +++ b/tests/ctest/core/CMakeLists.txt @@ -4,6 +4,13 @@ add_pcsx2_test(core_test StubHost.cpp ) +# The recompiler differential-test harness drives the arm64 JIT through +# arm64-only entry points (recEeExecuteBlock, recEeIsBlockLinked) and the +# arm64 microVU persist API, so it only builds on ARCH_ARM64. +if(ARCH_ARM64) + add_subdirectory(recompilers) +endif() + set(multi_isa_sources GS/swizzle_test_main.cpp ) diff --git a/tests/ctest/core/recompilers/CMakeLists.txt b/tests/ctest/core/recompilers/CMakeLists.txt new file mode 100644 index 0000000000..6f8ebe2188 --- /dev/null +++ b/tests/ctest/core/recompilers/CMakeLists.txt @@ -0,0 +1,90 @@ +# In-process differential test harness for the PS2 recompilers. +# +# Each test loads a short MIPS-I (IOP) or MIPS-III (EE) program at +# kProgramPc, seeds register state, runs through both the JIT and the +# interpreter, captures full architectural state, and gtest-diffs the +# two — any divergence is a JIT bug. See harness/JitTestHarness.h +# (IOP) and harness/EeRecTestHarness.h (EE) for the contracts. +# +# The EE harness drives the JIT via recEeExecuteBlock(cycles, park_pc), +# a bounded-cycle entry point alongside the production recExecute(). +# recEeIsBlockLinked(src_pc, dst_pc) backs the multiblock tests' +# block-link introspection. +add_pcsx2_test(recompiler_tests + main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../StubHost.cpp + harness/RecompilerTestEnvironment.cpp + harness/JitTestHarness.cpp + harness/EeRecTestHarness.cpp + harness/StateSnapshot.cpp + harness/VuSnapshot.cpp + harness/VuTestHarness.cpp + harness/VuReplay.cpp + iop_alu_tests.cpp + iop_jit_fuzz_tests.cpp + iop_shift_tests.cpp + iop_muldiv_tests.cpp + iop_loadstore_tests.cpp + iop_jump_tests.cpp + iop_branch_tests.cpp + iop_cop0_tests.cpp + iop_memory_access_tests.cpp + iop_regalloc_pressure_tests.cpp + iop_multiblock_tests.cpp + iop_smc_tests.cpp + iop_cop0_exception_tests.cpp + ee_rec_alu_tests.cpp + ee_rec_alu64_tests.cpp + ee_rec_alu_imm_tests.cpp + ee_rec_branch_tests.cpp + ee_rec_cop0_tests.cpp + ee_rec_fpu_tests.cpp + ee_rec_fpu_full_mode_tests.cpp + ee_rec_harness_validation_tests.cpp + ee_rec_iop_handoff_tests.cpp + ee_rec_jump_tests.cpp + ee_rec_loadstore_tests.cpp + ee_rec_mmi_coherence_tests.cpp + ee_rec_mmi_simd_tests.cpp + ee_rec_mmi_tests.cpp + ee_rec_move_tests.cpp + ee_rec_muldiv_tests.cpp + ee_rec_multiblock_tests.cpp + ee_rec_shift_tests.cpp + ee_rec_smc_tests.cpp + ee_rec_timeout_loop_tests.cpp + ee_rec_traps_tests.cpp + vu0_harness_validation_tests.cpp + vu0_alu_upper_tests.cpp + vu0_alu_lower_tests.cpp + vu0_q_pipeline_tests.cpp + vu0_integer_alu_tests.cpp + vu0_branch_delay_tests.cpp + vu0_flag_pipeline_tests.cpp + vu0_e_d_t_m_bit_tests.cpp + vu0_clamp_modes_tests.cpp + vu_ftoi_saturation_tests.cpp + vu_madda_acc_lane_tests.cpp + ee_vu0_cfc2_ctc2_tests.cpp + ee_vu0_qmfc2_qmtc2_tests.cpp + ee_vu0_cop2_macro_tests.cpp + vu1_alu_upper_tests.cpp + vu1_alu_lower_tests.cpp + vu1_efu_p_pipeline_tests.cpp + vu1_xgkick_tests.cpp + ee_vu1_vif_dispatch_tests.cpp + vu_capture_format_tests.cpp + vu_replay_tests.cpp +) + +target_include_directories(recompiler_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +target_link_libraries(recompiler_tests PUBLIC + PCSX2_FLAGS + PCSX2 + common +) + diff --git a/tests/ctest/core/recompilers/ee_rec_alu64_tests.cpp b/tests/ctest/core/recompilers/ee_rec_alu64_tests.cpp new file mode 100644 index 0000000000..0ee7094b41 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_alu64_tests.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// 64-bit (MIPS-III) ALU additions that the EE has but the IOP does not. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(EeRecAlu64, DadduBasic) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000000100000000ull); + h.SetGpr64(reg::a1, 0x0000000200000005ull); + h.LoadProgram({ee::DADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000300000005ull); +} + +TEST(EeRecAlu64, DadduCarriesThroughBit32) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x00000000FFFFFFFFull); + h.SetGpr64(reg::a1, 0x0000000000000001ull); + h.LoadProgram({ee::DADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000100000000ull); +} + +TEST(EeRecAlu64, DaddiuNegativeImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000000100000000ull); + h.LoadProgram({ee::DADDIU(reg::v0, reg::a0, -1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x00000000FFFFFFFFull); +} + +TEST(EeRecAlu64, DsubuWraps) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + h.SetGpr64(reg::a1, 1); + h.LoadProgram({ee::DSUBU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFFFFFFFull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_alu_imm_tests.cpp b/tests/ctest/core/recompilers/ee_rec_alu_imm_tests.cpp new file mode 100644 index 0000000000..89d13c3d79 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_alu_imm_tests.cpp @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Immediate-operand EE ALU coverage (ADDI/ADDIU, DADDI/DADDIU, ANDI/ORI/XORI, +// SLTI/SLTIU). Exercises the four dispatch paths of eeRecompileCodeRC1 for +// every AritImm handler — const-fold (GPR_IS_CONST1(rs)) vs runtime emit, +// with zero-immediate fast paths and sign/zero-extension edge cases. +// +// The const-fold path is provoked by preceding the target opcode with an +// immediate sequence (LUI + ORI) that the rec tracks as constant. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +// ---- ADDI / ADDIU (32-bit add + sign-extend) ------------------------------- + +TEST(EeRecAluImm, AddiPositiveImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1000); + h.LoadProgram({ADDI(reg::v0, reg::a0, 0x100)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1100ull); +} + +TEST(EeRecAluImm, AddiNegativeImmediateSignExtends) +{ + // Non-overflowing ADDI: 0x40000000 + (-1) stays in signed-32 range, so + // the recADDI emitter runs without raising an Overflow exception. The + // result is positive (0x3FFFFFFF), so the sxtw leaves the top 32 bits + // zero. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'4000'0000ull); + h.LoadProgram({ADDI(reg::v0, reg::a0, -1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'3FFF'FFFFull); +} + +TEST(EeRecAluImm, AddiOverflowTrapsAndReturnsToParkingLot) +{ + // 0x80000000 + (-1) overflows the 32-bit signed range, so MIPS-III + // raises an Overflow exception. With the harness's exception-vector + // stubs installed (RecompilerTestEnvironment.cpp), the trap returns + // cleanly via `jr ra` and rt is left at its pre-instruction value + // (architecturally: trap fires before commit). v0 was zero on entry, + // so zero is observed on exit — proof the trap was taken rather than + // silently committing wraparound arithmetic. + // + // Interp-only: no EE recompiler backend emits the ADDI overflow trap + // (titles do not rely on it; the per-op cost is not worth paying), so the + // JIT commits the wrapped result (0x7fffffff) and this test does not diff + // against the JIT. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'8000'0000ull); + h.LoadProgram({ADDI(reg::v0, reg::a0, -1)}); + h.RunInterpOnly(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecAluImm, AddiuZeroImmediateIsMoveAndSxtw) +{ + // Zero-imm fast path: hits the `Mov(Wt, Ws); Sxtw(Xt, Wt)` branch. + // Start with UD[0] high bit set in the low-32 so the sxtw actually + // propagates a 1. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234'5678'8000'0000ull); + h.LoadProgram({ADDIU(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'8000'0000ull); +} + +TEST(EeRecAluImm, AddiuConstFoldThroughLui) +{ + // LUI+ORI produces a const rs that the rec tracks; the ADDIU below + // should take the const-fold path (recADDI_const in RC1). + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::a0, 0x1234), + ORI(reg::a0, reg::a0, 0x5678), + ADDIU(reg::v0, reg::a0, 0x100), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'1234'5778ull); +} + +// ---- DADDI / DADDIU (64-bit add, sign-extended imm) ----------------------- + +TEST(EeRecAluImm, DaddiLow16PositiveImm) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1111'2222'3333'4444ull); + h.LoadProgram({ee::DADDI(reg::v0, reg::a0, 0x100)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1111'2222'3333'4544ull); +} + +TEST(EeRecAluImm, DaddiuSignExtendedImmWrapsLow32) +{ + // -1 sign-extends to 0xFFFF...FFFF; add wraps the whole 64-bit reg. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1000'0000'0000'0000ull); + h.LoadProgram({ee::DADDIU(reg::v0, reg::a0, -1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0FFF'FFFF'FFFF'FFFFull); +} + +TEST(EeRecAluImm, DaddiZeroImmediateIsCopy) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'CAFE'BABEull); + h.LoadProgram({ee::DADDI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEF'CAFE'BABEull); +} + +// ---- ANDI / ORI / XORI (zero-extended 16-bit imm) ------------------------- + +TEST(EeRecAluImm, AndiMasksLow16AndZerosRest) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'CAFE'BABEull); + h.LoadProgram({ANDI(reg::v0, reg::a0, 0xFF00)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0000'BA00ull); +} + +TEST(EeRecAluImm, AndiZeroImmediateIsMoveZero) +{ + // imm==0 hits the `Mov(Xt, 0)` fast path. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'FFFFull); + h.LoadProgram({ANDI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecAluImm, AndiNonEncodableImm) +{ + // 0x1234 is not an encodable arm64 logical immediate — vixl MA must + // synthesize it through a scratch register. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'FFFFull); + h.LoadProgram({ANDI(reg::v0, reg::a0, 0x1234)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1234ull); +} + +TEST(EeRecAluImm, OriSetsLow16WithoutClobberingHigh) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234'5678'9000'0000ull); + h.LoadProgram({ORI(reg::v0, reg::a0, 0xABCD)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1234'5678'9000'ABCDull); +} + +TEST(EeRecAluImm, OriZeroImmediateIsCopy) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xABCD'1234'5678'0000ull); + h.LoadProgram({ORI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xABCD'1234'5678'0000ull); +} + +TEST(EeRecAluImm, XoriTogglesLow16) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'FFFF'FFFFull); + h.LoadProgram({XORI(reg::v0, reg::a0, 0xF0F0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'FFFF'0F0Full); +} + +TEST(EeRecAluImm, XoriZeroImmediateIsCopy) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1111'2222'3333'4444ull); + h.LoadProgram({XORI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1111'2222'3333'4444ull); +} + +// ---- SLTI / SLTIU --------------------------------------------------------- + +TEST(EeRecAluImm, SltiSignedLessThanZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'FFFFull); // -1 signed + h.LoadProgram({SLTI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecAluImm, SltiSignedEqualIsZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.LoadProgram({SLTI(reg::v0, reg::a0, 42)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecAluImm, SltiNegativeImmediateBoundary) +{ + // Large negative imm: -32768 sign-extended. rs = -32768 should yield + // 0 (not less than), rs = -32769 (stored as wrap) should yield 1. + { + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'8000ull); // -32768 + h.LoadProgram({SLTI(reg::v0, reg::a0, -32768)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); + } + { + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'7FFFull); // -32769 + h.LoadProgram({SLTI(reg::v0, reg::a0, -32768)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); + } +} + +TEST(EeRecAluImm, SltiuUnsignedBelowMaxThroughSignExtension) +{ + // imm=-1 sign-extends to 0xFFFF...FFFF; any u64 rs != ~0 is below it. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + h.LoadProgram({SLTIU(reg::v0, reg::a0, -1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecAluImm, SltiuEqualToSignExtendedImmIsZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'FFFF'FFFFull); + h.LoadProgram({SLTIU(reg::v0, reg::a0, -1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecAluImm, SltiuSmallPositiveImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 5); + h.LoadProgram({SLTIU(reg::v0, reg::a0, 10)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +// ---- Writes to $zero are no-ops ------------------------------------------- + +TEST(EeRecAluImm, AddiToZeroIsNoOp) +{ + EeRecTestHarness h; + h.LoadProgram({ADDI(reg::zero, reg::zero, 1234)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::zero), 0ull); +} + +TEST(EeRecAluImm, XoriToZeroIsNoOp) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFull); + h.LoadProgram({XORI(reg::zero, reg::a0, 0xFFFF)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::zero), 0ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_alu_tests.cpp b/tests/ctest/core/recompilers/ee_rec_alu_tests.cpp new file mode 100644 index 0000000000..832f80478c --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_alu_tests.cpp @@ -0,0 +1,416 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// 32-bit EE ALU tests. The EE sign-extends every 32-bit result into the +// low 64 bits of the target GPR, so every test checks the full 64-bit +// representation — this is the whole point of the distinction from IOP. +// +// All tests run through Run() which executes both JIT and interpreter +// paths and diffs architectural state, so a JIT/interp divergence on any +// covered opcode fails the test. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(EeRecAlu, AddiuSignExtend) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 100); + h.LoadProgram({ADDIU(reg::v0, reg::a0, -7)}); + h.Run(); + // 93 sign-extended to 64 bits — top half is 0. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0000'005Dull); +} + +TEST(EeRecAlu, AddiuWrapsIntoNegative) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x7FFFFFFF); + h.LoadProgram({ADDIU(reg::v0, reg::a0, 1)}); + h.Run(); + // 0x80000000 sign-extends to 0xFFFFFFFF80000000 in the 64-bit GPR. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF80000000ull); +} + +TEST(EeRecAlu, AddiuOnlyLow32Matters) +{ + // High 32 bits of rs are ignored by ADDIU (32-bit add + sign-extend). + EeRecTestHarness h; + h.SetGpr128(reg::a0, /*lo=*/5, /*hi=*/0xDEADBEEFCAFEBABEull); + h.LoadProgram({ADDIU(reg::v0, reg::a0, 7)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 12ull); +} + +TEST(EeRecAlu, AdduBasic) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 20); + h.LoadProgram({ADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 30ull); +} + +TEST(EeRecAlu, SubuSignExtendsNegativeResult) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 5); + h.SetGpr64(reg::a1, 10); + h.LoadProgram({SUBU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFFFFFFBull); // -5 +} + +TEST(EeRecAlu, AndIsLow64Only) +{ + // The EE interpreter implements AND/OR/XOR/NOR as 64-bit ops that only + // write `.UD[0]` (low 64 bits) of the destination — `UD[1]` is left + // alone. Spec-lock this behavior; whether it's what the hardware does + // is a separate question, but what we care about is interp/JIT agree. + EeRecTestHarness h; + h.SetGpr128(reg::a0, 0xAAAA'AAAA'AAAA'AAAAull, 0xFFFF'FFFF'FFFF'FFFFull); + h.SetGpr128(reg::a1, 0x0F0F'0F0F'0F0F'0F0Full, 0x0F0F'0F0F'0F0F'0F0Full); + h.SetGpr128(reg::v0, 0ull, 0xDEAD'BEEF'CAFE'BABEull); // pre-state for UD[1] + h.LoadProgram({AND(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0A0A'0A0A'0A0A'0A0Aull); + EXPECT_EQ(h.InterpSnapshot().regs.GPR.r[reg::v0].UD[1], 0xDEAD'BEEF'CAFE'BABEull); +} + +TEST(EeRecAlu, OrIsLow64Only) +{ + EeRecTestHarness h; + h.SetGpr128(reg::a0, 0x00FF'00FF'00FF'00FFull, 0ull); + h.SetGpr128(reg::a1, 0xFF00'FF00'FF00'FF00ull, 0ull); + h.LoadProgram({OR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'FFFF'FFFFull); +} + +TEST(EeRecAlu, XorSelfIsZero) +{ + EeRecTestHarness h; + h.SetGpr128(reg::a0, 0x1234'5678'9ABC'DEF0ull, 0x1111'2222'3333'4444ull); + h.LoadProgram({XOR(reg::v0, reg::a0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); + EXPECT_EQ(h.InterpSnapshot().regs.GPR.r[reg::v0].UD[1], 0ull); +} + +TEST(EeRecAlu, NorLow64Only) +{ + EeRecTestHarness h; + h.SetGpr128(reg::a0, 0ull, 0ull); + h.SetGpr128(reg::a1, 0ull, 0ull); + h.LoadProgram({NOR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'FFFF'FFFFull); +} + +TEST(EeRecAlu, LuiShiftsAndSignExtends) +{ + EeRecTestHarness h; + h.LoadProgram({LUI(reg::v0, 0x8000)}); + h.Run(); + // 0x80000000 sign-extends into the 64-bit result. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF80000000ull); +} + +TEST(EeRecAlu, SltSigned64Bit) +{ + // EE SLT is the full-width signed compare (MIPS-III). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFFFFFFFFFBull); // -5 + h.SetGpr64(reg::a1, 3); + h.LoadProgram({SLT(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecAlu, SltuUnsigned64Bit) +{ + // Upper half of rs all-ones → unsigned max > 3. + EeRecTestHarness h; + h.SetGpr128(reg::a0, 0xFFFFFFFFFFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); + h.SetGpr64(reg::a1, 3); + h.LoadProgram({SLTU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecAlu, AndiZeroExtends) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFFFFFFFFFFull); + h.LoadProgram({ANDI(reg::v0, reg::a0, 0x1234)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1234ull); +} + +TEST(EeRecAlu, OriOperatesOn64BitLowSource) +{ + // ORI immediate is zero-extended and OR'd against rs.UD[0]; result + // lands in rt.UD[0]. rt.UD[1] is unchanged by the interpreter. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFF00000000ull); + h.LoadProgram({ORI(reg::v0, reg::a0, 0xABCD)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF0000ABCDull); +} + +TEST(EeRecAlu, SltiNegativeImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFFFFFFFFFFull); // -1 + h.LoadProgram({SLTI(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecAlu, SltiuSignExtendsImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + // imm=-1 sign-extends to 0xFFFFFFFFFFFFFFFF; 0 rhs. SLTU should give 0. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFF'80000000ull); + h.SetGpr64(reg::a1, 5); + h.LoadProgram({SLTU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +// --------------------------------------------------------------------------- +// Harness plumbing tests — these live in the EeRecHarness suite rather than +// a per-family suite because they test the harness itself, not any opcode. +// --------------------------------------------------------------------------- + +// Confirms Run() captures both JIT and interp post-states cleanly and that +// they agree on the trivial path. If pre_snapshot.Restore() failed between +// JIT and interp (e.g. cpuRegs not reset, memory not rewound), the interp +// result would differ from the JIT result even on this simple program — +// and the divergence would fire ADD_FAILURE inside Run() itself. The +// explicit GetGpr*Jit / GetGpr*Interp equality here catches a subtler +// regression: if the harness ever silently swapped the snapshots +// (copy-paste bug, field confusion), the diff would still be empty but +// the accessor returns would be wrong. Belt and suspenders. +TEST(EeRecHarness, JitAndInterpSnapshotsAgreeOnTrivialProgram) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEFull); + h.SetGpr64(reg::a1, 0xCAFE'BABEull); + h.LoadProgram({ + ADDU(reg::v0, reg::a0, reg::a1), + ADDU(reg::v1, reg::a0, reg::a1), + }); + h.Run(); + + EXPECT_EQ(h.GetGpr64Jit(reg::v0), h.GetGpr64Interp(reg::v0)); + EXPECT_EQ(h.GetGpr64Jit(reg::v1), h.GetGpr64Interp(reg::v1)); + + // Both snapshots must have the same program counter and register + // file — if the harness failed to re-seed before the interp run, + // interp would start from JIT's post-state and land somewhere else. + EXPECT_EQ(h.JitSnapshot().regs.pc, h.InterpSnapshot().regs.pc); +} + +// Cross-talk check: two tests in sequence, where the second reads cpuRegs +// fields that the first wrote. If the harness doesn't zero cpuRegs in its +// constructor (or the test framework's per-test teardown is incomplete), +// the second test sees leftover state and produces wrong results. The +// ctor's ZeroCpuRegs() is the thing under test here. +TEST(EeRecHarness, CrossTalkZZ_SeedsForContamination) +{ + // Defined first in the file; gtest runs tests in source-definition order, + // so this runs before the follow-up. Do not reorder/alphabetize these two + // tests. Intentionally leave a0 with a non-zero value that the next + // test's a0 default-of-zero depends on. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'CAFE'BABEull); + h.SetGpr64(reg::a1, 0xFFFF'FFFF'FFFF'FFFFull); + h.LoadProgram({ADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // The result here is not the focus — cpuRegs must be non-zero + // after this test runs so the next test can verify the harness + // resets state properly. + EXPECT_EQ(h.GetGpr64Jit(reg::v0), h.GetGpr64Interp(reg::v0)); +} + +TEST(EeRecHarness, CrossTalkZZ_FollowupSeesCleanState) +{ + // If the prior test's state leaked, a0 would still be nonzero and + // this test's ADDU reg::a0+reg::a0 would compute 2*DEADBEEFCAFEBABE + // instead of 0. The harness ctor must zero cpuRegs. + EeRecTestHarness h; + // Do NOT call SetGpr64 — rely on ctor's ZeroCpuRegs() for a0. + h.LoadProgram({ADDU(reg::v0, reg::a0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 0ull) << "cpuRegs.a0 leaked from prior test"; + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); + EXPECT_EQ(h.GetGpr64Jit(reg::v0), 0ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_branch_tests.cpp b/tests/ctest/core/recompilers/ee_rec_branch_tests.cpp new file mode 100644 index 0000000000..da9677df88 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_branch_tests.cpp @@ -0,0 +1,532 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Branch semantics unique to the EE: likely-branches (BEQL/BNEL/…) squash +// their delay slot when not taken. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; + +// Same layout as iop_branch_tests: +// 0x00: offset = 5 → taken target = 0x18 +// 0x04: NOP delay slot +// 0x08: ADDIU v0, zero, 1 not-taken marker +// 0x0C: J park; NOP; NOP +// 0x18: ADDIU v0, zero, 2 taken marker +// 0x1C: J park; NOP +inline void LoadBranchLayout(EeRecTestHarness& h, u32 branch_instr) +{ + h.LoadProgramNoTerm({ + branch_instr, NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); +} + +constexpr s16 kTakenOffset = 5; +} // namespace + +TEST(EeRecBranch, BeqTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.SetGpr64(reg::a1, 42); + LoadBranchLayout(h, BEQ(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BeqNotTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.SetGpr64(reg::a1, 43); + LoadBranchLayout(h, BEQ(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecBranch, BeqlSquashesDelaySlotWhenNotTaken) +{ + // BEQL + delay slot ADDIU a0, zero, 99. If not taken, delay slot + // must NOT execute. a0 is checked after. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 20); // 10 != 20 → not taken + h.LoadProgramNoTerm({ + ee::BEQL(reg::a0, reg::a1, kTakenOffset), + ADDIU(reg::a0, reg::zero, 99), // squashed delay slot + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // not-taken path + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 10ull); // delay slot DID NOT run +} + +TEST(EeRecBranch, BeqlExecutesDelaySlotWhenTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 10); // equal → taken + h.LoadProgramNoTerm({ + ee::BEQL(reg::a0, reg::a1, kTakenOffset), + ADDIU(reg::a0, reg::zero, 99), // delay slot runs + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 99ull); +} + +TEST(EeRecBranch, BnelSquashes) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 10); // equal → NOT taken (BNE-like) + h.LoadProgramNoTerm({ + ee::BNEL(reg::a0, reg::a1, kTakenOffset), + ADDIU(reg::a0, reg::zero, 99), // squashed + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 10ull); +} + +// ----- Likely-branch const-fold taken-target ----- +// +// When BOTH operands are compile-time constant, recBEQL/recBNEL dispatch to +// the *_const fast path. The bug: that path read the global `pc` AFTER +// recompileNextInstruction(true,...) advanced it by 4, so the taken target +// landed one instruction too far. recBEQ_const/recBNE_const and the *_process +// siblings all capture branchTo BEFORE the delay-slot recompile. +// +// Layout (relative byte addr in parens): +// (0x00) idx0 ADDIU v0,zero,7 sentinel +// (0x04) idx1 ADDIU a0,zero,5 const +// (0x08) idx2 ADDIU a1,zero,5 const -> both const, BEQL const-folds +// (0x0C) idx3 BEQL a0,a1,5 branch (taken when equal) +// (0x10) idx4 NOP delay slot (runs on taken) +// (0x14) idx5 ADDIU v0,zero,1 not-taken marker (dead on taken) +// (0x18) idx6 J park +// (0x1C) idx7 NOP +// (0x20) idx8 NOP +// (0x24) idx9 ADDIU v0,zero,2 CORRECT taken target (off=5: 0x10+5*4) +// (0x28) idx10 J park buggy off-by-4 target lands here -> v0 stays 7 +// (0x2C) idx11 NOP +// Correct: v0 == 2. Buggy: target = 5*4 + (branch+8) = 0x28, skips idx9 -> v0 == 7. + +TEST(EeRecBranch, BeqlConstFoldTakenTargetCorrect) +{ + EeRecTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::v0, reg::zero, 7), + ADDIU(reg::a0, reg::zero, 5), + ADDIU(reg::a1, reg::zero, 5), // a0 == a1 (const) -> BEQL taken + ee::BEQL(reg::a0, reg::a1, 5), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +TEST(EeRecBranch, BnelConstFoldTakenTargetCorrect) +{ + EeRecTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::v0, reg::zero, 7), + ADDIU(reg::a0, reg::zero, 5), + ADDIU(reg::a1, reg::zero, 6), // a0 != a1 (const) -> BNEL taken + ee::BNEL(reg::a0, reg::a1, 5), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +// ----- BNE ----------------------------------------------------------- + +TEST(EeRecBranch, BneTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 11); + LoadBranchLayout(h, BNE(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BneNotTakenWithSameRegisterFold) +{ + // Rs == Rt is a same-register inequality — provably false at compile + // time, so the handler folds to "always not taken" and the delay slot + // still runs (non-likely branch). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.LoadProgramNoTerm({ + BNE(reg::a0, reg::a0, kTakenOffset), + ADDIU(reg::t0, reg::zero, 7), // delay slot runs + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // not-taken path + EXPECT_EQ(h.GetGpr64Interp(reg::t0), 7ull); // delay slot ran +} + +TEST(EeRecBranch, BeqAlwaysTakenSameRegister) +{ + // Rs == Rt is provably equal — compile-time fold to "always taken" + // (no condition emitted; delay slot still runs). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 17); + LoadBranchLayout(h, BEQ(reg::a0, reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +// ----- 64-bit comparisons (EE branches inspect full UD[0]) ----------- + +TEST(EeRecBranch, BneInspectsFull64BitValue) +{ + // Two values whose lower-32 halves match but upper halves differ + // must be considered NOT equal — confirms 64-bit cmp. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'CAFEBABEull); + h.SetGpr64(reg::a1, 0xDEADBEEF'CAFEBABEull); + LoadBranchLayout(h, BNE(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); // taken +} + +// ----- BLTZ / BGEZ / BLEZ / BGTZ (single-register sign tests) -------- + +TEST(EeRecBranch, BltzNegativeTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-1)); + LoadBranchLayout(h, BLTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BltzZeroNotTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + LoadBranchLayout(h, BLTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecBranch, BgezZeroTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + LoadBranchLayout(h, BGEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BgezNegativeNotTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-5)); + LoadBranchLayout(h, BGEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecBranch, BlezZeroTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + LoadBranchLayout(h, BLEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BgtzPositiveTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 7); + LoadBranchLayout(h, BGTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BgtzZeroNotTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + LoadBranchLayout(h, BGTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +// ----- Likely sign-test branches squash on not-taken ---------------- + +TEST(EeRecBranch, BlezlSquashesDelaySlotWhenNotTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 7); // > 0 → not taken + h.LoadProgramNoTerm({ + ee::BLEZL(reg::a0, kTakenOffset), + ADDIU(reg::a0, reg::zero, 99), // squashed + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 7ull); +} + +TEST(EeRecBranch, BgtzlExecutesDelaySlotWhenTaken) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 5); // > 0 → taken + h.LoadProgramNoTerm({ + ee::BGTZL(reg::a0, kTakenOffset), + ADDIU(reg::t0, reg::zero, 33), // delay slot runs + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); + EXPECT_EQ(h.GetGpr64Interp(reg::t0), 33ull); +} + +// ----- Link branches write ra = pc + 8 (PC of insn after delay slot) - + +TEST(EeRecBranch, BltzalLinkRegisterReceivesPcPlus8) +{ + // Layout: BLTZAL is at kProgramPc; delay slot at +4; not-taken + // fallthrough at +8 → that's the value that should land in ra. + constexpr u32 kProgram = RecompilerTestEnvironment::kProgramPc; + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-1)); // negative → taken + h.SetGpr64(reg::ra, 0xDEADBEEF'DEADBEEFull); // pre-pollute ra hi half + LoadBranchLayout(h, BLTZAL(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); // taken + EXPECT_EQ(h.GetGpr64Interp(reg::ra), static_cast(kProgram + 8)); +} + +TEST(EeRecBranch, BgezalLinkRegisterWrittenEvenWhenNotTaken) +{ + // MIPS BGEZAL/BLTZAL link unconditionally — ra written before the + // branch decision. Negative Rs → BGEZAL not taken, but ra still + // changes. + constexpr u32 kProgram = RecompilerTestEnvironment::kProgramPc; + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-1)); // negative → not taken + LoadBranchLayout(h, BGEZAL(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // not taken + EXPECT_EQ(h.GetGpr64Interp(reg::ra), static_cast(kProgram + 8)); +} + +// ----- Const-prop survives across not-taken delay-slot save/restore -- +// +// Regression for SaveBranchState / LoadBranchState: const-prop state +// captured before the delay slot must be restored intact after the +// taken arm finishes, so the not-taken side sees the same compile-time +// constants. Construct: AND with a known immediate makes Rt const, then +// branch. Whichever side runs, the const must reach v0 unchanged. +TEST(EeRecBranch, BeqConstPropSurvivesBranch) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + h.SetGpr64(reg::a1, 5); // 0 != 5 → not taken + h.LoadProgramNoTerm({ + ANDI(reg::t0, reg::zero, 0), // t0 = 0 (const-prop) + BEQ(reg::a0, reg::a1, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::t0, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::t0, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +// Backward-BEQ poll loop where the block's first instruction is also its +// branch target. The canonical shape is a timer-tick measurement loop: +// LW v0, 0(a1) ; read counter +// NOPs +// BEQ v1, v0, poll ; back to LW if unchanged +// SLTI v0, a0, 14 ; delay slot — overwrites v0 +// +// The harness can't drive real HW-register changes, so the test simulates the +// same control-flow shape with an ALU counter instead of a timer read: +// v0 is incremented each iteration; BEQ v1, v0, back loops while equal. +// Exercises a block whose first instruction is also its branch target, and +// drives multiple iterations of the backward branch + delay-slot re-emit path. +TEST(EeRecBranch, BackwardBeqPollLoopExits) +{ + // v0 starts equal to v1 (loop taken once), then v0 is bumped so + // the second BEQ compare falls through. v1 gets the post-loop + // sentinel via the instruction after the delay slot. + EeRecTestHarness h; + h.SetGpr64(reg::v0, 0x42); + h.SetGpr64(reg::v1, 0x42); + h.SetGpr64(reg::a0, 0); + h.LoadProgramNoTerm({ + // 0x00 loop: + ADDIU(reg::v0, reg::v0, 1), // v0++ — simulates fresh poll + NOP, NOP, NOP, NOP, // BIOS has 4 NOPs between LW/BEQ + BEQ(reg::v1, reg::v0, -6), // 0x14 → target 0x00 + SLTI(reg::v0, reg::a0, 14), // delay slot (like BIOS) + // fall-through: + ADDIU(reg::v1, reg::zero, 0x99), // 0x1c post-loop sentinel + J(kPark), NOP, + }); + h.Run(); + // After one iteration v0=0x43, v1=0x42, BEQ falls through. Delay + // slot then writes v0 = (a0=0 < 14) = 1, so final v0 reflects the + // delay-slot result (matches BIOS's v0 reuse semantics). v1 must + // reach the sentinel — hitting it proves the back-branch was exited. + EXPECT_EQ(h.GetGpr64Interp(reg::v1), 0x99ull); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +// Const-operand fast paths in recSetBranchEQ. When one branch +// operand is const-folded to 0, the test collapses to a single +// compare-and-branch-on-zero (no Mov #0 + Cmp). A non-zero const folds to +// an immediate Cmp. These guard that the collapsed codegen still branches +// the right direction. +// +// Layout: t0 := const (ANDI/ADDIU from $zero), then BEQ/BNE(a0, t0). +// _Rt_ (t0) is const → PROCESS_CONSTT, live operand is a0. +TEST(EeRecBranch, BeqConstZeroTaken) +{ + // a0 == 0 == t0 → BEQ taken. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + h.LoadProgramNoTerm({ + ANDI(reg::t0, reg::zero, 0), // t0 = const 0 + BEQ(reg::a0, reg::t0, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BeqConstZeroNotTaken) +{ + // a0 != 0, t0 == 0 → BEQ not taken. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 7); + h.LoadProgramNoTerm({ + ANDI(reg::t0, reg::zero, 0), + BEQ(reg::a0, reg::t0, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecBranch, BneConstZeroTaken) +{ + // a0 != 0, t0 == 0 → BNE taken. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 7); + h.LoadProgramNoTerm({ + ANDI(reg::t0, reg::zero, 0), + BNE(reg::a0, reg::t0, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BneConstZeroNotTaken) +{ + // a0 == 0 == t0 → BNE not taken. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0); + h.LoadProgramNoTerm({ + ANDI(reg::t0, reg::zero, 0), + BNE(reg::a0, reg::t0, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +TEST(EeRecBranch, BeqConstNonZeroImmediateCmp) +{ + // t1 := const 7; BEQ(a0, t1). a0 == 7 → taken (immediate-Cmp path). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 7); + h.LoadProgramNoTerm({ + ADDIU(reg::t1, reg::zero, 7), // t1 = const 7 + BEQ(reg::a0, reg::t1, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecBranch, BeqConstNonZeroImmediateCmpNotTaken) +{ + // t1 := const 7; BEQ(a0, t1). a0 == 8 → not taken. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 8); + h.LoadProgramNoTerm({ + ADDIU(reg::t1, reg::zero, 7), + BEQ(reg::a0, reg::t1, kTakenOffset - 1), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); +} + +// Companion: BEQ back-branch that loops multiple iterations, driven by +// a register that flips under ALU. The inner block's compile-time layout +// (first insn == branch target) is the specific shape a tight timer-poll +// loop hangs in. +TEST(EeRecBranch, BackwardBneLoopMultipleIterations) +{ + EeRecTestHarness h; + h.SetGpr64(reg::v0, 0); + h.SetGpr64(reg::v1, 5); // loop 5× until v0 == 5 + h.LoadProgramNoTerm({ + // 0x00 loop: + ADDIU(reg::v0, reg::v0, 1), // v0++ + NOP, NOP, NOP, + BNE(reg::v1, reg::v0, -5), // 0x10 → target 0x00 while v0 != 5 + NOP, // delay slot + // fall-through at 0x18: + ADDIU(reg::t0, reg::zero, 42), + J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 5ull); + EXPECT_EQ(h.GetGpr64Interp(reg::t0), 42ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_cop0_tests.cpp b/tests/ctest/core/recompilers/ee_rec_cop0_tests.cpp new file mode 100644 index 0000000000..04ade74537 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_cop0_tests.cpp @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// COP0 opcodes on the EE: MFC0/MTC0 register moves, ERET return-from- +// exception, DI/EI enable/disable interrupts. The TLB-manipulation opcodes +// (TLBWI/TLBR/TLBP/TLBWR) are heavy interpreter delegations (COP0 is not +// perf-critical) and are not exercised here. +// +// Note on privileged mode: the test harness leaves Status in its zero-init +// state (KSU=0 kernel). COP0 access from kernel mode doesn't require +// Status.CU[0]; it's implicitly allowed. EnableCop0() is provided for tests +// that explicitly set CU[0] regardless, matching how real game code may +// run (MTC0 from a PS2 app is usually in kernel mode via SYSCALL). + +#include "harness/EeRecTestHarness.h" + +#include "R5900.h" +#include "Hw.h" +#include "Memory.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +// CP0 register indices (the CP0regs::n union). +constexpr u32 kCp0Count = 9; +constexpr u32 kCp0Status = 12; +constexpr u32 kCp0Cause = 13; +constexpr u32 kCp0Epc = 14; +constexpr u32 kCp0PRid = 15; +} // namespace + +TEST(EeRecCop0, Mfc0StatusReadsDefaultZero) +{ + // Fresh harness: Status is zero-initialized. MFC0 from Status into v0 + // should yield zero. The Diff path also confirms JIT and interp agree. + EeRecTestHarness h; + h.LoadProgram({ + MFC0(reg::v0, kCp0Status), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); +} + +TEST(EeRecCop0, Mtc0ThenMfc0Roundtrip) +{ + // Write to PRid (a writable-but-unused-by-dispatch register; Status would + // trigger cpuUpdateOperationMode side effects that would interfere with this test). + // Then read it back via MFC0. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEADBEEFu); + h.LoadProgram({ + MTC0(reg::a0, kCp0PRid), + MFC0(reg::v0, kCp0PRid), + }); + h.Run(); + h.ExpectGpr64(reg::v0, static_cast(static_cast(0xDEADBEEFu))); + EXPECT_EQ(h.GetCp0Interp(kCp0PRid), 0xDEADBEEFu); +} + +TEST(EeRecCop0, Mtc0EpcRoundtrip) +{ + // Write an EPC value via MTC0 then read via MFC0. Exercises a register + // index (14) that some TLB-refill/Eret tests rely on. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x12345678u); + h.LoadProgram({ + MTC0(reg::a0, kCp0Epc), + MFC0(reg::v0, kCp0Epc), + }); + h.Run(); + h.ExpectGpr64(reg::v0, static_cast(static_cast(0x12345678u))); + EXPECT_EQ(h.GetCp0Interp(kCp0Epc), 0x12345678u); +} + +TEST(EeRecCop0, EnableCop0HelperSetsCu0) +{ + // EnableCop0 sets Status.CU[0]. Observed via MFC0 after Run(). + EeRecTestHarness h; + h.EnableCop0(); + h.LoadProgram({ + MFC0(reg::v0, kCp0Status), + }); + h.Run(); + EXPECT_NE(h.GetGpr64Interp(reg::v0) & (1ull << 28), 0ull); +} + +TEST(EeRecCop0, DiClearsStatusEIE) +{ + // DI clears Status.EIE (bit 16). Pre-set EIE then DI clears it. + // Status bit layout: EIE = bit 16. DI's effect is delayed by + // one instruction (matches x86), so the inline EIE-clear is + // emitted *after* the next instruction — follow DI with a NOP so the clear + // lands in live code (a DI immediately before a block-terminating jump would + // emit the clear after the branch, same as x86 — not a realistic position). + EeRecTestHarness h; + h.SetStatusBits(1u << 16); // EIE = 1 + h.LoadProgram({ + ee::DI, NOP, + }); + h.Run(); + EXPECT_EQ(h.GetCp0Interp(kCp0Status) & (1u << 16), 0u); + EXPECT_EQ(h.GetCp0Jit(kCp0Status) & (1u << 16), 0u); +} + +TEST(EeRecCop0, EiSetsStatusEIE) +{ + // EI sets Status.EIE. Requires COP0 mode — kernel mode is fine from + // zero-init. Precondition: Status.EDI/EXL/ERL all clear (default). + EeRecTestHarness h; + h.LoadProgram({ + ee::EI, + }); + h.Run(); + EXPECT_NE(h.GetCp0Interp(kCp0Status) & (1u << 16), 0u); +} + +TEST(EeRecCop0, Mtc0CountWritesCountRegisterInline) +{ + // MTC0 to Count (rd=9) is inlined (no iFlushCall + Interp::MTC0 + // call). Both JIT and interp write CP0.r[9] = rt[31:0] and lastCOP0Cycle = + // cycle. CP0[9] is excluded from the auto-diff because _cpuTestTIMR advances + // Count by a few cycles at block end (nondeterministic). The write itself is + // deterministic, so assert Count is the written value plus only that small + // timer drift — a broken store (wrong reg / missing lastCOP0Cycle update) + // would land far outside this window (near 0, or base + full cycle count). + constexpr u32 kBase = 0x0BADF00Du; + EeRecTestHarness h; + h.SetGpr64(reg::a0, kBase); + h.LoadProgram({ + MTC0(reg::a0, kCp0Count), + }); + h.Run(); + EXPECT_GE(h.GetCp0Jit(kCp0Count), kBase); + EXPECT_LT(h.GetCp0Jit(kCp0Count), kBase + 0x100u); + EXPECT_GE(h.GetCp0Interp(kCp0Count), kBase); + EXPECT_LT(h.GetCp0Interp(kCp0Count), kBase + 0x100u); +} + +TEST(EeRecCop0, DiInUserModeDoesNotClearEIE) +{ + // Inline DI guard: EIE is cleared only when (EXL|ERL|EDI) + // set OR KSU == 0 (kernel). In user mode (KSU != 0) with no exception level, + // DI must leave EIE untouched. Exercises the skip branch of the new inline + // emitter. CU0 set so the COP0 op is usable in user mode. Status (CP0[12]) + // is auto-diffed, so JIT and interp are cross-checked. + EeRecTestHarness h; + h.EnableCop0(); // CU0 — COP0 usable in user mode + h.SetStatusBits((1u << 16) | 0x10); // EIE=1, KSU=user (bit 4) + h.LoadProgram({ + ee::DI, NOP, // NOP so DI's delayed inline guard lands in live code + }); + h.Run(); + EXPECT_NE(h.GetCp0Interp(kCp0Status) & (1u << 16), 0u); // EIE preserved + EXPECT_NE(h.GetCp0Jit(kCp0Status) & (1u << 16), 0u); +} + +TEST(EeRecCop0, MfC0CauseReadsBackSetCause) +{ + // Harness direct-set Cause via SetCp0, program MFC0 Cause, observe + // GPR has the value. Exercises the MFC0 path for a register the + // exception dispatcher also writes. + EeRecTestHarness h; + h.SetCp0(kCp0Cause, 0x12345678u); + h.LoadProgram({ + MFC0(reg::v0, kCp0Cause), + }); + h.Run(); + h.ExpectGpr64(reg::v0, static_cast(static_cast(0x12345678u))); +} + +TEST(EeRecCop0, Mtc0BreakpointRegisterIsLogOnly) +{ + // MTC0 to rd=24 (Debug breakpoint register) must be a no-op + // in the emulation model. Interp logs only; the JIT special-cases it and + // must not fall through to the default-branch store. + constexpr u32 kCp0Brk = 24; + EeRecTestHarness h; + h.SetCp0(kCp0Brk, 0xCAFEBABEu); // pre-set sentinel + h.SetGpr64(reg::a0, 0xDEADBEEFu); + h.LoadProgram({ + MTC0(reg::a0, kCp0Brk), + }); + h.Run(); + // Both JIT and interp must leave the sentinel untouched. + EXPECT_EQ(h.GetCp0Interp(kCp0Brk), 0xCAFEBABEu); + EXPECT_EQ(h.GetCp0Jit(kCp0Brk), 0xCAFEBABEu); +} + +TEST(EeRecCop0, Mfc0StatusMasksReservedBits) +{ + // MFC0 from Status (rd=12) must mask CP0.r[12] with 0xf0c79c1f + // before sign-extending to the GPR (per the interpreter's MFC0). Use a value + // where ONLY reserved bits (outside the mask) are set, so the live + // Status fields stay zero and don't perturb dispatcher interrupt logic. + // Reserved bits: 5, 6, 7, 8, 9, 13, 14, 19, 20, 21, 24, 25, 26, 27. + const u32 reserved_only = 0x0F3863E0u; // bits above ∧ ~0xf0c79c1f + EeRecTestHarness h; + h.SetCp0(kCp0Status, reserved_only); + h.LoadProgram({ + MFC0(reg::v0, kCp0Status), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); // masked → 0 +} + +// ---- COP0 branch on DMAC condition (BC0F/T/FL/TL) -------------------------- +// Native emitter replaces the interpreter fallback. The condition is +// (((DMAC_STAT | ~DMAC_PCR) & 0x3ff) == 0x3ff). +// PCR=0 -> ~PCR low bits all set -> condition always TRUE regardless of STAT. +// PCR=0x3ff with STAT=0 -> condition FALSE. Both JIT and interp read the same +// eeHw, so Run() diffs the taken/not-taken control flow; v0 marks the path. + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; +constexpr s16 kTakenOffset = 5; + +// 0x00: ; 0x04: NOP delay; 0x08: v0=1 (not taken); J park; +// 0x18: v0=2 (taken); J park. +inline void LoadBc0Layout(EeRecTestHarness& h, u32 branch_instr) +{ + h.LoadProgramNoTerm({ + branch_instr, NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); +} + +void SetDmac(u32 pcr, u32 stat) +{ + psHu32(DMAC_PCR) = pcr; + psHu32(DMAC_STAT) = stat; +} +} // namespace + +TEST(EeRecCop0, Bc0tTakenWhenConditionTrue) +{ + EeRecTestHarness h; + SetDmac(/*pcr=*/0, /*stat=*/0); // condition TRUE + LoadBc0Layout(h, BC0T(kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); // taken +} + +TEST(EeRecCop0, Bc0tNotTakenWhenConditionFalse) +{ + EeRecTestHarness h; + SetDmac(/*pcr=*/0x3ff, /*stat=*/0); // condition FALSE + LoadBc0Layout(h, BC0T(kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // fall through +} + +TEST(EeRecCop0, Bc0fTakenWhenConditionFalse) +{ + EeRecTestHarness h; + SetDmac(/*pcr=*/0x3ff, /*stat=*/0); // condition FALSE → BC0F branches + LoadBc0Layout(h, BC0F(kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); // taken +} + +TEST(EeRecCop0, Bc0fNotTakenWhenConditionTrue) +{ + EeRecTestHarness h; + SetDmac(/*pcr=*/0, /*stat=*/0); // condition TRUE → BC0F falls through + LoadBc0Layout(h, BC0F(kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // fall through +} + +TEST(EeRecCop0, Bc0tlSquashesDelaySlotWhenNotTaken) +{ + // Likely variant: when not taken, the delay slot must NOT execute. + EeRecTestHarness h; + SetDmac(/*pcr=*/0x3ff, /*stat=*/0); // condition FALSE → BC0TL not taken + h.LoadProgramNoTerm({ + BC0TL(kTakenOffset), + ADDIU(reg::a0, reg::zero, 99), // squashed delay slot + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.SetGpr64(reg::a0, 7); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 1ull); // not taken + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 7ull); // delay slot squashed +} diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp new file mode 100644 index 0000000000..3fbbbbf15e --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp @@ -0,0 +1,310 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// FPU "Full" / DOUBLE-precision mode coverage (CHECK_FPU_FULL, GameDB +// eeClampMode:3 — FFX, Max Payne, Dark Cloud 2, Klonoa 2, ~150 serials). +// +// These are JIT-ONLY tests. The shared interpreter (FPU.cpp `fpuDouble`) is +// single-precision and has no double path, so it cannot be the oracle: for the +// inputs that exercise the DOUBLE path the JIT legitimately diverges from the +// interp. Each test therefore uses RunJitNoDiff() and asserts GetFprBitsJit() +// against an independently hand-computed PS2 double-mode result. +// +// CAUTION for future test authors: RunJitNoDiff() sets interp_snapshot_ = +// jit_snapshot_ (the interp is not a valid oracle here). So in THIS file the +// interp-side accessors mirror the JIT — an EXPECT against InterpSnapshot() or +// a both-sides h.ExpectFpr() would pass tautologically. Assert only via the +// *Jit() accessors (GetFprBitsJit / GetAccBitsJit / JitSnapshot). +// +// The discriminator between full and fast mode is a PS2 "pseudo-infinity" +// operand (exp field 0xff, e.g. 0x7f800000 = a finite 2^128-scale number): +// full mode preserves it as 0x7f800000 (ToDouble complex path -> op -> +// ToPS2FPU to_complex path), while the single-precision fast path treats it as +// +Inf and clamps it to 0x7f7fffff. The PseudoInf* tests pin that the DOUBLE +// dispatch is taken: the fast-path value would fail them. + +#include "harness/EeRecTestHarness.h" + +#include "Config.h" + +#include +#include + +using namespace recompiler_tests; +using namespace mips; +using namespace mips::ee; + +namespace { +u32 FloatBits(float f) +{ + u32 bits; + std::memcpy(&bits, &f, sizeof(bits)); + return bits; +} + +constexpr u32 kFPUflagO = 0x00008000; +constexpr u32 kFPUflagSO = 0x00000010; + +// A PS2 single with exponent field 0xff is a valid finite number (1.0 * 2^128), +// not an IEEE infinity. Full mode must preserve it through an arithmetic op. +constexpr u32 kPs2HugePos = 0x7f800000; // +1.0 * 2^128 +constexpr u32 kPs2MaxPos = 0x7f7fffff; // +FLT_MAX (what the fast path clamps to) +} // namespace + +// ---- Normal-range arithmetic: the DOUBLE pipeline must not corrupt ordinary +// values (widen -> op -> narrow round-trips exactly for these). ---------- + +TEST(EeRecFpuFull, AddNormalRange) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, FloatBits(2.5f)); + h.SetFprBits(1, FloatBits(1.25f)); + h.LoadProgram({ADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), FloatBits(3.75f)); +} + +TEST(EeRecFpuFull, SubNormalRange) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, FloatBits(5.0f)); + h.SetFprBits(1, FloatBits(1.5f)); + h.LoadProgram({SUB_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), FloatBits(3.5f)); +} + +TEST(EeRecFpuFull, MulNormalRange) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, FloatBits(3.0f)); + h.SetFprBits(1, FloatBits(4.0f)); + h.LoadProgram({MUL_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), FloatBits(12.0f)); +} + +// ---- Pseudo-infinity preservation: the strip-fix discriminator. ------------ + +TEST(EeRecFpuFull, AddPseudoInfPreserved) +{ + // 0x7f800000 + 0.0 : full mode keeps the PS2 2^128 value; the single-prec + // fast path would treat it as +Inf and clamp to +FLT_MAX (0x7f7fffff). + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, kPs2HugePos); + h.SetFprBits(1, FloatBits(0.0f)); + h.LoadProgram({ADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), kPs2HugePos); + EXPECT_NE(h.GetFprBitsJit(2), kPs2MaxPos); // would be this on the fast path +} + +TEST(EeRecFpuFull, SubPseudoInfPreserved) +{ + // 0x7f800000 - 0.0 : same preservation through the SUB path. + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, kPs2HugePos); + h.SetFprBits(1, FloatBits(0.0f)); + h.LoadProgram({SUB_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), kPs2HugePos); +} + +// ---- Overflow clamp + sticky flags: ToPS2FPU_Full to_overflow path. -------- + +TEST(EeRecFpuFull, MulOverflowClampsAndSetsStickyFlags) +{ + // 1.0*2^127 (0x7f000000) * 8.0 = 2^130 > PS2 max -> clamp to the PS2 FPU + // maximum and raise O|SO in FCR31. Note the full-mode max is 0x7fffffff + // (exp 0xff is a *valid* PS2 exponent), NOT IEEE FLT_MAX 0x7f7fffff — the + // fast single-precision path clamps to 0x7f7fffff and never touches FCR31, + // so both the value and the O flag are full-mode discriminators. + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFcr31(0); + h.SetFprBits(0, 0x7f000000u); // 1.0 * 2^127 + h.SetFprBits(1, FloatBits(8.0f)); + h.LoadProgram({MUL_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0x7fffffffu); // PS2 FPU max (not FLT_MAX) + EXPECT_NE(h.GetFprBitsJit(2), kPs2MaxPos); // fast path would give this + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (kFPUflagO | kFPUflagSO), 0u); +} + +// ---- Accumulator-target ops (ADDA/SUBA/MULA write ACC, not Fd). ------------- + +TEST(EeRecFpuFull, AddaPseudoInfPreservedToAcc) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, kPs2HugePos); + h.SetFprBits(1, FloatBits(0.0f)); + h.LoadProgram({ADDA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), kPs2HugePos); +} + +TEST(EeRecFpuFull, MulaNormalRangeToAcc) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, FloatBits(2.0f)); + h.SetFprBits(1, FloatBits(3.0f)); + h.LoadProgram({MULA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), FloatBits(6.0f)); +} + +TEST(EeRecFpuFull, SubaNormalRangeToAcc) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, FloatBits(10.0f)); + h.SetFprBits(1, FloatBits(2.0f)); + h.LoadProgram({SUBA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), FloatBits(8.0f)); +} + +// ---- MADD/MSUB family (Fd = ACC +/- Fs*Ft, two roundings) ------------------ +// DOUBLE recMaddsub: full multiply -> guard-mask ACC -> branch on product/ +// ACC overflow -> accumulate in double. ------------------------------------ + +TEST(EeRecFpuFull, MaddNormalRange) +{ + // 2.0 + 3.0*4.0 = 14.0 + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(2.0f)); + h.SetFprBits(0, FloatBits(3.0f)); + h.SetFprBits(1, FloatBits(4.0f)); + h.LoadProgram({MADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), FloatBits(14.0f)); +} + +TEST(EeRecFpuFull, MsubNormalRange) +{ + // 20.0 - 3.0*4.0 = 8.0 + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(20.0f)); + h.SetFprBits(0, FloatBits(3.0f)); + h.SetFprBits(1, FloatBits(4.0f)); + h.LoadProgram({MSUB_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), FloatBits(8.0f)); +} + +TEST(EeRecFpuFull, MaddPseudoInfProductPreserved) +{ + // ACC=0 + (1.0*2^128)*1.0 : the product is a PS2 pseudo-inf (0x7f800000). + // Full mode preserves it through the multiply and the (0+x) accumulate; + // the fast path would clamp the product to FLT_MAX (0x7f7fffff). + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(0.0f)); + h.SetFprBits(0, kPs2HugePos); // 1.0 * 2^128 + h.SetFprBits(1, FloatBits(1.0f)); + h.LoadProgram({MADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), kPs2HugePos); + EXPECT_NE(h.GetFprBitsJit(2), kPs2MaxPos); +} + +TEST(EeRecFpuFull, MsubPseudoInfNegatesProduct) +{ + // 0.0 - (1.0*2^128)*1.0 = -(2^128) = 0xff800000 (negative pseudo-inf). + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(0.0f)); + h.SetFprBits(0, kPs2HugePos); + h.SetFprBits(1, FloatBits(1.0f)); + h.LoadProgram({MSUB_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0xff800000u); +} + +TEST(EeRecFpuFull, MaddProductOverflowClampsAndSetsFlags) +{ + // (1.0*2^127)*8.0 = 2^130 overflows PS2 range -> the multiply saturates on + // the product-overflow path: result is +PS2-max with O|SO set. (ACC=1.0 is + // dominated by the saturated product either way, so this pins the clamp + + // sticky flags, not the accumulate-skip itself.) + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFcr31(0); + h.SetAccBits(FloatBits(1.0f)); // dominated by the 2^130 product + h.SetFprBits(0, 0x7f000000u); // 1.0 * 2^127 + h.SetFprBits(1, FloatBits(8.0f)); + h.LoadProgram({MADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0x7fffffffu); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (kFPUflagO | kFPUflagSO), 0u); +} + +TEST(EeRecFpuFull, MaddaNormalRangeToAcc) +{ + // MADDA writes ACC: 1.0 + 2.0*3.0 = 7.0 + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(1.0f)); + h.SetFprBits(0, FloatBits(2.0f)); + h.SetFprBits(1, FloatBits(3.0f)); + h.LoadProgram({MADDA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), FloatBits(7.0f)); +} + +TEST(EeRecFpuFull, MsubaNormalRangeToAcc) +{ + // MSUBA writes ACC: 10.0 - 2.0*3.0 = 4.0 + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetAccBits(FloatBits(10.0f)); + h.SetFprBits(0, FloatBits(2.0f)); + h.SetFprBits(1, FloatBits(3.0f)); + h.LoadProgram({MSUBA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), FloatBits(4.0f)); +} + +TEST(EeRecFpuFull, MaddaProductOverflowSetsAccflag) +{ + // MADDA with an overflowing product: ACC clamps to PS2-max and the sticky + // ACCflag bit must be set so a *subsequent* op sees the saturated ACC. This + // is the accumulator-overflow propagation path unique to the *A variants. + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFcr31(0); + h.SetAccBits(FloatBits(1.0f)); + h.SetFprBits(0, 0x7f000000u); // 1.0 * 2^127 + h.SetFprBits(1, FloatBits(8.0f)); + h.LoadProgram({MADDA_S(0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetAccBitsJit(), 0x7fffffffu); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (kFPUflagO | kFPUflagSO), 0u); + EXPECT_NE(h.JitSnapshot().fprs.ACCflag & 1u, 0u); +} diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp new file mode 100644 index 0000000000..a6605130bc --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp @@ -0,0 +1,1145 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// FPU / COP1 coverage for the EE recompiler. Single-precision only (the +// PS2 FPU is 32-bit; the `DOUBLE::` namespace is internal accuracy emulation, +// not a user-visible double-precision ISA). +// +// Ops covered: MTC1/MFC1 bit moves, CTC1/CFC1 control-register moves, +// ADD.S/SUB.S/MUL.S/DIV.S, NEG.S/ABS.S/MOV.S, CVT.W.S, compare family +// (C.EQ.S/C.LT.S) + BC1T/BC1F. +// +// Value discipline: tests use small-integer float values and simple +// ratios to avoid PS2 FPU quirks (denormal flush-to-zero, peculiar NaN +// propagation) that only matter for full FPU correctness. + +#include "harness/EeRecTestHarness.h" + +#include "Config.h" +#include "common/FPControl.h" + +#include +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; + +// Scoped enable of CHECK_FPU_EXTRA_OVERFLOW (per-game GameDB clampMode>=2), +// which the harness leaves at its default-off. Restores on scope exit so the +// flag never leaks into sibling tests. +struct FpuExtraOverflowGuard +{ + bool saved = EmuConfig.Cpu.Recompiler.fpuExtraOverflow; + FpuExtraOverflowGuard() { EmuConfig.Cpu.Recompiler.fpuExtraOverflow = true; } + ~FpuExtraOverflowGuard() { EmuConfig.Cpu.Recompiler.fpuExtraOverflow = saved; } +}; + +u32 FloatBits(float f) +{ + u32 bits; + std::memcpy(&bits, &f, sizeof(bits)); + return bits; +} + +float BitsToFloat(u32 bits) +{ + float f; + std::memcpy(&f, &bits, sizeof(f)); + return f; +} +} // namespace + +TEST(EeRecFpu, Mtc1MovesGprBitsToFpr) +{ + // MTC1 copies GPR bits verbatim to the FPR; no conversion. 0x40490FDB + // is the IEEE-754 bit pattern for a value near π. + EeRecTestHarness h; + h.EnableCop1(); + h.SetGpr64(reg::a0, 0x40490FDBu); + h.LoadProgram({ + ee::MTC1(reg::a0, 1), // fpr1 = bits(a0) + }); + h.Run(); + h.ExpectFpr(1, 0x40490FDBu); +} + +TEST(EeRecFpu, Mfc1MovesFprBitsToGprWithSignExtend) +{ + // MFC1 copies the 32-bit FPR bit pattern into rt, sign-extended to 64-bit. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(2, 0x80000001u); // negative single-precision pattern + h.LoadProgram({ + ee::MFC1(reg::v0, 2), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0xFFFFFFFF80000001ull); +} + +// --------------------------------------------------------------------------- +// LWC1 / SWC1 — FPU 32-bit load/store +// +// These take the inline fastmem path (LDR/STR off RFASTMEMBASE + +// backpatch) when CHECK_FASTMEM is set, with the softmem C-call as the +// faulting-PC fallback. In the test build the harness wires fastmem, so +// these exercise the fast path. Round-trip + bit-exactness are the spec: +// LWC1 copies 32 raw bits into fpr[ft] verbatim (no FP conversion), and +// SWC1 copies fpr[ft]'s 32 bits to memory verbatim. +// --------------------------------------------------------------------------- +namespace { +constexpr u32 kScratch = RecompilerTestEnvironment::kScratchAddr; +} + +TEST(EeRecFpu, Lwc1LoadsRawBitsIntoFpr) +{ + EeRecTestHarness h; + h.EnableCop1(); + // Bit pattern with the sign bit set — proves no sign-extend / FP munge. + h.WriteU32(kScratch, 0x80490FDBu); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({ + ee::LWC1(2, 0, reg::a0), // fpr2 = mem32[a0] + }); + h.Run(); + h.ExpectFpr(2, 0x80490FDBu); +} + +TEST(EeRecFpu, Swc1StoresRawFprBitsToMemory) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(3, 0xDEADBEEFu); // raw pattern, not a clean float + h.SetGpr64(reg::a0, kScratch); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({ + ee::SWC1(3, 0, reg::a0), // mem32[a0] = fpr3 + }); + h.Run(); + EXPECT_EQ(h.ReadU32(kScratch), 0xDEADBEEFu); +} + +TEST(EeRecFpu, Lwc1Swc1RoundtripWithOffset) +{ + // Load from one slot, store to another via a non-zero immediate offset — + // exercises recComputeAddr's Add path and the fastmem index register. + EeRecTestHarness h; + h.EnableCop1(); + h.WriteU32(kScratch + 4, 0x3F800000u); // 1.0f bits + h.SetGpr64(reg::a0, kScratch); + h.TrackMemWindow(kScratch, 16); + h.LoadProgram({ + ee::LWC1(4, 4, reg::a0), // fpr4 = mem32[a0+4] + ee::SWC1(4, 8, reg::a0), // mem32[a0+8] = fpr4 + }); + h.Run(); + h.ExpectFpr(4, 0x3F800000u); + EXPECT_EQ(h.ReadU32(kScratch + 8), 0x3F800000u); +} + +TEST(EeRecFpu, AddSInteger) +{ + // 3.0 + 4.0 = 7.0 — no rounding quirks. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(7.0f)); +} + +TEST(EeRecFpu, SubSInteger) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 10.0f); + h.SetFpr(2, 3.0f); + h.LoadProgram({ + ee::SUB_S(3, 1, 2), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(7.0f)); +} + +TEST(EeRecFpu, MulSInteger) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 6.0f); + h.SetFpr(2, 7.0f); + h.LoadProgram({ + ee::MUL_S(3, 1, 2), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(42.0f)); +} + +TEST(EeRecFpu, DivSExactRatio) +{ + // 20 / 4 = 5. Exact IEEE-754 result, no rounding divergence. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 20.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::DIV_S(3, 1, 2), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(5.0f)); +} + +// ---- Native DIV.S: divide-by-zero corners. interp DIV_S is the oracle, so +// Run() diffs the value; both snapshots' FCR31 are also asserted directly +// to pin the sticky flags. --------------------------------------------- + +TEST(EeRecFpu, DivSNegativeQuotientExact) +{ + // 6 / -2 = -3, exact — no rounding-mode sensitivity, no D/I flags. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0); + h.SetFpr(1, 6.0f); + h.SetFpr(2, -2.0f); + h.LoadProgram({ee::DIV_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, FloatBits(-3.0f)); + const u32 mask = 0x20000u | 0x10000u; // I | D + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0u); +} + +TEST(EeRecFpu, DivSByZeroSetsDenormFlagsAndMax) +{ + // 4 / +0 = +posFmax, with D|SD raised (x/0). + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0); + h.SetFpr(1, 4.0f); + h.SetFprBits(2, 0x00000000u); // +0 + h.LoadProgram({ee::DIV_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, 0x7F7FFFFFu); + const u32 mask = 0x20000u | 0x10000u | 0x40u | 0x20u; // I|D|SI|SD + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); // D|SD + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); +} + +TEST(EeRecFpu, DivSByZeroNegativeDividendSignedMax) +{ + // -4 / +0 : sign(Fs^Ft) is negative -> -posFmax (0xff7fffff), D|SD. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0); + h.SetFpr(1, -4.0f); + h.SetFprBits(2, 0x00000000u); // +0 + h.LoadProgram({ee::DIV_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, 0xFF7FFFFFu); + const u32 mask = 0x10000u | 0x20u; // D|SD + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); +} + +TEST(EeRecFpu, DivSByNegativeZeroDivisorSign) +{ + // 8 / -0 : divisor is -0 (caught by the float==0 compare under FtZ); sign is + // driven by the divisor -> -posFmax, D|SD. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0); + h.SetFpr(1, 8.0f); + h.SetFprBits(2, 0x80000000u); // -0 + h.LoadProgram({ee::DIV_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, 0xFF7FFFFFu); + const u32 mask = 0x10000u | 0x20u; // D|SD + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); +} + +TEST(EeRecFpu, DivSZeroByZeroSetsInvalidFlags) +{ + // 0 / 0 -> +posFmax with I|SI raised (invalid), not D|SD. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0); + h.SetFprBits(1, 0x00000000u); // +0 + h.SetFprBits(2, 0x00000000u); // +0 + h.LoadProgram({ee::DIV_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, 0x7F7FFFFFu); + const u32 mask = 0x20000u | 0x10000u | 0x40u | 0x20u; // I|D|SI|SD + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); // I|SI + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); +} + +TEST(EeRecFpu, NegSFlipsSignBit) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.5f); + h.LoadProgram({ + ee::NEG_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, FloatBits(-3.5f)); +} + +TEST(EeRecFpu, AbsSClearsSignBit) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, -4.25f); + h.LoadProgram({ + ee::ABS_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, FloatBits(4.25f)); +} + +TEST(EeRecFpu, MovSBitCopy) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x12345678u); + h.LoadProgram({ + ee::MOV_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, 0x12345678u); +} + +// MOV.S fd,fd aliases the same host reg; the emit is skipped. The +// value must be preserved verbatim (the no-op is a true identity, not a drop). +TEST(EeRecFpu, MovSSelfMoveIsIdentity) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(5, 0xCAFEB0BAu); + h.LoadProgram({ + ee::MOV_S(5, 5), + }); + h.Run(); + h.ExpectFpr(5, 0xCAFEB0BAu); +} + +TEST(EeRecFpu, CvtWSTruncatesToward) +{ + // 3.7 → 3 (FCR31 rounding mode is RZ/RN/... — use a value where + // every IEEE-754 rounding mode agrees to avoid harness-dependent + // results). + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.LoadProgram({ + ee::CVT_W_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, 3u); // integer 3 stored as bit pattern in the FPR +} + +// ----- CVT.W NaN saturation ------------------------------------------ +// +// ARM64 Fcvtzs converts NaN → 0, but the PS2 (interp CVT_W) saturates a +// NaN input by sign: +NaN → 0x7fffffff, -NaN → 0x80000000 (never 0). Inject +// raw NaN via SetFprBits (MTC1/LWC1 bit-copies bypass the arithmetic clamp). +// Run()'s auto-diff compares the FPR result, so an unfixed bare Fcvtzs (→0) +// diverges from interp; ExpectFpr pins the PS2 spec value on both sides. +TEST(EeRecFpu, CvtWPositiveNanSaturatesToIntMax) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x7FC00000u); // +NaN + h.LoadProgram({ + ee::CVT_W_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, 0x7fffffffu); // +NaN → INT_MAX +} + +TEST(EeRecFpu, CvtWNegativeNanSaturatesToIntMin) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0xFFC00000u); // -NaN + h.LoadProgram({ + ee::CVT_W_S(2, 1), + }); + h.Run(); + h.ExpectFpr(2, 0x80000000u); // -NaN → INT_MIN +} + +// ----- SQRT.S sticky-flag handling ----------------------------------- +// +// PS2 SQRT.S clears the I|D cause flags unconditionally and sets I|SI when +// Ft is negative non-zero (interp SQRT_S, FPU.cpp; CHECK_FPU_EXTRA_FLAGS is +// hardcoded on). Run()'s auto-diff does not gate on fprc[31], so assert the +// flag bits directly on both snapshots (they must agree — the JIT matches +// interp). Result value (sqrt(|Ft|)) is unchanged and stays in the auto-diff. +TEST(EeRecFpu, SqrtSNegativeSetsInvalidStickyFlags) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, -4.0f); // negative non-zero + h.LoadProgram({ + ee::SQRT_S(2, 1), // fd=2, ft=1; result sqrt(4)=2.0 + }); + h.Run(); + h.ExpectFpr(2, 0x40000000u); // 2.0f + const u32 mask = 0x20000u | 0x10000u | 0x40u; // I | D | SI + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); // I|SI set, D clear + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); +} + +TEST(EeRecFpu, SqrtSPositiveClearsStaleIDFlags) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(0x20000u | 0x10000u); // pre-set stale I|D + h.SetFpr(1, 4.0f); // positive → no flag set + h.LoadProgram({ + ee::SQRT_S(2, 1), // result sqrt(4)=2.0 + }); + h.Run(); + h.ExpectFpr(2, 0x40000000u); // 2.0f + // I and D must be cleared; SI must NOT have been set (positive input). + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & (0x20000u | 0x10000u | 0x40u), 0u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & (0x20000u | 0x10000u | 0x40u), 0u); +} + +// ----- SQRT.S rounds to nearest regardless of FCR31 mode ------------- +// PS2 SQRT.S (like DIV.S) always rounds to nearest, independent of the +// configured EE rounding mode. The EE rec runs under host FPCR = FPUFPCR +// (ChopZero by default), so recSQRT_S must swap to the nearest-rounding +// FPUDivFPCR around the Fsqrt. This is not observable via Run()'s JIT-vs-interp +// auto-diff (the harness runs both under the host-default nearest FPCR, where +// the swap is a no-op). Instead replicate the real EE thread: set host FPCR to +// FPUFPCR (chop) and assert the JIT result directly. sqrt(5) is rounding- +// sensitive — nearest 0x400F1BBD vs round-toward-zero 0x400F1BBC. Without the +// in-op swap the Fsqrt would round under the ambient chop and produce +// 0x400F1BBC; with it the result is the PS2-correct nearest value. +TEST(EeRecFpu, SqrtSRoundsToNearestUnderChopFpcr) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprSingle(1, 5.0f); + h.LoadProgram({ee::SQRT_S(2, 1)}); + const FPControlRegister saved = FPControlRegister::GetCurrent(); + FPControlRegister::SetCurrent(EmuConfig.Cpu.FPUFPCR); // EE-thread ambient (chop) + h.RunJitNoDiff(); + FPControlRegister::SetCurrent(saved); + EXPECT_EQ(h.GetFprBitsJit(2), 0x400F1BBDu); // nearest-rounded sqrt(5) +} + +// ----- RSQRT.S deferred to interpreter -------------------------------- +// +// RSQRT.S sets D|SD when the divisor Ft is zero and I|SI when Ft is negative +// (interp RSQRT_S, FPU.cpp), and its Ft==0 branch returns ±posFmax keyed off +// the Ft sign. The op defers to the interpreter to handle flags and the +// zero-divisor result correctly. Assert the flag bits directly (Run() doesn't +// diff fprc[31]). +TEST(EeRecFpu, RsqrtSZeroDivisorSetsDenormFlags) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 1.0f); // fs (dividend) + h.SetFprBits(2, 0x00000000u); // ft (divisor) = +0 + h.LoadProgram({ + ee::RSQRT_S(3, 1, 2), // fd=3, fs=1, ft=2 + }); + h.Run(); + h.ExpectFpr(3, 0x7F7FFFFFu); // +posFmax (zero-divisor result) + const u32 mask = 0x20000u | 0x10000u | 0x40u | 0x20u; // I | D | SI | SD + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); // D|SD set + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x10000u | 0x20u); +} + +TEST(EeRecFpu, RsqrtSNegativeDivisorSetsInvalidFlags) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 2.0f); // fs (dividend) + h.SetFpr(2, -4.0f); // ft (divisor) negative + h.LoadProgram({ + ee::RSQRT_S(3, 1, 2), // fd=3; result 2/sqrt(4)=1.0 + }); + h.Run(); + h.ExpectFpr(3, 0x3F800000u); // 1.0f + const u32 mask = 0x20000u | 0x10000u | 0x40u | 0x20u; + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); // I|SI set + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & mask, 0x20000u | 0x40u); +} + +TEST(EeRecFpu, CEqSTrueSetsCc) +{ + // Pre: set fpr1 = fpr2 = 5.0. Expect FCR31.CC (bit 23) set to 1. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 5.0f); + h.SetFpr(2, 5.0f); + h.LoadProgram({ + ee::C_EQ_S(1, 2), + }); + h.Run(); + // Assert both sides: Run()'s auto-diff does not gate on fprc[31], so a JIT + // that never updates FCR31.CC would pass an interp-only assert. + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_NE(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, CEqSFalseClearsCc) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFcr31(1u << 23); // pre-set CC to 1 + h.SetFpr(1, 5.0f); + h.SetFpr(2, 6.0f); + h.LoadProgram({ + ee::C_EQ_S(1, 2), + }); + h.Run(); + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +// ----- compare-operand clamping -------------------------------------- +// +// The PS2 FPU has no Inf/NaN: C.cond.S clamps both operands to ±FLT_MAX +// (sign-preserving) before comparing, matching interp fpuDouble and the +// x86 JIT's fpuFloat3 (PMIN.SD vs 0x7f7fffff, PMIN.UD vs 0xff7fffff). +// A raw Fcmp on unclamped bit patterns makes the compare unordered on a +// NaN operand (all-false) where the PS2 wants an ordered compare against +// ±FLT_MAX. Inject raw Inf/NaN via SetFprBits (MTC1/LWC1 bit-copies bypass +// the arithmetic clamp in real games). +// +// Run()'s internal JIT-vs-interp diff catches the divergence; the explicit +// assert pins the PS2 spec value. The -NaN case validates *sign* preservation +// (fpuClampResult / Fminnm would wrongly fold -NaN to +FLT_MAX). + +// Assert on the JIT snapshot directly: the bug is JIT-side, and Run()'s +// auto-diff does not gate on fprc[31]. Without the operand clamp the raw Fcmp +// goes unordered on a NaN operand and leaves CC clear. +// Sign preservation: 0 < -NaN. The PS2 clamps -NaN to -FLT_MAX, so +// 0 < -FLT_MAX is FALSE (CC clear). A raw Fcmp on the NaN goes unordered, +// where ARM's "lt" (N!=V) is TRUE — so without the clamp CC is wrongly set. +// A sign-STRIPPING clamp (-NaN -> +FLT_MAX) would also wrongly set CC +// (0 < +FLT_MAX), so this case pins the sign-preserving SMIN/UMIN path. +TEST(EeRecFpu, CLtSZeroVsNegativeNaNIsFalse) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(2, 0.0f); + h.SetFprBits(1, 0xFFC00000u); // -NaN -> -FLT_MAX + h.LoadProgram({ + ee::C_LT_S(2, 1), // 0 < -FLT_MAX -> CC clear + }); + h.Run(); + EXPECT_EQ(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_EQ(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, CEqSPositiveNaNBothClampToMax) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x7FC00000u); // +NaN -> +FLT_MAX + h.SetFprBits(2, 0x7FC00000u); // +NaN -> +FLT_MAX + h.LoadProgram({ + ee::C_EQ_S(1, 2), // +FLT_MAX == +FLT_MAX -> CC set + }); + h.Run(); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_NE(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, CEqSInfinityClampsToMax) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x7F800000u); // +Inf -> +FLT_MAX + h.SetFprBits(2, 0x7F7FFFFFu); // +FLT_MAX (finite) + h.LoadProgram({ + ee::C_EQ_S(1, 2), // +FLT_MAX == +FLT_MAX -> CC set + }); + h.Run(); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_NE(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, Bc1tTakenWhenCcSet) +{ + // Layout: + // 0x00: C.EQ.S fpr1, fpr2 — equal → CC = 1 + // 0x04: BC1T +3 — taken, delay+3 words to target + // 0x08: NOP delay slot + // 0x0C: ADDIU v0, zero, 1 — not-taken marker + // 0x10: J park; NOP; NOP + // 0x1C: ADDIU v0, zero, 2 — taken target + // 0x20: J park; NOP + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 5.0f); + h.SetFpr(2, 5.0f); + h.LoadProgramNoTerm({ + ee::C_EQ_S(1, 2), + ee::BC1T(3), + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +TEST(EeRecFpu, Bc1fTakenWhenCcClear) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 5.0f); + h.SetFpr(2, 6.0f); + h.LoadProgramNoTerm({ + ee::C_EQ_S(1, 2), // 5 != 6 → CC = 0 + ee::BC1F(3), // taken + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +// The not-taken edge exercises the forward-skip test-bit-and-branch +// (Tbz/Tbnz on fprc[31] bit 23). The "Taken" tests above only prove the skip +// does NOT fire; these prove it fires in the right direction. +TEST(EeRecFpu, Bc1tNotTakenWhenCcClear) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 5.0f); + h.SetFpr(2, 6.0f); // 5 != 6 → CC = 0 + h.LoadProgramNoTerm({ + ee::C_EQ_S(1, 2), + ee::BC1T(3), // CC clear → not taken (skip fires) + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); +} + +TEST(EeRecFpu, Bc1fNotTakenWhenCcSet) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 5.0f); + h.SetFpr(2, 5.0f); // 5 == 5 → CC = 1 + h.LoadProgramNoTerm({ + ee::C_EQ_S(1, 2), + ee::BC1F(3), // CC set → not taken (skip fires) + NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); +} + +// =========================================================================== +// FPU accumulator family — ADDA / SUBA / MULA write to ACC (no Fd field). +// MADD / MSUB combine multiplication with ACC for Fd. MADDA / MSUBA do the +// same but write back to ACC. The PS2 ISA mandates two separate roundings +// (mul then add/sub) — these are NOT fused FMA. +// =========================================================================== + +TEST(EeRecFpu, AddaSWritesAccumulator) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetAcc(99.0f); // pre-state: ACC should be overwritten, not accumulated + h.LoadProgram({ee::ADDA_S(1, 2)}); + h.Run(); + h.ExpectAcc(FloatBits(7.0f)); +} + +TEST(EeRecFpu, SubaSWritesAccumulator) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 10.0f); + h.SetFpr(2, 3.0f); + h.SetAcc(99.0f); + h.LoadProgram({ee::SUBA_S(1, 2)}); + h.Run(); + h.ExpectAcc(FloatBits(7.0f)); +} + +TEST(EeRecFpu, MulaSWritesAccumulator) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 6.0f); + h.SetFpr(2, 7.0f); + h.SetAcc(99.0f); + h.LoadProgram({ee::MULA_S(1, 2)}); + h.Run(); + h.ExpectAcc(FloatBits(42.0f)); +} + +TEST(EeRecFpu, MaddSAddsProductToAccumulator) +{ + // fd = ACC + fs * ft = 10 + 3*4 = 22 + EeRecTestHarness h; + h.EnableCop1(); + h.SetAcc(10.0f); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ee::MADD_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, FloatBits(22.0f)); +} + +TEST(EeRecFpu, MsubSSubtractsProductFromAccumulator) +{ + // fd = ACC - fs * ft = 100 - 3*4 = 88 + EeRecTestHarness h; + h.EnableCop1(); + h.SetAcc(100.0f); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ee::MSUB_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, FloatBits(88.0f)); +} + +TEST(EeRecFpu, MaddaSAccumulatesIntoAccumulator) +{ + // ACC = ACC + fs * ft = 10 + 3*4 = 22; Fd field is not used + EeRecTestHarness h; + h.EnableCop1(); + h.SetAcc(10.0f); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ee::MADDA_S(1, 2)}); + h.Run(); + h.ExpectAcc(FloatBits(22.0f)); +} + +TEST(EeRecFpu, MsubaSSubtractsProductFromAccumulator) +{ + // ACC = ACC - fs * ft = 100 - 3*4 = 88 + EeRecTestHarness h; + h.EnableCop1(); + h.SetAcc(100.0f); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ee::MSUBA_S(1, 2)}); + h.Run(); + h.ExpectAcc(FloatBits(88.0f)); +} + +// ----- MADDA/MSUBA must NOT clamp the intermediate product ----------- +// +// Interp MADD_S/MSUB_S route the fs*ft product through fpuDouble (clamping it +// to +-fMax) before the accumulate, but MADDA_S/MSUBA_S (FPU.cpp) add the raw +// product directly and overflow-check only the final ACC. Clamping the product +// in all four ops diverges when fs*ft overflows: an overflowing product +// clamped to +fMax cancels against an opposite-signed ACC (-> 0) instead of +// overflowing the accumulate (-> +-fMax). Run()'s auto-diff compares ACC, and +// these cases are chosen so JIT and interp agree only without the product clamp. +TEST(EeRecFpu, MaddaSDoesNotClampIntermediateProduct) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetAccBits(0xFF7FFFFFu); // ACC = -fMax + h.SetFpr(1, 1e20f); + h.SetFpr(2, 1e20f); // fs*ft overflows single precision + h.LoadProgram({ee::MADDA_S(1, 2)}); + h.Run(); + // interp: -fMax + overflow -> +fMax. with product clamped: -fMax + (+fMax) = 0. + h.ExpectAcc(0x7F7FFFFFu); +} + +TEST(EeRecFpu, MsubaSDoesNotClampIntermediateProduct) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetAccBits(0x7F7FFFFFu); // ACC = +fMax + h.SetFpr(1, 1e20f); + h.SetFpr(2, 1e20f); // fs*ft overflows single precision + h.LoadProgram({ee::MSUBA_S(1, 2)}); + h.Run(); + // interp: +fMax - overflow -> -fMax. with product clamped: +fMax - (+fMax) = 0. + h.ExpectAcc(0xFF7FFFFFu); +} + +// ----- CHECK_FPU_EXTRA_OVERFLOW source clamp -------------------------- +// +// With the extra-overflow gate on (per-game clampMode>=2), the PS2 FPU recs +// clamp each fpr SOURCE to +-fMax before the op, matching interp fpuDouble and +// x86 recCommutativeOp/recMADDtemp (fpuFloat2). The divergence needs a poisoned +// fpr (raw Inf/NaN bits, reachable via MOV.S/LWC1/MTC1) AND fs*ft -> NaN: e.g. +// Inf*0. Without the input clamp the JIT computes Inf*0 = NaN and the result +// clamp folds it to +fMax; interp clamps Inf->fMax first, so fMax*0 = 0. +// Run()'s auto-diff plus ExpectFpr both pin JIT to interp. +TEST(EeRecFpu, MulSExtraOverflowClampsInfOperand) +{ + FpuExtraOverflowGuard guard; + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x7F800000u); // +Inf raw bits (poisoned fpr) + h.SetFpr(2, 0.0f); + h.LoadProgram({ee::MUL_S(3, 1, 2)}); + h.Run(); + h.ExpectFpr(3, 0x00000000u); // clamp(+Inf)*0 = +0; without input clamp -> +fMax +} + +TEST(EeRecFpu, MaddSExtraOverflowClampsInfOperand) +{ + FpuExtraOverflowGuard guard; + EeRecTestHarness h; + h.EnableCop1(); + h.SetAcc(5.0f); + h.SetFprBits(1, 0x7F800000u); // +Inf raw bits + h.SetFpr(2, 0.0f); + h.LoadProgram({ee::MADD_S(3, 1, 2)}); + h.Run(); + // fd = ACC + clamp(+Inf)*0 = 5 + 0 = 5; without input clamp: 5 + (Inf*0=NaN->fMax) -> +fMax + h.ExpectFpr(3, FloatBits(5.0f)); +} + +TEST(EeRecFpu, SqrtSPositiveValue) +{ + // PS2 SQRT.S takes sqrt of |ft|; argument is Ft, NOT Fs. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(2, 16.0f); + h.LoadProgram({ee::SQRT_S(3, 2)}); + h.Run(); + h.ExpectFpr(3, FloatBits(4.0f)); +} + +TEST(EeRecFpu, SqrtSNegativeArgumentReturnsAbsRoot) +{ + // SQRT.S of a negative value returns sqrt(|ft|) (no NaN) — PS2 quirk. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(2, -25.0f); + h.LoadProgram({ee::SQRT_S(3, 2)}); + h.Run(); + h.ExpectFpr(3, FloatBits(5.0f)); +} + +// =========================================================================== +// Allocator-state interaction patterns — consecutive ops sharing operands. +// The allocator path keeps operands in NEON across opcodes; an aliasing or +// writeback bug surfaces here, not in the single-op tests above. +// =========================================================================== + +TEST(EeRecFpu, AddSChainSameSourceTwice) +{ + // f3 = f1 + f2; then f4 = f3 + f2 (f2 re-used, allocator should keep it live) + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 1.0f); + h.SetFpr(2, 2.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::ADD_S(4, 3, 2), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(3.0f)); + h.ExpectFpr(4, FloatBits(5.0f)); +} + +TEST(EeRecFpu, AddSWriteSameAsRead) +{ + // f1 = f1 + f2 — destination aliases source. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 10.0f); + h.SetFpr(2, 5.0f); + h.LoadProgram({ee::ADD_S(1, 1, 2)}); + h.Run(); + h.ExpectFpr(1, FloatBits(15.0f)); +} + +TEST(EeRecFpu, AddaThenMaddChain) +{ + // Common geometry pattern: ADDA.S sets up ACC; MADD.S reads it. + // ACC = f1 + f2 = 7; fd = ACC + f3*f4 = 7 + 6 = 13 + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetFpr(3, 2.0f); + h.SetFpr(4, 3.0f); + h.LoadProgram({ + ee::ADDA_S(1, 2), + ee::MADD_S(5, 3, 4), + }); + h.Run(); + h.ExpectAcc(FloatBits(7.0f)); + h.ExpectFpr(5, FloatBits(13.0f)); +} + +TEST(EeRecFpu, MaddaChainAccumulates) +{ + // MULA.S then MADDA.S — common dot-product / vertex transform pattern. + // ACC = f1*f2 = 6; ACC += f3*f4 = 6 + 20 = 26 + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 2.0f); + h.SetFpr(2, 3.0f); + h.SetFpr(3, 4.0f); + h.SetFpr(4, 5.0f); + h.LoadProgram({ + ee::MULA_S(1, 2), + ee::MADDA_S(3, 4), + }); + h.Run(); + h.ExpectAcc(FloatBits(26.0f)); +} + +// --------------------------------------------------------------------------- +// MTC1 / MFC1 — allocator coherence with ADD_S-resident FPRs. +// +// ADD_S (routed through eeFPURecompileCode) leaves its destination FPR live +// in a NEON slot (MODE_WRITE) until block-end flush. MTC1 and MFC1 still +// bypass the allocator and go straight through memory at &fpuRegs.fpr[fs], so: +// - MFC1 after ADD_S reads stale fpr[fs] from memory. +// - MTC1 before block-end has its memory write clobbered when the +// allocator flushes the stale-but-now-live NEON slot. +// --------------------------------------------------------------------------- + +TEST(EeRecFpu, AddSThenMfc1ReadsAllocatorLiveResult) +{ + // ADD_S writes f3 (allocator-resident), MFC1 reads bits(f3) -> a0. + // MFC1 must see 7.0f, not the pre-test memory contents of fpr[3]. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetFprBits(3, 0xDEADBEEFu); // poison memory so a stale read is obvious + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::MFC1(reg::a0, 3), + }); + h.Run(); + h.ExpectFpr(3, FloatBits(7.0f)); + h.ExpectGpr64(reg::a0, static_cast(static_cast(static_cast(FloatBits(7.0f))))); +} + +TEST(EeRecFpu, Mtc1ThenAddSUsesFreshSource) +{ + // MTC1 writes fpr[1] from a0; ADD_S then reads f1. + // If the allocator had cached f1 from a SetFpr-time prefetch (or any + // earlier block), ADD_S would read the stale value rather than the + // MTC1 result. + EeRecTestHarness h; + h.EnableCop1(); + h.SetGpr64(reg::a0, FloatBits(10.0f)); + h.SetFpr(1, 99.0f); // pre-state: f1 = 99 in memory + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::MTC1(reg::a0, 1), // f1 <- bits(10.0) + ee::ADD_S(3, 1, 2), // f3 = f1 + f2 = 14 + }); + h.Run(); + h.ExpectFpr(3, FloatBits(14.0f)); +} + +TEST(EeRecFpu, Mtc1AfterAddSOverwritesAllocatorCachedFpr) +{ + // ADD_S leaves f3 live in the allocator. MTC1 then writes f3 in + // memory only. Block-end flush must NOT clobber the MTC1 write with + // the stale-but-allocator-held ADD_S result. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetGpr64(reg::a0, FloatBits(123.0f)); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), // f3 = 7 (allocator) + ee::MTC1(reg::a0, 3), // f3 = 123 (memory) + }); + h.Run(); + h.ExpectFpr(3, FloatBits(123.0f)); +} + +TEST(EeRecFpu, Mtc1ThenAddSReadsMtc1Value) +{ + // ADD_S writes f3 first (so f3 is allocator-resident), then MTC1 + // updates f3 in memory, then a SECOND ADD_S consumes f3. The second + // ADD_S must see the MTC1 value, not the cached allocator value. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetGpr64(reg::a0, FloatBits(100.0f)); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), // f3 = 7 (allocator-live) + ee::MTC1(reg::a0, 3), // f3 := 100 (memory) + ee::ADD_S(4, 3, 2), // f4 = f3 + f2; must read 100, not 7 + }); + h.Run(); + h.ExpectFpr(4, FloatBits(104.0f)); +} + +// --------------------------------------------------------------------------- +// Direct-memory ops that bypass the FPR allocator — must flush dirty NEON +// slots before reading and invalidate on writes. Each test pairs an ADD_S +// (writes live to allocator) with the op being tested. +// --------------------------------------------------------------------------- + +TEST(EeRecFpu, MovSAfterAddSPropagatesLiveValue) +{ + // ADD_S writes f3; MOV_S f4 = f3. MOV_S goes through memory copy. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetFprBits(4, 0xCAFEBABEu); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::MOV_S(4, 3), + }); + h.Run(); + h.ExpectFpr(4, FloatBits(7.0f)); +} + +// MFC1 reading an FPR that a preceding ADD_S left allocator-resident must read +// the live value straight from the host reg (the resident-read fast path), +// sign-extended into rt. f3=7.0 -> v0 = 0x0000000040E00000. +TEST(EeRecFpu, Mfc1AfterAddSReadsResidentValue) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), // f3 = 7 (allocator-resident) + ee::MFC1(reg::v0, 3), // v0 = sign_extend(bits(f3)) + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0x0000000040E00000ull); // 7.0f bits, +ve sign +} + +TEST(EeRecFpu, CEqAfterAddSReadsLiveOperand) +{ + // ADD_S writes f3 = 7; C_EQ_S f3, f4 (f4 = 7) -> CC should set. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetFpr(4, 7.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::C_EQ_S(3, 4), + }); + h.Run(); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_NE(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, CltAfterAddSReadsLiveOperand) +{ + // ADD_S writes f3 = 7; C_LT_S f3, f4 (f4 = 10) -> CC should set. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.SetFpr(4, 10.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::C_LT_S(3, 4), + }); + h.Run(); + EXPECT_NE(h.JitSnapshot().fprs.fprc[31] & (1u << 23), 0u); + EXPECT_NE(h.InterpSnapshot().fprs.fprc[31] & (1u << 23), 0u); +} + +TEST(EeRecFpu, CvtWAfterAddSReadsLiveSource) +{ + // ADD_S writes f3 = 7.0; CVT_W_S f4 = (int)f3 = 7. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 3.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), + ee::CVT_W_S(4, 3), + }); + h.Run(); + h.ExpectFpr(4, 7u); // int bits, not float bits +} + +TEST(EeRecFpu, DivSAfterAddSReadsLiveOperands) +{ + // ADD_S writes f3 = 20.0; DIV_S f4 = f3 / f1 = 20 / 4 = 5. + // DIV_S is natively emitted via eeFPURecompileCode (recDIV_S_xmm), which + // must read the allocator-live operands rather than stale memory. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFpr(1, 16.0f); + h.SetFpr(2, 4.0f); + h.LoadProgram({ + ee::ADD_S(3, 1, 2), // f3 = 20 + ee::DIV_S(4, 3, 2), // f4 = f3 / f2 = 5 + }); + h.Run(); + h.ExpectFpr(4, FloatBits(5.0f)); +} + +// ---- FpuMulHack (Tales of Destiny Remake gamefix) -------------------------- +// JIT-only: the interpreter MUL_S has no hack, so a hack-hit legitimately +// diverges from interp — assert GetFprBitsJit() under RunJitNoDiff(). The hack +// patches exactly 0.25 * (π) (0x3e800000 * 0x40490fdb) to 0x3f490fda. The +// shared emitFpuMul helper wires it into all of MUL/MULA/MADD/MSUB/MADDA/MSUBA. + +TEST(EeRecFpu, MulSFpuMulHackPatchesMagicProduct) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuMulHack(); + h.SetFprBits(0, 0x3e800000u); + h.SetFprBits(1, 0x40490fdbu); + h.LoadProgram({ee::MUL_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0x3f490fdau); +} + +TEST(EeRecFpu, MulSFpuMulHackOffGivesNativeProduct) +{ + // Gamefix off (default): the same operands produce the ordinary product, + // which JIT and interp agree on (Run diff), and which is NOT the patch value. + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(0, 0x3e800000u); + h.SetFprBits(1, 0x40490fdbu); + h.LoadProgram({ee::MUL_S(2, 0, 1)}); + h.Run(); + EXPECT_NE(h.GetFprBitsJit(2), 0x3f490fdau); +} + +TEST(EeRecFpu, MaddSFpuMulHackAppliesToProduct) +{ + // MADD routes its multiply through the same helper: ACC=0 + hack(Fs*Ft) -> + // the patched product. Proves the family-wide wiring, not just MUL_S. + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuMulHack(); + h.SetAccBits(0x00000000u); // +0 + h.SetFprBits(0, 0x3e800000u); + h.SetFprBits(1, 0x40490fdbu); + h.LoadProgram({ee::MADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0x3f490fdau); +} diff --git a/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp b/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp new file mode 100644 index 0000000000..313f2307ed --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Harness self-validation. These aren't tests of the EE rec — they're smoke +// tests of the EeRecTestHarness itself: that SetGpr/Run/GetGpr round-trips, +// that each instance starts from clean state, and that the JIT and interp +// paths agree (both ultimately delegate to the interpreter). +// +// Note: EeRecTestHarness's ctor calls ZeroCpuRegs() (a memset of cpuRegs), so +// every test starts from a zeroed cpuRegs regardless of what ran before it. +// The tests are therefore order-independent — there is no cross-test +// contamination to guard against, and no required ordering with other harness +// files. + +#include "harness/EeRecTestHarness.h" + +#include "R5900.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(EeRecHarnessValidation, TestOne_SetsRegisterToKnownPoison) +{ + // Set r9 (t1) to a recognizable value and confirm it round-trips: the + // harness seeds it, Run() executes a no-op program, and the interp + // snapshot reports the same value back. + EeRecTestHarness h; + h.SetGpr64(reg::t1, 0xDEADBEEFDEADBEEFull); + h.LoadProgram({NOP}); // no-op, just exercises Run() + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::t1), 0xDEADBEEFDEADBEEFull); +} + +TEST(EeRecHarnessValidation, TestTwo_DoesNotSeePreviousTestResidue) +{ + // New harness, no SetGpr. The ctor's ZeroCpuRegs scrub means r9 (t1) + // starts at 0 even though the previous test set it to a poison value — + // confirming each test gets clean cpuRegs — and a simple ADDIU computes + // correctly. (If the ctor scrub ever regressed, r9 would still hold the + // poison and the assertion below would fire.) + EeRecTestHarness h; + h.LoadProgram({ + ADDIU(reg::t0, reg::zero, 42), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::t1), 0ull) + << "r9 (t1) leaked from previous test's poison"; + EXPECT_EQ(h.GetGpr64Interp(reg::t0), 42ull); +} + +TEST(EeRecHarnessValidation, MultipleRunsInSameTestAreIdempotent) +{ + // Same harness, same program, called twice. Second Run() must produce + // identical results — proves that Run()'s internal state management + // doesn't accumulate over repeated invocations. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 5); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({ADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 12ull); + + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 12ull); +} + +TEST(EeRecHarnessValidation, DiffJitVsInterpIsTautologicalUnderDelegatingHarness) +{ + // Sanity: the JIT path emits a real block per guest pc (cached in + // recBlocks/recLUT), but each block's body is a sequence of + // `bl ` calls, so both paths ultimately run + // intCpu.Step(). Diff should be empty on any deterministic program. + // This catches regressions where real JIT opcode emission is + // accidentally enabled while the harness still delegates to interp. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 100); + h.SetGpr64(reg::a1, 200); + h.LoadProgram({ + ADDU(reg::v0, reg::a0, reg::a1), + AND (reg::v1, reg::a0, reg::a1), + ee::DADDU(reg::a2, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), h.GetGpr64Jit(reg::v0)); + EXPECT_EQ(h.GetGpr64Interp(reg::v1), h.GetGpr64Jit(reg::v1)); + EXPECT_EQ(h.GetGpr64Interp(reg::a2), h.GetGpr64Jit(reg::a2)); +} diff --git a/tests/ctest/core/recompilers/ee_rec_iop_handoff_tests.cpp b/tests/ctest/core/recompilers/ee_rec_iop_handoff_tests.cpp new file mode 100644 index 0000000000..ce04e94a08 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_iop_handoff_tests.cpp @@ -0,0 +1,765 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// EE → IOP handoff / cross-CPU SMC coverage. +// +// Real PS2 software uploads IOP code from the EE side via stores into the +// IOP RAM subsystem-bus window at 0x1C00_0000. The store chain: +// +// EE SW [interp step] +// → vtlb handler for 0x1C00_0000 region +// → _ext_memWrite32<9>(addr, v) +// → iopMemWrite32(addr & ~0x1C00_0000, v) +// → psxCpu->Clear(addr & ~3, 1) +// → recClearIOP → psxRecClearMem +// → iopClearRecLUT slot for the target page +// +// If any link is broken the IOP JIT dispatches the OLD cached block on +// re-entry at the same PC. These tests pin that end-to-end. The EE path +// runs through the EeRecTestHarness which executes both JIT and +// interp — where the EE rec delegates to the interpreter, +// the diff is tautological; where it emits real JIT code, that +// emission is exercised here. +// +// JitTestHarness ctor hoists `psxCpu = &psxRec` so `Clear` is meaningful +// (otherwise psxInt's Clear is a no-op). The test then drives the EE, +// then `iop.SetPc(...); iop.RunResume();` re-enters the IOP JIT WITHOUT +// force-invalidating — any stale block surfaces. + +#include "harness/EeRecTestHarness.h" +#include "harness/JitTestHarness.h" + +#include "IopMem.h" +#include "Memory.h" +#include "R3000A.h" + +#include +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { + +constexpr u32 kIopProgramPc = RecompilerTestEnvironment::kProgramPc; // 0x0001'0000 +constexpr u32 kIopBusWindow = 0x1C00'0000u; // EE view of IOP RAM + +// EE virtual-address of an IOP physical address through the subsystem +// bus window. Works for direct (physical) and kseg mirrors via the VMap. +constexpr u32 EeToIop(u32 iop_addr) { return kIopBusWindow | iop_addr; } + +// IOP opcode encodings the tests pin as spec. Computed here so a +// divergence is readable at the assertion site ("expected 0x24020064 +// = ADDIU v0, zero, 100"). +constexpr u32 kAddiuV0Zero100 = ADDIU(reg::v0, reg::zero, 100); // 0x24020064 +constexpr u32 kAddiuV0Zero200 = ADDIU(reg::v0, reg::zero, 200); // 0x240200C8 + +} // namespace + +// --------------------------------------------------------------------------- +// Isolation: vtlb → iopMemWrite → psxCpu->Clear chain, NO EE CPU in flight +// --------------------------------------------------------------------------- +// This test deliberately does NOT step the EE interpreter. It calls the +// EE-side vtlb entry point (`memWrite32`) directly from C, which hits the +// same handler the EE SW would — memWrite32 → _ext_memWrite32<9> +// → iopMemWrite32 → psxCpu->Clear. The goal is to isolate the vtlb chain +// from the "EE CPU running → cpuEventTest → dispatch IOP JIT" path, which +// is what the other tests in this file go through. If this test passes +// but the stepping tests crash, the fault is in the cpuEventTest→IOP-JIT +// entry, not in the EE→IOP memory handoff itself. +TEST(EeRecIopHandoff, DirectMemWriteThroughEeVtlbInvalidatesIopJitBlock) +{ + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 100), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u) + << "initial compile + run should set v0 = 100"; + + // Direct EE-side store — no intCpu.Step(), no cpuEventTest, no IOP + // JIT dispatch via the EE scheduler. `memWrite32` takes an EE virtual + // address; 0x1C01_0000 routes to the iop_memory vtlb handler which + // ends up in iopMemWrite32(0x00010000, ...) → psxCpu->Clear. + memWrite32(EeToIop(kIopProgramPc), kAddiuV0Zero200); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero200) + << "direct EE memWrite32 didn't reach IOP RAM — vtlb handler chain " + "is broken independent of any CPU stepping"; + + // Re-enter only the IOP JIT. If psxCpu->Clear fired as part of the + // store chain, the LUT slot is empty and the JIT re-compiles the new + // opcode. If Clear was skipped (e.g. psxCpu pointed at interp whose + // Clear is a no-op), the stale block runs and v0 stays 100. + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 200u) + << "IOP JIT cache not invalidated by the vtlb → iopMemWrite → Clear " + "chain — even without EE CPU in flight"; +} + +TEST(EeRecIopHandoff, EeSwLandsInIopRam) +{ + // Foundation: the 0x1C00_0000 window is live and routes EE stores + // to the IOP memory system. No IOP JIT state assertions — just + // prove bytes reach IOP RAM. + JitTestHarness iop; // hoists psxCpu = &psxRec for the test scope + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(0x0000'1000u)); + ee.SetGpr(reg::a1, 0xDEAD'BEEFu); + ee.LoadProgram({ + SW(reg::a1, 0, reg::a0), + }); + ee.Run(); + + EXPECT_EQ(iop.ReadU32(0x0000'1000u), 0xDEADBEEFu) + << "EE SW through 0x1C00_0000 bus window must land in IOP RAM"; +} + +TEST(EeRecIopHandoff, EeSwInvalidatesCachedIopBlock) +{ + // Seed and compile an IOP block that produces v0 = 100. + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 100), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u) + << "initial compile + run should set v0 = 100"; + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero100) + << "program word not actually in IOP RAM"; + + // From the EE, overwrite the first word with a 200-producing ADDIU. + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kAddiuV0Zero200); + ee.LoadProgram({ + SW(reg::a1, 0, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero200) + << "EE SW did not land in IOP RAM — vtlb → iopMemWrite chain broken"; + + // Re-enter the IOP JIT. If the EE's SW correctly triggered the + // psxCpu->Clear → LUT evict chain, the dispatcher MUST re-compile + // the new opcode at kIopProgramPc. If it didn't, v0 stays 100 + // (stale cached block). + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 200u) + << "IOP JIT cache was not invalidated by EE-side SW — this is " + "the EE→IOP handoff path that IOP program upload relies on"; +} + +TEST(EeRecIopHandoff, EeSwThroughKseg0MirrorInvalidates) +{ + // EE writes to 0x9C00_0000 + off (kseg0 cached mirror of physical + // 0x1C00_0000). The EE VMap pins 0x80..0x9F to physical 0..0x1F so the + // store routes through the same iop_memory handler. Same outcome as the + // physical-window test — lock that. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, 0x9C00'0000u | kIopProgramPc); // kseg0 mirror + ee.SetGpr(reg::a1, kAddiuV0Zero200); + ee.LoadProgram({ + SW(reg::a1, 0, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero200); + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 200u); +} + +TEST(EeRecIopHandoff, EeSbInvalidatesCachedIopBlock) +{ + // Byte-width SMC. MIPS is little-endian, so the low byte of the + // ADDIU encoding is imm[7:0]. Replacing the byte at offset 0 + // switches the immediate from 100 (0x64) to 200 (0xC8) without + // touching the rest of the instruction. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, 0xC8u); // new imm low byte + ee.LoadProgram({ + SB(reg::a1, 0, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero200) + << "byte-width EE write did not surgically replace the low byte"; + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 200u) + << "byte-width SMC from EE did not invalidate the IOP JIT block"; +} + +TEST(EeRecIopHandoff, EeShInvalidatesCachedIopBlock) +{ + // Half-width SMC. Low 16 bits of the ADDIU encoding ARE the + // immediate — replacing them switches the constant. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, 0x00C8u); // new imm16 + ee.LoadProgram({ + SH(reg::a1, 0, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero200); + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 200u) + << "half-width SMC from EE did not invalidate the IOP JIT block"; +} + +TEST(EeRecIopHandoff, MultiWordEeReplaceInvalidatesBlock) +{ + // Three-instruction IOP block; EE replaces all three words with + // new opcodes. The block must re-compile as a whole. + // + // Before: ADDIU v0,zero,10 ; ADDIU v1,zero,20 ; ADDU t0,v0,v1 → t0=30 + // After: ADDIU v0,zero,7 ; ADDIU v1,zero,35 ; ADDU t0,v0,v1 → t0=42 + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 10), + ADDIU(reg::v1, reg::zero, 20), + ADDU (reg::t0, reg::v0, reg::v1), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::t0), 30u); + + // EE program does three SWs, one per word. The second SetGpr for + // a1 is reassigned between stores. + constexpr u32 kNewW0 = ADDIU(reg::v0, reg::zero, 7); + constexpr u32 kNewW1 = ADDIU(reg::v1, reg::zero, 35); + constexpr u32 kNewW2 = ADDU (reg::t0, reg::v0, reg::v1); // same shape + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kNewW0); + ee.SetGpr(reg::a2, kNewW1); + ee.SetGpr(reg::a3, kNewW2); + ee.LoadProgram({ + SW(reg::a1, 0x0, reg::a0), + SW(reg::a2, 0x4, reg::a0), + SW(reg::a3, 0x8, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x0), kNewW0); + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x4), kNewW1); + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x8), kNewW2); + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::t0), 42u) + << "multi-word EE replacement did not fully invalidate the IOP block"; + EXPECT_EQ(iop.GetGprJit(reg::v0), 7u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 35u); +} + +TEST(EeRecIopHandoff, EeSwOutsideCodeRegionLeavesBlockCached) +{ + // Invalidation scope: EE writes far from the IOP code region. The + // cached block must stay intact; a RunResume executes the old + // opcode without recompilation. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 42)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 42u); + + // Scratch address deliberately in a far-away 64KB page — the IOP + // LUT is indexed by `addr >> 16`, so this SW MUST hit a different + // slot than the program's. + constexpr u32 kFarIopAddr = RecompilerTestEnvironment::kScratchAddr; // 0x0002'0000 + static_assert(kFarIopAddr >> 16 != kIopProgramPc >> 16, + "scratch addr must land in a different LUT page"); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kFarIopAddr)); + ee.SetGpr(reg::a1, 0xBAAD'BEEFu); + ee.LoadProgram({ + SW(reg::a1, 0, reg::a0), + }); + ee.Run(); + + EXPECT_EQ(iop.ReadU32(kFarIopAddr), 0xBAADBEEFu) + << "far-from-code EE SW did not actually land"; + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 42u) + << "EE SW to an unrelated address should not have disturbed the " + "cached IOP block at kIopProgramPc"; +} + +TEST(EeRecIopHandoff, CacheIsolationBitBlocksEeWrite) +{ + // Hardware quirk spec-lock: when the IOP's CP0 Status.IsC (bit 16) + // is set, iopMemWrite32 short-circuits BEFORE writing and BEFORE + // calling psxCpu->Clear. Even though the store + // came from the EE side, the IOP's CP0.Status gates the IOP + // memory backend. This test documents and locks that behavior; if it + // changes, real BIOS boot may regress. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + // Raise cache-isolation bit on the IOP — must happen AFTER iop.Run() + // (which resets psxRegs from pre_snapshot between JIT and interp + // phases) and BEFORE ee.Run() (whose SW triggers iopMemWrite32). + psxRegs.CP0.n.Status |= 0x1'0000u; + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kAddiuV0Zero200); + ee.LoadProgram({ + SW(reg::a1, 0, reg::a0), + }); + ee.Run(); + + // Store should have been dropped by the isolation gate. + EXPECT_EQ(iop.ReadU32(kIopProgramPc), kAddiuV0Zero100) + << "cache-isolation bit should have blocked the EE SW from landing"; + + // Restore default mode before the follow-on RunResume so the IOP's + // dispatcher doesn't hit the bit in flight. + psxRegs.CP0.n.Status &= ~0x1'0000u; + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 100u) + << "IOP block should still return 100 — the EE SW was isolated " + "out, so neither the RAM nor the JIT cache changed"; +} + +TEST(EeRecIopHandoff, SuccessiveEeSwsEachCauseRecompile) +{ + // Two rounds of EE-SMC-then-IOP-run back to back. Each round + // should see the JIT recompile with the most recent opcode. If + // the second Clear is swallowed (e.g. early-exit on a no-longer- + // block-head LUT slot), the third run returns the value from round + // 2 instead of round 3. + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + // Round 1: EE overwrites with ADDIU v0, zero, 200. + { + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kAddiuV0Zero200); + ee.LoadProgram({SW(reg::a1, 0, reg::a0)}); + ee.Run(); + } + iop.SetPc(kIopProgramPc); + iop.RunResume(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 200u) << "round 1: cache not invalidated"; + + // Round 2: EE overwrites again with ADDIU v0, zero, 300. + constexpr u32 kAddiuV0Zero300 = ADDIU(reg::v0, reg::zero, 300); + { + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kAddiuV0Zero300); + ee.LoadProgram({SW(reg::a1, 0, reg::a0)}); + ee.Run(); + } + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 300u) + << "round 2: second EE SMC failed to invalidate the block re-compiled " + "in round 1"; +} + +// --------------------------------------------------------------------------- +// Additional handoff-path coverage +// --------------------------------------------------------------------------- + +TEST(EeRecIopHandoff, EeSdWritesTwoWordsAndInvalidatesBothBlocks) +{ + // EE 64-bit SD to the IOP bus window. The EE store path splits the 64-bit + // store into two sequential iopMemWrite32 calls — one per word — each + // of which independently fires psxCpu->Clear. A single EE instruction + // should invalidate both words. + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 10), + ADDIU(reg::v1, reg::zero, 20), + ADDU (reg::t0, reg::v0, reg::v1), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::t0), 30u); + + // Two new opcodes packed into a single 64-bit value. MIPS is little- + // endian, so UD[0]'s low 32 bits land at [addr+0] and high 32 at + // [addr+4]. The target layout: word@+0 = kNewW0 (v0=7), word@+4 = kNewW1 (v1=35). + constexpr u32 kNewW0 = ADDIU(reg::v0, reg::zero, 7); + constexpr u32 kNewW1 = ADDIU(reg::v1, reg::zero, 35); + const u64 packed = static_cast(kNewW0) | (static_cast(kNewW1) << 32); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr64(reg::a1, packed); + ee.LoadProgram({ + ee::SD(reg::a1, 0, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x0), kNewW0) + << "low half of SD didn't land at +0"; + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x4), kNewW1) + << "high half of SD didn't land at +4"; + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::t0), 42u) + << "64-bit SD from EE didn't invalidate both half-words of the IOP " + "block — one or the other Clear call may have been skipped"; + EXPECT_EQ(iop.GetGprJit(reg::v0), 7u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 35u); +} + +TEST(EeRecIopHandoff, EeSwRewritesToJumpThenRecompiles) +{ + // SMC that installs CONTROL FLOW. Existing tests all rewrite ALU ops — + // the JIT might cache a block's control-flow shape separately (e.g. + // end-of-block detection, fall-through prediction). Rewriting to a J + // proves the re-lifted block observes the new terminator. + // + // Pre-stage block 2 in IOP RAM so that once block 1 jumps there, the + // dispatcher can compile+run it on demand. Block 2 is written but + // NOT compiled during the initial Run() — only block 1 is. + constexpr u32 kIopBlock2Pc = 0x0002'0000u; // far from kIopProgramPc + static_assert(kIopBlock2Pc >> 16 != kIopProgramPc >> 16, + "block 2 must land in a different LUT page to avoid co-invalidation"); + + JitTestHarness iop; + iop.LoadProgramAt(kIopBlock2Pc, { + ADDIU(reg::t0, reg::zero, 42), + }, /*append_jr_ra_term=*/true); + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 100), + ADDIU(reg::v1, reg::zero, 200), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + ASSERT_EQ(iop.GetGprJit(reg::v1), 200u); + ASSERT_EQ(iop.GetGprJit(reg::t0), 0u) + << "block 2 must not have run during the initial pass"; + + // From the EE, replace words 0 and 1 of block 1 with `J kIopBlock2Pc` + // and its delay-slot NOP. Word 2 (the old JR ra terminator) and word 3 + // (its NOP delay slot) stay in RAM but are unreachable after the J. + constexpr u32 kNewJ = J(kIopBlock2Pc); + constexpr u32 kNewNop = NOP; + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, kNewJ); + ee.SetGpr(reg::a2, kNewNop); + ee.LoadProgram({ + SW(reg::a1, 0x0, reg::a0), + SW(reg::a2, 0x4, reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x0), kNewJ); + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x4), kNewNop); + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::t0), 42u) + << "rewritten block 1 didn't actually transfer control to block 2 — " + "either the J wasn't re-lifted or block 2 wasn't entered"; + // v0 and v1 retain the values iop.Run() left them — the rewritten block + // 1 no longer writes to them. + EXPECT_EQ(iop.GetGprJit(reg::v0), 100u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 200u); +} + +TEST(EeRecIopHandoff, EeCopyLoopStreamingOpcodesAcrossWindow) +{ + // Models the BIOS-style code-upload loop: EE runs + // loop: LW rt,0(src); SW rt,0(dst); ADDIU src,+4; ADDIU dst,+4; + // ADDIU cnt,-1; BNE cnt,zero,loop; NOP + // streaming N IOP opcodes across the bus window in one shot. Each SW + // must fire its own Clear; the JIT must survive bulk invalidation and + // re-compile the final block from RAM. + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 10), + ADDIU(reg::v1, reg::zero, 20), + ADDU (reg::t0, reg::v0, reg::v1), + ADDIU(reg::s0, reg::zero, 99), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::t0), 30u); + ASSERT_EQ(iop.GetGprJit(reg::s0), 99u); + + // Pre-seed a 4-word source buffer in EE RAM far from the EE program. + constexpr u32 kEeSrcBuffer = 0x0003'0000u; + constexpr u32 kNewWords[] = { + ADDIU(reg::v0, reg::zero, 7), + ADDIU(reg::v1, reg::zero, 35), + ADDU (reg::t0, reg::v0, reg::v1), + ADDIU(reg::s0, reg::zero, 123), + }; + + EeRecTestHarness ee; + for (size_t i = 0; i < std::size(kNewWords); ++i) + ee.WriteU32(kEeSrcBuffer + static_cast(i * 4), kNewWords[i]); + + ee.SetGpr(reg::a0, kEeSrcBuffer); // src + ee.SetGpr(reg::a1, EeToIop(kIopProgramPc)); // dst + ee.SetGpr(reg::a2, static_cast(std::size(kNewWords))); // counter + + // Layout (word offsets from kProgramPc): + // 0 : LW a3, 0(a0) ; loop: + // 4 : SW a3, 0(a1) + // 8 : ADDIU a0, a0, 4 + // 12 : ADDIU a1, a1, 4 + // 16 : ADDIU a2, a2, -1 + // 20 : BNE a2, zero, -6 ; → word 0 + // 24 : NOP ; delay slot + // 25 : JR ra / NOP ; auto-appended terminator + // + // BNE at +20 targets +0. offset = (0 - (20+4))/4 = -6. + ee.LoadProgram({ + LW (reg::a3, 0, reg::a0), + SW (reg::a3, 0, reg::a1), + ADDIU(reg::a0, reg::a0, 4), + ADDIU(reg::a1, reg::a1, 4), + ADDIU(reg::a2, reg::a2, -1), + BNE (reg::a2, reg::zero, -6), + NOP, + }); + ee.Run(); + + for (size_t i = 0; i < std::size(kNewWords); ++i) + { + ASSERT_EQ(iop.ReadU32(kIopProgramPc + static_cast(i * 4)), kNewWords[i]) + << "copy loop word " << i << " didn't land"; + } + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 7u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 35u); + EXPECT_EQ(iop.GetGprJit(reg::t0), 42u); + EXPECT_EQ(iop.GetGprJit(reg::s0), 123u) + << "bulk EE-copy didn't invalidate the IOP block — one or more of " + "the 4 Clear calls was dropped"; +} + +TEST(EeRecIopHandoff, Sif1StyleDirectRamCopyPlusClear) +{ + // Spec-lock for the SIF1 DMA upload path. Unlike the + // vtlb-driven tests above, SIF1 bypasses the IOP vtlb entirely and: + // 1. memcpy's into iopPhysMem (direct pointer into iopMem->Main) + // 2. manually calls psxCpu->Clear(madr, readSize) once per chunk + // If `psxCpu->Clear` stops invalidating the LUT correctly (or readSize + // stops being passed in words), real SIF1 uploads would run stale code. + // This test mirrors the two-step shape with a synthetic "DMA chunk." + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 100), + ADDIU(reg::v1, reg::zero, 200), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + ASSERT_EQ(iop.GetGprJit(reg::v1), 200u); + + // 2-word synthetic upload — matches the two-iopMemWrite32 shape but + // through the SIF1 raw-RAM path instead. + const u32 new_words[] = { + ADDIU(reg::v0, reg::zero, 7), + ADDIU(reg::v1, reg::zero, 35), + }; + std::memcpy(iopPhysMem(kIopProgramPc), new_words, sizeof(new_words)); + // readSize in Sif1.cpp is in words (passed as arg 2 of Clear); this chunk + // is 2 words. + psxCpu->Clear(kIopProgramPc, 2); + + // Sanity: the synthetic copy touched RAM. (iop.ReadU32 goes through + // iopMemRead32 which also goes through the LUT, but the RAM region is + // the same.) + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x0), new_words[0]); + ASSERT_EQ(iop.ReadU32(kIopProgramPc + 0x4), new_words[1]); + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 7u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 35u) + << "SIF1-shape (memcpy + explicit Clear) didn't invalidate the " + "cached IOP block"; +} + +namespace { + +// Shared body for the byte-offset alignment sub-tests. Loads an ADDIU +// v0,zero,100 at kIopProgramPc, has the EE SB a single byte at `offset`, +// then re-runs the IOP JIT and checks that the cached block was rebuilt +// to reflect the post-write opcode (which must produce `expected_result` +// in `result_reg`). The point is that `psxCpu->Clear(addr & ~3, 1)` in +// iopMemWrite8 must word-round to the same aligned address regardless of +// which byte of the word was hit. +void RunSbOffsetCase(u32 offset, u8 new_byte, u32 expected_word, + u32 result_reg, u32 expected_result) +{ + JitTestHarness iop; + iop.LoadProgram({ADDIU(reg::v0, reg::zero, 100)}); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 100u); + + EeRecTestHarness ee; + ee.SetGpr(reg::a0, EeToIop(kIopProgramPc)); + ee.SetGpr(reg::a1, new_byte); + ee.LoadProgram({ + SB(reg::a1, static_cast(offset), reg::a0), + }); + ee.Run(); + + ASSERT_EQ(iop.ReadU32(kIopProgramPc), expected_word) + << "post-SB word read-back doesn't match expected encoding at offset " + << offset; + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(result_reg), expected_result) + << "SB at offset " << offset << " didn't invalidate the IOP block"; +} + +} // namespace + +TEST(EeRecIopHandoff, EeSbAtOffset1InvalidatesBlock) +{ + // Offset 1 = imm high byte of ADDIU. 0x24020064 → 0x24020164 gives + // ADDIU v0, zero, 0x164 = 356. + RunSbOffsetCase(/*offset=*/1, /*new_byte=*/0x01, + /*expected_word=*/0x2402'0164u, + reg::v0, /*expected=*/0x164u); +} + +TEST(EeRecIopHandoff, EeSbAtOffset2InvalidatesBlock) +{ + // Offset 2 = low byte of {rt[4:0], imm[15:8] not-applicable here since + // rt is high 5 bits of byte 2}. 0x24020064 → 0x24040064 rebinds rt + // from v0 (reg 2) to a0 (reg 4): ADDIU a0, zero, 100. After re-run, + // a0 = 100 and v0 is whatever iop.Run left it (iop.Run sets v0=100 + // and the rewritten block no longer writes v0, so it stays 100). + // The assertion checks a0 — the register the new opcode actually targets. + RunSbOffsetCase(/*offset=*/2, /*new_byte=*/0x04, + /*expected_word=*/0x2404'0064u, + reg::a0, /*expected=*/100u); +} + +TEST(EeRecIopHandoff, EeSbAtOffset3InvalidatesBlock) +{ + // Offset 3 = opcode byte. 0x24020064 → 0x3C020064 flips the + // instruction family from ADDIU to LUI: LUI v0, 0x0064 gives + // v0 = 0x0064'0000 = 6,553,600. This is the byte furthest from + // (addr & ~3); any off-by-one in the Clear's word-rounding would + // miss this slot first. + RunSbOffsetCase(/*offset=*/3, /*new_byte=*/0x3C, + /*expected_word=*/0x3C02'0064u, + reg::v0, /*expected=*/0x0064'0000u); +} + +TEST(EeRecIopHandoff, EeByteCopyLoopPreservesExecutability) +{ + // BIOS-style byte-copy loop (sans null termination): EE runs + // loop: LBU v0,0(src); SB v0,0(dst); ADDIU src,+1; ADDIU cnt,-1; + // BNE cnt,zero,loop; ADDIU dst,+1 (delay slot) + // copying 12 bytes (a 3-word IOP program) across the bus window. + // Each SB fires iopMemWrite8 → Clear on its word-aligned slot — so + // 12 individual Clears land, of which 4 hit the 1st word, 4 the 2nd, + // 4 the 3rd (for a contiguous-byte copy). + // + // The assertion is that the freshly-copied 3-instruction program + // actually executes — i.e., the JIT didn't leave any of the three + // words pointing at a stale compiled block. + JitTestHarness iop; + iop.LoadProgram({ + ADDIU(reg::v0, reg::zero, 1), + ADDIU(reg::v0, reg::zero, 2), + ADDIU(reg::v0, reg::zero, 3), + }); + iop.Run(); + ASSERT_EQ(iop.GetGprJit(reg::v0), 3u) + << "pre-compiled block should end with v0=3"; + + // Build a replacement 3-instruction program as 12 raw bytes in EE RAM. + const u32 new_words[] = { + ADDIU(reg::v0, reg::zero, 777), + ADDIU(reg::v1, reg::zero, 888), + ADDU (reg::t0, reg::v0, reg::v1), + }; + constexpr u32 kEeSrcBuffer = 0x0003'0000u; + constexpr u32 kCopyBytes = sizeof(new_words); // 12 + + EeRecTestHarness ee; + for (size_t i = 0; i < std::size(new_words); ++i) + ee.WriteU32(kEeSrcBuffer + static_cast(i * 4), new_words[i]); + + ee.SetGpr(reg::a0, kEeSrcBuffer); // src + ee.SetGpr(reg::a1, EeToIop(kIopProgramPc)); // dst + ee.SetGpr(reg::a2, kCopyBytes); // byte counter + + // Layout (word offsets from kProgramPc): + // 0 : LBU v0, 0(a0) ; loop: + // 4 : SB v0, 0(a1) + // 8 : ADDIU a0, a0, 1 + // 12 : ADDIU a2, a2, -1 + // 16 : BNE a2, zero, -5 ; → word 0 + // 20 : ADDIU a1, a1, 1 ; delay slot (runs after each BNE incl. fall-thru) + // 24 : JR ra / NOP ; auto-appended terminator + // + // BNE at +16 targets +0. offset = (0 - (16+4))/4 = -5. + ee.LoadProgram({ + LBU (reg::v0, 0, reg::a0), + SB (reg::v0, 0, reg::a1), + ADDIU(reg::a0, reg::a0, 1), + ADDIU(reg::a2, reg::a2, -1), + BNE (reg::a2, reg::zero, -5), + ADDIU(reg::a1, reg::a1, 1), + }); + ee.Run(); + + // Every byte of the 3-word program should have landed. + for (size_t i = 0; i < std::size(new_words); ++i) + { + ASSERT_EQ(iop.ReadU32(kIopProgramPc + static_cast(i * 4)), new_words[i]) + << "byte-copy loop didn't reassemble word " << i << " correctly"; + } + + iop.SetPc(kIopProgramPc); + iop.RunResume(); + EXPECT_EQ(iop.GetGprJit(reg::v0), 777u); + EXPECT_EQ(iop.GetGprJit(reg::v1), 888u); + EXPECT_EQ(iop.GetGprJit(reg::t0), 1665u) + << "per-byte EE writes across a 3-word IOP block didn't fully " + "invalidate the JIT cache — one of the 12 Clears was dropped " + "or a word fell through to stale code"; +} diff --git a/tests/ctest/core/recompilers/ee_rec_jump_tests.cpp b/tests/ctest/core/recompilers/ee_rec_jump_tests.cpp new file mode 100644 index 0000000000..064c86cec9 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_jump_tests.cpp @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Jump opcodes for the EE: J, JAL, JR, JALR. +// Architectural behaviors exercised here: +// - J absolute-in-256MB-region target, delay slot executes +// - JAL link register (r31) receives PC+8 +// - JR register target; ra-style returns +// - JALR link register is explicit (rd), can equal rs +// - Delay slot executes before the control transfer lands + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; +} // namespace + +TEST(EeRecJump, JHitsAbsoluteTargetViaDelaySlot) +{ + // J park. Delay slot sets v0=1. Control transfers to park, ending the run. + // Without `j park`, the fall-through would set v0=99. + EeRecTestHarness h; + h.LoadProgramNoTerm({ + J(kPark), + ADDIU(reg::v0, reg::zero, 1), + ADDIU(reg::v0, reg::zero, 99), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); +} + +TEST(EeRecJump, JalLinkRegisterReceivesPcPlus8) +{ + // JAL park — link register (ra) is PC+8. Program is at kProgramPc. + // So after JAL at kProgramPc: ra = kProgramPc + 8. + EeRecTestHarness h; + h.LoadProgramNoTerm({ + JAL(kPark), + NOP, // delay slot + }); + h.Run(); + h.ExpectGpr64(reg::ra, static_cast(static_cast(kProgramPc + 8))); +} + +TEST(EeRecJump, JrReturnsToAddressInRegister) +{ + // Manually set up "ra = park", then jr ra;nop. Should land at park. + // This is the default exit path of every LoadProgram() test, but this + // test asserts explicitly that JR alone (without any ADDIU side-effects) + // works as a return. + EeRecTestHarness h; + h.LoadProgramNoTerm({ + JR(reg::ra), + NOP, + ADDIU(reg::v0, reg::zero, 99), // not reached + }); + h.Run(); + // v0 is zero-initialized; fall-through path would have set it to 99. + h.ExpectGpr64(reg::v0, 0ull); +} + +TEST(EeRecJump, JalrLinkRegisterExplicitSeparateFromJumpTarget) +{ + // JALR rd=v1, rs=t0. t0 is set to park. rd=v1 receives PC+8. + EeRecTestHarness h; + h.SetGpr64(reg::t0, kPark); + h.LoadProgramNoTerm({ + JALR(reg::v1, reg::t0), + NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v1, static_cast(static_cast(kProgramPc + 8))); +} + +TEST(EeRecJump, JalrCanAliasLinkAndTarget) +{ + // JALR rd=rs (same reg). Target is evaluated *before* link is written, + // per MIPS-III: JALR with rd==rs is allowed, jump lands at old value of + // the register, link receives PC+8. + EeRecTestHarness h; + h.SetGpr64(reg::t0, kPark); + h.LoadProgramNoTerm({ + JALR(reg::t0, reg::t0), + NOP, + }); + h.Run(); + // After the jump: t0 = PC+8 (the link). + h.ExpectGpr64(reg::t0, static_cast(static_cast(kProgramPc + 8))); +} + +TEST(EeRecJump, DelaySlotRunsBeforeJumpTargetExecutes) +{ + // Delay-slot ADDIU sets v0=42 *before* the jump lands at park. So the + // post-state has v0=42, not whatever the fall-through would have produced. + EeRecTestHarness h; + h.LoadProgramNoTerm({ + J(kPark), + ADDIU(reg::v0, reg::zero, 42), // delay slot — runs + ADDIU(reg::v0, reg::zero, 99), // skipped + }); + h.Run(); + h.ExpectGpr64(reg::v0, 42ull); +} + +TEST(EeRecJump, JalLinkUpperHalfClearedAndSignExtended) +{ + // recJAL writes UL[0]=pc+4 + UL[1]=0 (mirrors x86: link is plain u32 zero- + // extended, not sign-extended). For a kProgramPc that fits in 31 bits, this + // matches s64-sign-extension. The contract under test: UD[0] == pc+8 with + // UD[1] left at its prior value (set nonzero pre-run to detect any + // stray full-128 write). + EeRecTestHarness h; + h.SetGpr128(reg::ra, 0xDEADBEEFCAFEBABEull, 0x1122334455667788ull); + h.LoadProgramNoTerm({ + JAL(kPark), + NOP, + }); + h.Run(); + h.ExpectGpr128(reg::ra, + static_cast(kProgramPc + 8), + 0x1122334455667788ull); +} + +TEST(EeRecJump, JrTargetCapturedBeforeDelaySlotClobbersRs) +{ + // Delay slot writes t0=garbage AFTER the JR captured t0=kPark into + // pcWriteback. Per MIPS, the captured target wins and execution lands at kPark. + EeRecTestHarness h; + h.SetGpr64(reg::t0, kPark); + h.LoadProgramNoTerm({ + JR(reg::t0), + ADDIU(reg::t0, reg::zero, 0x1234), // delay slot clobbers t0 + ADDIU(reg::v0, reg::zero, 99), // skipped + }); + h.Run(); + // t0's final value is the delay-slot write (sign-extended); v0 untouched. + h.ExpectGpr64(reg::t0, static_cast(static_cast(0x1234))); + h.ExpectGpr64(reg::v0, 0ull); +} + +TEST(EeRecJump, JalrWithRdZeroBehavesLikeJr) +{ + // JALR rd=0, rs=t0 — Rd==0 path skips the link write. Acts as plain jr. + EeRecTestHarness h; + h.SetGpr64(reg::t0, kPark); + h.LoadProgramNoTerm({ + JALR(reg::zero, reg::t0), + NOP, + ADDIU(reg::v0, reg::zero, 99), // skipped + }); + h.Run(); + h.ExpectGpr64(reg::zero, 0ull); + h.ExpectGpr64(reg::v0, 0ull); +} + +TEST(EeRecJump, JalrLinkUpperBitsZero) +{ + // JALR writes UD[0] = pc+8 (full 64-bit; recJALR uses x14 and stores to + // UD[0]). Distinct from JAL's UL[0]+UL[1] pair. Verify via UD[0]/UD[1] + // observation that the upper 64 bits of rd are not zeroed. + EeRecTestHarness h; + h.SetGpr64(reg::t0, kPark); + h.SetGpr128(reg::v1, 0xDEADBEEF, 0xAA55AA55AA55AA55ull); + h.LoadProgramNoTerm({ + JALR(reg::v1, reg::t0), + NOP, + }); + h.Run(); + h.ExpectGpr128(reg::v1, + static_cast(kProgramPc + 8), + 0xAA55AA55AA55AA55ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp b/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp new file mode 100644 index 0000000000..8e051b455b --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp @@ -0,0 +1,579 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Load/store tests for the 64-bit EE. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kScratch = RecompilerTestEnvironment::kScratchAddr; +} + +TEST(EeRecLoadStore, LwSignExtends) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x80000000u); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({LW(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF80000000ull); +} + +TEST(EeRecLoadStore, LwuZeroExtends) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x80000000u); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({ee::LWU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000080000000ull); +} + +TEST(EeRecLoadStore, LdFull64Bit) +{ + EeRecTestHarness h; + h.WriteU64(kScratch, 0x1122334455667788ull); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({ee::LD(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1122334455667788ull); +} + +TEST(EeRecLoadStore, SdFull64BitRoundtrip) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0xDEADBEEFCAFEBABEull); + h.TrackMemWindow(kScratch, 8); + h.LoadProgram({ee::SD(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU64(kScratch), 0xDEADBEEFCAFEBABEull); +} + +TEST(EeRecLoadStore, LbSignExtends) +{ + EeRecTestHarness h; + h.WriteU8(kScratch, 0xFF); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({LB(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecLoadStore, LbuZeroExtends) +{ + EeRecTestHarness h; + h.WriteU8(kScratch, 0xFF); + h.SetGpr64(reg::a0, kScratch); + h.LoadProgram({LBU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFull); +} + +// =========================================================================== +// Unaligned word loads — LWL / LWR +// +// LE MIPS-III semantics (R5900OpcodeImpl.cpp:656-717): each op reads the +// full 32-bit word at (EA & ~3) and merges with rt's low 32 bits via a +// shift+mask pair indexed by shift = EA & 3. LWL preserves the low (3-shift) +// bytes of rt; LWR preserves the high `shift` bytes. The merged 32-bit +// result is sign-extended into the 64-bit GPR — except LWR with shift==0 +// takes a special sign-extending path while shifts 1..3 only write the low +// 32 bits and preserve the upper 32 unchanged. Tests cover the four shifts +// per op plus the canonical LWR+LWL pair for an unaligned word load. +// +// Memory layout: mem32[kScratch+0] = 0x44332211 (LE bytes 11 22 33 44). +// Sentinel rt: 0xDEADBEEFCAFEBABE so preservation behavior is visible. +// =========================================================================== + +TEST(EeRecLoadStore, LwlShift0PartialLoad) +{ + // shift=0: result32 = (rt[low] & 0x00FFFFFF) | (mem << 24) + // = 0x00FEBABE | 0x11000000 = 0x11FEBABE; sign-extends positive. + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWL(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000011FEBABEull); +} + +TEST(EeRecLoadStore, LwlShift1) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWL(reg::v0, 1, reg::a0)}); + h.Run(); + // (0xCAFEBABE & 0xFFFF) | (0x44332211 << 16) = 0xBABE | 0x22110000 = 0x2211BABE + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x000000002211BABEull); +} + +TEST(EeRecLoadStore, LwlShift2) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWL(reg::v0, 2, reg::a0)}); + h.Run(); + // (0xCAFEBABE & 0xFF) | (0x44332211 << 8) = 0xBE | 0x33221100 = 0x332211BE + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x00000000332211BEull); +} + +TEST(EeRecLoadStore, LwlShift3FullWordSignExtendsNegative) +{ + // shift=3: full word load. Use bit-31-set memory to verify the s32→s64 + // sign-extension path through SD[0]. + EeRecTestHarness h; + h.WriteU32(kScratch, 0xFFEEDDCCu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWL(reg::v0, 3, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFEEDDCCull); +} + +TEST(EeRecLoadStore, LwrShift0FullWordSignExtendsNegative) +{ + // LWR shift==0: special-case sign-extends the merged 32-bit value to + // the full 64-bit destination. shift!=0 paths preserve the upper 32 bits + // instead — distinct codegen on the JIT side. + EeRecTestHarness h; + h.WriteU32(kScratch, 0xFFEEDDCCu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWR(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFEEDDCCull); +} + +TEST(EeRecLoadStore, LwrShift1PreservesUpper32) +{ + // shift=1: result32 = (rt[low] & 0xFF000000) | (mem >> 8). Upper 32 + // bits of rt MUST stay untouched (no sign-extend on shift!=0). + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWR(reg::v0, 1, reg::a0)}); + h.Run(); + // (0xCAFEBABE & 0xFF000000) | (0x44332211 >> 8) = 0xCA000000 | 0x00443322 + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEADBEEFCA443322ull); +} + +TEST(EeRecLoadStore, LwrShift2) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWR(reg::v0, 2, reg::a0)}); + h.Run(); + // (0xCAFEBABE & 0xFFFF0000) | (0x44332211 >> 16) = 0xCAFE0000 | 0x4433 + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEADBEEFCAFE4433ull); +} + +TEST(EeRecLoadStore, LwrShift3SingleByte) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0x44332211u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({LWR(reg::v0, 3, reg::a0)}); + h.Run(); + // (0xCAFEBABE & 0xFFFFFF00) | (0x44332211 >> 24) = 0xCAFEBA00 | 0x44 + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEADBEEFCAFEBA44ull); +} + +TEST(EeRecLoadStore, LwrLwlPairUnalignedWordLoad) +{ + // Canonical unaligned-word load via LWR+LWL. Memory bytes + // [0..7] = 99 11 22 33 44 AA BB CC are arranged so that the unaligned + // word at byte offset 1 = 0x44332211 (LE: 11 22 33 44). + EeRecTestHarness h; + h.WriteU32(kScratch + 0, 0x33221199u); // bytes 0..3: 99 11 22 33 + h.WriteU32(kScratch + 4, 0xCCBBAA44u); // bytes 4..7: 44 AA BB CC + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({ + LWR(reg::v0, 1, reg::a0), + LWL(reg::v0, 4, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000044332211ull); +} + +// =========================================================================== +// Unaligned word stores — SWL / SWR +// +// SWL/SWR mirror LWL/LWR but write to memory. They read the existing +// word at (EA & ~3), merge `shift` bytes of rt into it via shift+mask +// (R5900OpcodeImpl.cpp:813-858), and write the merged word back. +// =========================================================================== + +TEST(EeRecLoadStore, SwlShift0SingleByte) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0xAABBCCDDu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWL(reg::a1, 0, reg::a0)}); + h.Run(); + // new = (rt >> 24) | (mem & 0xFFFFFF00) = 0x12 | 0xAABBCC00 + EXPECT_EQ(h.ReadU32(kScratch), 0xAABBCC12u); +} + +TEST(EeRecLoadStore, SwlShift3FullWord) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0xAABBCCDDu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWL(reg::a1, 3, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kScratch), 0x12345678u); +} + +TEST(EeRecLoadStore, SwrShift0FullWord) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0xAABBCCDDu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWR(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kScratch), 0x12345678u); +} + +TEST(EeRecLoadStore, SwrShift3SingleByte) +{ + EeRecTestHarness h; + h.WriteU32(kScratch, 0xAABBCCDDu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWR(reg::a1, 3, reg::a0)}); + h.Run(); + // new = (rt << 24) | (mem & 0x00FFFFFF) = 0x78000000 | 0x00BBCCDD + EXPECT_EQ(h.ReadU32(kScratch), 0x78BBCCDDu); +} + +TEST(EeRecLoadStore, SwrSwlPairUnalignedWordStore) +{ + // Canonical unaligned-word store via SWR+SWL at byte offset 1. + // rt's 4 bytes (LE: 78 56 34 12) land in mem at addresses [1..4]. + EeRecTestHarness h; + h.WriteU32(kScratch + 0, 0xAAAAAAAAu); + h.WriteU32(kScratch + 4, 0xBBBBBBBBu); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kScratch, 8); + h.LoadProgram({ + SWR(reg::a1, 1, reg::a0), + SWL(reg::a1, 4, reg::a0), + }); + h.Run(); + // mem32[0]_new = (rt << 8) | (mem & 0xFF) = 0x34567800 | 0xAA = 0x345678AA + // mem32[4]_new = (rt >> 24) | (mem & 0xFFFFFF00) = 0x12 | 0xBBBBBB00 + EXPECT_EQ(h.ReadU32(kScratch + 0), 0x345678AAu); + EXPECT_EQ(h.ReadU32(kScratch + 4), 0xBBBBBB12u); +} + +// =========================================================================== +// Unaligned dword loads/stores — LDL / LDR / SDL / SDR +// +// R5900-only (MIPS-III); 8-way shift indexed by EA & 7. Same shape as +// LWL/LWR but operating on full 64-bit values, so no sign-extension +// branching exists. (R5900OpcodeImpl.cpp:741-901.) +// =========================================================================== + +TEST(EeRecLoadStore, LdrLdlPairUnalignedDwordLoad) +{ + // Bytes [0..15] = CC 11 22 33 44 55 66 77 88 DD EE FF 00 00 00 00. + // mem64[0] = 0x77665544332211CC, mem64[8] = 0x00000000FFEEDD88. + // Unaligned dword at byte 1 = bytes [1..8] = 0x8877665544332211. + EeRecTestHarness h; + h.WriteU64(kScratch + 0, 0x77665544332211CCull); + h.WriteU64(kScratch + 8, 0x00000000FFEEDD88ull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({ + ee::LDR(reg::v0, 1, reg::a0), + ee::LDL(reg::v0, 8, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x8877665544332211ull); +} + +TEST(EeRecLoadStore, SdrSdlPairUnalignedDwordStore) +{ + // Stores rt's 8 bytes (LE: F0 DE BC 9A 78 56 34 12) at byte addresses + // [1..8]. mem64[0] preserves byte 0; mem64[8] preserves bytes [9..15]. + EeRecTestHarness h; + h.WriteU64(kScratch + 0, 0xAAAAAAAAAAAAAAAAull); + h.WriteU64(kScratch + 8, 0xBBBBBBBBBBBBBBBBull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x123456789ABCDEF0ull); + h.TrackMemWindow(kScratch, 16); + h.LoadProgram({ + ee::SDR(reg::a1, 1, reg::a0), + ee::SDL(reg::a1, 8, reg::a0), + }); + h.Run(); + // mem64[0]_new = (rt << 8) | (mem & 0xFF) = 0x3456789ABCDEF000 | 0xAA + // mem64[8]_new = (rt >> 56) | (mem & 0xFFFFFFFFFFFFFF00) = 0x12 | 0xBBBB..00 + EXPECT_EQ(h.ReadU64(kScratch + 0), 0x3456789ABCDEF0AAull); + EXPECT_EQ(h.ReadU64(kScratch + 8), 0xBBBBBBBBBBBBBB12ull); +} + +// =========================================================================== +// Live-register preservation across the unaligned store/load. +// +// recUnalignedStoreWord/Double + recUnalignedLoadDouble provide inline +// fastmem read-modify-write codegen rather than an interpreter fallback. +// The EE GPR allocator parks guest registers in host +// NEON lanes starting at v0 (caller-saved per AAPCS), so a register that is +// NOT the store's Rs/Rt but is live across the op must survive it. The inline +// path preserves them two ways: on the fastmem fast path no call happens at all +// (and the backpatch thunk spills the live-register masks if a fault fires); +// on the softmem slow path the C call obeys AAPCS, and the op's own scratch +// (aligned addr / shift / Rt) lives in callee-saved temps. Run()'s JIT-vs- +// interp auto-diff over all 32 GPRs is the oracle. +// +// CAVEAT — these remain behavioral smoke-pins, not red/green repros: in this +// build the harness takes the fastmem path, where no live lane is ever at risk. +// Kept as a guard against future allocation changes. +// =========================================================================== + +TEST(EeRecLoadStore, SdlSdrPreservesLiveRegistersAcrossInterpCall) +{ + EeRecTestHarness h; + h.WriteU64(kScratch + 0, 0xAAAAAAAAAAAAAAAAull); + h.WriteU64(kScratch + 8, 0xBBBBBBBBBBBBBBBBull); + h.WriteU32(kScratch + 16, 0x00001000u); // non-const seed for t0 + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x123456789ABCDEF0ull); + h.TrackMemWindow(kScratch, 16); + h.LoadProgram({ + // t0 = mem32[seed] → non-const, allocated in a caller-saved NEON lane. + LW(reg::t0, 16, reg::a0), + // Derive several more non-const live values from t0. + ADDIU(reg::t1, reg::t0, 0x11), + ADDIU(reg::t2, reg::t0, 0x22), + ADDIU(reg::t3, reg::t0, 0x33), + ADDIU(reg::s0, reg::t0, 0x44), + ADDIU(reg::s1, reg::t0, 0x55), + // Unaligned store: its interpreter C-call must NOT clobber t0..s1. + ee::SDR(reg::a1, 1, reg::a0), + ee::SDL(reg::a1, 8, reg::a0), + // Read the live registers back after the call so they must survive it. + ADDU(reg::v0, reg::t0, reg::t1), + ADDU(reg::v0, reg::v0, reg::t2), + ADDU(reg::v0, reg::v0, reg::t3), + ADDU(reg::v0, reg::v0, reg::s0), + ADDU(reg::v0, reg::v0, reg::s1), + }); + h.Run(); + // Spec-lock the survivors (t0 = 0x1000; sum below) in addition to the + // auto-diff, so a clobber that happens to match interp is still caught. + h.ExpectGpr64(reg::t0, 0x1000ull); + h.ExpectGpr64(reg::v0, 0x1000ull * 6 + (0x11 + 0x22 + 0x33 + 0x44 + 0x55)); +} + +TEST(EeRecLoadStore, LdlLdrPreservesLiveRegistersAcrossInterpCall) +{ + EeRecTestHarness h; + h.WriteU64(kScratch + 0, 0x77665544332211CCull); + h.WriteU64(kScratch + 8, 0x00000000FFEEDD88ull); + h.WriteU32(kScratch + 16, 0x00002000u); // non-const seed for t0 + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v1, 0xDEADBEEFCAFEBABEull); // LDL/LDR target (= Rt) + h.LoadProgram({ + LW(reg::t0, 16, reg::a0), + ADDIU(reg::t1, reg::t0, 0x11), + ADDIU(reg::t2, reg::t0, 0x22), + ADDIU(reg::t3, reg::t0, 0x33), + ADDIU(reg::s0, reg::t0, 0x44), + ADDIU(reg::s1, reg::t0, 0x55), + // Unaligned load into v1; t0..s1 are live across its interpreter call. + ee::LDR(reg::v1, 1, reg::a0), + ee::LDL(reg::v1, 8, reg::a0), + ADDU(reg::v0, reg::t0, reg::t1), + ADDU(reg::v0, reg::v0, reg::t2), + ADDU(reg::v0, reg::v0, reg::t3), + ADDU(reg::v0, reg::v0, reg::s0), + ADDU(reg::v0, reg::v0, reg::s1), + }); + h.Run(); + h.ExpectGpr64(reg::t0, 0x2000ull); + h.ExpectGpr64(reg::v0, 0x2000ull * 6 + (0x11 + 0x22 + 0x33 + 0x44 + 0x55)); + h.ExpectGpr64(reg::v1, 0x8877665544332211ull); // the unaligned-load result +} + +// =========================================================================== +// Exhaustive alignment sweeps. +// +// The inline RMW codegen has a distinct shift/mask path per alignment plus a +// degenerate-shift special case (word shift==3, dword s==0/s==7). These sweeps +// hit every alignment 0..3 (word) / 0..7 (dword) for all six ops, with the +// interpreter as the oracle: Run() auto-diffs JIT vs interp post-state, +// including the tracked store-target memory window. +// =========================================================================== + +TEST(EeRecLoadStore, SwlAllAlignments) +{ + for (s16 off = 0; off <= 3; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU32(kScratch, 0x11223344u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x99887766AABBCCDDull); // Rt.UL[0] = 0xAABBCCDD + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWL(reg::a1, off, reg::a0)}); + h.Run(); + } +} + +TEST(EeRecLoadStore, SwrAllAlignments) +{ + for (s16 off = 0; off <= 3; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU32(kScratch, 0x11223344u); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x99887766AABBCCDDull); + h.TrackMemWindow(kScratch, 4); + h.LoadProgram({SWR(reg::a1, off, reg::a0)}); + h.Run(); + } +} + +TEST(EeRecLoadStore, SdlAllAlignments) +{ + for (s16 off = 0; off <= 7; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU64(kScratch, 0x1122334455667788ull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x99AABBCCDDEEFF00ull); + h.TrackMemWindow(kScratch, 8); + h.LoadProgram({ee::SDL(reg::a1, off, reg::a0)}); + h.Run(); + } +} + +TEST(EeRecLoadStore, SdrAllAlignments) +{ + for (s16 off = 0; off <= 7; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU64(kScratch, 0x1122334455667788ull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::a1, 0x99AABBCCDDEEFF00ull); + h.TrackMemWindow(kScratch, 8); + h.LoadProgram({ee::SDR(reg::a1, off, reg::a0)}); + h.Run(); + } +} + +TEST(EeRecLoadStore, LdlAllAlignments) +{ + for (s16 off = 0; off <= 7; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU64(kScratch, 0x1122334455667788ull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); // pre-existing Rt (preserved bytes) + h.LoadProgram({ee::LDL(reg::v0, off, reg::a0)}); + h.Run(); + } +} + +TEST(EeRecLoadStore, LdrAllAlignments) +{ + for (s16 off = 0; off <= 7; off++) + { + SCOPED_TRACE(testing::Message() << "offset=" << off); + EeRecTestHarness h; + h.WriteU64(kScratch, 0x1122334455667788ull); + h.SetGpr64(reg::a0, kScratch); + h.SetGpr64(reg::v0, 0xDEADBEEFCAFEBABEull); + h.LoadProgram({ee::LDR(reg::v0, off, reg::a0)}); + h.Run(); + } +} + +// =========================================================================== +// Multi-register unrolled unaligned 32-byte copy. +// +// An unaligned memcpy unrolled 32 bytes/iter — four LDL/LDR load-pairs into +// v0/a2/a3/t0, then four SDL/SDR store-pairs to (a0): +// +// LDL v0,7(v1) LDR v0,0(v1) ; dword 0 +// LDL a2,15(v1) LDR a2,8(v1) ; dword 1 +// LDL a3,23(v1) LDR a3,16(v1) ; dword 2 +// LDL t0,31(v1) LDR t0,24(v1) ; dword 3 +// SDL v0,7(a0) SDR v0,0(a0) +// SDL a2,15(a0) SDR a2,8(a0) +// SDL a3,23(a0) SDR a3,16(a0) +// SDL t0,31(a0) SDR t0,24(a0) +// +// Single LDL/LDR/SDL/SDR pairs pass (above); this is the full-block shape with +// four destination registers live across the loads-then-stores, swept over all +// source/destination alignments. Run()'s JIT-vs-interp auto-diff (GPRs + +// tracked dest memory) is the oracle; the per-byte checks also spec-lock the +// copy so a both-wrong result is still caught. +// =========================================================================== +TEST(EeRecLoadStore, MultiRegUnalignedDwordCopyBlock) +{ + constexpr u32 kSrc = kScratch; + constexpr u32 kDst = kScratch + 256; + for (u32 sa = 0; sa < 8; ++sa) + { + for (u32 da = 0; da < 8; ++da) + { + SCOPED_TRACE(testing::Message() << "src_align=" << sa << " dst_align=" << da); + EeRecTestHarness h; + const u32 src = kSrc + sa; + const u32 dst = kDst + da; + // 40 distinct source bytes so any mis-shift is visible in the copy. + for (u32 i = 0; i < 40; ++i) + h.WriteU8(src + i, static_cast(0x10 + i)); + // Destination sentinel (bytes outside [0,32) must be preserved). + for (u32 i = 0; i < 48; ++i) + h.WriteU8(kDst + i, 0xA5); + h.SetGpr64(reg::v1, src); + h.SetGpr64(reg::a0, dst); + h.TrackMemWindow(kDst, 48); + h.LoadProgram({ + ee::LDL(reg::v0, 7, reg::v1), ee::LDR(reg::v0, 0, reg::v1), + ee::LDL(reg::a2, 15, reg::v1), ee::LDR(reg::a2, 8, reg::v1), + ee::LDL(reg::a3, 23, reg::v1), ee::LDR(reg::a3, 16, reg::v1), + ee::LDL(reg::t0, 31, reg::v1), ee::LDR(reg::t0, 24, reg::v1), + ee::SDL(reg::v0, 7, reg::a0), ee::SDR(reg::v0, 0, reg::a0), + ee::SDL(reg::a2, 15, reg::a0), ee::SDR(reg::a2, 8, reg::a0), + ee::SDL(reg::a3, 23, reg::a0), ee::SDR(reg::a3, 16, reg::a0), + ee::SDL(reg::t0, 31, reg::a0), ee::SDR(reg::t0, 24, reg::a0), + }); + h.Run(); // auto-diffs JIT vs interp (GPRs + tracked dest memory) + for (u32 i = 0; i < 32; ++i) + EXPECT_EQ(h.ReadU8(dst + i), static_cast(0x10 + i)) << "copied byte " << i; + } + } +} diff --git a/tests/ctest/core/recompilers/ee_rec_mmi_coherence_tests.cpp b/tests/ctest/core/recompilers/ee_rec_mmi_coherence_tests.cpp new file mode 100644 index 0000000000..596ce2dcd4 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_mmi_coherence_tests.cpp @@ -0,0 +1,965 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// MMI allocator-coherence regression tests. +// +// The MMI ops routed through eeRecompileCodeXMM (PAND/POR/PXOR/PADDx/etc.) +// keep Rd's full 128 bits live in a NEON Q-reg owned by the EE allocator. +// Anything that subsequently reads the value must either: +// +// - go through the allocator (so it sees the live NEON copy), or +// - explicitly flush+invalidate the NEON slot before a direct-memory +// read (`mmiFlushReg`-style — _deleteEEreg(reg, 1)). +// +// These tests exercise the boundary in both directions: converted-MMI → X +// where X is an unconverted MMI op (PCPYLD/UD/H, PSLLW, PMTHI, PMTLO), a +// 64-bit scalar GPR op (DADDU, OR), a 128-bit store (SQ), and another +// converted MMI op (allocator reuse, MODE_WRITE-only reuse, three-deep +// chains). +// +// Run() diffs JIT vs interp post-state; ExpectGpr128 additionally pins the +// expected architectural value. A divergence here is the JIT failing to +// pick up the in-NEON value (stale memory) or failing to write back all +// 128 bits (zeroed upper-half). + +#include "harness/EeRecTestHarness.h" +#include "harness/RecompilerTestEnvironment.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { + +constexpr u64 kALo = 0x1111111111111111ull; +constexpr u64 kAHi = 0x2222222222222222ull; +constexpr u64 kBLo = 0x3333333333333333ull; +constexpr u64 kBHi = 0x4444444444444444ull; + +} // namespace + +// ============================================================================ +// Converted MMI → unconverted MMI op reading Rd via direct memory +// ============================================================================ + +// PAND writes v0 through the allocator (Rd in NEON); PCPYUD then reads v0 +// for both Rs and Rt. PCPYUD's path is mmiFlushReg(Rs/Rt) + mmiLoadReg(memory) — +// if mmiFlushReg fails to write back the NEON copy, PCPYUD reads stale memory. +TEST(EeRecMmiCoherence, PandThenPcpyudReadsLiveAllocatorValue) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + // Pre-seed v0 to a known-wrong sentinel so a stale-memory read shows up + // as the sentinel value instead of the PAND result. + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::PCPYUD(reg::v1, reg::v0, reg::v0), + }); + h.Run(); + // PAND result: lo = a0.lo & a1.lo, hi = a0.hi & a1.hi. + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + // PCPYUD v1, v0, v0 → v1 = {v0.hi, v0.hi}. + h.ExpectMmiPair(reg::v1, andHi, andHi); +} + +// PAND writes v0; PCPYLD then assembles {v0.lo, v0.lo}. Same flush-coherence +// path but exercises the lower-half read. +TEST(EeRecMmiCoherence, PandThenPcpyldReadsLiveAllocatorValue) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::PCPYLD(reg::v1, reg::v0, reg::v0), + }); + h.Run(); + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + // PCPYLD v1, rs, rt → {UD[0]=rt.lo, UD[1]=rs.lo}; rs = rt = v0 → both halves = v0.lo. + h.ExpectMmiPair(reg::v1, andLo, andLo); +} + +// PAND writes v0; PCPYH then duplicates v0.UH[0] across the lower 4 halfwords +// and v0.UH[4] across the upper 4. Bit pattern is engineered so the stale +// pre-PAND sentinel and the live PAND value have visibly different lane[0] +// and lane[4] halfwords. +TEST(EeRecMmiCoherence, PandThenPcpyhReadsLiveAllocatorValue) +{ + EeRecTestHarness h; + // a0 lo = 0xAAAA in lane[0], a1 lo = 0x00FF → AND = 0x00AA in lane[0]. + // a0 hi has 0xBBBB in lane[4-of-128], a1 hi has 0x0F0F → AND = 0x0B0B. + const u64 a0lo = 0x000000000000AAAAull; + const u64 a1lo = 0x00000000000000FFull; + const u64 a0hi = 0x000000000000BBBBull; + const u64 a1hi = 0x0000000000000F0Full; + h.SetMmiPair(reg::a0, a0lo, a0hi); + h.SetMmiPair(reg::a1, a1lo, a1hi); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::PCPYH(reg::v1, reg::v0), + }); + h.Run(); + h.ExpectMmiPair(reg::v0, a0lo & a1lo, a0hi & a1hi); + // PCPYH: replicate v0.UH[0]=0x00AA into lower 4 lanes; v0.UH[4]=0x0B0B + // into upper 4 lanes. (Lane[4] is the first halfword of the upper 64 + // bits, which is the low halfword of the upper UD = a0hi & a1hi.) + h.ExpectMmiPair(reg::v1, 0x00AA00AA00AA00AAull, 0x0B0B0B0B0B0B0B0Bull); +} + +// PAND writes v0; PSLLW shifts v0's 4 word lanes left by 4. PSLLW's path is +// mmiFlushReg(_Rt_)+mmiLoadReg → another direct-memory read that must see +// the live PAND result. +TEST(EeRecMmiCoherence, PandThenPsllwReadsLiveAllocatorValue) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000A0000000Bull, 0x0000000C0000000Dull); + h.SetMmiPair(reg::a1, 0x00000000FFFFFFFFull, 0xFFFFFFFF00000000ull); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::PSLLW(reg::v1, reg::v0, 4), + }); + h.Run(); + // PAND lane[0] = 0xB & 0xFFFFFFFF = 0xB; lane[1] = 0xA & 0 = 0; + // lane[2] = 0xD & 0 = 0; lane[3] = 0xC & 0xFFFFFFFF = 0xC. + const u64 andLo = 0x000000000000000Bull; + const u64 andHi = 0x0000000C00000000ull; + h.ExpectMmiPair(reg::v0, andLo, andHi); + // PSLLW by 4 shifts each 32-bit lane: {0xB, 0x0, 0x0, 0xC} << 4 + // = {0xB0, 0x0, 0x0, 0xC0}. + h.ExpectMmiPair(reg::v1, 0x00000000000000B0ull, 0x000000C000000000ull); +} + +// PAND writes v0; PMTHI uses v0 as the source for the 128-bit HI register +// via mmiLoadReg(_Rs_). Check both the resulting HI (via PMFHI roundtrip) +// and the survival of v0. +TEST(EeRecMmiCoherence, PandThenPmthiReadsLiveAllocatorValue) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.SetHi64(0xAAAAAAAAAAAAAAAAull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::PMTHI(reg::v0), + ee::PMFHI(reg::v1), + }); + h.Run(); + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + // HI = v0 = {andLo, andHi}; PMFHI v1 = HI. + h.ExpectMmiPair(reg::v1, andLo, andHi); +} + +// ============================================================================ +// Chained converted MMI ops — allocator reuse correctness +// ============================================================================ + +// Two converted MMI ops sharing Rd: PAND v0, a0, a1; POR v0, v0, a2. +// The allocator should keep v0 in the same Q-reg across both ops without a +// memory bounce. MODE_WRITE on the second op must NOT clobber the live +// MODE_READ data from the first. +TEST(EeRecMmiCoherence, ChainedPandThenPorSameDest) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFF00000000ull, 0xFFFF0000FFFF0000ull); + h.SetMmiPair(reg::a1, 0x00000000FFFFFFFFull, 0x0000FFFF0000FFFFull); + h.SetMmiPair(reg::a2, 0x1111111111111111ull, 0x2222222222222222ull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::POR(reg::v0, reg::v0, reg::a2), + }); + h.Run(); + // PAND: 0xFFFFFFFF00000000 & 0x00000000FFFFFFFF = 0; 0xFFFF0000FFFF0000 + // & 0x0000FFFF0000FFFF = 0. + // POR v0, 0, a2 = a2. + h.ExpectMmiPair(reg::v0, 0x1111111111111111ull, 0x2222222222222222ull); +} + +// Three-deep chain: PAND, POR, PXOR all writing to the same destination +// register v0. Exercises repeated allocator reuse and ensures the running +// 128-bit value tracks correctly through three back-to-back updates. +TEST(EeRecMmiCoherence, ThreeDeepChainedMmiUpdatesSameDest) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00FF00FF00FF00FFull, 0x00FF00FF00FF00FFull); + h.SetMmiPair(reg::a1, 0x0F0F0F0F0F0F0F0Full, 0x0F0F0F0F0F0F0F0Full); + h.SetMmiPair(reg::a2, 0xFFFFFFFFFFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); + h.SetMmiPair(reg::a3, 0xAAAAAAAAAAAAAAAAull, 0x5555555555555555ull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), // v0 = 0x00FF & 0x0F0F = 0x000F (per byte: 0x0F0F0F0F) + ee::POR (reg::v0, reg::v0, reg::a2), // v0 |= 0xFF…FF → all FF + ee::PXOR(reg::v0, reg::v0, reg::a3), // v0 ^= a3 + }); + h.Run(); + const u64 expectedLo = 0xFFFFFFFFFFFFFFFFull ^ 0xAAAAAAAAAAAAAAAAull; + const u64 expectedHi = 0xFFFFFFFFFFFFFFFFull ^ 0x5555555555555555ull; + h.ExpectMmiPair(reg::v0, expectedLo, expectedHi); +} + +// Source-reuse chain: PAND v0, a0, a1; POR v1, v0, a2. The second op +// reads v0 (live in NEON from the first op) and writes a different Rd. +TEST(EeRecMmiCoherence, ChainedPorReadsLivePandResult) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + h.SetMmiPair(reg::a2, 0x0F0F0F0F0F0F0F0Full, 0xF0F0F0F0F0F0F0F0ull); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::POR (reg::v1, reg::v0, reg::a2), + }); + h.Run(); + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + h.ExpectMmiPair(reg::v1, andLo | 0x0F0F0F0F0F0F0F0Full, andHi | 0xF0F0F0F0F0F0F0F0ull); +} + +// ============================================================================ +// Converted MMI → 128-bit store (SQ) +// ============================================================================ + +// PAND writes v0; SQ stores v0 to memory at sp+0. SQ must see the +// post-PAND value, not the seeded sentinel. +TEST(EeRecMmiCoherence, PandThenSqStoresLiveResult) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + const u32 kScratch = RecompilerTestEnvironment::kScratchAddr; + h.SetGpr64(reg::sp, kScratch); + h.TrackMemWindow(kScratch, 16); + h.LoadProgram({ + ee::PAND(reg::v0, reg::a0, reg::a1), + ee::SQ (reg::v0, 0, reg::sp), + }); + h.Run(); + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + EXPECT_EQ(h.ReadU64(kScratch + 0), andLo) << "SQ stored stale .lo"; + EXPECT_EQ(h.ReadU64(kScratch + 8), andHi) << "SQ stored stale .hi"; +} + +// ============================================================================ +// Converted MMI → 64-bit scalar GPR read +// ============================================================================ + +// PAND writes v0 (full 128 bits live in NEON); DADDU reads v0's lower 64 +// via _eeMoveGPRtoR. _eeMoveGPRtoR IS allocator-aware (checks NEON for +// MODE_READ slot), but the converted MMI uses MODE_WRITE-only, so the +// allocator check might miss it and fall through to stale memory. +TEST(EeRecMmiCoherence, PandThenDadduReadsLiveLowerHalf) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, kALo, kAHi); + h.SetMmiPair(reg::a1, kBLo, kBHi); + h.SetMmiPair(reg::v0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.LoadProgram({ + ee::PAND (reg::v0, reg::a0, reg::a1), + ee::DADDU(reg::v1, reg::v0, reg::zero), + }); + h.Run(); + const u64 andLo = kALo & kBLo; + const u64 andHi = kAHi & kBHi; + h.ExpectMmiPair(reg::v0, andLo, andHi); + // DADDU v1, v0, zero copies v0's lower 64 bits to v1 (and sign-extends + // or zero-fills the upper 64 — DADDU writes UD[0] only). + EXPECT_EQ(h.GetGpr64Jit(reg::v1), andLo); +} + +// ============================================================================ +// Coverage for converted MMI ops not in ee_rec_mmi_simd_tests.cpp. +// PEXT*/PPAC*/PMAX*/PMIN*/PABS*/PADSBH/PINTH/PINTEH are part of the +// eeRecompileCodeXMM conversion but missing from the existing test suite. +// Test against the interpreter to verify each NEON emit matches PS2 semantics. +// ============================================================================ + +TEST(EeRecMmiCoherence, PextlwInterleavesLowerWords) +{ + // PS2: rd.UL[0]=rt.UL[0], rd.UL[1]=rs.UL[0], rd.UL[2]=rt.UL[1], rd.UL[3]=rs.UL[1] + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAA11111111ull, 0xFFFFFFFFFFFFFFFFull); // rs.UL[0]=0x11.., rs.UL[1]=0xAA.. + h.SetMmiPair(reg::a1, 0xBBBBBBBB22222222ull, 0xFFFFFFFFFFFFFFFFull); // rt.UL[0]=0x22.., rt.UL[1]=0xBB.. + h.LoadProgram({ee::PEXTLW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Expected: lane[0]=rt[0]=0x22222222, lane[1]=rs[0]=0x11111111, + // lane[2]=rt[1]=0xBBBBBBBB, lane[3]=rs[1]=0xAAAAAAAA + h.ExpectMmiPair(reg::v0, 0x1111111122222222ull, 0xAAAAAAAABBBBBBBBull); +} + +TEST(EeRecMmiCoherence, PextuwInterleavesUpperWords) +{ + // PS2: rd.UL[0]=rt.UL[2], rd.UL[1]=rs.UL[2], rd.UL[2]=rt.UL[3], rd.UL[3]=rs.UL[3] + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111111111111ull, 0xAAAAAAAA33333333ull); + h.SetMmiPair(reg::a1, 0x2222222222222222ull, 0xBBBBBBBB44444444ull); + h.LoadProgram({ee::PEXTUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // lane[0]=rt.UL[2]=0x44444444, lane[1]=rs.UL[2]=0x33333333, + // lane[2]=rt.UL[3]=0xBBBBBBBB, lane[3]=rs.UL[3]=0xAAAAAAAA + h.ExpectMmiPair(reg::v0, 0x3333333344444444ull, 0xAAAAAAAABBBBBBBBull); +} + +TEST(EeRecMmiCoherence, PpacwPacksLowerWordsOfEachHalf) +{ + // PS2: rd.UL[0]=rt.UL[0], rd.UL[1]=rt.UL[2], rd.UL[2]=rs.UL[0], rd.UL[3]=rs.UL[2] + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFF11111111ull, 0xFFFFFFFF22222222ull); + h.SetMmiPair(reg::a1, 0xFFFFFFFF33333333ull, 0xFFFFFFFF44444444ull); + h.LoadProgram({ee::PPACW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x4444444433333333ull, 0x2222222211111111ull); +} + +TEST(EeRecMmiCoherence, PmaxwReturnsLanewiseSignedMax) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00000005FFFFFFFEull, 0x80000000FFFFFFFFull); // {-2, 5, -1, INT_MIN} + h.SetMmiPair(reg::a1, 0x00000003FFFFFFF6ull, 0x000000017FFFFFFFull); // {-10, 3, INT_MAX, 1} + h.LoadProgram({ee::PMAXW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Per-lane signed max: {-2,5}vs{-10,3} → {-2,5}; {INT_MIN,-1}vs{INT_MAX,1} → {INT_MAX, 1} + h.ExpectMmiPair(reg::v0, 0x00000005FFFFFFFEull, 0x000000017FFFFFFFull); +} + +TEST(EeRecMmiCoherence, PminhReturnsLanewiseSignedMin) +{ + EeRecTestHarness h; + // 8 halfword lanes; small mixed positives/negatives + h.SetMmiPair(reg::a0, 0x0001FFFF00020003ull, 0x80007FFFFFFE0004ull); + h.SetMmiPair(reg::a1, 0xFFFE00020001FFF0ull, 0x000180000003FFFFull); + h.LoadProgram({ee::PMINH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Per-lane signed min — recompute from the inputs above. + auto laneMin = [](u16 x, u16 y) -> u16 { return ((s16)x < (s16)y) ? x : y; }; + auto pack = [&](u64 a, u64 b) { + u64 r = 0; + for (int i = 0; i < 4; ++i) + { + u16 x = (u16)(a >> (i*16)); + u16 y = (u16)(b >> (i*16)); + r |= (u64)laneMin(x, y) << (i*16); + } + return r; + }; + h.ExpectMmiPair(reg::v0, + pack(0x0001FFFF00020003ull, 0xFFFE00020001FFF0ull), + pack(0x80007FFFFFFE0004ull, 0x000180000003FFFFull)); +} + +TEST(EeRecMmiCoherence, PabswAbsoluteValueOfSignedWords) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a1, 0xFFFFFFFB80000000ull, 0x000000077FFFFFFFull); // {INT_MIN, -5, INT_MAX, 7} + h.LoadProgram({ee::PABSW(reg::v0, reg::a1)}); // PABSW v0, rt=a1 + h.Run(); + // PABSW PS2 spec: saturates INT_MIN → INT_MAX (NOT 0x80000000). + h.ExpectMmiPair(reg::v0, 0x000000057FFFFFFFull, 0x000000077FFFFFFFull); +} + +TEST(EeRecMmiCoherence, PadsbhSubsLowerHalfAddsUpperHalf) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00100020003000A0ull, 0x0080007000600050ull); + h.SetMmiPair(reg::a1, 0x0001000200030004ull, 0x0008000700060005ull); + h.LoadProgram({ee::PADSBH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Lower 4 halfwords: rs - rt; upper 4 halfwords: rs + rt + h.ExpectMmiPair(reg::v0, + 0x000F001E002D009Cull, // {0x10-1, 0x20-2, 0x30-3, 0xA0-4} + 0x0088007700660055ull); // {0x80+8, 0x70+7, 0x60+6, 0x50+5} +} + +// ============================================================================ +// Three-way allocator pressure — exercise q8-q15 saturation. +// ============================================================================ + +// Six MMI ops in flight, each writing a different destination, sharing +// source registers. Should drive the allocator's eviction path on q8-q15. +TEST(EeRecMmiCoherence, SixMmiOpsExerciseAllocatorEviction) +{ + EeRecTestHarness h; + const u64 base = 0x0011223344556677ull; + for (u32 r = 4; r < 12; ++r) + h.SetMmiPair(r, base + r, base + r + 0x100); + h.LoadProgram({ + ee::PAND(12, 4, 5), + ee::POR (13, 6, 7), + ee::PXOR(14, 8, 9), + ee::PADDW(15, 10, 11), + ee::PSUBW(16, 4, 6), + ee::PADDH(17, 5, 7), + }); + h.Run(); + h.ExpectMmiPair(12, (base + 4) & (base + 5), (base + 0x104) & (base + 0x105)); + h.ExpectMmiPair(13, (base + 6) | (base + 7), (base + 0x106) | (base + 0x107)); + h.ExpectMmiPair(14, (base + 8) ^ (base + 9), (base + 0x108) ^ (base + 0x109)); +} + +// ============================================================================ +// Operand-aliasing regressions — Rd == Rs / Rt with multi-emit MMI ops. +// +// When Rd aliases Rs (or Rt), the allocator returns the SAME Q-reg for qd +// and qs, so writing qd first clobbers qs for any subsequent emit step. +// PADSBH / PINTH / PINTEH (multi-instruction emits referencing qs/qt after +// the qd write) are the canonical at-risk shapes — they stage *intermediate* +// halves in scratch registers but still keep qs/qt live across the sequence. +// ============================================================================ + +// PADSBH with Rd == Rs: the emit computes the low-half difference into qd +// (which clobbers qs when qd == qs), then needs the original qs again for the +// upper-half sum — so a non-alias-safe sequence would read a corrupted source. +// Expected behavior: rd.UH[0..3] = orig_rs.UH[0..3] - rt.UH[0..3]; +// rd.UH[4..7] = orig_rs.UH[4..7] + rt.UH[4..7]. +TEST(EeRecMmiCoherence, PadsbhRdAliasesRs) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00100020003000A0ull, 0x0080007000600050ull); + h.SetMmiPair(reg::a1, 0x0001000200030004ull, 0x0008000700060005ull); + // Rd = Rs = a0; this is the aliasing case. + h.LoadProgram({ee::PADSBH(reg::a0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::a0, + 0x000F001E002D009Cull, // {0x10-1, 0x20-2, 0x30-3, 0xA0-4} (sub of original a0 - a1) + 0x0088007700660055ull); // {0x80+8, 0x70+7, 0x60+6, 0x50+5} (add of original a0 + a1) +} + +// The symmetric Rd == Rt aliasing case: qt (rather than qs) shares the +// destination Q-reg, so the emit must not corrupt rt before its last use. +TEST(EeRecMmiCoherence, PadsbhRdAliasesRt) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00100020003000A0ull, 0x0080007000600050ull); + h.SetMmiPair(reg::a1, 0x0001000200030004ull, 0x0008000700060005ull); + h.LoadProgram({ee::PADSBH(reg::a1, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::a1, + 0x000F001E002D009Cull, + 0x0088007700660055ull); +} + +// PINTH with Rd == Rs: a multi-step cross-lane interleave emit. +// PS2 PINTH: rd = interleave(rt.UH[0..3], rs.UH[4..7]). +TEST(EeRecMmiCoherence, PinthRdAliasesRs) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0011002200330044ull, 0x0055006600770088ull); + h.SetMmiPair(reg::a1, 0xAAAABBBBCCCCDDDDull, 0xEEEEFFFF11112222ull); + h.LoadProgram({ee::PINTH(reg::a0, reg::a0, reg::a1)}); + h.Run(); + // PS2 interp: rd.US[0]=rt.US[0], rd.US[1]=rs.US[4], + // rd.US[2]=rt.US[1], rd.US[3]=rs.US[5], + // rd.US[4]=rt.US[2], rd.US[5]=rs.US[6], + // rd.US[6]=rt.US[3], rd.US[7]=rs.US[7]. + // rs lower halfwords {0x44, 0x33, 0x22, 0x11}; + // upper halfwords {0x88, 0x77, 0x66, 0x55} + // rt lower {0xDDDD, 0xCCCC, 0xBBBB, 0xAAAA}; + // upper {0x2222, 0x1111, 0xFFFF, 0xEEEE} + // Interleave rt.low with rs.upper: + // {rt[0]=0xDDDD, rs[4]=0x88, rt[1]=0xCCCC, rs[5]=0x77, + // rt[2]=0xBBBB, rs[6]=0x66, rt[3]=0xAAAA, rs[7]=0x55} + h.ExpectMmiPair(reg::a0, + 0x0077CCCC0088DDDDull, + 0x0055AAAA0066BBBBull); +} + +// PINTEH with Rd == Rs: a multi-step even-lane deinterleave-then-interleave emit. +TEST(EeRecMmiCoherence, PintehRdAliasesRs) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0011002200330044ull, 0x0055006600770088ull); + h.SetMmiPair(reg::a1, 0xAAAABBBBCCCCDDDDull, 0xEEEEFFFF11112222ull); + h.LoadProgram({ee::PINTEH(reg::a0, reg::a0, reg::a1)}); + h.Run(); + // PS2 PINTEH: rd = interleave even halfwords of rs and rt. + // rs evens (UH[0], UH[2], UH[4], UH[6]) = {0x44, 0x22, 0x88, 0x66} + // rt evens = {0xDDDD, 0xBBBB, 0x2222, 0xFFFF} + // rd = {rt_e[0]=0xDDDD, rs_e[0]=0x44, rt_e[1]=0xBBBB, rs_e[1]=0x22, + // rt_e[2]=0x2222, rs_e[2]=0x88, rt_e[3]=0xFFFF, rs_e[3]=0x66} + h.ExpectMmiPair(reg::a0, + 0x0022BBBB0044DDDDull, + 0x0066FFFF00882222ull); +} + +// Mix MMI with scalar reg pressure — alternating MMI and 64-bit ALU ops +// to confuse the NEON-vs-scalar-GPR allocator hand-off. +TEST(EeRecMmiCoherence, MmiInterleavedWith64BitAlu) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000000000005ull, 0x00000000000000A0ull); + h.SetMmiPair(reg::a1, 0x0000000000000003ull, 0x0000000000000050ull); + h.SetGpr64(reg::a2, 100); + h.SetGpr64(reg::a3, 7); + h.LoadProgram({ + ee::PAND (reg::v0, reg::a0, reg::a1), // v0 = {5&3, 0xA0&0x50} = {1, 0} + ee::DADDU(reg::v1, reg::a2, reg::a3), // v1 = 100 + 7 = 107 + ee::POR (reg::t0, reg::v0, reg::a0), // t0 = v0 | a0 = {5, 0xA0} + ee::DSUBU(reg::t1, reg::v1, reg::a3), // t1 = 107 - 7 = 100 + ee::PXOR (reg::t2, reg::t0, reg::a1), // t2 = t0 ^ a1 = {5^3, 0xA0^0x50} = {6, 0xF0} + }); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000000000000001ull, 0x0000000000000000ull); + EXPECT_EQ(h.GetGpr64Jit(reg::v1), 107u); + h.ExpectMmiPair(reg::t0, 0x0000000000000005ull, 0x00000000000000A0ull); + EXPECT_EQ(h.GetGpr64Jit(reg::t1), 100u); + h.ExpectMmiPair(reg::t2, 0x0000000000000006ull, 0x00000000000000F0ull); +} + +// ============================================================================ +// Upper-64 preservation when a scalar op writes only UD[0] of a 128-bit-live +// register. +// +// MMI ops keep Rd live in a NEON slot with MODE_WRITE; the slot's 128 bits +// are authoritative over memory. Scalar ops that target the same register +// (LUI, MFLO, MOVZ, ADDIU, ...) call `_deleteEEreg(reg, 0)` which drops the +// NEON slot WITHOUT writing it back. The 32/64-bit scalar then writes only +// UD[0]. UD[1] of the slot — which the interpreter unambiguously preserves — +// is silently lost. +// +// This surfaces as visual artifacts when vertex/color-pack data loses its +// upper 64 between a pack/unpack MMI and a subsequent scalar update of the +// packed result. +// ============================================================================ + +// PADDW writes v3 (lower + upper both nonzero). LUI v3 then sets UD[0] only — +// UD[1] must remain the PADDW upper-64 result. Reading v3 via SQ (which goes +// through `mmiFlushReg`/`_deleteEEreg(reg,1)`) reveals whether the upper 64 +// was preserved. +TEST(EeRecMmiCoherence, PaddwThenLuiPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000100000002ull, 0x0000000300000004ull); // {1,2,3,4} + h.SetMmiPair(reg::a1, 0x0000000500000006ull, 0x0000000700000008ull); // {5,6,7,8} + h.LoadProgram({ + ee::PADDW(reg::v0, reg::a0, reg::a1), // v0 = {6,8,10,12} + LUI (reg::v0, 0x1234), // v0.UD[0] = 0x12340000; v0.UD[1] preserved + }); + h.Run(); + // Interpreter LUI semantics: UD[0] = sign-extend((u32)imm << 16) = 0x12340000. + // PADDW result UL[2]=4+8=0xC, UL[3]=3+7=0xA → UD[1] = (0xA<<32)|0xC = 0x0A_0000000C. + h.ExpectMmiPair(reg::v0, 0x0000000012340000ull, 0x0000000A0000000Cull); +} + +// MFLO writes UD[0] only. +TEST(EeRecMmiCoherence, PaddwThenMfloPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000100000002ull, 0x0000000300000004ull); + h.SetMmiPair(reg::a1, 0x0000000500000006ull, 0x0000000700000008ull); + h.SetLo64(0xDEADBEEFCAFEBABEull); + h.LoadProgram({ + ee::PADDW(reg::v0, reg::a0, reg::a1), // v0 = {6,8,10,12} + MFLO (reg::v0), // v0.UD[0] = LO; UD[1] preserved + }); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xDEADBEEFCAFEBABEull, 0x0000000A0000000Cull); +} + +// MOVZ writes UD[0] only when the condition fires. +TEST(EeRecMmiCoherence, PaddwThenMovzPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000100000002ull, 0x0000000300000004ull); + h.SetMmiPair(reg::a1, 0x0000000500000006ull, 0x0000000700000008ull); + h.SetGpr64(reg::a2, 0xAAAAAAAA55555555ull); // value to move + h.SetGpr64(reg::a3, 0); // condition: zero → MOVZ fires + h.LoadProgram({ + ee::PADDW(reg::v0, reg::a0, reg::a1), // v0 = {6,8,10,12} + ee::MOVZ (reg::v0, reg::a2, reg::a3), // v0.UD[0] = a2; UD[1] preserved + }); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xAAAAAAAA55555555ull, 0x0000000A0000000Cull); +} + +// ADDIU writes UD[0] (sign-extend 32-bit add to 64-bit). UD[1] preserved. +TEST(EeRecMmiCoherence, PaddwThenAddiuPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000100000002ull, 0x0000000300000004ull); + h.SetMmiPair(reg::a1, 0x0000000500000006ull, 0x0000000700000008ull); + h.LoadProgram({ + ee::PADDW(reg::v0, reg::a0, reg::a1), // v0 = {6,8,10,12}; UD[0]=0x00000008_00000006 + ADDIU (reg::v0, reg::v0, 1), // v0.UD[0] = sign-extend((s32)UL[0] + 1) + }); + h.Run(); + // PADDW UL[0] = a0.UL[0] (=0x2) + a1.UL[0] (=0x6) = 8. ADDIU sign-extends (8+1)=9 to UD[0]. + // UD[1] preserved = (PADDW UL[3]=0xA, UL[2]=0xC) → 0x0A_0000000C. + h.ExpectMmiPair(reg::v0, 0x0000000000000009ull, 0x0000000A0000000Cull); +} + +// Pack/unpack on alloc path produces a 128-bit result; scalar op then writes +// UD[0] only. This is the archetypal pattern: PEXTLW assembles a packed +// vertex word, then a scalar instruction tweaks the lower half. +TEST(EeRecMmiCoherence, PextlwThenLuiPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAA11111111ull, 0xCCCCCCCC33333333ull); + h.SetMmiPair(reg::a1, 0xBBBBBBBB22222222ull, 0xDDDDDDDD44444444ull); + h.LoadProgram({ + ee::PEXTLW(reg::v0, reg::a0, reg::a1), // v0 = {0x22,0x11,0xBB,0xAA} + LUI (reg::v0, 0x1234), // v0.UD[0] = 0x12340000; UD[1] preserved + }); + h.Run(); + // PEXTLW result UD[1] = {0xBBBBBBBB, 0xAAAAAAAA} = 0xAAAAAAAABBBBBBBB + h.ExpectMmiPair(reg::v0, 0x0000000012340000ull, 0xAAAAAAAABBBBBBBBull); +} + +// ============================================================================ +// Pack/unpack-specific patterns: r0 source idioms, aliasing, chains. +// Artifacts surface when pack/unpack ops are on the alloc path; lane-wise +// alloc-path ops don't trigger. These tests target what's STRUCTURALLY +// different about pack/unpack (cross-lane Zip/Uzp + r0-zero-extend idiom). +// ============================================================================ + +// PEXTLW with _Rs_ == r0 — common zero-extend-lower-words idiom. +// The JIT allocates r0 as a NEON slot. Verify the result still matches interp. +TEST(EeRecMmiCoherence, PextlwWithRsZeroZeroExtends) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a1, 0x1111111122222222ull, 0xCCCCCCCCDDDDDDDDull); + h.LoadProgram({ee::PEXTLW(reg::v0, reg::zero, reg::a1)}); + h.Run(); + // PS2: rd.UL[0]=rt.UL[0]=0x22222222; rd.UL[1]=rs.UL[0]=0 + // rd.UL[2]=rt.UL[1]=0x11111111; rd.UL[3]=rs.UL[1]=0 + h.ExpectMmiPair(reg::v0, 0x0000000022222222ull, 0x0000000011111111ull); +} + +// PEXTLB with _Rs_ == r0 — zero-extend lower bytes to halfwords. +TEST(EeRecMmiCoherence, PextlbWithRsZeroZeroExtends) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a1, 0x0102030405060708ull, 0x090A0B0C0D0E0F10ull); + h.LoadProgram({ee::PEXTLB(reg::v0, reg::zero, reg::a1)}); + h.Run(); + // PS2 PEXTLB: rd.UC[2i]=rt.UC[i], rd.UC[2i+1]=rs.UC[i] for i in 0..7. + // With rs=0: each rt byte becomes the LOW byte of a halfword (high=0). + // rt UD[0]=0x0102030405060708 ⇒ rt.UC[0..7] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}. + // rd bytes UC[0..15] = {0x08, 0, 0x07, 0, 0x06, 0, 0x05, 0, 0x04, 0, 0x03, 0, 0x02, 0, 0x01, 0}. + // As halfwords (lo→hi): {0x0008, 0x0007, 0x0006, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001}. + h.ExpectMmiPair(reg::v0, 0x0005000600070008ull, 0x0001000200030004ull); +} + +// PEXTLW with _Rd_ == _Rs_ aliasing — allocator gives same Q-reg for qs and qd. +// The interleave is well-defined when destination aliases source, but verify. +TEST(EeRecMmiCoherence, PextlwRdAliasesRs) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAA11111111ull, 0xCCCCCCCC33333333ull); + h.SetMmiPair(reg::a1, 0xBBBBBBBB22222222ull, 0xDDDDDDDD44444444ull); + h.LoadProgram({ee::PEXTLW(reg::a0, reg::a0, reg::a1)}); // _Rd_=_Rs_=a0 + h.Run(); + h.ExpectMmiPair(reg::a0, 0x1111111122222222ull, 0xAAAAAAAABBBBBBBBull); +} + +// PEXTLW with _Rd_ == _Rt_ aliasing. +TEST(EeRecMmiCoherence, PextlwRdAliasesRt) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAA11111111ull, 0xCCCCCCCC33333333ull); + h.SetMmiPair(reg::a1, 0xBBBBBBBB22222222ull, 0xDDDDDDDD44444444ull); + h.LoadProgram({ee::PEXTLW(reg::a1, reg::a0, reg::a1)}); // _Rd_=_Rt_=a1 + h.Run(); + h.ExpectMmiPair(reg::a1, 0x1111111122222222ull, 0xAAAAAAAABBBBBBBBull); +} + +// PEXTLW with _Rs_ == _Rt_ (same register twice). +TEST(EeRecMmiCoherence, PextlwSameSource) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAA11111111ull, 0xCCCCCCCC33333333ull); + h.LoadProgram({ee::PEXTLW(reg::v0, reg::a0, reg::a0)}); // Rs == Rt + h.Run(); + // Each lower word of a0 interleaved with itself: {a0[0], a0[0], a0[1], a0[1]} + h.ExpectMmiPair(reg::v0, 0x1111111111111111ull, 0xAAAAAAAAAAAAAAAAull); +} + +// Vertex-unpack-style chain: 4 PEXTL ops with r0 building progressive zero-extension. +TEST(EeRecMmiCoherence, ChainedPextlZeroExtendsByteToWord) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFFFFFFFFFFull, 0x0102030405060708ull); + // Use upper half of a0 (low bytes interpretation: 0x08,0x07,...,0x01) + h.LoadProgram({ + ee::PEXTUB(reg::v0, reg::zero, reg::a0), // 0-extend upper bytes of a0 to halfwords in v0 + ee::PEXTLH(reg::v1, reg::zero, reg::v0), // 0-extend lower halfwords of v0 to words in v1 + }); + h.Run(); + // PEXTUB(v0, rs=0, rt=a0): interleave the upper 8 bytes of a0 with 0 → + // halfwords {0x0008,0x0007,0x0006,0x0005, 0x0004,0x0003,0x0002,0x0001}. + h.ExpectMmiPair(reg::v0, 0x0005000600070008ull, 0x0001000200030004ull); + // PEXTLH(v1, rs=0, rt=v0): zero-extend the lower 4 halfwords of v0 to words + // → {0x00000008,0x00000007,0x00000006,0x00000005}. + h.ExpectMmiPair(reg::v1, 0x0000000700000008ull, 0x0000000500000006ull); +} + +// Long chain: many pack/unpack ops with allocator reuse + scalar interleaving. +// If there's any latent bug in slot reuse across pack/unpack chains, this +// flushes it out. Mimics real vertex-unpacking blocks. +TEST(EeRecMmiCoherence, LongPackUnpackChainWithScalarMixin) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111122222222ull, 0x3333333344444444ull); + h.SetMmiPair(reg::a1, 0x5555555566666666ull, 0x7777777788888888ull); + h.SetMmiPair(reg::a2, 0x99999999AAAAAAAAull, 0xBBBBBBBBCCCCCCCCull); + h.SetMmiPair(reg::a3, 0xDDDDDDDDEEEEEEEEull, 0xFFFFFFFF00000000ull); + h.SetGpr64(reg::t0, 1); + h.SetGpr64(reg::t1, 2); + h.LoadProgram({ + ee::PEXTLW(reg::v0, reg::a0, reg::a1), // pack lower words + ee::PEXTUW(reg::v1, reg::a2, reg::a3), // pack upper words + ee::PPACW (reg::s0, reg::v0, reg::v1), // compress + ee::PEXTLH(reg::s1, reg::v0, reg::v1), // halfword interleave + ee::PEXTLB(reg::s2, reg::a0, reg::a2), // byte interleave + ee::PEXTUB(reg::s3, reg::a1, reg::a3), // byte interleave upper + ee::PPACH (reg::s4, reg::s2, reg::s3), // halfword pack + ee::PPACB (reg::s5, reg::s4, reg::s1), // byte pack + ADDIU (reg::t2, reg::t0, 100), // scalar interleave + ee::PEXTLW(reg::s6, reg::s5, reg::s4), // continue pack chain + ee::DADDU (reg::t3, reg::t1, reg::t2), // scalar + ee::PEXTUW(reg::s7, reg::s6, reg::s0), // final pack + }); + h.Run(); + // h.Run() diffs JIT vs interp automatically; any divergence on any + // register reads as a failure here. No ExpectMmiPair needed. +} + +// Same chain but with self-aliasing every other op. +TEST(EeRecMmiCoherence, PackUnpackChainWithSelfAlias) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xDEADBEEFCAFEBABEull, 0x0123456789ABCDEFull); + h.SetMmiPair(reg::a1, 0xFEEDFACEC0FFEE42ull, 0xFEDCBA9876543210ull); + h.SetMmiPair(reg::a2, 0xAAAA5555AAAA5555ull, 0x5555AAAA5555AAAAull); + h.LoadProgram({ + ee::PEXTLW(reg::v0, reg::a0, reg::a1), + ee::PEXTUW(reg::v0, reg::v0, reg::a2), // self-alias _Rd_==_Rs_ + ee::PPACW (reg::v0, reg::a0, reg::v0), // self-alias _Rd_==_Rt_ + ee::PEXTLH(reg::v0, reg::v0, reg::v0), // _Rs_==_Rt_==_Rd_ + ee::PPACB (reg::v0, reg::v0, reg::a1), // self-alias again + }); + h.Run(); +} + +// Pressure test: 16 pack/unpack ops writing 8 different destinations to +// force allocator eviction with pack/unpack still pending. +TEST(EeRecMmiCoherence, PackUnpackAllocatorEvictionPressure) +{ + EeRecTestHarness h; + for (u32 r = 4; r < 12; ++r) + h.SetMmiPair(r, 0x1100000000000000ull + r, 0x2200000000000000ull + r); + // 16 ops: writes to r12..r19; reads from r4..r11 + h.LoadProgram({ + ee::PEXTLW(12, 4, 5), + ee::PEXTUW(13, 6, 7), + ee::PPACW (14, 8, 9), + ee::PPACH (15, 10, 11), + ee::PEXTLB(16, 4, 6), + ee::PEXTUB(17, 5, 7), + ee::PPACB (18, 8, 10), + ee::PEXTLH(19, 9, 11), + ee::PEXTLW(20, 12, 13), // reads outputs from above + ee::PEXTUW(21, 14, 15), + ee::PPACW (22, 16, 17), + ee::PPACH (23, 18, 19), + ee::PEXTLB(24, 20, 21), + ee::PEXTUB(25, 22, 23), + ee::PPACB (26, 24, 25), + ee::PEXTLH(27, 26, 20), + }); + h.Run(); +} + +// ===================================================================================================== +// MODE_WRITE-only NEON slot must be authoritative for _eeMoveGPRtoR +// +// An MMI op via eeRecompileCodeXMM allocates Rd's NEON slot with MODE_WRITE +// only (no MODE_READ flag — XMMINFO_READD is not set for MMI). _eeMoveGPRtoR +// must check both MODE_READ and MODE_WRITE when searching for a live NEON +// slot, or it falls through to a stale cpuRegs.GPR.r[reg].UD[0] +// load. Every EE load/store via recComputeAddr / recPrepStoreValue funnels +// through _eeMoveGPRtoR — so if the base or store-value register was last +// touched by an MMI op, the address computation must use the live NEON value. +// +// Shape: PEXTLW writes $t0 (NEON MODE_WRITE-only). The very next SW uses $t0 +// as base. The correct behavior is for SW to use the post-PEXTLW $t0 value, +// not the value held in memory before the PEXTLW. +// ===================================================================================================== +TEST(EeRecMmiCoherence, PextlwDestThenStoreUsesLiveBase) +{ + constexpr u32 kStaleAddr = RecompilerTestEnvironment::kScratchAddr + 0x100; + constexpr u32 kLiveAddr = RecompilerTestEnvironment::kScratchAddr + 0x200; + + EeRecTestHarness h; + // Pre-populate sentinels at both candidate addresses so we can tell which one got the SW. + h.WriteU32(kStaleAddr, 0x11111111u); + h.WriteU32(kLiveAddr, 0x22222222u); + h.TrackMemWindow(kStaleAddr, 4); + h.TrackMemWindow(kLiveAddr, 4); + + // $t0 starts pointing at kStaleAddr (the wrong address, if _eeMoveGPRtoR misses the live slot). + h.SetGpr64(reg::t0, kStaleAddr); + // PEXTLW Rd, Rs, Rt: rd.UL[0] = rt.UL[0]. So setting a1.UL[0] = kLiveAddr makes + // the post-PEXTLW $t0 point at kLiveAddr. + h.SetMmiPair(reg::a0, + (0xDEAD0000ull << 32) | 0xCAFE0000ull, // UL[0]=0xCAFE0000, UL[1]=0xDEAD0000 + 0xFEEDFACEFEEDFACEull); + h.SetMmiPair(reg::a1, + (0xBEEF0000ull << 32) | static_cast(kLiveAddr), // UL[0]=kLiveAddr, UL[1]=0xBEEF0000 + 0xFEEDC0DEFEEDC0DEull); + h.SetGpr64(reg::a2, 0xCAFEBABEu); + + h.LoadProgram({ + ee::PEXTLW(reg::t0, reg::a0, reg::a1), // $t0.UL[0] = a1.UL[0] = kLiveAddr + SW (reg::a2, 0, reg::t0), // store $a2 at addr $t0+0 (should be kLiveAddr) + }); + h.Run(); // diffs JIT vs interp. + + EXPECT_EQ(h.ReadU32(kLiveAddr), 0xCAFEBABEu); + EXPECT_EQ(h.ReadU32(kStaleAddr), 0x11111111u); // must be untouched +} + +// Symmetric: PEXTLW writes the STORE-VALUE register (not the base). +// recPrepStoreValue also goes through _eeMoveGPRtoR. +TEST(EeRecMmiCoherence, PextlwDestThenStoreUsesLiveValue) +{ + constexpr u32 kAddr = RecompilerTestEnvironment::kScratchAddr + 0x300; + + EeRecTestHarness h; + h.WriteU32(kAddr, 0xDEADDEADu); + h.TrackMemWindow(kAddr, 4); + + // Pre-MMI value of $v0 (lower 32) is 0xFACEFACE — this is the "stale" sentinel. + h.SetGpr64(reg::v0, 0xFACEFACEu); + h.SetGpr64(reg::a0, kAddr); + // a1.UL[0] = 0xCAFEBABE → after PEXTLW, $v0.UL[0] = 0xCAFEBABE. + h.SetMmiPair(reg::a1, + (0xBEEF0000ull << 32) | 0xCAFEBABEull, + 0x0000000000000000ull); + h.SetMmiPair(reg::a2, 0x00000000DEAD0000ull, 0x0000000000000000ull); + + h.LoadProgram({ + ee::PEXTLW(reg::v0, reg::a2, reg::a1), // $v0.UL[0] = 0xCAFEBABE + SW (reg::v0, 0, reg::a0), // store $v0 at $a0 (= kAddr) + }); + h.Run(); + + // SW writes the live (post-PEXTLW) $v0.UL[0] = 0xCAFEBABE. + EXPECT_EQ(h.ReadU32(kAddr), 0xCAFEBABEu); +} + +// Symmetric: PEXTLW writes the BASE register, LW reads from it (load side). +TEST(EeRecMmiCoherence, PextlwDestThenLoadUsesLiveBase) +{ + constexpr u32 kStaleAddr = RecompilerTestEnvironment::kScratchAddr + 0x400; + constexpr u32 kLiveAddr = RecompilerTestEnvironment::kScratchAddr + 0x500; + + EeRecTestHarness h; + h.WriteU32(kStaleAddr, 0x77777777u); // wrong base: stale value + h.WriteU32(kLiveAddr, 0x88888888u); // correct base: live PEXTLW result + h.SetGpr64(reg::t0, kStaleAddr); + h.SetMmiPair(reg::a0, 0x0ull, 0x0ull); + h.SetMmiPair(reg::a1, + (0xBEEF0000ull << 32) | static_cast(kLiveAddr), + 0x0ull); + + h.LoadProgram({ + ee::PEXTLW(reg::t0, reg::a0, reg::a1), // $t0.UL[0] = kLiveAddr + LW (reg::v0, 0, reg::t0), // $v0 = mem32[$t0] + }); + h.Run(); + + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF88888888ull); // sign-extended +} + +// Symmetric: PPACW (different cross-lane shuffle) — same _eeMoveGPRtoR path. +TEST(EeRecMmiCoherence, PpacwDestThenStoreUsesLiveBase) +{ + constexpr u32 kStaleAddr = RecompilerTestEnvironment::kScratchAddr + 0x600; + constexpr u32 kLiveAddr = RecompilerTestEnvironment::kScratchAddr + 0x700; + + EeRecTestHarness h; + h.WriteU32(kStaleAddr, 0xAAAAAAAAu); + h.WriteU32(kLiveAddr, 0xBBBBBBBBu); + h.TrackMemWindow(kStaleAddr, 4); + h.TrackMemWindow(kLiveAddr, 4); + + h.SetGpr64(reg::t0, kStaleAddr); + // PPACW: rd.UL[0] = rt.UL[0]. Set a1.UL[0] = kLiveAddr. + h.SetMmiPair(reg::a0, 0x0ull, 0x0ull); + h.SetMmiPair(reg::a1, + (0xBEEF0000ull << 32) | static_cast(kLiveAddr), + 0x0ull); + h.SetGpr64(reg::a2, 0x55555555u); + + h.LoadProgram({ + ee::PPACW(reg::t0, reg::a0, reg::a1), // $t0.UL[0] = a1.UL[0] = kLiveAddr + SW (reg::a2, 0, reg::t0), + }); + h.Run(); + + EXPECT_EQ(h.ReadU32(kLiveAddr), 0x55555555u); + EXPECT_EQ(h.ReadU32(kStaleAddr), 0xAAAAAAAAu); +} + +// Lane-wise MMI op for symmetry: should ALSO produce a live base. Confirms the +// fix covers lane-wise ops, not just pack/unpack; the staleness is in the +// allocator/load path and applies to all MODE_WRITE-only MMI slots. +TEST(EeRecMmiCoherence, PaddwDestThenStoreUsesLiveBase) +{ + constexpr u32 kStaleAddr = RecompilerTestEnvironment::kScratchAddr + 0x800; + constexpr u32 kLiveAddr = RecompilerTestEnvironment::kScratchAddr + 0x900; + + EeRecTestHarness h; + h.WriteU32(kStaleAddr, 0xCCCCCCCCu); + h.WriteU32(kLiveAddr, 0xDDDDDDDDu); + h.TrackMemWindow(kStaleAddr, 4); + h.TrackMemWindow(kLiveAddr, 4); + + h.SetGpr64(reg::t0, kStaleAddr); + // PADDW: rd.UL[i] = rs.UL[i] + rt.UL[i] (lane-wise). Set a0+a1 lane 0 = kLiveAddr. + h.SetMmiPair(reg::a0, static_cast(kLiveAddr - 0x1000), 0x0ull); + h.SetMmiPair(reg::a1, 0x1000ull, 0x0ull); + h.SetGpr64(reg::a2, 0x66666666u); + + h.LoadProgram({ + ee::PADDW(reg::t0, reg::a0, reg::a1), // $t0.UL[0] = (kLiveAddr - 0x1000) + 0x1000 = kLiveAddr + SW (reg::a2, 0, reg::t0), + }); + h.Run(); + + EXPECT_EQ(h.ReadU32(kLiveAddr), 0x66666666u); + EXPECT_EQ(h.ReadU32(kStaleAddr), 0xCCCCCCCCu); +} + + +// Symmetric: PPACW then scalar write. +TEST(EeRecMmiCoherence, PpacwThenAddiuPreservesUpper64) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFF11111111ull, 0xFFFFFFFF22222222ull); + h.SetMmiPair(reg::a1, 0xFFFFFFFF33333333ull, 0xFFFFFFFF44444444ull); + h.LoadProgram({ + ee::PPACW(reg::v0, reg::a0, reg::a1), // v0 = {0x44,0x33,0x22,0x11} lo→hi + ADDIU (reg::v0, reg::v0, 0), // v0.UD[0] = sign-extend(v0.UL[0]+0); UD[1] preserved + }); + h.Run(); + // PPACW: rd.UL[0]=rt.UL[0]=0x33333333, rd.UL[1]=rt.UL[2]=0x44444444 (sign-extended via ADDIU's + // 32-bit add over UL[0]=0x33333333 → 0x33333333 sign-extended to UD[0]). + // UD[1] preserved = {rs.UL[0]=0x11111111, rs.UL[2]=0x22222222} = 0x22222222_11111111 + h.ExpectMmiPair(reg::v0, 0x0000000033333333ull, 0x2222222211111111ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_mmi_simd_tests.cpp b/tests/ctest/core/recompilers/ee_rec_mmi_simd_tests.cpp new file mode 100644 index 0000000000..204e845014 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_mmi_simd_tests.cpp @@ -0,0 +1,482 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// MMI paired-word SIMD coverage. Complements the existing ee_rec_mmi_tests.cpp +// (MADD + PLZCW) with representative ops from every MMI SIMD sub-family. +// +// Full 128-bit paired-word coverage exercising SetGpr128/ExpectGpr128. Not +// every MMI0/1/2/3 sub-op is exhausted; this file proves the harness + +// encoders work for each family and provides a regression base for further +// MMI ports. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +// ---------------- MMI0: parallel arithmetic ---------------- + +TEST(EeRecMmiSimd, PaddwPacked32BitLanes) +{ + // Two 32-bit lanes per 64-bit half of the 128-bit reg; four lanes total. + // a0 = {1, 2, 3, 4}, a1 = {10, 20, 30, 40}, expected = {11, 22, 33, 44}. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000200000001ull, 0x0000000400000003ull); + h.SetMmiPair(reg::a1, 0x000000140000000Aull, 0x000000280000001Eull); + h.LoadProgram({ee::PADDW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x000000160000000Bull, 0x0000002C00000021ull); +} + +TEST(EeRecMmiSimd, PaddhPacked16BitLanes) +{ + // Eight 16-bit lanes. a0 = 8×0x0001, a1 = 8×0x0002 → expect 8×0x0003. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0001000100010001ull, 0x0001000100010001ull); + h.SetMmiPair(reg::a1, 0x0002000200020002ull, 0x0002000200020002ull); + h.LoadProgram({ee::PADDH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0003000300030003ull, 0x0003000300030003ull); +} + +TEST(EeRecMmiSimd, PaddbPacked8BitLanesWrapOnOverflow) +{ + // 16 × 8-bit lanes. 0xFF + 0x01 = 0x00 (wrap on u8 overflow, PADDB is + // non-saturating). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFFFFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); + h.SetMmiPair(reg::a1, 0x0101010101010101ull, 0x0101010101010101ull); + h.LoadProgram({ee::PADDB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0ull, 0ull); +} + +TEST(EeRecMmiSimd, PsubwPacked32) +{ + // a0 = {50, 100, 150, 300}, a1 = {5, 10, 20, 30}, expect {45, 90, 130, 270}. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000006400000032ull, 0x0000012C00000096ull); + h.SetMmiPair(reg::a1, 0x0000000A00000005ull, 0x0000001E00000014ull); + h.LoadProgram({ee::PSUBW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000005A0000002Dull, 0x0000010E00000082ull); +} + +// ---------------- MMI0: parallel compare-greater-than (signed) ---------------- + +TEST(EeRecMmiSimd, PcgtwSignedLanewise) +{ + // Lane result: 0xFFFFFFFF when rs > rt (signed), 0 otherwise. Test both. + // a0 = {5, -1}, a1 = {3, 0} → expect {all-ones, 0}. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFFFFFF00000005ull, 0); + h.SetMmiPair(reg::a1, 0x0000000000000003ull, 0); + h.LoadProgram({ee::PCGTW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x00000000FFFFFFFFull, 0); +} + +// ---------------- MMI1: parallel compare-equal ---------------- + +TEST(EeRecMmiSimd, PceqwLaneEqualSetsAllOnes) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000001200000042ull, 0); + h.SetMmiPair(reg::a1, 0x0000001200000043ull, 0); + h.LoadProgram({ee::PCEQW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // UD[0]: lane 0 (0x42 != 0x43) → 0, lane 1 (0x12 == 0x12) → 0xFFFFFFFF. + // UD[1]: both zero-initialized, both lanes equal → all-ones. + h.ExpectMmiPair(reg::v0, 0xFFFFFFFF00000000ull, 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecMmiSimd, PceqbByteLanewise) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1122334455667788ull, 0); + h.SetMmiPair(reg::a1, 0x1022334400667700ull, 0); + h.LoadProgram({ee::PCEQB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Byte indices are LE within UD[0]: UC[0]=LSB, UC[7]=MSB. + // a0.UC[0..7] = 88 77 66 55 44 33 22 11 + // a1.UC[0..7] = 00 77 66 00 44 33 22 10 + // equal? N Y Y N Y Y Y N + // rd.UC[0..7] = 00 FF FF 00 FF FF FF 00 + // Reassembled as UD[0] (MSB first when written as hex): + // byte7=00 byte6=FF byte5=FF byte4=FF byte3=00 byte2=FF byte1=FF byte0=00 + h.ExpectMmiPair(reg::v0, 0x00FFFFFF00FFFF00ull, 0xFFFFFFFFFFFFFFFFull); +} + +// ---------------- MMI2: logical AND, XOR ---------------- + +TEST(EeRecMmiSimd, PandBitwise128) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xF0F0F0F0F0F0F0F0ull, 0xAAAAAAAAAAAAAAAAull); + h.SetMmiPair(reg::a1, 0xFFFF0000FFFF0000ull, 0x5555555555555555ull); + h.LoadProgram({ee::PAND(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xF0F00000F0F00000ull, 0ull); +} + +TEST(EeRecMmiSimd, PxorBitwise128) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xDEADBEEFDEADBEEFull, 0xCAFEBABECAFEBABEull); + h.SetMmiPair(reg::a1, 0xFFFFFFFFFFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); + h.LoadProgram({ee::PXOR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, ~0xDEADBEEFDEADBEEFull, ~0xCAFEBABECAFEBABEull); +} + +// ---------------- MMI3: logical OR, NOR ---------------- + +TEST(EeRecMmiSimd, PorBitwise128) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xF0F0000000000000ull, 0x0000000000000001ull); + h.SetMmiPair(reg::a1, 0x00000F0F00000000ull, 0x8000000000000000ull); + h.LoadProgram({ee::POR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xF0F00F0F00000000ull, 0x8000000000000001ull); +} + +// `por rd, r0, rt` — the canonical PS2 128-bit register-move idiom. Exercises +// recPOR's s_zero special-case (register copy, no allocated r0 / Movi+Orr). +TEST(EeRecMmiSimd, PorR0SourceIsRegisterMove) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a1, 0x0123456789ABCDEFull, 0xFEDCBA9876543210ull); + h.LoadProgram({ee::POR(reg::v0, reg::zero, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0123456789ABCDEFull, 0xFEDCBA9876543210ull); +} + +// `por rd, rs, r0` — t_zero special-case (register copy from rs). +TEST(EeRecMmiSimd, PorR0TargetIsRegisterMove) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAABBCCDDEEFF0011ull, 0x2233445566778899ull); + h.LoadProgram({ee::POR(reg::v0, reg::a0, reg::zero)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xAABBCCDDEEFF0011ull, 0x2233445566778899ull); +} + +// `por rd, r0, r0` — both r0, materializes a 128-bit zero. +TEST(EeRecMmiSimd, PorBothR0IsZero) +{ + EeRecTestHarness h; + h.LoadProgram({ee::POR(reg::v0, reg::zero, reg::zero)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0ull, 0ull); +} + +TEST(EeRecMmiSimd, PnorIsBitwiseComplementOfOr) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00FF00FF00FF00FFull, 0); + h.SetMmiPair(reg::a1, 0xFF00FF00FF00FF00ull, 0); + h.LoadProgram({ee::PNOR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // a0|a1 = 0xFFFFFFFFFFFFFFFF → NOR = 0. + h.ExpectMmiPair(reg::v0, 0ull, ~0ull); +} + +// ---------------- MMI3: copy-halves and paired copy ---------------- + +TEST(EeRecMmiSimd, PcpyhReplicatesLowHalfwordsAcrossAllLanes) +{ + // PCPYH: each 16-bit halfword of rd takes the low halfword of the + // corresponding 64-bit source half. So rt.UD[0] low 16 bits → rd.UD[0] + // all four halves; rt.UD[1] low 16 bits → rd.UD[1] all four halves. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1122334455667788ull, 0xAABBCCDDEEFF0011ull); + h.LoadProgram({ee::PCPYH(reg::v0, reg::a0)}); + h.Run(); + // Low halfword of a0.UD[0] = 0x7788; replicated across 4 halves = 0x7788778877887788 + // Low halfword of a0.UD[1] = 0x0011; replicated = 0x0011001100110011 + h.ExpectMmiPair(reg::v0, 0x7788778877887788ull, 0x0011001100110011ull); +} + +TEST(EeRecMmiSimd, PcpyhAliasedRdEqualsRt) +{ + // Register-resident rewrite must stay correct when the allocator + // hands back qd == qt — the qd write happens after rt.H[4] is broadcast to + // scratch, so the source half is not clobbered mid-shuffle. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1122334455667788ull, 0xAABBCCDDEEFF0011ull); + h.LoadProgram({ee::PCPYH(reg::a0, reg::a0)}); + h.Run(); + h.ExpectMmiPair(reg::a0, 0x7788778877887788ull, 0x0011001100110011ull); +} + +TEST(EeRecMmiSimd, PcpyldAssemblesLowHalvesFromRsAndRt) +{ + // PCPYLD: rd.UD[0] = rt.UD[0], rd.UD[1] = rs.UD[0]. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAAAAAAAAAAull, 0xBBBBBBBBBBBBBBBBull); + h.SetMmiPair(reg::a1, 0xCCCCCCCCCCCCCCCCull, 0xDDDDDDDDDDDDDDDDull); + h.LoadProgram({ee::PCPYLD(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xCCCCCCCCCCCCCCCCull, 0xAAAAAAAAAAAAAAAAull); +} + +TEST(EeRecMmiSimd, PcpyudAssemblesHighHalvesFromRsAndRt) +{ + // PCPYUD: rd.UD[0] = rs.UD[1], rd.UD[1] = rt.UD[1]. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAAAAAAAAAAAAAAull, 0xBBBBBBBBBBBBBBBBull); + h.SetMmiPair(reg::a1, 0xCCCCCCCCCCCCCCCCull, 0xDDDDDDDDDDDDDDDDull); + h.LoadProgram({ee::PCPYUD(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xBBBBBBBBBBBBBBBBull, 0xDDDDDDDDDDDDDDDDull); +} + +// ---------------- MMI0: saturating signed add/sub ---------------- + +TEST(EeRecMmiSimd, PaddsbClampsSignedByteOverflow) +{ + // Per byte: s8 add, clamped to [-128, +127]. Mix: (127 + 1) → +127, + // (-128 + -1) → -128, (10 + 20) → 30, (-5 + 3) → -2. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x7F800AFB0102FFFFull, 0); + h.SetMmiPair(reg::a1, 0x01FF14030001FFFFull, 0); + // Bytes (MSB→LSB): 7F+01=80→7F, 80+FF=7F→80, 0A+14=1E, FB+03=FE, + // 01+00=01, 02+01=03, FF+FF=FE, FF+FF=FE + h.LoadProgram({ee::PADDSB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x7F801EFE0103FEFEull, 0); +} + +TEST(EeRecMmiSimd, PaddshClampsSignedHalfwordOverflow) +{ + // s16 lanes: (0x7FFF + 1) → 0x7FFF (+max clamp), + // (0x8000 + -1) → 0x8000 (-max clamp), + // (0x0001 + 0x0002) → 0x0003 (no clamp). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x7FFF80000001FFFFull, 0); + h.SetMmiPair(reg::a1, 0x0001FFFF00020001ull, 0); + h.LoadProgram({ee::PADDSH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x7FFF800000030000ull, 0); +} + +TEST(EeRecMmiSimd, PaddswClampsSignedWordOverflow) +{ + // s32 lanes: (INT_MAX + 1) → INT_MAX, (INT_MIN + -1) → INT_MIN, + // (100 + 200) → 300, (-5 + 3) → -2. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x7FFFFFFF00000064ull, 0xFFFFFFFB80000000ull); + // UD[0] lanes: lo=0x00000064 (100), hi=0x7FFFFFFF (INT_MAX). + // UD[1] lanes: lo=0x80000000 (INT_MIN), hi=0xFFFFFFFB (-5). + h.SetMmiPair(reg::a1, 0x00000001000000C8ull, 0x00000003FFFFFFFFull); + // UD[0] lanes: lo=0x000000C8 (200), hi=0x00000001. + // UD[1] lanes: lo=0xFFFFFFFF (-1), hi=0x00000003. + h.LoadProgram({ee::PADDSW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // UD[0]: lo=100+200=300=0x12C, hi=INT_MAX+1 clamps to INT_MAX. + // UD[1]: lo=INT_MIN+-1 clamps to INT_MIN, hi=-5+3=-2=0xFFFFFFFE. + h.ExpectMmiPair(reg::v0, 0x7FFFFFFF0000012Cull, 0xFFFFFFFE80000000ull); +} + +TEST(EeRecMmiSimd, PsubsbClampsSignedByteUnderflow) +{ + // (-128 - 1) → -128 (clamp), (127 - -1) → 127 (clamp). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x807F00FF00000000ull, 0); + h.SetMmiPair(reg::a1, 0x01FFFF0100000000ull, 0); + // 80-01 = -128-1 = -129 → clamp -128 = 80 + // 7F-FF = 127-(-1) = 128 → clamp 127 = 7F + // 00-FF = 0-(-1) = 1 + // FF-01 = -1-1 = -2 = FE + h.LoadProgram({ee::PSUBSB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x807F01FE00000000ull, 0); +} + +TEST(EeRecMmiSimd, PsubshClampsSignedHalfwordUnderflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x80007FFF00000000ull, 0); + h.SetMmiPair(reg::a1, 0x0001FFFF00000000ull, 0); + // 8000 - 0001 = -32768-1 = clamp -32768 = 8000 + // 7FFF - FFFF = 32767-(-1) = clamp 32767 = 7FFF + h.LoadProgram({ee::PSUBSH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x80007FFF00000000ull, 0); +} + +TEST(EeRecMmiSimd, PsubswClampsSignedWordUnderflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x7FFFFFFF80000000ull, 0); + h.SetMmiPair(reg::a1, 0xFFFFFFFF00000001ull, 0); + // UD[0] lo: 80000000 - 00000001 = INT_MIN-1 → clamp INT_MIN = 0x80000000 + // UD[0] hi: 7FFFFFFF - FFFFFFFF = INT_MAX-(-1) → clamp INT_MAX = 0x7FFFFFFF + h.LoadProgram({ee::PSUBSW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x7FFFFFFF80000000ull, 0); +} + +// ---------------- MMI1: saturating unsigned add/sub ---------------- + +TEST(EeRecMmiSimd, PaddubClampsUnsignedByteOverflow) +{ + // (0xFF + 0x01) → 0xFF (clamp), (0x80 + 0x80) → 0xFF (clamp), + // (0x10 + 0x20) → 0x30 (no clamp). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFF801000000000FFull, 0); + h.SetMmiPair(reg::a1, 0x0180200000000001ull, 0); + h.LoadProgram({ee::PADDUB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xFFFF3000000000FFull, 0); +} + +TEST(EeRecMmiSimd, PadduhClampsUnsignedHalfwordOverflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xFFFF8000000000FFull, 0); + h.SetMmiPair(reg::a1, 0x00018000FFFF0001ull, 0); + // FFFF+0001=FFFF clamp, 8000+8000=FFFF clamp, 0000+FFFF=FFFF, 00FF+0001=0100. + h.LoadProgram({ee::PADDUH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xFFFFFFFFFFFF0100ull, 0); +} + +TEST(EeRecMmiSimd, PadduwClampsUnsignedWordOverflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00000000FFFFFFFFull, 0x80000000FFFFFFFFull); + h.SetMmiPair(reg::a1, 0x8000000000000001ull, 0x8000000000000001ull); + // UD[0] lo: FFFFFFFF + 00000001 = clamp FFFFFFFF + // UD[0] hi: 00000000 + 80000000 = 80000000 + // UD[1] lo: FFFFFFFF + 00000001 = clamp FFFFFFFF + // UD[1] hi: 80000000 + 80000000 = clamp FFFFFFFF + h.LoadProgram({ee::PADDUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x80000000FFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecMmiSimd, PsububClampsUnsignedByteUnderflow) +{ + // (0x00 - 0x01) → 0x00 (clamp), (0x80 - 0x80) → 0x00, (0xFF - 0x01) → 0xFE. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x008010FF00000000ull, 0); + h.SetMmiPair(reg::a1, 0x0180010000000000ull, 0); + h.LoadProgram({ee::PSUBUB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x00000FFF00000000ull, 0); +} + +TEST(EeRecMmiSimd, PsubuhClampsUnsignedHalfwordUnderflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000800000FFFFFFull, 0); + h.SetMmiPair(reg::a1, 0x000180000000FFFEull, 0); + // 0000-0001=clamp 0000, 8000-8000=0000, 00FF-0000=00FF, FFFF-FFFE=0001. + h.LoadProgram({ee::PSUBUH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000000000FF0001ull, 0); +} + +TEST(EeRecMmiSimd, PsubuwClampsUnsignedWordUnderflow) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x00000000FFFFFFFFull, 0x7FFFFFFF80000000ull); + h.SetMmiPair(reg::a1, 0x00000001FFFFFFFFull, 0x80000000FFFFFFFFull); + // UD[0] lo: FFFFFFFF - FFFFFFFF = 00000000 + // UD[0] hi: 00000000 - 00000001 = clamp 0 + // UD[1] lo: 80000000 - FFFFFFFF = clamp 0 + // UD[1] hi: 7FFFFFFF - 80000000 = clamp 0 + h.LoadProgram({ee::PSUBUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000000000000000ull, 0); +} + +// ---------------- MMI0: missing parallel sub/compare fills ---------------- + +TEST(EeRecMmiSimd, PsubhPacked16BitLanes) +{ + // 8 × u16 lanes, wrap subtraction (PSUBH is non-saturating). + // rs = {0x0014, 0x0028, 0x003C, 0x0050, 0x0064, 0x0078, 0x008C, 0x00A0} + // rt = {0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008} + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0050003C00280014ull, 0x00A0008C00780064ull); + h.SetMmiPair(reg::a1, 0x0004000300020001ull, 0x0008000700060005ull); + h.LoadProgram({ee::PSUBH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // {19, 38, 57, 76} / {95, 114, 133, 152} + h.ExpectMmiPair(reg::v0, 0x004C003900260013ull, 0x009800850072005Full); +} + +TEST(EeRecMmiSimd, PsubhWrapsOnUnderflow) +{ + // 0 - 1 = 0xFFFF on every lane (wrap, not saturate). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0, 0); + h.SetMmiPair(reg::a1, 0x0001000100010001ull, 0x0001000100010001ull); + h.LoadProgram({ee::PSUBH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xFFFFFFFFFFFFFFFFull, 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecMmiSimd, PsubbPacked8BitLanesWrap) +{ + // 16 × u8 lanes, wrap subtraction. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1010101010101010ull, 0x2020202020202020ull); + h.SetMmiPair(reg::a1, 0x0202020202020202ull, 0x0505050505050505ull); + h.LoadProgram({ee::PSUBB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0E0E0E0E0E0E0E0Eull, 0x1B1B1B1B1B1B1B1Bull); +} + +TEST(EeRecMmiSimd, PcgthSignedHalfword) +{ + // 8 × s16 lanes; 0xFFFF if rs > rt signed, else 0. rs=1 in every lane; + // rt mixes signed values around the compare boundary. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0001000100010001ull, 0x0001000100010001ull); + h.SetMmiPair(reg::a1, 0xFFFF000000010000ull, 0x00010001FFFFFFFFull); + // rt lanes (LE order): lo US[0..3] = {0, 1, 0, -1}; hi US[4..7] = {-1, -1, 1, 1}. + // PCGTH rs(=1) > rt: lo {T, F, T, T}, hi {T, T, F, F}. + h.LoadProgram({ee::PCGTH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // lo (US[3..0] LE): {T, T, F, T} = {0xFFFF, 0xFFFF, 0x0000, 0xFFFF} + // hi (US[7..4] LE): {F, F, T, T} = {0x0000, 0x0000, 0xFFFF, 0xFFFF} + h.ExpectMmiPair(reg::v0, 0xFFFFFFFF0000FFFFull, 0x00000000FFFFFFFFull); +} + +TEST(EeRecMmiSimd, PcgtbSignedByte) +{ + // 16 × s8 lanes. rs=1 in every lane; rt with mixed signed values. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0101010101010101ull, 0x0101010101010101ull); + h.SetMmiPair(reg::a1, 0xFF00010002FF0100ull, 0x00FF010001FF00FFull); + // LE: lo UC[0..7] = {0, 1, FF, 2, 0, 1, 0, FF} → signed {0, 1, -1, 2, 0, 1, 0, -1} + // hi UC[8..15] = {FF, 0, FF, 1, 0, 1, FF, 0} → signed {-1, 0, -1, 1, 0, 1, -1, 0} + // 1>x signed: + // lo: {T, F, T, F, T, F, T, T} → bytes {FF, 00, FF, 00, FF, 00, FF, FF} + // hi: {T, T, T, F, T, F, T, T} → bytes {FF, FF, FF, 00, FF, 00, FF, FF} + h.LoadProgram({ee::PCGTB(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // LE u64 lo (UC[7]<<56 ... | UC[0]) = 0xFF_FF_00_FF_00_FF_00_FF + // LE u64 hi (UC[15]<<56 ... | UC[8]) = 0xFF_FF_00_FF_00_FF_FF_FF + h.ExpectMmiPair(reg::v0, 0xFFFF00FF00FF00FFull, 0xFFFF00FF00FFFFFFull); +} + +TEST(EeRecMmiSimd, PceqhHalfwordEqual) +{ + // 8 × u16 lanes, 0xFFFF if equal, else 0. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x000A0009000B000Aull, 0x000C000B000A000Aull); + h.SetMmiPair(reg::a1, 0x000A000A000A000Aull, 0x000A000A000A000Aull); + // equal pattern (lo): {T, F, F, T} = lane0..3 → 0xFFFF, 0x0000, 0x0000, 0xFFFF + // LE u64 (lane3..lane0) = 0xFFFF 0000 0000 FFFF + // Hi: {T, T, F, F} → 0x0000 0000 FFFF FFFF + h.LoadProgram({ee::PCEQH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xFFFF00000000FFFFull, 0x00000000FFFFFFFFull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_mmi_tests.cpp b/tests/ctest/core/recompilers/ee_rec_mmi_tests.cpp new file mode 100644 index 0000000000..4bca94d3c8 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_mmi_tests.cpp @@ -0,0 +1,764 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// MMI non-SIMD coverage — accumulator-mode arithmetic, pack/unpack, lane +// exchange, HI/LO transfer, and parallel-shift-immediate. Parallel-arith +// SIMD (PADDx/PSUBx/PCEQx/PCGTx) lives in ee_rec_mmi_simd_tests.cpp. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(EeRecMmi, MaddAccumulates32Bit) +{ + // MADD: (HI:LO) = (HI:LO) + rs * rt, with 32-bit sign-extended operands. + // Also writes the new LO into rd. + EeRecTestHarness h; + h.SetLo64(100); // accumulator seed + h.SetHi64(0); + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 20); + h.LoadProgram({ee::MADD(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp() & 0xFFFFFFFFull, 300ull); // 100 + 10*20 + EXPECT_EQ(h.GetGpr64Interp(reg::v0) & 0xFFFFFFFFull, 300ull); +} + +TEST(EeRecMmi, Plzcw) +{ + // PLZCW writes the sign-bit run-length of each 32-bit half of rs.UD[0] + // into the two 32-bit halves of rd.UD[0]. + // lo32 = 0x00000001 → sign bit 0, 30 leading zeros follow → count = 30 + // hi32 = 0xFFFFFFFF → all-ones, 31 leading ones follow → count = 31 + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFF00000001ull); + h.LoadProgram({ee::PLZCW(reg::v0, reg::a0)}); + h.Run(); + const u64 v = h.GetGpr64Interp(reg::v0); + EXPECT_EQ(v & 0xFFFFFFFFull, 30ull); + EXPECT_EQ((v >> 32) & 0xFFFFFFFFull, 31ull); +} + +// =========================================================================== +// HI/LO 128-bit transfer — PMTHI / PMFHI / PMTLO / PMFLO +// +// PS2 HI/LO are 128-bit (used by MULT1 / DIV1 / parallel multiply pipeline +// on the second 64-bit half). PMTHI/PMTLO move a full 128-bit GPR into +// the HI/LO register; PMFHI/PMFLO read it back. Tested here as a +// round-trip: GPR → HI/LO via PMTHIx → GPR via PMFHIx, ExpectGpr128. +// =========================================================================== + +TEST(EeRecMmi, PmthiPmfhiRoundTrip128Bit) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0123456789ABCDEFull, 0xFEDCBA9876543210ull); + h.LoadProgram({ + ee::PMTHI(reg::a0), + ee::PMFHI(reg::v0), + }); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0123456789ABCDEFull, 0xFEDCBA9876543210ull); +} + +TEST(EeRecMmi, PmtloPmfloRoundTrip128Bit) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xCAFEBABEDEADBEEFull, 0xBADC0FFEEDFACE99ull); + h.LoadProgram({ + ee::PMTLO(reg::a0), + ee::PMFLO(reg::v0), + }); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xCAFEBABEDEADBEEFull, 0xBADC0FFEEDFACE99ull); +} + +// PMTHL.LW writes Rs's four words into the EVEN-indexed word +// lanes of LO and HI, preserving the odd-indexed lanes (the upper half of +// each 64-bit LO/HI word): +// LO.UL[0] = Rs.UL[0] LO.UL[2] = Rs.UL[2] +// HI.UL[0] = Rs.UL[1] HI.UL[2] = Rs.UL[3] +// The odd-indexed UL[1]/UL[3] of LO/HI must stay untouched. +TEST(EeRecMmi, PmthlWritesEvenWordsToLoAndHiPreservingOdds) +{ + EeRecTestHarness h; + // Pre-seed LO/HI with sentinels in odd-indexed words (UL[1], UL[3]). + // SetLoPair(lo_qw, hi_qw) writes LO.UD[0]=lo_qw, LO.UD[1]=hi_qw, so + // 0xAAAA000000000000 puts 0xAAAA0000 in UL[1] and 0 in UL[0]. + h.SetLoPair(0xAAAA000000000000ull, 0xBBBB000000000000ull); + h.SetHiPair(0xCCCC000000000000ull, 0xDDDD000000000000ull); + // Rs words [0..3] = 0x1111_1111, 0x2222_2222, 0x3333_3333, 0x4444_4444. + h.SetMmiPair(reg::a0, 0x2222222211111111ull, 0x4444444433333333ull); + h.LoadProgram({ + ee::PMTHL(reg::a0), + ee::PMFLO(reg::v0), + ee::PMFHI(reg::v1), + }); + h.Run(); + // LO = [Rs.UL[0], LO.UL[1]_kept, Rs.UL[2], LO.UL[3]_kept] + // = [0x11111111, 0xAAAA0000, 0x33333333, 0xBBBB0000] + h.ExpectMmiPair(reg::v0, 0xAAAA000011111111ull, 0xBBBB000033333333ull); + // HI = [Rs.UL[1], HI.UL[1]_kept, Rs.UL[3], HI.UL[3]_kept] + // = [0x22222222, 0xCCCC0000, 0x44444444, 0xDDDD0000] + h.ExpectMmiPair(reg::v1, 0xCCCC000022222222ull, 0xDDDD000044444444ull); +} + +// =========================================================================== +// Halfword interleave — PINTH / PINTEH +// +// PINTH (MMI.cpp:1121): rd[i*2] = rt.US[i]; rd[i*2+1] = rs.US[i+4] +// — pairs lo halves of rt with hi halves of rs. +// PINTEH (MMI.cpp:1498): rd[i*2] = rt.US[i*2]; rd[i*2+1] = rs.US[i*2] +// — interleaves even halves of rt and rs. +// =========================================================================== + +TEST(EeRecMmi, PinthLoLanesOfRtPairedWithHiLanesOfRs) +{ + // rt halves [t0..t7] = 0x10..0x17, rs halves [s0..s7] = 0x80..0x87. + // rd = {t0, s4, t1, s5, t2, s6, t3, s7} = {10,84, 11,85, 12,86, 13,87}. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0013001200110010ull, 0x0017001600150014ull); // rs (a0) + h.SetMmiPair(reg::a1, 0x0083008200810080ull, 0x0087008600850084ull); // rt (a1) + h.LoadProgram({ee::PINTH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // lo qword: {t0, s4, t1, s5} = {0x80, 0x14, 0x81, 0x15} → 0x0015 0081 0014 0080 + // hi qword: {t2, s6, t3, s7} = {0x82, 0x16, 0x83, 0x17} → 0x0017 0083 0016 0082 + h.ExpectMmiPair(reg::v0, 0x0015008100140080ull, 0x0017008300160082ull); +} + +TEST(EeRecMmi, PintehEvenHalvesInterleaved) +{ + // PINTEH: rd.US[2k] = rt.US[2k]; rd.US[2k+1] = rs.US[2k] for k=0..3. + // Drops the odd-indexed halves of both inputs. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0xAAAA0001AAAA0002ull, 0xAAAA0003AAAA0004ull); // rs even = {2,1,4,3} + h.SetMmiPair(reg::a1, 0xBBBB0010BBBB0020ull, 0xBBBB0030BBBB0040ull); // rt even = {0x20,0x10,0x40,0x30} + h.LoadProgram({ee::PINTEH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // rd.US[0]=rt.US[0]=0x20, rd.US[1]=rs.US[0]=0x02, rd.US[2]=rt.US[2]=0x10, + // rd.US[3]=rs.US[2]=0x01 → lo qword 0x0001 0010 0002 0020 + // rd.US[4]=rt.US[4]=0x40, rd.US[5]=rs.US[4]=0x04, rd.US[6]=rt.US[6]=0x30, + // rd.US[7]=rs.US[6]=0x03 → hi qword 0x0003 0030 0004 0040 + h.ExpectMmiPair(reg::v0, 0x0001001000020020ull, 0x0003003000040040ull); +} + +// =========================================================================== +// Halfword/word lane shuffles — PEXEH, PEXEW, PEXCH, PEXCW, PROT3W, PREVH +// +// Pure permutations of rt's lanes into rd; rs is unused (encoded as 0). +// Semantics per MMI.cpp. +// =========================================================================== + +TEST(EeRecMmi, PexehSwapsHalfwordLanes0and2) +{ + // rd.US[0]<->rt.US[2]; rd.US[1]=rt.US[1]; rd.US[3]=rt.US[3]; + // rd.US[4]<->rt.US[6]; rd.US[5]=rt.US[5]; rd.US[7]=rt.US[7]; + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0003000200010000ull, 0x0007000600050004ull); + h.LoadProgram({ee::PEXEH(reg::v0, reg::a0)}); + h.Run(); + // lo: {US[2], US[1], US[0], US[3]} = {0x2, 0x1, 0x0, 0x3} → 0x0003000000010002 + // hi: {US[6], US[5], US[4], US[7]} = {0x6, 0x5, 0x4, 0x7} → 0x0007000400050006 + h.ExpectMmiPair(reg::v0, 0x0003000000010002ull, 0x0007000400050006ull); +} + +TEST(EeRecMmi, PexewSwapsWordLanes0and2) +{ + // rd.UL[0]<->rt.UL[2]; rd.UL[1]=rt.UL[1]; rd.UL[3]=rt.UL[3]. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111100000000ull, 0x3333333322222222ull); + h.LoadProgram({ee::PEXEW(reg::v0, reg::a0)}); + h.Run(); + // rd.UL[0]=UL[2]=0x22222222, rd.UL[1]=UL[1]=0x11111111 + // → lo qword (UL[1]<<32)|UL[0] = 0x1111111122222222 + // rd.UL[2]=UL[0]=0x00000000, rd.UL[3]=UL[3]=0x33333333 + // → hi qword = 0x3333333300000000 + h.ExpectMmiPair(reg::v0, 0x1111111122222222ull, 0x3333333300000000ull); +} + +TEST(EeRecMmi, Prot3wRotatesLow3Words) +{ + // rd.UL[0]=rt.UL[1]; rd.UL[1]=rt.UL[2]; rd.UL[2]=rt.UL[0]; rd.UL[3]=rt.UL[3]. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x22222222 | (0x11111111ull << 32), 0x44444444 | (0x33333333ull << 32)); + // rt.UL = {0x22222222, 0x11111111, 0x44444444, 0x33333333} + h.LoadProgram({ee::PROT3W(reg::v0, reg::a0)}); + h.Run(); + // rd.UL = {0x11111111, 0x44444444, 0x22222222, 0x33333333} + h.ExpectMmiPair(reg::v0, 0x4444444411111111ull, 0x3333333322222222ull); +} + +TEST(EeRecMmi, PexchSwapsHalfwordsWithinEachWord) +{ + // rd.US[0]=rt.US[0]; rd.US[1]<->rt.US[2]; rd.US[3]=rt.US[3]; + // rd.US[4]=rt.US[4]; rd.US[5]<->rt.US[6]; rd.US[7]=rt.US[7]; + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0003000200010000ull, 0x0007000600050004ull); + h.LoadProgram({ee::PEXCH(reg::v0, reg::a0)}); + h.Run(); + // lo: {US[0], US[2], US[1], US[3]} = {0, 2, 1, 3} + // hi: {US[4], US[6], US[5], US[7]} = {4, 6, 5, 7} + h.ExpectMmiPair(reg::v0, 0x0003000100020000ull, 0x0007000500060004ull); +} + +TEST(EeRecMmi, PexcwSwapsMiddleWordLanes) +{ + // rd.UL = {rt.UL[0], rt.UL[2], rt.UL[1], rt.UL[3]}. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111100000000ull, 0x3333333322222222ull); + // UL[] = {0, 0x11111111, 0x22222222, 0x33333333} + h.LoadProgram({ee::PEXCW(reg::v0, reg::a0)}); + h.Run(); + // {UL[0], UL[2], UL[1], UL[3]} = {0, 0x22222222, 0x11111111, 0x33333333} + h.ExpectMmiPair(reg::v0, 0x2222222200000000ull, 0x3333333311111111ull); +} + +TEST(EeRecMmi, PrevhReversesHalfwordsWithinEachQword) +{ + // rd.US[0]=rt.US[3]; rd.US[1]=rt.US[2]; rd.US[2]=rt.US[1]; rd.US[3]=rt.US[0] + // (and same shape for upper qword: US[4..7] reversed). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0003000200010000ull, 0x0007000600050004ull); + h.LoadProgram({ee::PREVH(reg::v0, reg::a0)}); + h.Run(); + // lo: {US[3], US[2], US[1], US[0]} = {3, 2, 1, 0} → 0x0000 0001 0002 0003 + // hi: {US[7], US[6], US[5], US[4]} = {7, 6, 5, 4} → 0x0004 0005 0006 0007 + h.ExpectMmiPair(reg::v0, 0x0000000100020003ull, 0x0004000500060007ull); +} + +// --------------------------------------------------------------------------- +// Aliased rd == rt for the shuffle rewrites. +// The 2-/3-op NEON idioms must read rt fully before writing rd; if the +// allocator hands back the same Q-reg for both, a non-alias-safe sequence +// would corrupt the source mid-shuffle. These pin the aliased path the +// rd != rt tests above don't exercise. +// --------------------------------------------------------------------------- + +TEST(EeRecMmi, PexewAliasedRdEqualsRt) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111100000000ull, 0x3333333322222222ull); + h.LoadProgram({ee::PEXEW(reg::a0, reg::a0)}); + h.Run(); + // rd.UL = {UL[2],UL[1],UL[0],UL[3]} = {0x22,0x11,0x00,0x33} + h.ExpectMmiPair(reg::a0, 0x1111111122222222ull, 0x3333333300000000ull); +} + +TEST(EeRecMmi, Prot3wAliasedRdEqualsRt) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x22222222 | (0x11111111ull << 32), 0x44444444 | (0x33333333ull << 32)); + // rt.UL = {0x22222222, 0x11111111, 0x44444444, 0x33333333} + h.LoadProgram({ee::PROT3W(reg::a0, reg::a0)}); + h.Run(); + // rd.UL = {UL[1],UL[2],UL[0],UL[3]} = {0x11,0x44,0x22,0x33} + h.ExpectMmiPair(reg::a0, 0x4444444411111111ull, 0x3333333322222222ull); +} + +TEST(EeRecMmi, PexcwAliasedRdEqualsRt) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1111111100000000ull, 0x3333333322222222ull); + // UL[] = {0, 0x11111111, 0x22222222, 0x33333333} + h.LoadProgram({ee::PEXCW(reg::a0, reg::a0)}); + h.Run(); + // rd.UL = {UL[0],UL[2],UL[1],UL[3]} = {0, 0x22, 0x11, 0x33} + h.ExpectMmiPair(reg::a0, 0x2222222200000000ull, 0x3333333311111111ull); +} + +// =========================================================================== +// QFSRV — funnel-shift {Rs:Rt} right by cpuRegs.sa bytes. +// sa is set via MTSAB(rs,imm) -> sa = (GPR[rs].UL[0] & 0xF) ^ (imm & 0xF). +// Run() auto-diffs JIT vs interp; ExpectMmiPair pins the concrete result. +// =========================================================================== + +TEST(EeRecMmi, QfsrvAdjacentSourceContiguous) +{ + // Rs == Rt+1 (a1 == a0+1) hits the contiguous-memory path that reads the + // two source registers directly and skips the temp-buffer stores. sa = 4 bytes. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1122334455667788ull, 0x99AABBCCDDEEFF00ull); // Rt + h.SetMmiPair(reg::a1, 0xAABBCCDD11223344ull, 0x5566778899AABBCCull); // Rs + h.LoadProgram({ee::MTSAB(reg::zero, 4), ee::QFSRV(reg::v0, reg::a1, reg::a0)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xDDEEFF0011223344ull, 0x1122334499AABBCCull); +} + +TEST(EeRecMmi, QfsrvNonAdjacentSource) +{ + // Rs != Rt+1 (a2 != a0+1) takes the temp-buffer path. Same Rt/Rs values, + // sa = 7 bytes. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x1122334455667788ull, 0x99AABBCCDDEEFF00ull); // Rt + h.SetMmiPair(reg::a2, 0xAABBCCDD11223344ull, 0x5566778899AABBCCull); // Rs + h.LoadProgram({ee::MTSAB(reg::zero, 7), ee::QFSRV(reg::v0, reg::a2, reg::a0)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xAABBCCDDEEFF0011ull, 0xBBCCDD1122334499ull); +} + +TEST(EeRecMmi, QfsrvAdjacentRtZeroUsesTempBuffer) +{ + // Rt == r0 with Rs == Rt+1 (at == zero+1): the contiguous path is gated off + // (it would depend on GPR.r[0] memory holding zero), so this must fall to + // the temp-buffer path which zero-fills r0 explicitly. sa = 4 bytes. + EeRecTestHarness h; + h.SetMmiPair(reg::at, 0xAABBCCDD11223344ull, 0x5566778899AABBCCull); // Rs (at == reg 1) + h.LoadProgram({ee::MTSAB(reg::zero, 4), ee::QFSRV(reg::v0, reg::at, reg::zero)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0ull, 0x1122334400000000ull); +} + +// =========================================================================== +// Parallel shift immediate — PSLLH / PSRLH / PSRAH (8 × u16) + +// PSLLW / PSRLW / PSRAW (4 × u32) +// +// Top-level MMI table (funct = 0x34..0x3F) with shift amount in sa. +// PSxxxH masks sa with 0xF (only 4 bits used since lane is 16-bit). +// =========================================================================== + +TEST(EeRecMmi, PsllhShiftsEachHalfwordLeft) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0080000800040002ull, 0x000F00200040FFFFull); + h.LoadProgram({ee::PSLLH(reg::v0, reg::a0, 4)}); + h.Run(); + // Each halfword << 4 (truncated to 16 bits). + // lo: {2<<4, 4<<4, 8<<4, 0x80<<4} = {0x20, 0x40, 0x80, 0x800} + // hi: {0xFFFF<<4 & 0xFFFF=0xFFF0, 0x40<<4=0x400, 0x20<<4=0x200, 0xF<<4=0xF0} + h.ExpectMmiPair(reg::v0, 0x0800008000400020ull, 0x00F002000400FFF0ull); +} + +TEST(EeRecMmi, PsrlhLogicalShiftRight) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0080004000200010ull, 0xF000800040002000ull); + h.LoadProgram({ee::PSRLH(reg::v0, reg::a0, 4)}); + h.Run(); + // lo: {0x10>>4, 0x20>>4, 0x40>>4, 0x80>>4} = {0x1, 0x2, 0x4, 0x8} + // hi: {0x2000>>4, 0x4000>>4, 0x8000>>4, 0xF000>>4} = {0x200, 0x400, 0x800, 0xF00} + h.ExpectMmiPair(reg::v0, 0x0008000400020001ull, 0x0F00080004000200ull); +} + +TEST(EeRecMmi, PsrahArithmeticShiftRightSignExtends) +{ + // PSRAH preserves the sign bit (16-bit signed shift right). + // Lanes with high bit set become 0xFFFF (sign-extended). + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x80008000FFFF8000ull, 0x000180007FFF0001ull); + h.LoadProgram({ee::PSRAH(reg::v0, reg::a0, 8)}); + h.Run(); + // lane: shift right 8, signed. + // lo: {0x8000>>8 sign ext = 0xFF80, 0xFFFF>>8 = 0xFFFF, 0x8000 = 0xFF80, 0x8000 = 0xFF80} + // hi: {0x0001>>8 = 0x0000, 0x7FFF>>8 = 0x007F, 0x8000 = 0xFF80, 0x0001 = 0x0000} + h.ExpectMmiPair(reg::v0, 0xFF80FF80FFFFFF80ull, 0x0000FF80007F0000ull); +} + +// PSxxxH masks the shift amount with 0xF before shifting each 16-bit lane +// (the interpreter does (_Sa_ & 0xf), and a halfword shift is only defined for +// counts in [0,15]). Verify sa ≥ 16 wraps to sa & 0xf. + +TEST(EeRecMmi, PsllhMasksSaToFourBits) +{ + // sa=18 → effective shift = 2. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0080000800040002ull, 0x000F00200040FFFFull); + h.LoadProgram({ee::PSLLH(reg::v0, reg::a0, 18)}); + h.Run(); + // Each halfword << 2 (truncated to 16 bits). + // lo: {2<<2, 4<<2, 8<<2, 0x80<<2} = {0x8, 0x10, 0x20, 0x200} + // hi: {0xFFFF<<2 & 0xFFFF=0xFFFC, 0x40<<2=0x100, 0x20<<2=0x80, 0xF<<2=0x3C} + h.ExpectMmiPair(reg::v0, 0x0200002000100008ull, 0x003C00800100FFFCull); +} + +TEST(EeRecMmi, PsrlhMasksSaToFourBits) +{ + // sa=20 → effective shift = 4. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0080004000200010ull, 0xF000800040002000ull); + h.LoadProgram({ee::PSRLH(reg::v0, reg::a0, 20)}); + h.Run(); + // lo: {0x10>>4, 0x20>>4, 0x40>>4, 0x80>>4} = {0x1, 0x2, 0x4, 0x8} + // hi: {0x2000>>4, 0x4000>>4, 0x8000>>4, 0xF000>>4} = {0x200, 0x400, 0x800, 0xF00} + h.ExpectMmiPair(reg::v0, 0x0008000400020001ull, 0x0F00080004000200ull); +} + +TEST(EeRecMmi, PsrahMasksSaToFourBits) +{ + // sa=24 → effective shift = 8. + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x80008000FFFF8000ull, 0x000180007FFF0001ull); + h.LoadProgram({ee::PSRAH(reg::v0, reg::a0, 24)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0xFF80FF80FFFFFF80ull, 0x0000FF80007F0000ull); +} + +TEST(EeRecMmi, PsllwShiftsEachWordLeft) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0000000200000001ull, 0x0000000400000003ull); + h.LoadProgram({ee::PSLLW(reg::v0, reg::a0, 4)}); + h.Run(); + // Each 32-bit word << 4. {1, 2, 3, 4} → {0x10, 0x20, 0x30, 0x40}. + h.ExpectMmiPair(reg::v0, 0x0000002000000010ull, 0x0000004000000030ull); +} + +TEST(EeRecMmi, PsrlwLogicalShiftRight) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x80000000FFFFFFFFull, 0x000000FF00000010ull); + h.LoadProgram({ee::PSRLW(reg::v0, reg::a0, 4)}); + h.Run(); + // {0xFFFFFFFF >> 4, 0x80000000 >> 4, 0x10 >> 4, 0xFF >> 4} + // = {0x0FFFFFFF, 0x08000000, 0x1, 0xF} + h.ExpectMmiPair(reg::v0, 0x080000000FFFFFFFull, 0x0000000F00000001ull); +} + +TEST(EeRecMmi, PsrawArithmeticShiftRightSignExtends) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x800000007FFFFFFFull, 0xFFFFFFF000000010ull); + h.LoadProgram({ee::PSRAW(reg::v0, reg::a0, 4)}); + h.Run(); + // {0x7FFFFFFF >> 4 = 0x07FFFFFF, 0x80000000 sign>>4 = 0xF8000000, + // 0x10 >> 4 = 0x1, 0xFFFFFFF0 sign>>4 = 0xFFFFFFFF} + h.ExpectMmiPair(reg::v0, 0xF800000007FFFFFFull, 0xFFFFFFFF00000001ull); +} + +// =========================================================================== +// Parallel multiply (16-bit lanes) — PMULTH / PMADDH / PMSUBH +// +// Eight 16-bit signed multiplies: r[i] = rs.SH[i] * rt.SH[i], i = 0..7. +// Result distribution (MMI.cpp:1156-1184): +// LO.UL[0..3] (re-)receive { r0, r1, r4, r5 } +// HI.UL[0..3] (re-)receive { r2, r3, r6, r7 } +// Rd.UL[0..3] = { LO[0], HI[0], LO[2], HI[2] } (post-update) +// PMADDH / PMSUBH read+modify the existing LO/HI; PMULTH overwrites. +// =========================================================================== + +TEST(EeRecMmi, PmulthSignedHwordMultiplyDistributesAcrossHiLoRd) +{ + EeRecTestHarness h; + // rs.SH[0..7] = { 1, 2, 3, 4, 5, 6, 7, 8 } + h.SetMmiPair(reg::a0, 0x0004000300020001ull, 0x0008000700060005ull); + // rt.SH[0..7] = { 0x0A, 0x14, 0x1E, 0x28, 0x32, 0x3C, 0x46, 0x50 } + h.SetMmiPair(reg::a1, 0x0028001E0014000Aull, 0x00500046003C0032ull); + h.LoadProgram({ee::PMULTH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Products r[i] = rs.SH[i] * rt.SH[i]: + // r0=0x0A, r1=0x28, r2=0x5A, r3=0xA0, r4=0xFA, r5=0x168, r6=0x1EA, r7=0x280 + // Rd = { r0, r2, r4, r6 } as 4×32 = { 0x0A, 0x5A, 0xFA, 0x1EA } + // Rd lo qw [UL1 UL0] = 0x0000005A 0000000A + // Rd hi qw [UL3 UL2] = 0x000001EA 000000FA + h.ExpectMmiPair(reg::v0, 0x0000005A0000000Aull, 0x000001EA000000FAull); +} + +TEST(EeRecMmi, PmaddhAccumulatesIntoHiLoAndWritesRdEvenLanes) +{ + EeRecTestHarness h; + // rs.SH = { 1, 1, 1, 1, 2, 2, 2, 2 }, rt.SH = { 3, 3, 3, 3, 4, 4, 4, 4 } + // Products r[0..3] = 3, r[4..7] = 8. + // LO seed UL[0..1] only (harness limit) = { 100, 200 }; LO.UL[2..3] left at 0. + h.SetMmiPair(reg::a0, 0x0001000100010001ull, 0x0002000200020002ull); + h.SetMmiPair(reg::a1, 0x0003000300030003ull, 0x0004000400040004ull); + h.SetLo64(0x00000064000000C8ull); // LO.UL[0]=200(0xC8), LO.UL[1]=100(0x64). UL[2..3]=0. + h.SetHi64(0x0000003200000019ull); // HI.UL[0]=25(0x19), HI.UL[1]=50(0x32). UL[2..3]=0. + h.LoadProgram({ee::PMADDH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // new_LO = LO + {r0,r1,r4,r5} = {200,100,0,0} + {3,3,8,8} = {203,103,8,8} + // new_HI = HI + {r2,r3,r6,r7} = {25,50,0,0} + {3,3,8,8} = {28,53,8,8} + // Rd = { new_LO[0], new_HI[0], new_LO[2], new_HI[2] } = { 203, 28, 8, 8 } + // = { 0xCB, 0x1C, 0x08, 0x08 } + h.ExpectMmiPair(reg::v0, 0x0000001C000000CBull, 0x0000000800000008ull); +} + +TEST(EeRecMmi, PmsubhSubtractsFromHiLoAndWritesRdEvenLanes) +{ + EeRecTestHarness h; + h.SetMmiPair(reg::a0, 0x0001000100010001ull, 0x0002000200020002ull); + h.SetMmiPair(reg::a1, 0x0003000300030003ull, 0x0004000400040004ull); + h.SetLo64(0x00000064000000C8ull); // LO.UL[0]=200, LO.UL[1]=100 + h.SetHi64(0x0000003200000019ull); // HI.UL[0]=25, HI.UL[1]=50 + h.LoadProgram({ee::PMSUBH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // new_LO = LO - {r0,r1,r4,r5} = {200,100,0,0} - {3,3,8,8} = {197,97,-8,-8} + // new_HI = HI - {r2,r3,r6,r7} = {25,50,0,0} - {3,3,8,8} = {22,47,-8,-8} + // Rd = { 197, 22, -8, -8 } = { 0xC5, 0x16, 0xFFFFFFF8, 0xFFFFFFF8 } + h.ExpectMmiPair(reg::v0, 0x00000016000000C5ull, 0xFFFFFFF8FFFFFFF8ull); +} + +// =========================================================================== +// PMULTW / PMULTUW — signed/unsigned 32x32->64 multiply on even-indexed lanes +// prod[0] = Rs.SL[0] * Rt.SL[0] (SL for PMULTW, UL for PMULTUW) +// prod[1] = Rs.SL[2] * Rt.SL[2] +// LO.UD[k] = sign-extended low32 of prod[k] +// HI.UD[k] = sign-extended high32 of prod[k] +// Rd.UD[k] = full 64-bit product +// =========================================================================== + +TEST(EeRecMmi, PmultwSignedMultiplyDistributesAcrossHiLoRd) +{ + EeRecTestHarness h; + // Rs.SL[0]=0x40000000, Rs.SL[2]=-2; SL[1]/SL[3] don't matter. + h.SetMmiPair(reg::a0, 0x0000000040000000ull, 0x00000000FFFFFFFEull); + // Rt.SL[0]=0x10, Rt.SL[2]=3 + h.SetMmiPair(reg::a1, 0x0000000000000010ull, 0x0000000000000003ull); + h.LoadProgram({ee::PMULTW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // prod[0] = 0x40000000 * 0x10 = 0x4_00000000 + // Rd.UD[0] = 0x0000000400000000 + // prod[1] = (-2) * 3 = -6 = 0xFFFFFFFFFFFFFFFA (signed 64) + // Rd.UD[1] = 0xFFFFFFFFFFFFFFFA + h.ExpectMmiPair(reg::v0, 0x0000000400000000ull, 0xFFFFFFFFFFFFFFFAull); +} + +TEST(EeRecMmi, PmultuwUnsignedMultiplyDistributesAcrossHiLoRd) +{ + EeRecTestHarness h; + // Rs.UL[0]=0xFFFFFFFF, Rs.UL[2]=0x100 + h.SetMmiPair(reg::a0, 0x00000000FFFFFFFFull, 0x0000000000000100ull); + // Rt.UL[0]=2, Rt.UL[2]=0x200 + h.SetMmiPair(reg::a1, 0x0000000000000002ull, 0x0000000000000200ull); + h.LoadProgram({ee::PMULTUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // prod[0] = 0xFFFFFFFF * 2 = 0x1FFFFFFFE (unsigned 64) + // Rd.UD[0] = 0x00000001FFFFFFFE + // prod[1] = 0x100 * 0x200 = 0x20000 + // Rd.UD[1] = 0x0000000000020000 + h.ExpectMmiPair(reg::v0, 0x00000001FFFFFFFEull, 0x0000000000020000ull); +} + +// =========================================================================== +// PHMADH / PHMSBH — horizontal signed-16x16 multiply with paired (sum / diff) +// +// Eight products r[i] = Rs.SH[i] * Rt.SH[i], paired into k=0..3 (i = 2k, 2k+1). +// PHMADH: pair_sum[k] = r[2k] + r[2k+1] +// PHMSBH: pair_diff[k] = r[2k+1] - r[2k] +// "firsttemp[k]" = r[2k+1] (the second product in each pair). +// LO = { pair[0], firsttemp[0] , pair[2], firsttemp[2] } (PHMADH) +// { pair[0], ~firsttemp[0] , pair[2], ~firsttemp[2] } (PHMSBH) +// HI = { pair[1], firsttemp[1] , pair[3], firsttemp[3] } (PHMADH) +// { pair[1], ~firsttemp[1] , pair[3], ~firsttemp[3] } (PHMSBH) +// Rd = { pair[0], pair[1], pair[2], pair[3] } +// =========================================================================== + +TEST(EeRecMmi, PhmadhHorizontalSignedHwordMultiplyAddsPairsAcrossHiLoRd) +{ + EeRecTestHarness h; + // Rs.SH = { 1, 2, 3, 4, 5, 6, 7, 8 } + h.SetMmiPair(reg::a0, 0x0004000300020001ull, 0x0008000700060005ull); + // Rt.SH = { 10, 20, 30, 40, 50, 60, 70, 80 } + h.SetMmiPair(reg::a1, 0x0028001E0014000Aull, 0x00500046003C0032ull); + h.LoadProgram({ee::PHMADH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // Products: r0=10, r1=40, r2=90, r3=160, r4=250, r5=360, r6=490, r7=640 + // pair_sums: 50, 250, 610, 1130 + // Rd = { 50, 250, 610, 1130 } = { 0x32, 0xFA, 0x262, 0x46A } + h.ExpectMmiPair(reg::v0, 0x000000FA00000032ull, 0x0000046A00000262ull); +} + +TEST(EeRecMmi, PhmsbhHorizontalSignedHwordMultiplySubtractsPairsAcrossHiLoRd) +{ + EeRecTestHarness h; + // Same operands as PHMADH for easy comparison. + h.SetMmiPair(reg::a0, 0x0004000300020001ull, 0x0008000700060005ull); + h.SetMmiPair(reg::a1, 0x0028001E0014000Aull, 0x00500046003C0032ull); + h.LoadProgram({ee::PHMSBH(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // pair_diffs: r1-r0=30, r3-r2=70, r5-r4=110, r7-r6=150 + // Rd = { 30, 70, 110, 150 } = { 0x1E, 0x46, 0x6E, 0x96 } + h.ExpectMmiPair(reg::v0, 0x000000460000001Eull, 0x000000960000006Eull); +} + +// =========================================================================== +// PEXT5 / PPAC5 — RGB1555 <-> BGRA8 lane-wise pack / unpack. +// +// PEXT5 (expand per 32-bit lane): +// rd = ((rt & 0x001F) << 3) | ((rt & 0x03E0) << 6) +// | ((rt & 0x7C00) << 9) | ((rt & 0x8000) << 16); +// PPAC5 (inverse): +// rd = ((rt >> 3) & 0x001F) | ((rt >> 6) & 0x03E0) +// | ((rt >> 9) & 0x7C00) | ((rt >> 16) & 0x8000); +// =========================================================================== + +TEST(EeRecMmi, Pext5ExpandsRgb1555ToBgra8PerLane) +{ + EeRecTestHarness h; + // UL[0] = 0xFFFF (all R/G/B/A bits set → 0x80F8F8F8) + // UL[1] = 0 (zero → zero) + // UL[2] = 0x5555 (alt bits → 0x00A850A8) + // UL[3] = 0xAAAA (alt bits with A=1 → 0x8050A850) + h.SetMmiPair(reg::a0, 0x000000000000FFFFull, 0x0000AAAA00005555ull); + h.LoadProgram({ee::PEXT5(reg::v0, reg::a0)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000000080F8F8F8ull, 0x8050A85000A850A8ull); +} + +TEST(EeRecMmi, Ppac5PacksBgra8BackToRgb1555PerLane) +{ + EeRecTestHarness h; + // Inverse of the Pext5 test — feed the expanded (BGRA8) values back in. + h.SetMmiPair(reg::a0, 0x0000000080F8F8F8ull, 0x8050A85000A850A8ull); + h.LoadProgram({ee::PPAC5(reg::v0, reg::a0)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x000000000000FFFFull, 0x0000AAAA00005555ull); +} + +// =========================================================================== +// PMFHL — move from HI/LO with one of five lane patterns selected by sa: +// sa=0 LW : Rd = { LO[0], HI[0], LO[2], HI[2] } (even words) +// sa=1 UW : Rd = { LO[1], HI[1], LO[3], HI[3] } (odd words) +// sa=2 SLW: Rd.UD[k] = sat(s64(HI.UL[2k]:LO.UL[2k])) → sign-extended s32 +// sa=3 LH : Rd.US = { LO[0], LO[2], HI[0], HI[2], +// LO[4], LO[6], HI[4], HI[6] } (even halfwords) +// sa=4 SH : Rd.US lanes = PMFHL_CLAMP(LO.UL[0..3] and HI.UL[0..3]) interleaved +// at 32-bit-pair granularity. +// =========================================================================== + +TEST(EeRecMmi, PmfhlLwInterleavesEvenWordsOfLoAndHi) +{ + EeRecTestHarness h; + // LO.UL = {0x10000001, 0x20000002, 0x30000003, 0x40000004} + h.SetLoPair(0x2000000210000001ull, 0x4000000430000003ull); + // HI.UL = {0xA000000A, 0xB000000B, 0xC000000C, 0xD000000D} + h.SetHiPair(0xB000000BA000000Aull, 0xD000000DC000000Cull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x00)}); + h.Run(); + // Rd.UL = {LO[0]=0x10000001, HI[0]=0xA000000A, LO[2]=0x30000003, HI[2]=0xC000000C} + h.ExpectMmiPair(reg::v0, 0xA000000A10000001ull, 0xC000000C30000003ull); +} + +TEST(EeRecMmi, PmfhlUwInterleavesOddWordsOfLoAndHi) +{ + EeRecTestHarness h; + h.SetLoPair(0x2000000210000001ull, 0x4000000430000003ull); + h.SetHiPair(0xB000000BA000000Aull, 0xD000000DC000000Cull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x01)}); + h.Run(); + // Rd.UL = {LO[1]=0x20000002, HI[1]=0xB000000B, LO[3]=0x40000004, HI[3]=0xD000000D} + h.ExpectMmiPair(reg::v0, 0xB000000B20000002ull, 0xD000000D40000004ull); +} + +TEST(EeRecMmi, PmfhlSlwSaturatesComposedS64InRange) +{ + EeRecTestHarness h; + // Pair 0: HI[0]:LO[0] = 0x00000000:0x12345678 = 0x12345678 (in range, positive) + // Rd.UD[0] = (s64)(s32)0x12345678 = 0x0000000012345678 + // Pair 1: HI[2]:LO[2] = 0xFFFFFFFF:0xFEDCBA98 = 0xFFFFFFFFFEDCBA98 (= -19088744, in range) + // Rd.UD[1] = (s64)(s32)0xFEDCBA98 = 0xFFFFFFFFFEDCBA98 + h.SetLoPair(0x0000000012345678ull, 0x00000000FEDCBA98ull); + h.SetHiPair(0x0000000000000000ull, 0x00000000FFFFFFFFull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x02)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x0000000012345678ull, 0xFFFFFFFFFEDCBA98ull); +} + +TEST(EeRecMmi, PmfhlSlwSaturatesComposedS64OutOfRange) +{ + EeRecTestHarness h; + // Pair 0: HI[0]:LO[0] = 0x00000001:0x00000000 = 0x100000000 (= 4294967296 > INT_MAX) + // Rd.UD[0] = (s64)INT32_MAX = 0x000000007FFFFFFF + // Pair 1: HI[2]:LO[2] = 0xFFFFFFFE:0xFFFFFFFF = 0xFFFFFFFEFFFFFFFF (= -4294967297 < INT_MIN) + // Rd.UD[1] = (s64)INT32_MIN = 0xFFFFFFFF80000000 + h.SetLoPair(0x0000000000000000ull, 0x00000000FFFFFFFFull); + h.SetHiPair(0x0000000000000001ull, 0x00000000FFFFFFFEull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x02)}); + h.Run(); + h.ExpectMmiPair(reg::v0, 0x000000007FFFFFFFull, 0xFFFFFFFF80000000ull); +} + +TEST(EeRecMmi, PmfhlLhInterleavesEvenHalfwordsOfLoAndHi) +{ + EeRecTestHarness h; + // LO.US = {0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x8888} + h.SetLoPair(0x4444333322221111ull, 0x8888777766665555ull); + // HI.US = {0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD, 0xEEEE, 0xFFFF, 0x9999, 0x1010} + h.SetHiPair(0xDDDDCCCCBBBBAAAAull, 0x10109999FFFFEEEEull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x03)}); + h.Run(); + // Rd.US = {LO[0]=0x1111, LO[2]=0x3333, HI[0]=0xAAAA, HI[2]=0xCCCC, + // LO[4]=0x5555, LO[6]=0x7777, HI[4]=0xEEEE, HI[6]=0x9999} + h.ExpectMmiPair(reg::v0, 0xCCCCAAAA33331111ull, 0x9999EEEE77775555ull); +} + +TEST(EeRecMmi, PmfhlShSignedSaturates32To16AndInterleaves) +{ + EeRecTestHarness h; + // LO.UL = {0x00001234, 0x12345678, 0x80000000, 0xFFFFFFFE} + // PMFHL_CLAMP → {0x1234, 0x7FFF, 0x8000, 0xFFFE} + h.SetLoPair(0x1234567800001234ull, 0xFFFFFFFE80000000ull); + // HI.UL = {0xFFFF1000, 0x00007FFF, 0xFFFF8000, 0x00000ABC} + // sign-interpreted: {-61440, 32767, -32768, 2748} + // PMFHL_CLAMP → {0x8000, 0x7FFF, 0x8000, 0x0ABC} + h.SetHiPair(0x00007FFFFFFF1000ull, 0x00000ABCFFFF8000ull); + h.LoadProgram({ee::PMFHL(reg::v0, 0x04)}); + h.Run(); + // Rd.US = {sat(LO[0]), sat(LO[1]), sat(HI[0]), sat(HI[1]), + // sat(LO[2]), sat(LO[3]), sat(HI[2]), sat(HI[3])} + // = {0x1234, 0x7FFF, 0x8000, 0x7FFF, 0x8000, 0xFFFE, 0x8000, 0x0ABC} + h.ExpectMmiPair(reg::v0, 0x7FFF80007FFF1234ull, 0x0ABC8000FFFE8000ull); +} + +// ============================================================================ +// PMADDUW — 2-lane unsigned 32x32+64 multiply-accumulate +// tempu[k] = (LO.UL[2k] | HI.UL[2k]<<32) + Rs.UL[2k]*Rt.UL[2k] (u64) +// LO.UD[k] = sign-ext s32 of tempu[k] low32 +// HI.UD[k] = sign-ext s32 of tempu[k] high32 +// Rd.UD[k] = tempu[k] +// Run() asserts LO/HI also match between JIT and interp; ExpectMmiPair +// covers Rd explicitly. +// ============================================================================ + +TEST(EeRecMmi, PmadduwAccumulatesAcrossBothLanesNoOverflow) +{ + EeRecTestHarness h; + // Rs.UL = {2, _, 3, _}; Rt.UL = {10, _, 20, _} + h.SetMmiPair(reg::a0, 0x0000000000000002ull, 0x0000000000000003ull); + h.SetMmiPair(reg::a1, 0x000000000000000Aull, 0x0000000000000014ull); + // LO.UL[0]=100=0x64, LO.UL[2]=200=0xC8 + h.SetLoPair(0x0000000000000064ull, 0x00000000000000C8ull); + // HI.UL[0]=1, HI.UL[2]=2 + h.SetHiPair(0x0000000000000001ull, 0x0000000000000002ull); + h.LoadProgram({ee::PMADDUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // tempu[0] = 0x100000064 + 20 = 0x100000078 + // tempu[1] = 0x2000000C8 + 60 = 0x200000104 + h.ExpectMmiPair(reg::v0, 0x0000000100000078ull, 0x0000000200000104ull); +} + +TEST(EeRecMmi, PmadduwOverflowsProductIntoHighHalf) +{ + EeRecTestHarness h; + // Rs.UL = {0xFFFFFFFF, _, 0x00010000, _} + h.SetMmiPair(reg::a0, 0x00000000FFFFFFFFull, 0x0000000000010000ull); + // Rt.UL = {0x00000002, _, 0x00020000, _} + h.SetMmiPair(reg::a1, 0x0000000000000002ull, 0x0000000000020000ull); + h.SetLo64(0); + h.SetHi64(0); + h.LoadProgram({ee::PMADDUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // product[0] = 0xFFFFFFFF*2 = 0x1FFFFFFFE + // product[1] = 0x10000*0x20000 = 0x200000000 + h.ExpectMmiPair(reg::v0, 0x00000001FFFFFFFEull, 0x0000000200000000ull); +} + +TEST(EeRecMmi, PmadduwCarriesAcrossWord32Boundary) +{ + EeRecTestHarness h; + // Lane 0: composed = 0xAB_FFFFFFFF, product = 2 → sum = 0xAC_00000001 + // (validates that the 64-bit accumulate carries across bit 32, i.e. + // the codegen really does a 64-bit-lane add rather than two + // independent 32-bit adds.) + h.SetMmiPair(reg::a0, 0x0000000000000002ull, 0x0000000000000005ull); + h.SetMmiPair(reg::a1, 0x0000000000000001ull, 0x0000000000000007ull); + // LO.UL[0]=0xFFFFFFFF, LO.UL[2]=10 + h.SetLoPair(0x00000000FFFFFFFFull, 0x000000000000000Aull); + // HI.UL[0]=0xAB, HI.UL[2]=0 + h.SetHiPair(0x00000000000000ABull, 0x0000000000000000ull); + h.LoadProgram({ee::PMADDUW(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // tempu[0] = 0xAB_FFFFFFFF + 2 = 0xAC_00000001 + // tempu[1] = 0x00_0000000A + 5*7 = 0x2D = 0x00000000_0000002D + h.ExpectMmiPair(reg::v0, 0x000000AC00000001ull, 0x000000000000002Dull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_move_tests.cpp b/tests/ctest/core/recompilers/ee_rec_move_tests.cpp new file mode 100644 index 0000000000..c7d236352f --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_move_tests.cpp @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Move-family opcodes: LUI, MFHI/MFLO, MTHI/MTLO, +// MOVZ, MOVN. Verified under DiffJitVsInterp via EeRecTestHarness. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +// ---- LUI ---------------------------------------------------------------- + +TEST(EeRecMove, LuiPositiveImmediate) +{ + EeRecTestHarness h; + h.LoadProgram({LUI(reg::v0, 0x1234)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'1234'0000ull); +} + +TEST(EeRecMove, LuiNegativeImmediateSignExtends) +{ + // Bit 15 of imm set → result has bit 31 set → sign-extends to 64. + EeRecTestHarness h; + h.LoadProgram({LUI(reg::v0, 0x8000)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'8000'0000ull); +} + +TEST(EeRecMove, LuiZeroImmediateClearsLow64) +{ + // Even with rt holding garbage, LUI rt, 0 must zero the lower 64. + EeRecTestHarness h; + h.SetGpr64(reg::v0, 0xDEAD'BEEF'CAFE'BABEull); + h.LoadProgram({LUI(reg::v0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0ull); +} + +TEST(EeRecMove, LuiThenAddiBuildsConstant) +{ + // Common MIPS idiom: LUI hi; ADDIU low. Tests const-prop chain. + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x1234), + ADDIU(reg::v0, reg::v0, 0x5678), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'1234'5678ull); +} + +// ---- MFHI / MFLO -------------------------------------------------------- + +TEST(EeRecMove, MfhiCopiesHi) +{ + EeRecTestHarness h; + h.SetHi64(0xABCD'EF01'2345'6789ull); + h.LoadProgram({MFHI(reg::v0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xABCD'EF01'2345'6789ull); +} + +TEST(EeRecMove, MfloCopiesLo) +{ + EeRecTestHarness h; + h.SetLo64(0x1122'3344'5566'7788ull); + h.LoadProgram({MFLO(reg::v0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1122'3344'5566'7788ull); +} + +TEST(EeRecMove, MfhiToZeroIsNoOp) +{ + // MFHI to r0 must have no architectural effect — r0 stays zero. + EeRecTestHarness h; + h.SetHi64(0xDEAD'BEEFull); + h.LoadProgram({MFHI(reg::zero)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::zero), 0ull); +} + +TEST(EeRecMove, MfhiPreservesIndependentLo) +{ + EeRecTestHarness h; + h.SetHi64(0x1111'1111'1111'1111ull); + h.SetLo64(0x2222'2222'2222'2222ull); + h.LoadProgram({MFHI(reg::v0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1111'1111'1111'1111ull); + EXPECT_EQ(h.GetLo64Interp(), 0x2222'2222'2222'2222ull); +} + +// ---- MTHI / MTLO -------------------------------------------------------- + +TEST(EeRecMove, MthiWritesHi) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABE'1234'5678ull); + h.LoadProgram({MTHI(reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetHi64Interp(), 0xCAFE'BABE'1234'5678ull); +} + +TEST(EeRecMove, MtloWritesLo) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFEED'FACE'8765'4321ull); + h.LoadProgram({MTLO(reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 0xFEED'FACE'8765'4321ull); +} + +TEST(EeRecMove, MthiFromZeroClearsHi) +{ + EeRecTestHarness h; + h.SetHi64(0xDEAD'BEEFull); + h.LoadProgram({MTHI(reg::zero)}); + h.Run(); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMove, MthiFromConstPropSource) +{ + // LUI const-folds rs, then MTHI must still propagate the value to HI. + EeRecTestHarness h; + h.LoadProgram({ + LUI (reg::a0, 0xABCD), + MTHI(reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetHi64Interp(), 0xFFFF'FFFF'ABCD'0000ull); +} + +TEST(EeRecMove, MfhiAfterMthiRoundTrip) +{ + // MTHI then MFHI on the same value — exercises HI memory readback in + // the same block. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0123'4567'89AB'CDEFull); + h.LoadProgram({ + MTHI(reg::a0), + MFHI(reg::v0), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0123'4567'89AB'CDEFull); +} + +// ---- MOVZ / MOVN -------------------------------------------------------- + +TEST(EeRecMove, MovzMovesWhenRtZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); // rs + h.SetGpr64(reg::a1, 0); // rt — triggers move + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); // rd pre-state + h.LoadProgram({ee::MOVZ(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xCAFE'BABEull); +} + +TEST(EeRecMove, MovzPreservesRdWhenRtNonzero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); + h.SetGpr64(reg::a1, 1); // non-zero — preserve rd + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ee::MOVZ(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEFull); +} + +TEST(EeRecMove, MovnMovesWhenRtNonzero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); + h.SetGpr64(reg::a1, 1); // non-zero — triggers move + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ee::MOVN(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xCAFE'BABEull); +} + +TEST(EeRecMove, MovnPreservesRdWhenRtZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); + h.SetGpr64(reg::a1, 0); // zero — preserve rd + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ee::MOVN(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEFull); +} + +TEST(EeRecMove, MovzMoves64BitFullValue) +{ + // Verify the lower-64 path moves all 64 bits, not just the low 32. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xABCD'EF01'2345'6789ull); + h.SetGpr64(reg::a1, 0); + h.LoadProgram({ee::MOVZ(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xABCD'EF01'2345'6789ull); +} + +TEST(EeRecMove, MovzWithRsEqualsRdIsNoOp) +{ + // `MOVZ rd, rd, rt` is the trivially-useless form; recMOVZ short-circuits. + EeRecTestHarness h; + h.SetGpr64(reg::v0, 0x1111'2222'3333'4444ull); + h.SetGpr64(reg::a1, 1); // would-block, but short-circuit returns first + h.LoadProgram({ee::MOVZ(reg::v0, reg::v0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x1111'2222'3333'4444ull); +} + +TEST(EeRecMove, MovzConstRtNonzeroStaticallyDeadIsNoOp) +{ + // rt is loaded from a LUI-fold (const-prop), value != 0 → MOVZ never + // fires. rd must keep its pre-state. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ + LUI (reg::a1, 0x0001), // rt = 0x0001'0000 (≠ 0) + ee::MOVZ(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEFull); +} + +TEST(EeRecMove, MovnConstRtZeroStaticallyDeadIsNoOp) +{ + // rt const-folds to 0, MOVN never fires. rd preserved. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABEull); + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ + LUI (reg::a1, 0x0000), // rt = 0 + ee::MOVN(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEFull); +} + +TEST(EeRecMove, MovzConstRtZeroAlwaysFires) +{ + // rt const-folds to 0 → MOVZ unconditionally moves rs to rd. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xCAFE'BABE'1234'5678ull); + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.LoadProgram({ + LUI (reg::a1, 0x0000), // rt = 0 + ee::MOVZ(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xCAFE'BABE'1234'5678ull); +} + +TEST(EeRecMove, MovzConstRsNonzeroRtRuntimeZeroFires) +{ + // rs is const (const-fold via LUI+ORI), rt is runtime value 0 → + // dispatcher takes the consts-path (rs constant), move fires. + EeRecTestHarness h; + h.SetGpr64(reg::v0, 0xDEAD'BEEFull); + h.SetGpr64(reg::a1, 0); // runtime zero + h.LoadProgram({ + LUI (reg::a0, 0xABCD), + ORI (reg::a0, reg::a0, 0x1234), // rs = 0xFFFF'FFFF'ABCD'1234 + ee::MOVZ(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'ABCD'1234ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_muldiv_tests.cpp b/tests/ctest/core/recompilers/ee_rec_muldiv_tests.cpp new file mode 100644 index 0000000000..75d7c81cbd --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_muldiv_tests.cpp @@ -0,0 +1,469 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// MultDiv-family opcodes: MULT, MULTU, DIV, DIVU, MADD, MADDU. Verified under +// DiffJitVsInterp via EeRecTestHarness — every test exercises both the arm64 +// JIT emitter and the interpreter and gtest-diffs their final architectural +// state. + +#include "harness/EeRecTestHarness.h" + +#include + +#include + +using namespace recompiler_tests; +using namespace mips; + +// ---- MULT --------------------------------------------------------------- + +TEST(EeRecMulDiv, MultSignExtendsResult) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x12345); + h.SetGpr64(reg::a1, 0x10); + h.LoadProgram({MULT(reg::a0, reg::a1)}); + h.Run(); + // 0x12345 * 0x10 = 0x123450 — fits in 32 bits. Assert the FULL 64-bit + // LO/HI (no mask) so the zero-extension this test is named for is checked. + EXPECT_EQ(h.GetLo64Interp(), 0x123450ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MultNegativeProduct) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-2)); + h.SetGpr64(reg::a1, 3); + h.LoadProgram({MULT(reg::a0, reg::a1)}); + h.Run(); + // lo = -6 (as s32 → sign-extended). hi = all-ones (sign of -6). + EXPECT_EQ(static_cast(h.GetLo64Interp()), -6); + EXPECT_EQ(static_cast(h.GetHi64Interp()), -1); +} + +TEST(EeRecMulDiv, MultLargeProductSpillsIntoHi) +{ + // 0x10000 * 0x10000 = 0x100000000 → LO = 0 (low 32), HI = 1. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x10000); + h.SetGpr64(reg::a1, 0x10000); + h.LoadProgram({MULT(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), 0); + EXPECT_EQ(static_cast(h.GetHi64Interp()), 1); +} + +TEST(EeRecMulDiv, MultIntMinByTwo) +{ + // 0x80000000 * 2 (signed) = 0xFFFFFFFF00000000 as s64. + // LO = 0, HI = -1 (each sign-extended to 64). + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(static_cast(0x80000000))); + h.SetGpr64(reg::a1, 2); + h.LoadProgram({MULT(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 0ull); + EXPECT_EQ(h.GetHi64Interp(), 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecMulDiv, MultZeroLeavesHiLoZero) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234567); + h.SetGpr64(reg::a1, 0); + h.LoadProgram({MULT(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 0ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MultConstFold) +{ + // Pre-LUI/ADDIU both operands so const-prop fires; both sides fold + // statically and the const-const emitter runs. + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x0000), // v0 = 0 + ADDIU(reg::v0, reg::v0, 100), + LUI(reg::v1, 0x0000), + ADDIU(reg::v1, reg::v1, 200), + MULT(reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 20000ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +// ---- MULTU -------------------------------------------------------------- + +TEST(EeRecMulDiv, MultuLargeUnsigned) +{ + // 0xFFFFFFFF * 0xFFFFFFFF = 0xFFFFFFFE00000001 (u64). + // LO = 1, HI = 0xFFFFFFFE (sign-extended = 0xFFFFFFFFFFFFFFFE). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFFull); + h.SetGpr64(reg::a1, 0xFFFFFFFFull); + h.LoadProgram({MULTU(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 1ull); + EXPECT_EQ(h.GetHi64Interp(), 0xFFFFFFFFFFFFFFFEull); +} + +TEST(EeRecMulDiv, MultuTopBitSetLowBitSet) +{ + // 0x80000000 * 0x80000000 (unsigned) = 0x4000000000000000. + // LO = 0, HI = 0x40000000. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x80000000ull); + h.SetGpr64(reg::a1, 0x80000000ull); + h.LoadProgram({MULTU(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 0ull); + EXPECT_EQ(h.GetHi64Interp(), 0x40000000ull); +} + +TEST(EeRecMulDiv, MultuConstFold) +{ + EeRecTestHarness h; + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, -1), // v0 = -1 (UL[0] = 0xFFFFFFFF), const + ADDIU(reg::v1, reg::zero, 2), // v1 = 2, const + MULTU(reg::v0, reg::v1), + }); + h.Run(); + // 0xFFFFFFFF * 2 = 0x1FFFFFFFE. LO = 0xFFFFFFFE (sign-ext = -2), HI = 1. + EXPECT_EQ(static_cast(h.GetLo64Interp()), -2); + EXPECT_EQ(h.GetHi64Interp(), 1ull); +} + +// ---- DIV ---------------------------------------------------------------- + +TEST(EeRecMulDiv, DivSimple) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 100); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({DIV(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 14ull); // 100/7 = 14 + EXPECT_EQ(h.GetHi64Interp(), 2ull); // 100%7 = 2 +} + +TEST(EeRecMulDiv, DivNegativeQuotient) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-100)); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({DIV(reg::a0, reg::a1)}); + h.Run(); + // C++20 / MIPS truncate toward zero: -100 / 7 = -14, -100 % 7 = -2. + EXPECT_EQ(static_cast(h.GetLo64Interp()), -14); + EXPECT_EQ(static_cast(h.GetHi64Interp()), -2); +} + +TEST(EeRecMulDiv, DivIntMinByMinusOneOverflow) +{ + // MIPS overflow case: 0x80000000 / -1 → LO = 0x80000000, HI = 0. + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(static_cast(0x80000000))); + h.SetGpr64(reg::a1, static_cast(-1)); + h.LoadProgram({DIV(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), + static_cast(static_cast(0x80000000))); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, DivByZeroPositiveDividend) +{ + // MIPS: rs >= 0 / 0 → LO = -1, HI = rs. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234); + h.SetGpr64(reg::a1, 0); + h.LoadProgram({DIV(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), -1); + EXPECT_EQ(h.GetHi64Interp(), 0x1234ull); +} + +TEST(EeRecMulDiv, DivByZeroNegativeDividend) +{ + // MIPS: rs < 0 / 0 → LO = +1, HI = rs. + EeRecTestHarness h; + h.SetGpr64(reg::a0, static_cast(-0x1234)); + h.SetGpr64(reg::a1, 0); + h.LoadProgram({DIV(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 1ull); + EXPECT_EQ(static_cast(h.GetHi64Interp()), -0x1234); +} + +TEST(EeRecMulDiv, DivConstFold) +{ + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x0000), + ADDIU(reg::v0, reg::v0, 1000), + LUI(reg::v1, 0x0000), + ADDIU(reg::v1, reg::v1, 13), + DIV(reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 76ull); // 1000/13 = 76 + EXPECT_EQ(h.GetHi64Interp(), 12ull); // 1000%13 = 12 +} + +TEST(EeRecMulDiv, DivConstFoldOverflow) +{ + // Const fold path of INT_MIN / -1. + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x8000), // v0 UL[0] = 0x80000000 (INT_MIN) + ADDIU(reg::v1, reg::zero, -1), // v1 UL[0] = 0xFFFFFFFF (-1) + DIV(reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), + static_cast(static_cast(0x80000000))); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, DivConstFoldByZero) +{ + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x0000), + ADDIU(reg::v0, reg::v0, -42), // v0 = -42 (negative) + LUI(reg::v1, 0x0000), // v1 = 0 + DIV(reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 1ull); // neg divby0 → q=+1 + EXPECT_EQ(static_cast(h.GetHi64Interp()), -42); +} + +// ---- DIVU --------------------------------------------------------------- + +TEST(EeRecMulDiv, Divu) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 100); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({DIVU(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 14ull); // 100/7 = 14 + EXPECT_EQ(h.GetHi64Interp(), 2ull); // 100%7 = 2 +} + +TEST(EeRecMulDiv, DivuLargeDividend) +{ + // Dividend with top bit set — would be negative for DIV, positive for DIVU. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFFull); // u32 = 4294967295 + h.SetGpr64(reg::a1, 2); + h.LoadProgram({DIVU(reg::a0, reg::a1)}); + h.Run(); + // 4294967295 / 2 = 2147483647 (0x7FFFFFFF), remainder 1. + EXPECT_EQ(h.GetLo64Interp(), 0x7FFFFFFFull); + EXPECT_EQ(h.GetHi64Interp(), 1ull); +} + +TEST(EeRecMulDiv, DivuByZero) +{ + // MIPS DIVU / 0 → LO = 0xFFFFFFFF, HI = rs. LO sign-extends to -1 s64. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234); + h.SetGpr64(reg::a1, 0); + h.LoadProgram({DIVU(reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), -1); + EXPECT_EQ(h.GetHi64Interp(), 0x1234ull); +} + +TEST(EeRecMulDiv, DivuConstFold) +{ + EeRecTestHarness h; + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, -1), // UL[0] = 0xFFFFFFFF, const + ADDIU(reg::v1, reg::zero, 2), // v1 = 2, const + DIVU(reg::v0, reg::v1), + }); + h.Run(); + // 0xFFFFFFFF / 2 = 0x7FFFFFFF, rem 1. + EXPECT_EQ(h.GetLo64Interp(), 0x7FFFFFFFull); + EXPECT_EQ(h.GetHi64Interp(), 1ull); +} + +TEST(EeRecMulDiv, DivuConstFoldByZero) +{ + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::v0, 0x0000), + ADDIU(reg::v0, reg::v0, 0x7FFF), // v0 = 0x7FFF + LUI(reg::v1, 0x0000), // v1 = 0 + DIVU(reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetLo64Interp()), -1); + EXPECT_EQ(h.GetHi64Interp(), 0x7FFFull); +} + +// ---- Pipeline-1 --------------------------------------------------------- +// +// MULT1 — MMI pipeline-1 signed multiply (writes HI1:LO1). + +TEST(EeRecMulDiv, Mult1UsesSecondPipeline) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 6); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({ee::MULT1(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 42ull); +} + +// ---- MADD --------------------------------------------------------------- + +TEST(EeRecMulDiv, MaddAccumulatesIntoHiLo) +{ + // HI:LO starts at (0, 10). MADD adds (3 * 5) = 15 → new LO = 25. + EeRecTestHarness h; + h.SetLo64(10); + h.SetHi64(0); + h.SetGpr64(reg::a0, 3); + h.SetGpr64(reg::a1, 5); + h.LoadProgram({ee::MADD(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 25ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MaddNegativeProductDecrementsAccum) +{ + // HI:LO = 100. MADD adds (-3 * 7) = -21. New HI:LO = 79. + EeRecTestHarness h; + h.SetLo64(100); + h.SetHi64(0); + h.SetGpr64(reg::a0, static_cast(-3)); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({ee::MADD(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 79ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MaddCarryFromLoToHi) +{ + // Force carry across the LO/HI boundary of the 64-bit accumulator. + // HI:LO starts at (0, 0xFFFFFFFE) == 0xFFFFFFFE. + // MADD adds (4 * 2) = 8 → new accumulator = 0x100000006. + // Expected: LO = 6 (sign-ext 0x6), HI = 1. + EeRecTestHarness h; + h.SetLo64(static_cast(static_cast(0xFFFFFFFE))); // sign-ext -2 + h.SetHi64(0); + h.SetGpr64(reg::a0, 4); + h.SetGpr64(reg::a1, 2); + h.LoadProgram({ee::MADD(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 6ull); + EXPECT_EQ(h.GetHi64Interp(), 1ull); +} + +TEST(EeRecMulDiv, MaddWritesRdWithLo) +{ + // rd gets the sign-extended LO as well as the HI:LO pair. + EeRecTestHarness h; + h.SetLo64(0); + h.SetHi64(0); + h.SetGpr64(reg::a0, 6); + h.SetGpr64(reg::a1, 7); + h.LoadProgram({ee::MADD(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 42ull); + EXPECT_EQ(h.GetLo64Interp(), 42ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MaddConstFold) +{ + // Const-fold path: both rs/rt flagged const, no actual multiply emitted + // — handler loads accumulator and adds the precomputed product. + EeRecTestHarness h; + h.SetLo64(1000); + h.SetHi64(0); + h.LoadProgram({ + LUI(reg::v0, 0x0000), + ADDIU(reg::v0, reg::v0, 11), + LUI(reg::v1, 0x0000), + ADDIU(reg::v1, reg::v1, 13), + ee::MADD(reg::a0, reg::v0, reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 1143ull); // 1000 + 11*13 + EXPECT_EQ(h.GetHi64Interp(), 0ull); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 1143ull); +} + +// ---- MADDU -------------------------------------------------------------- + +TEST(EeRecMulDiv, MadduSimple) +{ + EeRecTestHarness h; + h.SetLo64(10); + h.SetHi64(0); + h.SetGpr64(reg::a0, 3); + h.SetGpr64(reg::a1, 4); + h.LoadProgram({ee::MADDU(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 22ull); // 10 + 3*4 + EXPECT_EQ(h.GetHi64Interp(), 0ull); +} + +TEST(EeRecMulDiv, MadduUnsignedOperands) +{ + // rs=rt=0xFFFFFFFF; unsigned product = 0xFFFFFFFE00000001. LO=1, HI=0xFFFFFFFE. + // HI:LO accumulator starts at 0. + EeRecTestHarness h; + h.SetLo64(0); + h.SetHi64(0); + h.SetGpr64(reg::a0, 0xFFFFFFFFull); + h.SetGpr64(reg::a1, 0xFFFFFFFFull); + h.LoadProgram({ee::MADDU(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 1ull); + EXPECT_EQ(h.GetHi64Interp(), 0xFFFFFFFFFFFFFFFEull); +} + +TEST(EeRecMulDiv, MadduCarryFromLoToHi) +{ + // Same carry test as MADD but unsigned path. + EeRecTestHarness h; + h.SetLo64(static_cast(static_cast(0xFFFFFFFE))); + h.SetHi64(0); + h.SetGpr64(reg::a0, 4); + h.SetGpr64(reg::a1, 2); + h.LoadProgram({ee::MADDU(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetLo64Interp(), 6ull); + EXPECT_EQ(h.GetHi64Interp(), 1ull); +} + +TEST(EeRecMulDiv, MadduConstFold) +{ + EeRecTestHarness h; + h.SetLo64(500); + h.SetHi64(0); + h.LoadProgram({ + LUI(reg::v0, 0x0000), + ADDIU(reg::v0, reg::v0, 20), + LUI(reg::v1, 0x0000), + ADDIU(reg::v1, reg::v1, 25), + ee::MADDU(reg::a0, reg::v0, reg::v1), + }); + h.Run(); + // 500 + 20*25 = 1000; LO sign-extended into a0. + EXPECT_EQ(h.GetLo64Interp(), 1000ull); + EXPECT_EQ(h.GetHi64Interp(), 0ull); + EXPECT_EQ(h.GetGpr64Interp(reg::a0), 1000ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_multiblock_tests.cpp b/tests/ctest/core/recompilers/ee_rec_multiblock_tests.cpp new file mode 100644 index 0000000000..60a22367b7 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_multiblock_tests.cpp @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Cross-block control-flow for the EE. ExpectBlockLinked queries the block +// link multimap (recEeIsBlockLinked -> Arm64BaseBlocks::IsLinked): it returns +// true when a SetBranchImm patch site inside the source block targets the +// destination block. The J/BEQ branch handlers emit SetBranchImm, which records +// such a link via recBlocks.Link, so the cross-block link assertions here are +// live — control flows Block A -> Block B and the static link site is present. +// +// Parallels iop_multiblock_tests.cpp. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; + +// Two distinct basic blocks landing in the same 4KB region but separated by +// enough space to be independently compiled. +constexpr u32 kBlockAPc = kProgramPc; +constexpr u32 kBlockBPc = kProgramPc + 0x100; +} // namespace + +TEST(EeRecMultiblock, JCrossesBlockBoundary) +{ + // Block A: ADDIU v0, zero, 1; J B; NOP + // Block B: ADDIU v0, zero, 2; J park; NOP + // The `j B` must land at kBlockBPc and continue executing. + EeRecTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::v0, reg::zero, 1), + J(kBlockBPc), + NOP, + }); + // Block B is a separate write into the program-region memory. + h.WriteU32(kBlockBPc + 0, ADDIU(reg::v0, reg::zero, 2)); + h.WriteU32(kBlockBPc + 4, J(kPark)); + h.WriteU32(kBlockBPc + 8, NOP); + h.Run(); + // If the J fell through (no cross-block transfer), v0 would be 1. + h.ExpectGpr64(reg::v0, 2ull); + // The J opcode handler emits SetBranchImm, which records a link patch site + // (recBlocks.Link) within block A targeting block B. The J instruction lives + // at kBlockAPc + 4. + h.ExpectBlockLinked(kBlockAPc + 4, kBlockBPc); +} + +TEST(EeRecMultiblock, BeqTakenCrossesBlockBoundary) +{ + // BEQ a0,a0,+offset (always taken) to block B. Exercises the branch- + // imm-to-imm path through the dispatcher, which the JIT will short- + // circuit via LinkArm64. + // + // Offset from PC+4 = kBlockBPc means offset = (0x100 / 4) = 0x40. + // But BEQ offset is in words and relative to PC+4, so: + // target = (PC+4) + (offset * 4) + // 0x100 = 4 + (offset * 4) → offset = 0x3F + // Start at kBlockAPc (= kProgramPc), so PC+4 = kProgramPc+4, target + // = kBlockBPc = kProgramPc + 0x100 → offset*4 = 0xFC → offset = 0x3F. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.LoadProgramNoTerm({ + BEQ(reg::a0, reg::a0, 0x3F), // always taken + NOP, // delay slot + }); + h.WriteU32(kBlockBPc + 0, ADDIU(reg::v0, reg::zero, 7)); + h.WriteU32(kBlockBPc + 4, J(kPark)); + h.WriteU32(kBlockBPc + 8, NOP); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); + // The BEQ opcode handler emits SetBranchImm for the taken target, recording + // a link patch site within block A (the BEQ lives at kBlockAPc). + h.ExpectBlockLinked(kBlockAPc, kBlockBPc); +} + +// Tight backward-branch loop. The scanner (recRecompile) sees the BNE at +12 +// with a backward target of +4 (> startpc, < branch index), so it ends the +// block AT +4 — the loop head becomes its own linkable block. Block +// granularity is architecturally invisible, so this guards correctness of the +// truncated-block loop rather than RED-proving the split: a 3-iteration +// countdown must still produce a0=0, a1=3. +TEST(EeRecMultiblock, BackwardBranchLoopBlockSplit) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a1, 0); + h.LoadProgramNoTerm({ + ADDIU(reg::a0, reg::zero, 3), // +0 counter = 3 (startpc) + ADDIU(reg::a1, reg::a1, 1), // +4 loop head (backward branch target) + ADDIU(reg::a0, reg::a0, -1), // +8 counter-- + BNE(reg::a0, reg::zero, -3), // +12 branch back to +4 when a0 != 0 + NOP, // +16 delay slot (always executes) + J(kPark), // +20 exit + NOP, // +24 J delay slot + }); + h.Run(); + h.ExpectGpr64(reg::a0, 0ull); + h.ExpectGpr64(reg::a1, 3ull); +} + +TEST(EeRecMultiblock, ThreeBlockChainExecutesInOrder) +{ + // Block A → B → C via J chain. Each block sets a different GPR so the + // test can verify all three executed in the right order. + constexpr u32 kBlockCPc = kProgramPc + 0x200; + EeRecTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::a0, reg::zero, 1), + J(kBlockBPc), + NOP, + }); + h.WriteU32(kBlockBPc + 0, ADDIU(reg::a1, reg::zero, 2)); + h.WriteU32(kBlockBPc + 4, J(kBlockCPc)); + h.WriteU32(kBlockBPc + 8, NOP); + h.WriteU32(kBlockCPc + 0, ADDIU(reg::a2, reg::zero, 3)); + h.WriteU32(kBlockCPc + 4, J(kPark)); + h.WriteU32(kBlockCPc + 8, NOP); + h.Run(); + h.ExpectGpr64(reg::a0, 1ull); + h.ExpectGpr64(reg::a1, 2ull); + h.ExpectGpr64(reg::a2, 3ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_shift_tests.cpp b/tests/ctest/core/recompilers/ee_rec_shift_tests.cpp new file mode 100644 index 0000000000..d0b86bb223 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_shift_tests.cpp @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Shifts. SLL/SRL/SRA sign-extend the 32-bit result into the 64-bit GPR. +// DSLL / DSRL / DSRA / DSLL32 / DSRL32 / DSRA32 are true 64-bit shifts. + +#include "harness/EeRecTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(EeRecShift, SllSignExtendsHighBit) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.LoadProgram({SLL(reg::v0, reg::a0, 31)}); + h.Run(); + // Result 0x80000000 sign-extends to 0xFFFFFFFF80000000. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFF80000000ull); +} + +TEST(EeRecShift, SraSignFillsHigh64) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x80000000); + h.LoadProgram({SRA(reg::v0, reg::a0, 1)}); + h.Run(); + // SRA on 0x80000000 by 1 = 0xC0000000 → sign-extends. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFC0000000ull); +} + +TEST(EeRecShift, DsllFull64Bit) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.LoadProgram({ee::DSLL(reg::v0, reg::a0, 31)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000080000000ull); +} + +TEST(EeRecShift, Dsll32) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.LoadProgram({ee::DSLL32(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000000100000000ull); +} + +TEST(EeRecShift, Dsll32With31Shifts63Total) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.LoadProgram({ee::DSLL32(reg::v0, reg::a0, 31)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x8000000000000000ull); +} + +TEST(EeRecShift, DsrlKeepsZeroFill) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x8000000000000000ull); + h.LoadProgram({ee::DSRL(reg::v0, reg::a0, 1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x4000000000000000ull); +} + +TEST(EeRecShift, DsraSignFill) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x8000000000000000ull); + h.LoadProgram({ee::DSRA(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xF800000000000000ull); +} + +TEST(EeRecShift, Dsra32) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFFF00000000ull); + h.LoadProgram({ee::DSRA32(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFFFFFFFFFFFFFFull); +} + +TEST(EeRecShift, DsllvMasksShiftBy63) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.SetGpr64(reg::a1, 65); // low 6 bits = 1 + h.LoadProgram({ee::DSLLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +TEST(EeRecShift, SllvMasksShiftBy31) +{ + // 32-bit variable shifts use only the low 5 bits, not low 6. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.SetGpr64(reg::a1, 33); // low 5 bits = 1 + h.LoadProgram({SLLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 2ull); +} + +// ---- Additional coverage extensions ---------------------------------------- +// Extensions below exercise the remaining dispatcher paths and edge cases. + +// SLL with sa=0 is the canonical MIPS sign-extending move. +TEST(EeRecShift, SllByZeroIsLow32SignExtend) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'8000'0001ull); + h.LoadProgram({SLL(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'8000'0001ull); +} + +TEST(EeRecShift, SrlByImmZeroExtendsLow32) +{ + // 0xFFFF'FFFF >> 4 = 0x0FFF'FFFF; positive 32-bit so high zero. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234'5678'FFFF'FFFFull); + h.LoadProgram({SRL(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0FFF'FFFFull); +} + +TEST(EeRecShift, SrlByZeroIsSignExtendingMove) +{ + // SRL with sa=0 still discards high 32 bits and sign-extends low 32. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'8000'0000ull); + h.LoadProgram({SRL(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'8000'0000ull); +} + +TEST(EeRecShift, DsllByZeroIsCopy) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xDEAD'BEEF'CAFE'BABEull); + h.LoadProgram({ee::DSLL(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xDEAD'BEEF'CAFE'BABEull); +} + +TEST(EeRecShift, Dsrl32WithExtraSa) +{ + // sa=4 → effective shift = 36. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1000'0000'0000'0000ull); + h.LoadProgram({ee::DSRL32(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0100'0000ull); +} + +// SRLV / SRAV — variable counterparts of SRL / SRA. +TEST(EeRecShift, SrlvByRegisterZeroExtends) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x1234'5678'F000'0000ull); + h.SetGpr64(reg::a1, 0x0000'0000'0000'0004ull); + h.LoadProgram({SRLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0F00'0000ull); +} + +TEST(EeRecShift, SravByRegisterSignExtends) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'F000'0000ull); + h.SetGpr64(reg::a1, 0x0000'0000'0000'0004ull); + h.LoadProgram({SRAV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + // 0xF000'0000 >> 4 (arith) = 0xFF00'0000; sign-extend. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFFFF'FFFF'FF00'0000ull); +} + +TEST(EeRecShift, DsrlvAcrossWordBoundary) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFF'0000'0000ull); + h.SetGpr64(reg::a1, 0x0000'0000'0000'0010ull); + h.LoadProgram({ee::DSRLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'FFFF'FFFF'0000ull); +} + +TEST(EeRecShift, DsravArithRightAcrossWordBoundary) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x8000'0000'0000'0000ull); + h.SetGpr64(reg::a1, 0x0000'0000'0000'0008ull); + h.LoadProgram({ee::DSRAV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0xFF80'0000'0000'0000ull); +} + +// Const-fold paths — provoked by preceding LUI/ORI sequences that the rec +// tracks as constant. + +TEST(EeRecShift, SllConstFoldThroughLui) +{ + // LUI tracks a0 as const; SLL hits recSLL_const. + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::a0, 0x0001), // a0 = 0x0001'0000 + SLL(reg::v0, reg::a0, 4), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0010'0000ull); +} + +TEST(EeRecShift, SllvConstSPath) +{ + // rs (a1) const, rt (a0) runtime → recSLLV_consts. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x0000'0000'0000'00FFull); + h.LoadProgram({ + ORI(reg::a1, reg::zero, 0x0010), + SLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'00FF'0000ull); +} + +TEST(EeRecShift, SllvConstTPath) +{ + // rt (a0) const, rs (a1) runtime → recSLLV_constt. + EeRecTestHarness h; + h.SetGpr64(reg::a1, 0x0000'0000'0000'0008ull); + h.LoadProgram({ + LUI(reg::a0, 0x0001), // a0 = 0x0001'0000 + SLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + // 0x0001'0000 << 8 = 0x0100'0000. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0100'0000ull); +} + +TEST(EeRecShift, SllvBothConst) +{ + // Both operands const → recSLLV_const compile-time fold. + EeRecTestHarness h; + h.LoadProgram({ + ORI(reg::a0, reg::zero, 0x0001), + ORI(reg::a1, reg::zero, 0x0004), + SLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'0000'0000'0010ull); +} + +TEST(EeRecShift, DsllvConstTPath) +{ + // rt const, rs runtime → recDSLLV_constt. + EeRecTestHarness h; + h.SetGpr64(reg::a1, 0x0000'0000'0000'0010ull); + h.LoadProgram({ + LUI(reg::a0, 0x1234), + ORI(reg::a0, reg::a0, 0x5678), // a0 = 0x0000'0000'1234'5678 + ee::DSLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + // 0x1234'5678 << 16 = 0x1234'5678'0000. + EXPECT_EQ(h.GetGpr64Interp(reg::v0), 0x0000'1234'5678'0000ull); +} + +// Writes to $zero are no-ops. +TEST(EeRecShift, SllToZeroIsNoOp) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFF'FFFFull); + h.LoadProgram({SLL(reg::zero, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::zero), 0ull); +} + +TEST(EeRecShift, DsllvToZeroIsNoOp) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 1); + h.SetGpr64(reg::a1, 4); + h.LoadProgram({ee::DSLLV(reg::zero, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::zero), 0ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_smc_tests.cpp b/tests/ctest/core/recompilers/ee_rec_smc_tests.cpp new file mode 100644 index 0000000000..20eaf37ff4 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_smc_tests.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Self-modifying-code coverage for the EE recompiler. +// +// Parallels iop_smc_tests.cpp — which caught a real psxRecClearMem +// merge-semantics bug during IOP port testing. The same chain applies to +// the EE: memWrite → vtlb store → Cpu->Clear → recClear invalidates the +// cached block, next dispatch re-compiles. +// +// These tests are architecturally correct and serve as JIT regression gates +// for block compilation. + +#include "harness/EeRecTestHarness.h" + +#include "Memory.h" +#include "R5900.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; +} // namespace + +TEST(EeRecSmc, GuestSwOverwritesInstructionAheadOfPc) +{ + // Block layout: + // 0x00: LUI a1, hi(ADDIU v0, zero, 0x1337) + // 0x04: ORI a1, a1, lo(...) + // 0x08: SW a1, 0(a0) — self-modify the word at 0x10 + // 0x0C: NOP (alignment / give SW time to settle) + // 0x10: ADDIU v0, zero, 0x0BAD — replaced before we get here + // 0x14: JR ra / NOP — appended by LoadProgram + // + // Interp-only. A guest SW into a page holding compiled code must + // invalidate the covering block, but the test harness does not wire + // page-protection SIGSEGV backpatching, so a guest SW into a compiled + // page does not auto-invalidate. The harness-driven TriggerSmc() case + // (next test) still works because it calls memWrite32 + recClear from + // the *host* side; only guest-emitted SW misses here. + constexpr u32 kNewInstr = ADDIU(reg::v0, reg::zero, 0x1337); + const u16 hi = static_cast(kNewInstr >> 16); + const u16 lo = static_cast(kNewInstr & 0xFFFFu); + + EeRecTestHarness h; + h.SetGpr64(reg::a0, kProgramPc + 0x10); + h.LoadProgram({ + LUI(reg::a1, hi), + ORI(reg::a1, reg::a1, lo), + SW(reg::a1, 0, reg::a0), // overwrite insn @ 0x10 + NOP, + ADDIU(reg::v0, reg::zero, 0x0BAD), // becomes ADDIU v0,0x1337 + }); + h.RunInterpOnly(); + h.ExpectGpr64(reg::v0, 0x1337ull); +} + +TEST(EeRecSmc, TriggerSmcHelperRewritesMemory) +{ + // Demonstrates the TriggerSmc harness helper. Program reads the word + // at kProgramPc+0x100 (which the helper rewrites pre-run) into v0 and + // returns. Tests the "harness rewrites then jits" discipline that + // iop_smc_tests uses for programmatic SMC fixtures. + EeRecTestHarness h; + h.SetGpr64(reg::a0, kProgramPc + 0x100); + h.LoadProgram({ + LW(reg::v0, 0, reg::a0), + }); + h.TriggerSmc(kProgramPc + 0x100, 0xDEADBEEFu); + h.TrackMemWindow(kProgramPc + 0x100, 4); + h.Run(); + // LW sign-extends, 0xDEADBEEF has bit 31 set, so v0 is sign-extended. + h.ExpectGpr64(reg::v0, 0xFFFFFFFFDEADBEEFull); +} + +TEST(EeRecSmc, RewriteAdjacentWordDoesNotAffectCurrentInstruction) +{ + // Write to the word *after* the last real instruction. No effect on + // the currently-executing block. Guard against an over-aggressive + // Cpu->Clear implementation that invalidates unrelated words. + EeRecTestHarness h; + h.SetGpr64(reg::a0, kProgramPc + 0x200); + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, 42), + SW(reg::v0, 0, reg::a0), // write beyond program + }); + h.TrackMemWindow(kProgramPc + 0x200, 4); + h.Run(); + h.ExpectGpr64(reg::v0, 42ull); + EXPECT_EQ(h.ReadU32(kProgramPc + 0x200), 42u); +} + +// Regression gate for recClear straddler-block fnptr reset. +// +// The bug: recClear's per-word reset loop only visited words inside +// [addr, end), missing block STARTs that lie before addr but whose body +// extends into the cleared range. Combined with Arm64BaseBlocks::Remove() +// patching only the compiled-code stub, this left BLOCK(startpc)->fnptr +// pointing at the just-overwritten stub. The next dispatch from startpc +// followed the stub's `B JITCompile` redirect into JITCompile, which then +// tripped the recRecompile fnptr assertion because BLOCK->fnptr was the +// stub address, not JITCompile. BLOCK(startpc)->fnptr must be reset to the +// JIT-compile entry so re-dispatch recompiles cleanly. +// +// The production trigger is the fastmem-backpatch path: vtlb calls +// Cpu->Clear(guest_pc, 1) with a MID-block PC. SimulateFastmemFault() +// mimics that single production entry. +TEST(EeRecSmc, StraddlerBlockRecClearResetsStartFnptr) +{ + EeRecTestHarness h; + + // 31-instruction block (block extent: kProgramPc..kProgramPc+0x7C, plus + // the harness-appended JR ra/NOP at +0x7C/+0x80). Long enough that any + // mid-block fault PC < endpc is the straddler-from-below scenario. + std::initializer_list program = { + ADDIU(reg::v0, reg::zero, 0), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + ADDIU(reg::v0, reg::v0, 0x100), ADDIU(reg::v0, reg::v0, 0x100), + }; + h.LoadProgram(program); + + // First pass: compile, run JIT + interp from a fresh cache, diff. + // v0 = 30 * 0x100 = 0x1E00. + h.Run(EeRecTestHarness::RunMode::FreshCache); + h.ExpectGpr64(reg::v0, 0x1E00ull); + + // Single-word Clear at instruction index 16 (offset 0x40) — well inside + // the block (block startpc=0..endpc=0x7C). The straddler-from-below case. + h.SimulateFastmemFault(kProgramPc + 0x40); + + // Second pass: re-dispatch from kProgramPc. Block was invalidated mid- + // extent. BLOCK(startpc)->fnptr must be reset to JITCompile so dispatch + // recompiles cleanly; if it remains stale, the recRecompile fnptr + // assertion fires (process abort). + h.Run(EeRecTestHarness::RunMode::PreserveCache); + h.ExpectGpr64(reg::v0, 0x1E00ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_timeout_loop_tests.cpp b/tests/ctest/core/recompilers/ee_rec_timeout_loop_tests.cpp new file mode 100644 index 0000000000..bec21b84f9 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_timeout_loop_tests.cpp @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Regression coverage for the EE recompiler's timeout-loop speedhack +// (recSkipTimeoutLoop / its detection in recRecompile). +// +// The recompiler only treats a block as a skippable timeout loop when the +// block's terminating branch targets the block's OWN start (a self-loop): +// +// s_nBlockFF = false; +// if (s_branchTo == startpc) { ...analyse... } +// else { is_timeout_loop = false; } +// +// Without that `else`, a block of the shape +// addiu reg,reg,-1 / bne reg,zero, / nop +// — i.e. the counter-decrement TOP of a real counted compute loop whose first +// branch is an early-exit guard that jumps FORWARD into the body — gets +// misclassified as a timeout loop. recSkipTimeoutLoop then fast-forwards the +// counter to zero and jumps to the block end, skipping the loop body entirely. +// +// A real game exercises this pattern in its per-frame DMA-list build loop: the +// counter-decrement TOP block ends in a forward early-exit branch, not a +// self-loop; misclassifying it as a timeout loop skips the body and corrupts +// the GIF DMA chain, so the game's completion wait loops never satisfy and +// gameplay hangs. +// +// This test runs jit vs interp through Run()'s auto-diff with the WaitLoop +// speedhack enabled. The timeout-loop skip is jit-only (the interpreter always +// executes the body), so a misdetected loop diverges and Run() fails. +// +// A genuine self-looping timeout loop is deliberately NOT auto-diffed here: when +// the speedhack fires it fast-forwards the counter to nextEventCycle and exits via +// the event path, leaving the counter only partially drained — i.e. it diverges +// from naive interpreter spinning BY DESIGN (correctness then depends on the +// in-game event the loop was waiting for, which the harness doesn't model). The +// self-loop guard provably leaves genuine timeout loops unaffected: a self-loop +// has s_branchTo == startpc, so the `else { is_timeout_loop = false; }` guard +// never fires for it. + +#include "harness/EeRecTestHarness.h" + +#include "Config.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; + +// RAII toggle for EmuConfig.Speedhacks.WaitLoop (the gate on recSkipTimeoutLoop). +struct ScopedWaitLoop +{ + bool prev; + explicit ScopedWaitLoop(bool on) : prev(EmuConfig.Speedhacks.WaitLoop) { EmuConfig.Speedhacks.WaitLoop = on; } + ~ScopedWaitLoop() { EmuConfig.Speedhacks.WaitLoop = prev; } +}; +} // namespace + +// A counted compute loop whose TOP block is `addiu ctr,-1 / bne ctr,zero,FWD / +// nop`, where the bne is an early-exit guard jumping FORWARD into the body — NOT +// a self-loop. The body does observable work ($a1 += 5 per iteration). Without +// the self-loop guard, the jit would skip the body ($a1 stays 0) while the +// interpreter runs it ($a1 == 15). Run()'s jit-vs-interp auto-diff catches +// the divergence. +// +// Layout (offsets from kProgramPc): +// +0x00 addiu t4,t4,-1 ; loop TOP (startpc); counter-- +// +0x04 bne t4,zero,+0x14 ; early-exit guard -> body (FORWARD) +// +0x08 nop ; delay slot +// +0x0C j park ; loop EXIT (taken when t4 falls through == 0) +// +0x10 nop ; j delay slot +// +0x14 addiu a1,a1,5 ; BODY: observable work +// +0x18 bne t4,zero,+0x00 ; loop back to TOP +// +0x1C nop ; delay slot +TEST(EeRecTimeoutLoop, ForwardEarlyExitGuardIsNotATimeoutLoop) +{ + ScopedWaitLoop wl(true); + + EeRecTestHarness h; + h.SetGpr64(reg::t4, 4); // 3 body iterations (t4: 4->3->2->1, then exits at 0) + h.SetGpr64(reg::a1, 0); + h.LoadProgramNoTerm({ + ADDIU(reg::t4, reg::t4, -1), // +0x00 TOP + BNE(reg::t4, reg::zero, 3), // +0x04 -> body at +0x14 (PC+4 +0xC) + NOP, // +0x08 delay slot + J(kPark), // +0x0C exit + NOP, // +0x10 j delay slot + ADDIU(reg::a1, reg::a1, 5), // +0x14 BODY + BNE(reg::t4, reg::zero, -7), // +0x18 -> TOP at +0x00 (PC+4 -0x1C) + NOP, // +0x1C delay slot + }); + h.Run(); + + // If the loop body were skipped, a1 would be 0. + h.ExpectGpr64(reg::a1, 15ull); + h.ExpectGpr64(reg::t4, 0ull); +} + +// --------------------------------------------------------------------------- +// Negative-shape sweep. +// +// The timeout-loop skip fires only when ALL of these hold (the EE recompiler's +// recRecompile detection + StartRecomp guard + recSkipTimeoutLoop): +// (1) is_timeout_loop: the block is ONLY [nops] addiu reg,reg,-N [nops] +// bne reg,zero,target nop — any other instruction clears it, a second +// decrement clears it, a bne whose Rt!=zero clears it; +// (2) timeout_reg >= 0 && timeout_has_bne; +// (3) s_branchTo == startpc — the branch is a SELF-loop (the guard the N1 +// test above covers). +// Each negative below violates exactly ONE condition while otherwise looking +// timeout-loop-shaped, and asserts jit == interp (Run()'s auto-diff) with +// WaitLoop ON — i.e. the block must be recompiled normally, not skipped. +// +// A POSITIVE test (a genuine all-nop self-loop that SHOULD be skipped) is +// deliberately omitted: when the skip fires it fast-forwards the counter to the +// next event and exits via the event path, leaving the counter only partially +// drained — it diverges from naive interpreter spinning BY DESIGN, so Run()'s +// auto-diff cannot validate it. The positive side is covered system-level by a +// WaitLoop-on-vs-off EE-RAM differential. +// --------------------------------------------------------------------------- + +// A genuine SELF-loop (bne back to startpc — passes condition 3) whose block has +// a SECOND in-place decrement in the body (`addiu a1,a1,-1`). The canonical +// timeout loop has exactly one decrement; a second one is off-pattern and the +// block must be recompiled normally. This is defended in depth: the +// "timeout_reg already set" term clears is_timeout_loop on the second addiu, and +// even if that term were removed the second addiu would reassign timeout_reg to +// $a1, after which the "timeout_reg != bne's Rs" backstop rejects the +// `bne t4` (verified: breaking the first term alone leaves this test green — the +// backstop catches it). So N2 locks the combined invariant "a multi-decrement +// self-loop is never a timeout loop"; the observable side effect ($a1, a register +// recSkipTimeoutLoop never touches) goes stale if the block is ever skipped. +// +// +0x00 TOP: addiu t4,t4,-1 ; counter-- (timeout_reg = t4) +// +0x04 addiu a1,a1,-1 ; BODY: off-pattern 2nd decrement +// +0x08 bne t4,zero,TOP ; SELF-loop back to +0x00 +// +0x0C nop ; delay slot +// +0x10 j park ; loop exit +// +0x14 nop +TEST(EeRecTimeoutLoop, SelfLoopWithAluBodyIsNotSkipped) +{ + ScopedWaitLoop wl(true); + + EeRecTestHarness h; + h.SetGpr64(reg::t4, 4); // 4 iterations: t4 4->3->2->1->0 + h.SetGpr64(reg::a1, 100); // body decrements a1 once per iteration + h.LoadProgramNoTerm({ + ADDIU(reg::t4, reg::t4, -1), // +0x00 TOP + ADDIU(reg::a1, reg::a1, -1), // +0x04 body + BNE(reg::t4, reg::zero, -3), // +0x08 -> TOP (PC+4 -0xC) + NOP, // +0x0C delay slot + J(kPark), // +0x10 exit + NOP, // +0x14 j delay slot + }); + h.Run(); + + h.ExpectGpr64(reg::a1, 96ull); // 100 - 4; stays 100 if the body were skipped + h.ExpectGpr64(reg::t4, 0ull); +} + +// A SELF-loop whose body performs a memory store. The `sw` (opcode != +// addiu/bne/nop) is the SOLE is_timeout_loop killer here (a different branch +// than N2's second-decrement path), so this isolates that guard. If the loop +// were skipped the store would never land and the seeded sentinel would survive. +// +// +0x00 TOP: addiu t4,t4,-1 ; counter-- (timeout_reg = t4) +// +0x04 sw t4,0(t2) ; store counter -> clears is_timeout_loop +// +0x08 bne t4,zero,TOP ; SELF-loop back to +0x00 +// +0x0C nop ; delay slot +// +0x10 j park ; loop exit +// +0x14 nop +TEST(EeRecTimeoutLoop, SelfLoopWithStoreBodyIsNotSkipped) +{ + ScopedWaitLoop wl(true); + + constexpr u32 kData = RecompilerTestEnvironment::kScratchAddr; + + EeRecTestHarness h; + h.SetGpr64(reg::t4, 3); // 3 iterations: stores t4 = 2, 1, then 0 (last store wins) + h.SetGpr64(reg::t2, kData); // store address + h.WriteU32(kData, 0xEEEEEEEEu); // sentinel: survives iff the body is skipped + h.TrackMemWindow(kData, 4); // include the store in Run()'s jit-vs-interp diff + h.LoadProgramNoTerm({ + ADDIU(reg::t4, reg::t4, -1), // +0x00 TOP + SW(reg::t4, 0, reg::t2), // +0x04 store counter + BNE(reg::t4, reg::zero, -3), // +0x08 -> TOP (PC+4 -0xC) + NOP, // +0x0C delay slot + J(kPark), // +0x10 exit + NOP, // +0x14 j delay slot + }); + h.Run(); + + // Body ran on both sides: the final counter value (0) reached memory, not the sentinel. + EXPECT_EQ(h.ReadU32(kData), 0u); + h.ExpectGpr64(reg::t4, 0ull); +} + +// A counted SELF-loop whose terminating `bne` compares the counter against a +// NONZERO register (a "spin until t4 == a2" wait), not against zero. The Rt!=0 +// check clears is_timeout_loop, so it must run to its natural exit. +// +// +0x00 TOP: addiu t4,t4,-1 ; counter-- (timeout_reg = t4) +// +0x04 bne t4,a2,TOP ; Rt = a2 (nonzero) -> clears is_timeout_loop +// +0x08 nop ; delay slot +// +0x0C j park ; loop exit (t4 == a2) +// +0x10 nop +TEST(EeRecTimeoutLoop, CounterComparedAgainstNonzeroRegIsNotSkipped) +{ + ScopedWaitLoop wl(true); + + EeRecTestHarness h; + h.SetGpr64(reg::t4, 5); // loop while t4 != a2: 5->4->3->2 (stops at 2) + h.SetGpr64(reg::a2, 2); + h.LoadProgramNoTerm({ + ADDIU(reg::t4, reg::t4, -1), // +0x00 TOP + BNE(reg::t4, reg::a2, -2), // +0x04 -> TOP (PC+4 -0x8) + NOP, // +0x08 delay slot + J(kPark), // +0x0C exit + NOP, // +0x10 j delay slot + }); + h.Run(); + + // Natural exit leaves t4 == a2 == 2; a wrongly-fired skip would drive it toward 0. + h.ExpectGpr64(reg::t4, 2ull); + h.ExpectGpr64(reg::a2, 2ull); +} diff --git a/tests/ctest/core/recompilers/ee_rec_traps_tests.cpp b/tests/ctest/core/recompilers/ee_rec_traps_tests.cpp new file mode 100644 index 0000000000..bab18cafa5 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_rec_traps_tests.cpp @@ -0,0 +1,369 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Trap instructions on the EE: TEQ/TGE/TLT/TNE/TGEU/TLTU family + +// TEQI/TGEI/TLTI/TNEI/TGEIU/TLTIU immediate variants + SYSCALL + BREAK. +// +// Semantics per MIPS-III: when the condition is true, a trap exception +// fires and control transfers to the common exception vector (0x80000180 +// with BEV=0). PC is saved in EPC, Cause is updated. +// +// For "trap taken" tests the BEV=0 exception vectors are pre-installed with +// `jr ra; nop` stubs by RecompilerTestEnvironment::SetUp() — the handler +// returns to the harness's parking lot via ra=kParkingPc, giving a +// well-defined post-state without per-test bootstrap. + +#include "harness/EeRecTestHarness.h" + +#include "R5900.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kCauseTrapExCode = 0x34; // ExcCode=0x0D (trap), shifted << 2 +} // namespace + +// ---------------- Register-register trap: not taken ---------------- + +TEST(EeRecTraps, TeqNotTakenFallsThrough) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 20); + h.LoadProgram({ + ee::TEQ(reg::a0, reg::a1), // 10 != 20 → not taken + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +TEST(EeRecTraps, TneNotTakenFallsThrough) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 10); + h.SetGpr64(reg::a1, 10); + h.LoadProgram({ + ee::TNE(reg::a0, reg::a1), // 10 == 10 → not taken + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +TEST(EeRecTraps, TltNotTakenForGreaterOrEqual) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 20); + h.SetGpr64(reg::a1, 10); // 20 < 10 → false → no trap + h.LoadProgram({ + ee::TLT(reg::a0, reg::a1), + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +TEST(EeRecTraps, TgeNotTakenForLess) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 5); + h.SetGpr64(reg::a1, 10); // 5 >= 10 → false → no trap + h.LoadProgram({ + ee::TGE(reg::a0, reg::a1), + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +// ---------------- Immediate trap: not taken ---------------- + +TEST(EeRecTraps, TeqiNotTakenFallsThrough) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.LoadProgram({ + ee::TEQI(reg::a0, 99), // 42 != 99 → no trap + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +TEST(EeRecTraps, TltiNotTakenForGreaterOrEqual) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 100); + h.LoadProgram({ + ee::TLTI(reg::a0, 50), // 100 < 50 → false → no trap + ADDIU(reg::v0, reg::zero, 7), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +// ---------------- Register-register trap: taken ---------------- + +TEST(EeRecTraps, TeqTakenRaisesException) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 42); + h.SetGpr64(reg::a1, 42); + h.LoadProgram({ + ee::TEQ(reg::a0, reg::a1), // 42 == 42 → trap fires + ADDIU(reg::v0, reg::zero, 99), // should NOT execute + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); // fall-through did not run + // Cause low byte has ExcCode<<2 = 0x34 (trap). + EXPECT_EQ(h.GetCp0Interp(13) & 0xFFu, kCauseTrapExCode); +} + +// ---------------- SYSCALL / BREAK (always taken) ---------------- + +TEST(EeRecTraps, SyscallAlwaysTaken) +{ + EeRecTestHarness h; + h.LoadProgram({ + SYSCALL_(), + ADDIU(reg::v0, reg::zero, 99), // should NOT execute + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); + // SYSCALL ExcCode=8, Cause.ExcCode<<2 = 0x20. + EXPECT_EQ(h.GetCp0Interp(13) & 0xFFu, 0x20u); +} + +TEST(EeRecTraps, BreakAlwaysTaken) +{ + EeRecTestHarness h; + h.LoadProgram({ + BREAK, + ADDIU(reg::v0, reg::zero, 99), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); + // BREAK ExcCode=9, Cause.ExcCode<<2 = 0x24. + EXPECT_EQ(h.GetCp0Interp(13) & 0xFFu, 0x24u); +} + +// ---------------- Register-register trap: full taken coverage ---------------- + +namespace { +template +void RunRegRegTrapTaken(Encode enc, u64 a0, u64 a1) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, a0); + h.SetGpr64(reg::a1, a1); + h.LoadProgram({ + enc(reg::a0, reg::a1), + ADDIU(reg::v0, reg::zero, 99), // should NOT execute + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); + EXPECT_EQ(h.GetCp0Interp(13) & 0xFFu, kCauseTrapExCode); +} + +template +void RunImmTrapTaken(Encode enc, u64 a0, s16 imm) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, a0); + h.LoadProgram({ + enc(reg::a0, imm), + ADDIU(reg::v0, reg::zero, 99), + }); + h.Run(); + h.ExpectGpr64(reg::v0, 0ull); + EXPECT_EQ(h.GetCp0Interp(13) & 0xFFu, kCauseTrapExCode); +} +} // namespace + +TEST(EeRecTraps, TgeTakenRaisesException) +{ + RunRegRegTrapTaken(ee::TGE, 100, 50); // 100 >= 50 (signed) → trap +} + +TEST(EeRecTraps, TgeuTakenRaisesException) +{ + // TGEU compares unsigned; -1 (= 0xFF..) >= 1 unsigned → trap. + RunRegRegTrapTaken(ee::TGEU, static_cast(-1), 1); +} + +TEST(EeRecTraps, TltTakenRaisesException) +{ + RunRegRegTrapTaken(ee::TLT, static_cast(-5), 5); // -5 < 5 (signed) → trap +} + +TEST(EeRecTraps, TltuTakenRaisesException) +{ + RunRegRegTrapTaken(ee::TLTU, 1, static_cast(-1)); // 1 < ~0 unsigned → trap +} + +TEST(EeRecTraps, TneTakenRaisesException) +{ + RunRegRegTrapTaken(ee::TNE, 7, 9); +} + +// ---------------- Immediate trap: full taken coverage ---------------- + +TEST(EeRecTraps, TgeiTakenRaisesException) +{ + RunImmTrapTaken(ee::TGEI, 100, 50); // 100 >= 50 → trap +} + +TEST(EeRecTraps, TgeiuTakenRaisesException) +{ + // _Imm_ is sign-extended to 64-bit before the unsigned compare per MIPS-III, + // so an imm of -1 produces 0xFFFF_FFFF_FFFF_FFFF; rs=0xFF..F is >= that → trap. + RunImmTrapTaken(ee::TGEIU, static_cast(-1), -1); +} + +TEST(EeRecTraps, TltiTakenRaisesException) +{ + RunImmTrapTaken(ee::TLTI, static_cast(-5), 5); // -5 < 5 → trap +} + +TEST(EeRecTraps, TltiuTakenRaisesException) +{ + // 1 < (sign-extended) -1 = 0xFF..F unsigned → trap. + RunImmTrapTaken(ee::TLTIU, 1, -1); +} + +TEST(EeRecTraps, TeqiTakenRaisesException) +{ + RunImmTrapTaken(ee::TEQI, static_cast(-1), -1); +} + +TEST(EeRecTraps, TneiTakenRaisesException) +{ + RunImmTrapTaken(ee::TNEI, 5, 9); +} + +// ---------------- MFSA / MTSA / MTSAB / MTSAH ---------------- +// +// SA is u32 in cpuRegs.sa; MFSA zero-extends to 64. Test JIT vs interp via +// the snapshot machinery (DiffEe asserts both paths match) — sa is included +// in the EeSnapshot so any divergence fails the test. + +TEST(EeRecTraps, MtsaCopiesFullRegister) +{ + // PS2 spec (R5900OpcodeImpl.cpp:1265): cpuRegs.sa = (u32)rs[lo]. No mask. + // Only MTSAB/MTSAH narrow the value (low 4 / low 3 bits respectively). + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0xFFFFFFF7u); + h.LoadProgram({ + ee::MTSA(reg::a0), + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0xFFFFFFF7u); +} + +TEST(EeRecTraps, MtsabXorsLow4BitsWithImmediate) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x5u); + h.LoadProgram({ + ee::MTSAB(reg::a0, 0x3), // (5 & 0xF) ^ (3 & 0xF) = 6 + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0x6u); +} + +TEST(EeRecTraps, MtsahShiftsLeftByOne) +{ + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x5u); + h.LoadProgram({ + ee::MTSAH(reg::a0, 0x3), // ((5 & 7) ^ (3 & 7)) << 1 = 6 << 1 = 0xC + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0xCu); +} + +TEST(EeRecTraps, MfsaReadsSaZeroExtended) +{ + EeRecTestHarness h; + h.LoadProgram({ + ee::MTSA(reg::a0), + ee::MFSA(reg::v0), + }); + h.SetGpr64(reg::a0, 0xCu); + h.Run(); + // rd should hold the value of sa, zero-extended to 64-bit. + h.ExpectGpr64(reg::v0, 0xCull); +} + +TEST(EeRecTraps, MfsaToZeroIsNoOp) +{ + // rd=r0 should not modify state; the JIT path early-returns. + EeRecTestHarness h; + h.SetGpr64(reg::a0, 0x5u); + h.LoadProgram({ + ee::MTSA(reg::a0), + ee::MFSA(reg::zero), + }); + h.Run(); + h.ExpectGpr64(reg::zero, 0ull); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0x5u); +} + +// ---------------- SYNC (no-op) ---------------- + +TEST(EeRecTraps, SyncIsNoOp) +{ + EeRecTestHarness h; + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, 7), + ee::SYNC, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 7ull); +} + +// ---------------- Const-prop fold paths for MTSA family ---------------- + +TEST(EeRecTraps, MtsaConstFoldsAtCompile) +{ + // Exercise the GPR_IS_CONST1 fast path: rs is set via a LUI+ORI sequence + // that the const-prop tracker captures. Same end-state as the runtime + // path (full 32-bit copy, no mask); a divergence here would point at a + // const-fold bug. + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::a0, 0), + ORI(reg::a0, reg::a0, 0xF7u), // a0 = 0xF7 (const-tracked) + ee::MTSA(reg::a0), + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0xF7u); +} + +TEST(EeRecTraps, MtsabConstFoldsAtCompile) +{ + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::a0, 0), + ORI(reg::a0, reg::a0, 0xAu), + ee::MTSAB(reg::a0, 0x5), // (0xA & 0xF) ^ (0x5 & 0xF) = 0xF + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0xFu); +} + +TEST(EeRecTraps, MtsahConstFoldsAtCompile) +{ + EeRecTestHarness h; + h.LoadProgram({ + LUI(reg::a0, 0), + ORI(reg::a0, reg::a0, 0x6u), + ee::MTSAH(reg::a0, 0x3), // ((6 & 7) ^ (3 & 7)) << 1 = 5 << 1 = 0xA + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.sa, 0xAu); +} diff --git a/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp new file mode 100644 index 0000000000..3059172d0c --- /dev/null +++ b/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// EE↔VU0 CFC2/CTC2 handoff DiffJitVsInterp suite. +// +// CFC2 ($t = VU0.VI[$d]) and CTC2 (VU0.VI[$d] = $t) are the EE-side +// gateway to VU0's integer/control register file. Both engines share the +// same VU0 register bank (vuRegs[0]) so the test contract is: +// 1. EE GPR target gets the correct VI value (CFC2). +// 2. VU0 VI target gets the correct EE value (CTC2). +// 3. Special-register paths (REG_R, REG_FBRST, REG_VPU_STAT, ...) hit +// the correct fallback emitter — they're not plain MOVs. +// +// VU0 capture is enabled so any divergence in vuRegs[0].VI mid-program is +// flagged. + +#include "harness/EeRecTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace mips; +using namespace mips::ee; + +namespace { + +constexpr u32 r_t0 = 8; +constexpr u32 r_t1 = 9; +constexpr u32 r_t2 = 10; + +} // namespace + +// ========================================================================= +// CFC2 — EE reads VU0.VI[fs] +// ========================================================================= + +TEST(EeVu0Cfc2, ReadsPlainViLow16BitsZeroExtended) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VI[1] is a plain 16-bit VI register; only low 16 bits are architectural. + h.SeedVu0Vi(1, 0x1234); + h.LoadProgram({CFC2(r_t0, 1)}); + h.Run(); + // CFC2 sign-extends per real hardware, but for low values the high half + // is zero. Both engines should agree on the full 64-bit GPR. + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); + EXPECT_EQ(static_cast(h.GetGpr64Jit(r_t0)), 0x1234u); +} + +TEST(EeVu0Cfc2, ReadsHighBit15SignExtendsTo32) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 0x8000); // bit 15 set → sign-extends to 0xFFFF8000 + h.LoadProgram({CFC2(r_t0, 1)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegRClampsAndSignExtends) +{ + // REG_R is the random-number register (24-bit valid). CFC2 of REG_R is a + // recCall fallback in the JIT — exercise the mask path. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_R].UL = 0x12345678u; + h.LoadProgram({CFC2(r_t0, REG_R)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegStatusFlagFullWidth) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_STATUS_FLAG].UL = 0x00000A05u; + h.LoadProgram({CFC2(r_t0, REG_STATUS_FLAG)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegMacFlagFullWidth) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_MAC_FLAG].UL = 0x0000ABCDu; + h.LoadProgram({CFC2(r_t0, REG_MAC_FLAG)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegClipFlag24BitWidth) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_CLIP_FLAG].UL = 0x00ABCDEFu; + h.LoadProgram({CFC2(r_t0, REG_CLIP_FLAG)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegFbrstAfterEeWrite) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_FBRST].UL = 0x0000000Cu; // D-stop + T-stop + h.LoadProgram({CFC2(r_t0, REG_FBRST)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsRegTpcAsByteAddressDivBy8) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_TPC].UL = 0x40; // pair index 8 + h.LoadProgram({CFC2(r_t0, REG_TPC)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Cfc2, ReadsViZeroIsZero) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.LoadProgram({CFC2(r_t0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), 0u); +} + +// ========================================================================= +// CTC2 — EE writes VU0.VI[fs] +// ========================================================================= + +TEST(EeVu0Ctc2, WritesPlainViLow16Bits) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr64(r_t0, 0xDEADBEEFu); + h.LoadProgram({CTC2(r_t0, 1)}); + h.Run(); + // VI[1] retains only low 16 bits. + EXPECT_EQ(h.GetVu0ViJit(1), 0xBEEFu); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); +} + +TEST(EeVu0Ctc2, WritesRegStatusFlagMasksStickyFieldAndDenormalizesToMicroStatusflags) +{ + // CTC2 to REG_STATUS_FLAG is microVU-aware: only the 0xFC0 "sticky" field + // is taken from the GPR, the low-6 current-flag bits in VI[STATUS] are + // preserved, and the result is denormalized + broadcast into all four + // lanes of micro_statusflags (which the microVU JIT reads for flag sync). + // Mirrors x86 microVU_Macro.inl recCTC2. + // + // BY-DESIGN JIT-vs-interp divergence: the shared interpreter CTC2 + // (VU0.cpp) falls through to a plain full-width VI store with no masking + // and no micro_statusflags update — same divergence x86 has. So we opt + // VI[STATUS] out of Run()'s auto-diff and assert the JIT post-state + // directly. micro_statusflags is pipeline state (PipelinePermissive + // already ignores it in the auto-diff). + EeRecTestHarness h; + h.EnableVu0Capture(); + h.IgnoreVu0Vi(REG_STATUS_FLAG); + h.EnableCop1(); + h.SeedVu0Vi(REG_STATUS_FLAG, 0x3Fu); // pre-existing current-flag bits 0-5 + h.SetGpr64(r_t0, 0xFFFFFFFFu); + h.LoadProgram({CTC2(r_t0, REG_STATUS_FLAG)}); + h.Run(); + + // VI[STATUS] = (0x3F & 0x3F) | (0xFFFFFFFF & 0xFC0) = 0x3F | 0xFC0 = 0xFFF. + EXPECT_EQ(h.Vu0JitSnapshot().regs.VI[REG_STATUS_FLAG].UL, 0xFFFu); + + // Denormalize 0xFFF: ((s>>3)&0x18) | ((s<<11)&0x1800) | ((s<<14)&0x3cf0000) + // = 0x18 | 0x1800 | 0x3cf0000 = 0x3cf1818, in all 4 lanes. + const u32 expected_denorm = 0x3cf1818u; + for (int lane = 0; lane < 4; ++lane) + EXPECT_EQ(h.Vu0JitSnapshot().regs.micro_statusflags[lane], expected_denorm) + << "micro_statusflags lane " << lane; +} + +TEST(EeVu0Ctc2, WritesRegMacFlagIsReadOnly) +{ + // REG_MAC_FLAG is read-only on CTC2 (per real hardware) — neither engine + // should modify the cell. Pre-load with a sentinel and verify it persists. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + vuRegs[0].VI[REG_MAC_FLAG].UL = 0xCAFEBABEu; + h.SetGpr64(r_t0, 0x11111111u); + h.LoadProgram({CTC2(r_t0, REG_MAC_FLAG)}); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(REG_MAC_FLAG), h.GetVu0ViInterp(REG_MAC_FLAG)); +} + +TEST(EeVu0Ctc2, WritesRegClipFlagDualWritesStructAndVi) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr64(r_t0, 0x00ABCDEFu); + h.LoadProgram({CTC2(r_t0, REG_CLIP_FLAG)}); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(REG_CLIP_FLAG), h.GetVu0ViInterp(REG_CLIP_FLAG)); +} + +TEST(EeVu0Ctc2, WritesRegFbrstFullCallFallback) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr64(r_t0, 0x0000000Fu); + h.LoadProgram({CTC2(r_t0, REG_FBRST)}); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(REG_FBRST), h.GetVu0ViInterp(REG_FBRST)); +} + +TEST(EeVu0Ctc2, WritesViZeroIsNoop) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr64(r_t0, 0xFFFFFFFFu); + h.LoadProgram({CTC2(r_t0, 0)}); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(0), 0u); + EXPECT_EQ(h.GetVu0ViInterp(0), 0u); +} + +// ========================================================================= +// Round trips — CTC2 then CFC2 (and vice versa) +// ========================================================================= + +TEST(EeVu0Cfc2Ctc2RoundTrip, CtcThenCfcMatchesOriginalLow16) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr64(r_t0, 0x4321); + h.LoadProgram({ + CTC2(r_t0, 5), + CFC2(r_t1, 5), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetGpr64Jit(r_t1)), 0x4321u); + EXPECT_EQ(h.GetGpr64Jit(r_t1), h.GetGpr64Interp(r_t1)); +} + +TEST(EeVu0Cfc2Ctc2RoundTrip, CfcThenCtcShufflesViBetweenIndices) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(3, 0x9876); + h.LoadProgram({ + CFC2(r_t0, 3), + CTC2(r_t0, 7), + CFC2(r_t2, 7), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetGpr64Jit(r_t2)), 0x9876u); + EXPECT_EQ(h.GetVu0ViJit(7), 0x9876u); + EXPECT_EQ(h.GetGpr64Jit(r_t2), h.GetGpr64Interp(r_t2)); + EXPECT_EQ(h.GetVu0ViJit(7), h.GetVu0ViInterp(7)); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp new file mode 100644 index 0000000000..de2e867a38 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp @@ -0,0 +1,1213 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// EE-issued COP2 macro-mode VU ops + VCALLMS microprogram kick. +// +// Macro mode: an EE instruction with primary opcode COP2 (0x12) and CO=1 +// (bit 25) executes one VU upper-pipe op against VU0.VF[]. The funct field +// (bits[5:0]) selects the op (mirrors the VU upper-pipe primary table: +// 0x28=VADD, 0x2A=VMUL, 0x2C=VSUB, 0x2F=VMINI, ...). The XYZW destination +// mask occupies bits[24:21]. Macro mode does the denormalize/normalize +// dance around every op — unlike microprogram mode where flags stay packed +// across the entire program. The macro-mode COP2 path emits NEON-direct +// codegen and is among the highest-fragility hand-written NEON in the tree. +// +// VCALLMS: COP2 + CO=1 + funct=0x38, with the start-PC-divided-by-8 in +// bits[20:6] (15-bit imm). Behavior: _vu0FinishMicro() drains the pending +// microprogram, copies macro-mode flags into microVU's instances, kicks +// the program at the supplied PC, runs to E-bit. Tests seed the +// microprogram via SeedVu0Microprogram + verify post-state. + +#include "harness/EeRecTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace mips; +using namespace mips::ee; +using namespace vu; + +namespace { + +constexpr u32 mask_xyzw = 0xF; +constexpr u32 mask_yz = 0x6; // bits[23:22]: y(23) and z(22) +constexpr u32 mask_x = 0x8; // bit[24]: x only + +inline VuOp UpperOnly(u32 upper) { return VuOp{0, upper}; } + +} // namespace + +// ========================================================================= +// COP2 macro-mode VADD / VSUB / VMUL — basic arithmetic +// ========================================================================= + +TEST(EeVu0Cop2Macro, VaddXyzwSumsLanes) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.LoadProgram({VADD_C2(mask_xyzw, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 11.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 33.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 44.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VsubXyzwDifferencesLanes) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 100.0f, 200.0f, 300.0f, 400.0f); + h.SeedVu0Vf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.LoadProgram({VSUB_C2(mask_xyzw, 3, 1, 2)}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 90.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 360.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VmulXyzwProductsLanes) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 2.0f, 3.0f, 4.0f, 5.0f); + h.SeedVu0Vf(2, 10.0f, 10.0f, 10.0f, 10.0f); + h.LoadProgram({VMUL_C2(mask_xyzw, 3, 1, 2)}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 20.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 50.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VaddMaskedYZOnlyTouchesYZ) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SeedVu0Vf(3, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({VADD_C2(mask_yz, 3, 1, 2)}); + h.Run(); + // x and w preserved (99), y and z written. + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 99.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 33.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 99.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VmaxKeepsLargerLane) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 5.0f, 1.0f, 8.0f, 3.0f); + h.SeedVu0Vf(2, 4.0f, 7.0f, 2.0f, 6.0f); + h.LoadProgram({VMAX_C2(mask_xyzw, 3, 1, 2)}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 8.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 6.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VminiKeepsSmallerLane) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 5.0f, 1.0f, 8.0f, 3.0f); + h.SeedVu0Vf(2, 4.0f, 7.0f, 2.0f, 6.0f); + h.LoadProgram({VMINI_C2(mask_xyzw, 3, 1, 2)}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 3.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, MultipleMacroOpsBackToBack) +{ + // Three back-to-back macro ops — exercises the JIT's denormalize/normalize + // transitions across instructions (the highest-fragility area of the macro emit path). + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 10.0f, 10.0f, 10.0f, 10.0f); + h.LoadProgram({ + VADD_C2(mask_xyzw, 3, 1, 2), // vf3 = vf1 + vf2 + VMUL_C2(mask_xyzw, 4, 3, 2), // vf4 = vf3 * vf2 + VSUB_C2(mask_xyzw, 5, 4, 1), // vf5 = vf4 - vf1 + }); + h.Run(); + for (char l : {'x','y','z','w'}) + { + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); + EXPECT_EQ(h.GetVu0VfBitsJit(4, l), h.GetVu0VfBitsInterp(4, l)); + EXPECT_EQ(h.GetVu0VfBitsJit(5, l), h.GetVu0VfBitsInterp(5, l)); + } +} + +// ========================================================================= +// COP2 macro-mode VIADD / VISUB / VIAND / VIOR — integer-bank ops +// ========================================================================= + +TEST(EeVu0Cop2Macro, ViaddTouchesViBank) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 100); + h.SeedVu0Vi(2, 250); + h.LoadProgram({VIADD_C2(/*id*/3, /*is*/1, /*it*/2)}); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(3), 350u); + EXPECT_EQ(h.GetVu0ViJit(3), h.GetVu0ViInterp(3)); +} + +TEST(EeVu0Cop2Macro, ViandIorChainProducesExpectedMask) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 0xF0F0u); + h.SeedVu0Vi(2, 0x0FF0u); + h.SeedVu0Vi(3, 0x000Fu); + h.LoadProgram({ + VIAND_C2(4, 1, 2), // vi4 = 0xF0F0 & 0x0FF0 = 0x00F0 + VIOR_C2 (5, 4, 3), // vi5 = 0x00F0 | 0x000F = 0x00FF + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(4), 0x00F0u); + EXPECT_EQ(h.GetVu0ViJit(5), 0x00FFu); + EXPECT_EQ(h.GetVu0ViJit(4), h.GetVu0ViInterp(4)); + EXPECT_EQ(h.GetVu0ViJit(5), h.GetVu0ViInterp(5)); +} + +// ========================================================================= +// Macro-flag visibility — CFC2 of MAC/STATUS/CLIP immediately after a +// macro VADD. Exercises the per-op flag denormalize at every +// macro-instruction boundary. +// ========================================================================= + +TEST(EeVu0Cop2Macro, CfcMacFlagAfterVaddSeesUpdatedFlags) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VADD vf3 = vf0 + vf0 → all-zero result lanes → MAC = 0x000E. + h.LoadProgram({ + VADD_C2(mask_xyzw, /*fd*/3, /*fs*/0, /*ft*/0), + CFC2(/*rt*/8, REG_MAC_FLAG), + }); + h.Run(); + // Both engines should see the same MAC value in the EE GPR. + EXPECT_EQ(h.GetGpr64Jit(8), h.GetGpr64Interp(8)); +} + +TEST(EeVu0Cop2Macro, CfcStatusFlagAfterVaddAgrees) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.LoadProgram({ + VADD_C2(mask_xyzw, 3, 0, 0), + CFC2(8, REG_STATUS_FLAG), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(8), h.GetGpr64Interp(8)); +} + +// ========================================================================= +// VCALLMS — kick a microprogram from EE +// ========================================================================= + +TEST(EeVu0Vcallms, BasicMicroprogramRunsToEbit) +{ + // Microprogram at byte offset 0: + // pair 0: VADD vf2, vf1, vf0 (FMAC; sums lanes) + // pair 1: E-bit NOP (terminate after 1 useful pair + delay slot) + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.5f, 2.5f, 3.5f, 4.5f); + h.SeedVu0Microprogram(0, { + VuOp{0, vu::VADD_U(vu::mask::xyzw, /*fd*/2, /*fs*/1, /*ft*/0)}, + EBitNopPair(), + }); + h.LoadProgram({VCALLMS(/*startpc_div8*/0)}); + h.Run(); + // vf0 = (0,0,0,1.0f); vf1 + vf0 = (1.5, 2.5, 3.5, 5.5). + EXPECT_FLOAT_EQ(h.GetVu0VfJit(2, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(2, 'y'), 2.5f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(2, 'z'), 3.5f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(2, 'w'), 5.5f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(2, l), h.GetVu0VfBitsInterp(2, l)); +} + +TEST(EeVu0Vcallms, MicroprogramAtNonZeroStartPC) +{ + // Microprogram at byte offset 16 (pair 2). Two NOPs at offsets 0..15 + // would dispatch as garbage, but VCALLMS jumps directly to the start. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(4, 7.0f, 7.0f, 7.0f, 7.0f); + h.SeedVu0Microprogram(16, { + VuOp{0, vu::VADD_U(vu::mask::xyzw, /*fd*/5, /*fs*/4, /*ft*/4)}, // vf5 = 14 + EBitNopPair(), + }); + h.LoadProgram({VCALLMS(/*startpc_div8*/16/8)}); // pair index 2 + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(5, 'x'), 14.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(5, 'w'), 14.0f); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(5, l), h.GetVu0VfBitsInterp(5, l)); +} + +TEST(EeVu0Vcallms, MicroprogramSetsViVisibleViaCfc2) +{ + // Microprogram writes vi3 = 0x1234 then E-bits. EE then CFC2's it. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Microprogram(0, { + VuOp{vu::VIADDIU_L(/*it*/3, /*is*/0, 0x1234), vu::VNOP_U()}, + EBitNopPair(), + }); + h.LoadProgram({ + VCALLMS(0), + CFC2(/*rt*/8, /*fs*/3), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetGpr64Jit(8)), 0x1234u); + EXPECT_EQ(h.GetGpr64Jit(8), h.GetGpr64Interp(8)); +} + +TEST(EeVu0Vcallmsr, KickFromCmsar1Register) +{ + // VCALLMSR reads the start PC from VI[REG_CMSAR1] (* 8). Seed it. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(6, 100.0f, 200.0f, 300.0f, 400.0f); + // Place microprogram at byte offset 0; CMSAR1 = 0 (pair index 0). + vuRegs[0].VI[REG_CMSAR1].UL = 0; + h.SeedVu0Microprogram(0, { + VuOp{0, vu::VADD_U(vu::mask::xyzw, /*fd*/7, /*fs*/6, /*ft*/0)}, + EBitNopPair(), + }); + h.LoadProgram({VCALLMSR()}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(7, 'x'), 100.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(7, 'w'), 401.0f); // + vf0.w (1.0) + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(7, l), h.GetVu0VfBitsInterp(7, l)); +} + +// ========================================================================= +// COP2 macro-mode VSQI — VF→Mem store with VI post-increment +// ========================================================================= +// +// VSQI fs, vi(it)++: Mem[VI[it] * 16] = VF[fs]; VI[it]++. Exercises the +// COP2 wrapper that drives mVU_SQI from macro-mode dispatch. Witnesses: +// 1. VI[it] post-increment — direct via GetVu0Vi*. +// 2. VU0 mem write — indirect via a follow-on VLQI that reads the same +// address into a VF reg (VLQI is still interp fallback so it's a +// shared decoder between passes; the store is the only divergence +// surface, so a roundtrip mismatch pins the JIT VSQI side). + +TEST(EeVu0Cop2Macro, VsqiPostIncrementsViAndStoresFullQuad) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VI[1] is the address-index for VSQI; VI[3] re-reads the same slot via + // VLQI. Seed them to the same starting index (5 → byte offset 0x50). + h.SeedVu0Vi(1, 5); + h.SeedVu0Vi(3, 5); + h.SeedVu0VfBits(2, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.LoadProgram({ + VSQI_C2(mask_xyzw, /*fs*/2, /*it*/1), + VLQI_C2(mask_xyzw, /*ft*/5, /*is*/3), + }); + h.Run(); + // VI[1] post-incremented to 6 on both engines. + EXPECT_EQ(h.GetVu0ViJit(1), 6u); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); + // VI[3] also post-incremented by VLQI. + EXPECT_EQ(h.GetVu0ViJit(3), 6u); + EXPECT_EQ(h.GetVu0ViJit(3), h.GetVu0ViInterp(3)); + // VF[5] round-trips the stored bits — JIT VSQI hit the right Mem slot + // with the right lane order. + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), 0x11111111u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'y'), 0x22222222u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'z'), 0x33333333u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), 0x44444444u); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(5, l), h.GetVu0VfBitsInterp(5, l)); +} + +TEST(EeVu0Cop2Macro, VsqiSequentialStoresAdvanceVi) +{ + // Three back-to-back VSQI to verify VI post-inc accumulates correctly + // (each VSQI uses VI[1] as the address and bumps it by 1) and each + // stored quad lands at a distinct Mem slot. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 10); + h.SeedVu0Vi(3, 10); // VLQI readback starts at the same slot + h.SeedVu0VfBits(2, 0xAAAAAAA1u, 0xAAAAAAA2u, 0xAAAAAAA3u, 0xAAAAAAA4u); + h.SeedVu0VfBits(4, 0xBBBBBBB1u, 0xBBBBBBB2u, 0xBBBBBBB3u, 0xBBBBBBB4u); + h.SeedVu0VfBits(6, 0xCCCCCCC1u, 0xCCCCCCC2u, 0xCCCCCCC3u, 0xCCCCCCC4u); + h.LoadProgram({ + VSQI_C2(mask_xyzw, /*fs*/2, /*it*/1), + VSQI_C2(mask_xyzw, /*fs*/4, /*it*/1), + VSQI_C2(mask_xyzw, /*fs*/6, /*it*/1), + // Read the three stored slots back into VF[7], VF[8], VF[9] + VLQI_C2(mask_xyzw, /*ft*/7, /*is*/3), + VLQI_C2(mask_xyzw, /*ft*/8, /*is*/3), + VLQI_C2(mask_xyzw, /*ft*/9, /*is*/3), + }); + h.Run(); + // VI[1] = 10 + 3 = 13 + EXPECT_EQ(h.GetVu0ViJit(1), 13u); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); + // Each slot round-trips its source quad in order. + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'x'), 0xAAAAAAA1u); + EXPECT_EQ(h.GetVu0VfBitsJit(8, 'x'), 0xBBBBBBB1u); + EXPECT_EQ(h.GetVu0VfBitsJit(9, 'x'), 0xCCCCCCC1u); + for (u32 r : {7u, 8u, 9u}) + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(r, l), h.GetVu0VfBitsInterp(r, l)); +} + +TEST(EeVu0Cop2Macro, VsqiMaskedPartialWritePreservesOtherLanes) +{ + // Partial-mask VSQI: only specific lanes are written. The mVU_SQI emit + // path branches on full vs partial (Ldr+Merge+Str). Validates the + // partial path. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // Pre-fill Mem[VI[1]*16] with a sentinel by storing VF[3] full-quad first. + h.SeedVu0Vi(1, 20); + h.SeedVu0Vi(3, 20); // readback + h.SeedVu0VfBits(3, 0xDEAD0001u, 0xDEAD0002u, 0xDEAD0003u, 0xDEAD0004u); + // Source for masked store: only Y+W should land. + h.SeedVu0VfBits(2, 0xFFFFFFFFu, 0xCAFE2222u, 0xFFFFFFFFu, 0xCAFE4444u); + h.LoadProgram({ + VSQI_C2(mask_xyzw, /*fs*/3, /*it*/1), // seed Mem with sentinel; VI[1]=21 + VSQI_C2(/*y+w=*/0x5, /*fs*/2, /*it*/1), // partial — only Y and W lanes + // readback: VF[5]=full seed, VF[6]=masked merge + VLQI_C2(mask_xyzw, /*ft*/5, /*is*/3), + VLQI_C2(mask_xyzw, /*ft*/6, /*is*/3), + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(1), 22u); + // VF[5] is the seed quad — full-mask store path. + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), 0xDEAD0001u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'y'), 0xDEAD0002u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'z'), 0xDEAD0003u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), 0xDEAD0004u); + // VF[6] is the merged quad: X,Z = previous-slot sentinel (zero — fresh + // Mem cell from VI[1] = 21), Y,W = from VF[2]. + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'y'), 0xCAFE2222u); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'w'), 0xCAFE4444u); + for (u32 r : {5u, 6u}) + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(r, l), h.GetVu0VfBitsInterp(r, l)); +} + +// ========================================================================= +// COP2 macro-mode VLQI / VLQD / VSQD — load with VI post-inc, load+store +// with VI pre-dec +// ========================================================================= +// +// All three reuse the same mVU emit pipeline as VSQI. The VLQI path also +// happens to be exercised as a readback witness in the VSQI tests above — +// the dedicated tests here pin VLQI's correctness on its own (masked +// partial load merge, multi-step pre-decrement). + +TEST(EeVu0Cop2Macro, VlqiMaskedReadPreservesUntouchedLanesOfTargetVf) +{ + // VLQI with partial mask: only the masked lanes of VF[ft] are overwritten. + // Exercises mVU_LQI's mVUloadMem partial-mask path. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 30); + h.SeedVu0Vi(3, 30); + // Seed source quad in Mem via VSQI of VF[2]. + h.SeedVu0VfBits(2, 0xAAAA0001u, 0xAAAA0002u, 0xAAAA0003u, 0xAAAA0004u); + // Pre-fill destination VF[7] with sentinel — masked lanes should survive. + h.SeedVu0VfBits(7, 0xDEAD1111u, 0xDEAD2222u, 0xDEAD3333u, 0xDEAD4444u); + h.LoadProgram({ + VSQI_C2(mask_xyzw, /*fs*/2, /*it*/1), // Mem[30*16] = VF[2]; VI[1]=31 + VLQI_C2(/*x+z=*/0xA, /*ft*/7, /*is*/3), // VF[7].xz <- Mem; .yw preserved; VI[3]=31 + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(3), 31u); + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'x'), 0xAAAA0001u); // loaded + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'y'), 0xDEAD2222u); // preserved + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'z'), 0xAAAA0003u); // loaded + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'w'), 0xDEAD4444u); // preserved + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(7, l), h.GetVu0VfBitsInterp(7, l)); +} + +TEST(EeVu0Cop2Macro, VsqdPredecrementsViAndStoresFullQuad) +{ + // VSQD: --VI[it]; Mem[VI[it]*16] = VF[fs]. Pre-decrement is the + // arithmetic difference vs VSQI; the rest of the emit pipeline is shared. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VI[1] starts at 40 → VSQD decrements to 39, stores at slot 39. + // VLQD readback decrements VI[3] from 40 to 39 then loads slot 39 → match. + h.SeedVu0Vi(1, 40); + h.SeedVu0Vi(3, 40); + h.SeedVu0VfBits(2, 0xBBBB0001u, 0xBBBB0002u, 0xBBBB0003u, 0xBBBB0004u); + h.LoadProgram({ + VSQD_C2(mask_xyzw, /*fs*/2, /*it*/1), // VI[1] = 39; Mem[39*16] = VF[2] + VLQD_C2(mask_xyzw, /*ft*/5, /*is*/3), // VI[3] = 39; VF[5] = Mem[39*16] + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(1), 39u); + EXPECT_EQ(h.GetVu0ViJit(3), 39u); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); + EXPECT_EQ(h.GetVu0ViJit(3), h.GetVu0ViInterp(3)); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), 0xBBBB0001u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), 0xBBBB0004u); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(5, l), h.GetVu0VfBitsInterp(5, l)); +} + +// ========================================================================= +// COP2 macro-mode VMTIR / VMFIR / VILWR / VISWR — integer-bank transfers +// ========================================================================= +// +// VMTIR fsf, it, fs: VI[it] = VF[fs].lane(fsf) (extract a single 32-bit +// lane, store low 16 bits to VI bank). +// VMFIR mask, ft, is: VF[ft].mask = sign_extend(VI[is]) to 32-bit and +// broadcast across selected lanes. +// VILWR mask, it, is: VI[it] = Mem[VI[is] * 16].lane(mask) (lane index +// comes from mask — pick the single set bit). +// VISWR mask, it, is: Mem[VI[it] * 16].lane(mask) = VI[is]. + +TEST(EeVu0Cop2Macro, VmtirCopiesLaneFromVfToVi) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(2, 0x11111111u, 0x22222222u, 0x33333333u, 0x44447777u); + h.LoadProgram({ + VMTIR_C2(/*fsf=w*/3, /*it*/1, /*fs*/2), // VI[1] = VF[2].w low16 = 0x7777 + VMTIR_C2(/*fsf=x*/0, /*it*/2, /*fs*/2), // VI[2] = VF[2].x low16 = 0x1111 + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(1), 0x7777u); + EXPECT_EQ(h.GetVu0ViJit(2), 0x1111u); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); + EXPECT_EQ(h.GetVu0ViJit(2), h.GetVu0ViInterp(2)); +} + +TEST(EeVu0Cop2Macro, VmfirBroadcastsSignExtendedViToMaskedVfLanes) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // Negative VI value (16-bit sign-extends to 32-bit FFFFC000). + h.SeedVu0Vi(1, 0xC000u); + // Pre-fill VF[3] sentinel — masked lanes should survive. + h.SeedVu0VfBits(3, 0xDEAD1111u, 0xDEAD2222u, 0xDEAD3333u, 0xDEAD4444u); + h.LoadProgram({ + VMFIR_C2(/*y+z=*/0x6, /*ft*/3, /*is*/1), // VF[3].yz <- 0xFFFFC000; .xw preserved. + }); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'x'), 0xDEAD1111u); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'y'), 0xFFFFC000u); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'z'), 0xFFFFC000u); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'w'), 0xDEAD4444u); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VilwrViswrRoundtripViThroughMem) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 0x1234u); // value (interp: _It_ = value source) + h.SeedVu0Vi(2, 60); // VISWR addr base (interp: _Is_ = addr) + h.SeedVu0Vi(3, 60); // VILWR addr (same slot) + h.LoadProgram({ + // Mem[VI[2]*16].xyzw = VI[1] = 0x1234. _It_ = value, _Is_ = addr. + VISWR_C2(mask_xyzw, /*it*/1, /*is*/2), + // VI[5] = Mem[VI[3]*16].x = 0x1234. ILWR's mask picks one lane. + VILWR_C2(/*x=*/0x8, /*it*/5, /*is*/3), + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(5), 0x1234u); + EXPECT_EQ(h.GetVu0ViJit(5), h.GetVu0ViInterp(5)); +} + +TEST(EeVu0Cop2Macro, VsqdVlqdInterleavedAdvanceCorrectly) +{ + // Two pre-decrement stores followed by two pre-decrement loads. Stresses + // VI tracking across multiple SPEC2 dispatches in one block. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 50); // store ptr (decrements to 49, then 48) + h.SeedVu0Vi(3, 50); // load ptr (decrements to 49, then 48) + h.SeedVu0VfBits(2, 0xC0DE0001u, 0xC0DE0002u, 0xC0DE0003u, 0xC0DE0004u); + h.SeedVu0VfBits(4, 0xCAFE0001u, 0xCAFE0002u, 0xCAFE0003u, 0xCAFE0004u); + h.LoadProgram({ + VSQD_C2(mask_xyzw, /*fs*/2, /*it*/1), // VI[1]=49; Mem[49*16]=VF[2] + VSQD_C2(mask_xyzw, /*fs*/4, /*it*/1), // VI[1]=48; Mem[48*16]=VF[4] + VLQD_C2(mask_xyzw, /*ft*/7, /*is*/3), // VI[3]=49; VF[7]=Mem[49*16] (VF[2]) + VLQD_C2(mask_xyzw, /*ft*/8, /*is*/3), // VI[3]=48; VF[8]=Mem[48*16] (VF[4]) + }); + h.Run(); + EXPECT_EQ(h.GetVu0ViJit(1), 48u); + EXPECT_EQ(h.GetVu0ViJit(3), 48u); + // VF[7] should hold the FIRST stored quad (slot 49) = VF[2]. + EXPECT_EQ(h.GetVu0VfBitsJit(7, 'x'), 0xC0DE0001u); + // VF[8] should hold the SECOND stored quad (slot 48) = VF[4]. + EXPECT_EQ(h.GetVu0VfBitsJit(8, 'x'), 0xCAFE0001u); + for (u32 r : {7u, 8u}) + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(r, l), h.GetVu0VfBitsInterp(r, l)); +} + +// ---- R-register / LFSR group (RINIT, RGET, RNEXT, RXOR) ---- +// VRINIT fsf, fs: VI[REG_R] = 0x3F800000 | (VF[fs].lane(fsf) & 0x7FFFFF). +// VRGET mask, ft: VF[ft].mask = VI[REG_R] (full 32-bit, broadcast). +// VRNEXT mask, ft: advance LFSR R, then same as VRGET. +// VRXOR fsf, fs: VI[REG_R] = 0x3F800000 | ((R ^ VF[fs].lane(fsf)) & 0x7FFFFF). + +TEST(EeVu0Cop2Macro, VrinitVrgetRoundtripsRegisterR) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VF[2].y mantissa = 0x12345 (low 23 bits). RINIT keeps only low 23. + h.SeedVu0VfBits(2, 0xDEAD0000u, 0x80012345u, 0xDEAD0000u, 0xDEAD0000u); + h.LoadProgram({ + VRINIT_C2(/*fsf=y*/1, /*fs*/2), + VRGET_C2 (mask_xyzw, /*ft*/3), + }); + h.Run(); + const u32 expected_r = 0x3F800000u | (0x80012345u & 0x007FFFFFu); + EXPECT_EQ(h.GetVu0ViJit(REG_R), expected_r); + EXPECT_EQ(h.GetVu0ViJit(REG_R), h.GetVu0ViInterp(REG_R)); + for (char l : {'x','y','z','w'}) + { + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), expected_r); + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); + } +} + +TEST(EeVu0Cop2Macro, VrnextAdvancesLfsrAndBroadcastsAgreesWithInterp) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(2, 0u, 0x000ABCDEu, 0u, 0u); + h.LoadProgram({ + VRINIT_C2(/*fsf=y*/1, /*fs*/2), + VRNEXT_C2(mask_xyzw, /*ft*/4), + VRNEXT_C2(mask_xyzw, /*ft*/5), + }); + h.Run(); + // LFSR is bit-exact across hosts; demand JIT == interp. + EXPECT_EQ(h.GetVu0ViJit(REG_R), h.GetVu0ViInterp(REG_R)); + for (u32 r : {4u, 5u}) + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(r, l), h.GetVu0VfBitsInterp(r, l)); + // VF[5] should hold the LATER (advanced twice) R; VF[4] the once-advanced. + EXPECT_NE(h.GetVu0VfBitsJit(4, 'x'), h.GetVu0VfBitsJit(5, 'x')); +} + +TEST(EeVu0Cop2Macro, VrxorXorsMantissaIntoRegisterR) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0x800ABCDEu); // .w mantissa = 0xABCDE + h.SeedVu0VfBits(3, 0x00055555u, 0u, 0u, 0u); // .x mantissa = 0x55555 + h.LoadProgram({ + VRINIT_C2(/*fsf=w*/3, /*fs*/2), // R = 0x3F800000 | 0x0ABCDE + VRXOR_C2 (/*fsf=x*/0, /*fs*/3), // R = 0x3F800000 | ((0x0ABCDE ^ 0x55555) & 0x7FFFFF) + }); + h.Run(); + const u32 init_mantissa = 0x800ABCDEu & 0x007FFFFFu; // 0x0ABCDE + const u32 xor_with = 0x00055555u & 0x007FFFFFu; // 0x055555 + const u32 expected_r = 0x3F800000u | (init_mantissa ^ xor_with); + EXPECT_EQ(h.GetVu0ViJit(REG_R), expected_r); + EXPECT_EQ(h.GetVu0ViJit(REG_R), h.GetVu0ViInterp(REG_R)); +} + +// ---- VU0->VU1 access path (mVUaddrFix bit 0x400 branch) ---- +// +// VU0 macro-mode load/store with a VI address that has bit 0x400 set must +// route through VU1.VF[] (see VUops.cpp _vuLQ + GET_VU_MEM: byte addr bit +// 0x4000, quadword addr bit 0x400). The arm64 mVUaddrFix VU0->VU1 path +// emits a byte/u128-unit conversion that is easy to get wrong — a silent +// SEGV is the failure mode when VSQI/VLQI route through this emitter. + +TEST(EeVu0Cop2Macro, VlqiAcrossVu0Vu1AccessBoundaryLoadsVu1Vf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // VI[1] = 0x40A: bit 0x400 set => VU0 access into VU1.VF[10]. + h.SeedVu0Vi(1, 0x40Au); + // Seed VU1.VF[10] directly — both passes read the same source state + // (VLQI is read-only on VU1; no need for capture_vu1_ snapshotting). + h.SeedVu1VfBits(10, 0xCAFEBABEu, 0xDEADBEEFu, 0x12345678u, 0x9ABCDEF0u); + // Pre-fill VF[5] sentinel so partial-mask bugs would show up too. + h.SeedVu0VfBits(5, 0x55555555u, 0x55555555u, 0x55555555u, 0x55555555u); + h.LoadProgram({ + VLQI_C2(mask_xyzw, /*ft*/5, /*is*/1), + }); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), 0xCAFEBABEu); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'y'), 0xDEADBEEFu); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'z'), 0x12345678u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), 0x9ABCDEF0u); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(5, l), h.GetVu0VfBitsInterp(5, l)); + // VI[1] should still post-increment to 0x40B even on the VU1 access path. + EXPECT_EQ(h.GetVu0ViJit(1), 0x40Bu); + EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); +} + +// ========================================================================= +// COP2 macro-mode VOPMSUB / VOPMULA — outer-product cross math +// ========================================================================= +// +// PS2 OPMSUB: VF[fd].xyz = ACC.xyz - VF[fs].yzx * VF[ft].zxy (W untouched) +// PS2 OPMULA: ACC.xyz = VF[fs].yzx * VF[ft].zxy (ACC.w untouched) +// +// Hardware always writes XYZ lanes only — the W lane of the destination is +// preserved regardless of what the instruction's dest mask encodes. The +// PS2 SDK disassembler hard-codes ".xyz" for these ops; the interpreter +// (VUops.cpp _vuOPMSUB / _vuOPMULA) only writes XYZ. + +TEST(EeVu0Cop2Macro, VopmsubXyzCrossProductMatchesInterp) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // fd seeded with a sentinel W to verify it is preserved. + h.SeedVu0VfBits(3, 0x11111111u, 0x22222222u, 0x33333333u, 0xCAFEBABEu); + // fs, ft, ACC chosen so the cross-product math is non-degenerate. + h.SeedVu0Vf(1, /*x*/1.0f, /*y*/2.0f, /*z*/3.0f, /*w*/4.0f); + h.SeedVu0Vf(2, /*x*/5.0f, /*y*/6.0f, /*z*/7.0f, /*w*/8.0f); + h.SeedVu0Acc(/*x*/100.0f, /*y*/200.0f, /*z*/300.0f, /*w*/400.0f); + + // Encode with dest=XYZ (the standard, what SDK assemblers emit). + h.LoadProgram({VOPMSUB_C2(/*mask*/0xE, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.Run(); + + // Hardware semantics: + // fd.x = ACC.x - fs.y * ft.z = 100 - 2*7 = 86 + // fd.y = ACC.y - fs.z * ft.x = 200 - 3*5 = 185 + // fd.z = ACC.z - fs.x * ft.y = 300 - 1*6 = 294 + // fd.w = preserved sentinel + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 86.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 185.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 294.0f); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'w'), 0xCAFEBABEu); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VopmsubDestXyzwStillPreservesW) +{ + // Some games / hand-written code emit VOPMSUB with dest=XYZW even though + // the assembler convention is XYZ. PS2 hardware ignores the W bit of the + // dest field for OPMSUB — only XYZ are ever written. The interpreter + // matches this (VUops.cpp:866-868 only writes i.x/i.y/i.z, never i.w). + // JIT must too. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(3, 0x11111111u, 0x22222222u, 0x33333333u, 0xCAFEBABEu); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.SeedVu0Acc(100.0f, 200.0f, 300.0f, 400.0f); + + h.LoadProgram({VOPMSUB_C2(/*mask*/0xF, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.Run(); + + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), 86.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 185.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 294.0f); + // W must be preserved even though mask said XYZW. + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'w'), 0xCAFEBABEu); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)); +} + +TEST(EeVu0Cop2Macro, VopmulaXyzWritesAccLeavesAccWUntouched) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 5.0f, 6.0f, 7.0f, 8.0f); + // Sentinel W in ACC — expected to be preserved. + h.SeedVu0AccBits(0x10000000u, 0x20000000u, 0x30000000u, 0xCAFEBABEu); + + h.LoadProgram({VOPMULA_C2(/*mask*/0xE, /*fs*/1, /*ft*/2)}); + h.Run(); + + // ACC.x = fs.y * ft.z = 2*7 = 14 + // ACC.y = fs.z * ft.x = 3*5 = 15 + // ACC.z = fs.x * ft.y = 1*6 = 6 + // ACC.w = preserved. + const u32 acc_x_bits = h.GetVu0AccBitsJit('x'); + float acc_x; + std::memcpy(&acc_x, &acc_x_bits, sizeof(acc_x)); + EXPECT_FLOAT_EQ(acc_x, 14.0f); + EXPECT_EQ(h.GetVu0AccBitsJit('w'), 0xCAFEBABEu); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0AccBitsJit(l), h.GetVu0AccBitsInterp(l)); +} + +TEST(EeVu0Cop2Macro, VopmulaDestXyzwStillPreservesAccW) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.SeedVu0AccBits(0x10000000u, 0x20000000u, 0x30000000u, 0xCAFEBABEu); + + h.LoadProgram({VOPMULA_C2(/*mask*/0xF, /*fs*/1, /*ft*/2)}); + h.Run(); + + EXPECT_EQ(h.GetVu0AccBitsJit('w'), 0xCAFEBABEu); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0AccBitsJit(l), h.GetVu0AccBitsInterp(l)); +} + +// ========================================================================= +// VFTOIx NaN saturation +// ========================================================================= +// +// COP2 macro-mode VFTOI0/4/12/15 must saturate NaN inputs to a sign-based +// INT_MAX / INT_MIN, matching mVU_FTOIx and the interpreter +// (floatToInt -> (sign ? 0x80000000 : 0x7fffffff)). +// NEON Fcvtzs returns 0 for NaN; the macro path must emit a sign-based +// fixup after Fcvtzs to match interp on NaN lanes. Finite overflow and +// ±Inf already saturate correctly inside Fcvtzs. + +TEST(EeVu0Cop2Macro, Vftoi0SaturatesNanSignBased) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // x: +NaN, y: -NaN, z: +2.5 (-> 2 trunc), w: -2.5 (-> -2 trunc) + h.SeedVu0VfBits(1, 0x7FC00000u, 0xFFC00000u, 0x40200000u, 0xC0200000u); + h.LoadProgram({VFTOI0_C2(mask_xyzw, /*ft=dst*/2, /*fs=src*/1)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'x'), 0x7FFFFFFFu); // +NaN -> INT_MAX + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'y'), 0x80000000u); // -NaN -> INT_MIN + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'z'), 2u); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'w'), static_cast(-2)); // 0xFFFFFFFE + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(2, l), h.GetVu0VfBitsInterp(2, l)); +} + +TEST(EeVu0Cop2Macro, Vftoi4SaturatesNanAndScalesFinite) +{ + // VFTOI4 scales by 2^4 before truncating; the NaN fixup is identical and + // exercises the helper's fbits!=0 branch. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // x: +NaN, y: -NaN, z: +2.5 (*16 = 40), w: -2.5 (*16 = -40) + h.SeedVu0VfBits(1, 0x7FC00000u, 0xFFC00000u, 0x40200000u, 0xC0200000u); + h.LoadProgram({VFTOI4_C2(mask_xyzw, /*ft=dst*/2, /*fs=src*/1)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'x'), 0x7FFFFFFFu); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'y'), 0x80000000u); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'z'), 40u); + EXPECT_EQ(h.GetVu0VfBitsJit(2, 'w'), static_cast(-40)); + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(2, l), h.GetVu0VfBitsInterp(2, l)); +} + +// ========================================================================= +// VCLIP micro_clipflags broadcast +// ========================================================================= +// +// COP2 macro-mode VCLIP writes the new clip flag to VU0.clipflag and +// VI[REG_CLIP_FLAG], but must also broadcast it into all four lanes of +// micro_clipflags: a subsequent VU0 microprogram loads its clip-flag +// instances directly from micro_clipflags at prologue. +// Without the broadcast those instances are stale (pre-VCLIP). micro_clipflags +// is pipeline state (PipelinePermissive ignores it in the auto-diff), so the +// JIT post-state is asserted directly. + +TEST(EeVu0Cop2Macro, VclipBroadcastsClipflagToMicroClipflags) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // fs.xyz large positive magnitudes vs ft.w = 1.0 bound → several clip bits. + h.SeedVu0VfBits(1, 0x40000000u /*2.0 x*/, 0x40800000u /*4.0 y*/, + 0x41000000u /*8.0 z*/, 0u); + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0x3F800000u /*1.0 w bound*/); + // Sentinel so the strip-fix check is deterministic: without the broadcast + // micro_clipflags keeps this value rather than the computed clip flag. + for (int i = 0; i < 4; ++i) + vuRegs[0].micro_clipflags[i] = 0xDEADBEEFu; + // Reset the internal running clip (VCLIP shifts it <<6 then ORs new bits); + // otherwise a prior VCLIP test's residue contaminates this one. Mirrors the + // explicit resets in VclipSignedIntegerCompareWithNaNLane / ...DenormalBoundAndShift. + vuRegs[0].clipflag = 0u; + + h.LoadProgram({VCLIP_C2(/*ft*/2, /*fs*/1)}); + h.Run(); + + const u32 clip = h.Vu0JitSnapshot().regs.VI[REG_CLIP_FLAG].UL; + ASSERT_NE(clip, 0u) << "test program must produce a nonzero clip flag"; + ASSERT_NE(clip, 0xDEADBEEFu); + for (int lane = 0; lane < 4; ++lane) + EXPECT_EQ(h.Vu0JitSnapshot().regs.micro_clipflags[lane], clip) + << "micro_clipflags lane " << lane; +} + +// ========================================================================= +// VCLIP vectorized signed-integer clip test +// ========================================================================= +// +// VCLIP packs six clip bits (+x@0,-x@1,+y@2,-y@3,+z@4,-z@5) from the signed- +// integer comparison (s32)(fs.lane ^ {0,0x80000000}) > value, where value = +// |ft.w| (or 0x007FFFFF when ft.w is denormal). The arm64 codegen vectorizes +// this with two NEON Cmgt (SCMGT) compares; this MUST stay an INTEGER compare, +// not an FP compare — the interp oracle compares bit patterns as s32, so a NaN +// lane participates (NaN's exponent=0xFF makes a positive-NaN a large positive +// s32). An FP-compare rewrite (Fcmgt) would return false for NaN and diverge. +// +// Discriminator mixes a positive clip, a negative clip, and a +NaN lane: +// ft.w = 1.0 -> value = 0x3F800000 +// fs.x = +2.0 (0x40000000) -> s32 > value -> +x (bit0, 0x01) +// fs.y = -2.0 (0xC0000000) -> (fs^sign) > value -> -y (bit3, 0x08) +// fs.z = +NaN (0x7FC00000) -> s32 > value (integer!) -> +z (bit4, 0x10) +// expected clip = 0x19. + +TEST(EeVu0Cop2Macro, VclipSignedIntegerCompareWithNaNLane) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x40000000u /*+2.0 x*/, 0xC0000000u /*-2.0 y*/, + 0x7FC00000u /*+NaN z*/, 0u); + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0x3F800000u /*1.0 w bound*/); + vuRegs[0].clipflag = 0u; // internal running clip (<<6'd by VCLIP); not VI[REG_CLIP_FLAG] + h.LoadProgram({VCLIP_C2(/*ft*/2, /*fs*/1)}); + h.Run(); + EXPECT_EQ(h.Vu0JitSnapshot().regs.VI[REG_CLIP_FLAG].UL, 0x19u); + EXPECT_EQ(h.Vu0JitSnapshot().regs.VI[REG_CLIP_FLAG].UL, + h.Vu0InterpSnapshot().regs.VI[REG_CLIP_FLAG].UL); +} + +// Denormal ft.w forces value = 0x007FFFFF, and the previous clip flag shifts +// left by 6 before the new bits merge. fs.x = 0x00800000 (smallest normal, +// 0x00800000 > 0x007FFFFF as s32) sets +x; fs.y/z below the bound set nothing. +// Seeds a prior clip so the <<6 path is exercised: 0x01 -> 0x40, then |+x(0x01). + +TEST(EeVu0Cop2Macro, VclipDenormalBoundAndShift) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x00800000u /*x just over denormal bound*/, + 0x00400000u /*y denormal, below*/, 0u, 0u); + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0x00000001u /*denormal w*/); + vuRegs[0].clipflag = 0x01u; // prior internal flag, shifted <<6 -> 0x40 + h.LoadProgram({VCLIP_C2(/*ft*/2, /*fs*/1)}); + h.Run(); + EXPECT_EQ(h.Vu0JitSnapshot().regs.VI[REG_CLIP_FLAG].UL, 0x41u); // 0x40 | +x + EXPECT_EQ(h.Vu0JitSnapshot().regs.VI[REG_CLIP_FLAG].UL, + h.Vu0InterpSnapshot().regs.VI[REG_CLIP_FLAG].UL); +} + +// ========================================================================= +// VMULAw broadcast-Ft pre-clamp on full mask +// ========================================================================= +// +// MULAw with all four dest lanes active clamps the broadcast Ft before the +// multiply (x86 mVU_MULAw — the "Superman - Shadow Of Apokolips" gamefix). +// Discriminator: Fs=0, Ft.w=+Inf. With the pre-clamp +// Ft -> FLT_MAX and 0*FLT_MAX = 0, matching the interp (which clamps both +// operands via vuDouble). Without it 0*Inf = NaN, which the result-clamp turns +// into +FLT_MAX (0x7f7fffff) — a JIT-vs-interp divergence. + +TEST(EeVu0Cop2Macro, VmulawClampsBroadcastFtOnFullMask) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0u, 0u, 0u, 0u); // fs = 0 + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0x7F800000u); // ft.w = +Inf (broadcast lane) + h.LoadProgram({VMULAw_C2(mask_xyzw, /*fs*/1, /*ft*/2)}); + h.Run(); + for (char l : {'x','y','z','w'}) + { + EXPECT_EQ(h.GetVu0AccBitsJit(l), 0u) << "ACC lane " << l; + EXPECT_EQ(h.GetVu0AccBitsJit(l), h.GetVu0AccBitsInterp(l)) << "lane " << l; + } +} + +// ========================================================================= +// VMADDx broadcast-Fs pre-clamp +// ========================================================================= +// +// MADDx/y/z/w clamp the Fs operand before the multiply: x86 mVU_MADDx passes +// cFs, and the interp routes Fs through vuDouble. The arm64 COP2 macro must +// do the same. Discriminator: Fs=+Inf, Ft.x=0, ACC=0. +// With the pre-clamp Fs -> FLT_MAX and FLT_MAX*0 = 0, so fd = ACC + 0 = 0, +// matching interp. Without it Inf*0 = NaN, which the result-clamp folds to +// +FLT_MAX (0x7f7fffff) — a JIT-vs-interp divergence. +// +// (MSUBx/y/z/w pass clampType=0 in x86 mVU_FMACd, so Fs is intentionally NOT +// clamped there — a shared by-design JIT divergence, not touched by this fix.) + +TEST(EeVu0Cop2Macro, VmaddxClampsBroadcastFs) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x7F800000u, 0x7F800000u, 0x7F800000u, 0x7F800000u); // fs = +Inf + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0u); // ft.x = 0 (broadcast lane) + h.SeedVu0AccBits(0u, 0u, 0u, 0u); // acc = 0 + h.LoadProgram({VMADDx_C2(mask_xyzw, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.Run(); + for (char l : {'x','y','z','w'}) + { + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), 0u) << "fd lane " << l; + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)) << "lane " << l; + } +} + +// ========================================================================= +// VMULy broadcast-Fs pre-clamp +// ========================================================================= +// +// MULx/y/z/w clamp Fs before the multiply on EVERY mask, and additionally clamp +// the broadcast Ft when all four lanes are active: x86 mVU_MULx passes +// (_XYZW_PS)?(cFs|cFt):cFs, and the interp routes both operands through +// vuDouble (TOTA / Disgaea / Ice Age on VU0). The arm64 COP2 macro must clamp +// Fs before the multiply, not only the result. +// +// This uses a PARTIAL (x-only) dest mask so only the always-on cFs clamp fires +// (cFt is gated on the full mask), pinning the behaviour that distinguishes MUL +// from ADD/SUB. Discriminator: fs.x=+Inf, ft.y=0 (MULy broadcasts lane y). With +// the pre-clamp fs.x -> FLT_MAX and FLT_MAX*0 = 0, matching interp. Without it +// Inf*0 = NaN, which the result-clamp folds to +FLT_MAX (0x7f7fffff) — a +// JIT-vs-interp divergence. + +TEST(EeVu0Cop2Macro, VmulyClampsBroadcastFsOnPartialMask) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x7F800000u, 0u, 0u, 0u); // fs.x = +Inf + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0u); // ft.y = 0 (broadcast lane) + h.SeedVu0VfBits(3, 0u, 0u, 0u, 0u); // fd = 0 + h.LoadProgram({VMULy_C2(mask_x, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(3, 'x'), 0u) << "fd.x"; + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l)) << "lane " << l; +} + +// ========================================================================= +// VMULAx/y/z broadcast-Fs pre-clamp to ACC +// ========================================================================= +// +// The accumulator multiply variants MULAx/y/z clamp Fs before the multiply on +// EVERY mask (x86 mVU_MULAx/y/z pass cFs; the interp routes both operands +// through vuDouble). The COP2 macro path must clamp Fs, not only the result. +// Same shape as the MUL→fd fix but writing ACC instead of VF[fd]. Discriminator: +// fs.x=+Inf, ft.z=0 (MULAz broadcasts lane z), partial x-only mask so only the +// always-on cFs clamp fires. With the pre-clamp fs.x -> FLT_MAX, FLT_MAX*0 = 0 +// (interp). Without it Inf*0 = NaN, result-clamped to +FLT_MAX — a divergence. + +TEST(EeVu0Cop2Macro, VmulazClampsBroadcastFsOnPartialMask) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x7F800000u, 0u, 0u, 0u); // fs.x = +Inf + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0u); // ft.z = 0 (broadcast lane) + h.SeedVu0AccBits(0u, 0u, 0u, 0u); // acc = 0 + h.LoadProgram({VMULAz_C2(mask_x, /*fs*/1, /*ft*/2)}); + h.Run(); + EXPECT_EQ(h.GetVu0AccBitsJit('x'), 0u) << "acc.x"; + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0AccBitsJit(l), h.GetVu0AccBitsInterp(l)) << "lane " << l; +} + +// MULAw additionally clamps Fs (not only the broadcast Ft on full mask). +// x86 mVU_MULAw passes cFs always plus cFt on the full mask. Discriminator +// pins the cFs gap: fs.x=+Inf, +// ft.w=0, partial x-only mask (cFt does not fire). With cFs fs.x -> FLT_MAX and +// FLT_MAX*0 = 0 (interp); without it Inf*0 = NaN -> +FLT_MAX. + +TEST(EeVu0Cop2Macro, VmulawClampsBroadcastFsOnPartialMask) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0x7F800000u, 0u, 0u, 0u); // fs.x = +Inf + h.SeedVu0VfBits(2, 0u, 0u, 0u, 0u); // ft.w = 0 (broadcast lane) + h.SeedVu0AccBits(0u, 0u, 0u, 0u); // acc = 0 + h.LoadProgram({VMULAw_C2(mask_x, /*fs*/1, /*ft*/2)}); + h.Run(); + EXPECT_EQ(h.GetVu0AccBitsJit('x'), 0u) << "acc.x"; + for (char l : {'x','y','z','w'}) + EXPECT_EQ(h.GetVu0AccBitsJit(l), h.GetVu0AccBitsInterp(l)) << "lane " << l; +} + +// ========================================================================= +// COP2 condition branches — BC2F / BC2T / BC2FL / BC2TL. +// Native codegen for BC2x: CP2COND is bit 8 of VU0.VI[REG_VPU_STAT] +// (COP2.cpp:11): BC2F taken when clear, +// BC2T when set; FL/TL squash the delay slot when not taken. Run() diffs +// jit-vs-interp and ExpectGpr64 pins both, so each test triple-checks. +// ========================================================================= +namespace { +constexpr u32 kCop2Park = RecompilerTestEnvironment::kParkingPc; +constexpr s16 kCop2TakenOffset = 5; // branch@0x00, offset 5 → target 0x18 +constexpr u32 kVpuStatCp2 = 0x100; // CP2COND = bit 8 + +// Cross-test isolation + seed. EnableVu0Capture only resets VF[0]/VI[0], so a +// preceding macro-VU fixture can leave VU0 with a stale pending micro / ebit / +// VPU_STAT, which the JIT branch's flush drains (clearing VPU_STAT) while the +// interp path doesn't — a spurious VU0 post-state diff. ZeroGlobals(0) gives a +// clean idle VU0 before seeding CP2COND. Mirrors the EnableVu1VifCapture reset. +inline void SetupCop2Branch(EeRecTestHarness& h, u32 vpu_stat) +{ + VuSnapshot::ZeroGlobals(0); + h.EnableCop1(); + h.EnableVu0Capture(); + h.SeedVu0Vi(REG_VPU_STAT, vpu_stat); + // The branch only READS CP2COND (bit 8). But the JIT dispatcher + // (recEeExecuteBlock) runs the EE event-test/VU0-sync path on its way to + // the parking PC and recomputes VPU_STAT, whereas the interp single-stepper + // doesn't — so the post-run VPU_STAT diverges even though the branch + // arithmetic (which marker runs) is identical and correct. Exclude VPU_STAT + // from the VU0 diff; branch correctness is pinned by ExpectGpr64 + DiffEe. + h.IgnoreVu0Vi(REG_VPU_STAT); +} + +// branch@0x00 / NOP delay@0x04 / ADDIU v0,1 (not-taken)@0x08 / J park / +// NOP / NOP / ADDIU v0,2 (taken)@0x18 / J park / NOP +inline void LoadCop2BranchLayout(EeRecTestHarness& h, u32 branch_instr) +{ + h.LoadProgramNoTerm({ + branch_instr, NOP, + ADDIU(reg::v0, reg::zero, 1), J(kCop2Park), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kCop2Park), NOP, + }); +} +} // namespace + +TEST(EeVu0Cop2Macro, Bc2tTakenWhenCondSet) +{ + EeRecTestHarness h; + SetupCop2Branch(h, kVpuStatCp2); // CP2COND = 1 → BC2T taken + LoadCop2BranchLayout(h, BC2T(kCop2TakenOffset)); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +TEST(EeVu0Cop2Macro, Bc2tNotTakenWhenCondClear) +{ + EeRecTestHarness h; + SetupCop2Branch(h, 0); // CP2COND = 0 → BC2T not taken + LoadCop2BranchLayout(h, BC2T(kCop2TakenOffset)); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); +} + +TEST(EeVu0Cop2Macro, Bc2fTakenWhenCondClear) +{ + EeRecTestHarness h; + SetupCop2Branch(h, 0); // CP2COND = 0 → BC2F taken + LoadCop2BranchLayout(h, BC2F(kCop2TakenOffset)); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); +} + +TEST(EeVu0Cop2Macro, Bc2fNotTakenWhenCondSet) +{ + EeRecTestHarness h; + SetupCop2Branch(h, kVpuStatCp2); // CP2COND = 1 → BC2F not taken + LoadCop2BranchLayout(h, BC2F(kCop2TakenOffset)); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); +} + +TEST(EeVu0Cop2Macro, Bc2tlTakenExecutesDelaySlot) +{ + // Likely + taken: delay slot DOES execute. Put a marker in the delay slot. + EeRecTestHarness h; + SetupCop2Branch(h, kVpuStatCp2); // CP2COND = 1 → BC2TL taken + h.SetGpr(reg::t0, 7); + h.LoadProgramNoTerm({ + BC2TL(kCop2TakenOffset), + ADDIU(reg::t0, reg::zero, 99), // delay slot — runs when taken + ADDIU(reg::v0, reg::zero, 1), J(kCop2Park), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kCop2Park), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 2ull); // taken + h.ExpectGpr64(reg::t0, 99ull); // delay slot executed +} + +TEST(EeVu0Cop2Macro, Bc2tlNotTakenSquashesDelaySlot) +{ + // Likely + not taken: delay slot is SQUASHED. t0 keeps its seeded value. + EeRecTestHarness h; + SetupCop2Branch(h, 0); // CP2COND = 0 → BC2TL not taken + h.SetGpr(reg::t0, 7); + h.LoadProgramNoTerm({ + BC2TL(kCop2TakenOffset), + ADDIU(reg::t0, reg::zero, 99), // delay slot — squashed when not taken + ADDIU(reg::v0, reg::zero, 1), J(kCop2Park), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kCop2Park), NOP, + }); + h.Run(); + h.ExpectGpr64(reg::v0, 1ull); // not taken + h.ExpectGpr64(reg::t0, 7ull); // delay slot squashed +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/ee_vu0_qmfc2_qmtc2_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_qmfc2_qmtc2_tests.cpp new file mode 100644 index 0000000000..207ddbfe9f --- /dev/null +++ b/tests/ctest/core/recompilers/ee_vu0_qmfc2_qmtc2_tests.cpp @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// EE↔VU0 QMFC2/QMTC2/MFC2/MTC2 handoff DiffJitVsInterp suite. +// +// QMFC2 / QMTC2 are 128-bit transfers between an EE GPR (the full 128-bit +// register file) and VU0.VF[fs]. MFC2 / MTC2 are 32-bit transfers reading +// or writing one VF lane (selected by the broadcast field — but in the EE +// encoding the lane is implicit and matches the lane index in VF.UL[]). +// +// Real games depend on the "full quadword" semantics of QMTC2 because VF +// registers always hold a 4-tuple of float32. The interlock bit on QMTC2 +// triggers _vu0WaitMicro() — that sync-barrier path needs its own test in +// the VCALLMS suite; here we focus on the data-transfer correctness. + +#include "harness/EeRecTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace mips; +using namespace mips::ee; + +namespace { + +constexpr u32 r_t0 = 8; +constexpr u32 r_t1 = 9; + +inline u32 FloatBits(float f) { u32 b; std::memcpy(&b, &f, sizeof(b)); return b; } + +} // namespace + +// ========================================================================= +// QMFC2 — read full 128-bit VF[fs] into EE GPR +// ========================================================================= + +TEST(EeVu0Qmfc2, ReadsAllFourLanesOfVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, 0xAABBCCDD, 0x11223344, 0x55667788, 0xDEADBEEF); + h.LoadProgram({QMFC2(r_t0, 1)}); + h.Run(); + // EE GPR holds four 32-bit lanes packed [w hi64 | z | y | x lo64] in + // the integer-typed view — VU lanes match GPR.UL[0..3]. + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); + EXPECT_EQ(static_cast(h.GetGpr64Jit(r_t0)), 0xAABBCCDDu); + EXPECT_EQ(static_cast(h.GetGpr64Jit(r_t0) >> 32), 0x11223344u); +} + +TEST(EeVu0Qmfc2, ReadsVf0HardwiredZeroOneFloat) +{ + // VF[0] = (0, 0, 0, 1.0f). QMFC2 reads the full quadword. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.LoadProgram({QMFC2(r_t0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Qmfc2, ReadsVfWithSpecialFloatBitsPreservesPayload) +{ + // NaN payload preservation matters — the interpreter copies bits, the + // JIT must do the same (no float-load that would canonicalize NaNs). + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(2, 0x7FC12345, 0xFFC67890, 0x7F800000, 0xFF800000); + h.LoadProgram({QMFC2(r_t0, 2)}); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +// ========================================================================= +// QMTC2 — write full 128-bit EE GPR into VF[fs] +// ========================================================================= + +TEST(EeVu0Qmtc2, WritesAllFourLanesOfVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr128(r_t0, 0x1122334455667788ull, 0xAABBCCDDEEFF0011ull); + h.LoadProgram({QMTC2(r_t0, 5)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), 0x55667788u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'y'), 0x11223344u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'z'), 0xEEFF0011u); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), 0xAABBCCDDu); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'x'), h.GetVu0VfBitsInterp(5, 'x')); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'y'), h.GetVu0VfBitsInterp(5, 'y')); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'z'), h.GetVu0VfBitsInterp(5, 'z')); + EXPECT_EQ(h.GetVu0VfBitsJit(5, 'w'), h.GetVu0VfBitsInterp(5, 'w')); +} + +TEST(EeVu0Qmtc2, WritesVfZeroIsNoop) +{ + // VF[0] is hardwired (0, 0, 0, 1.0f); QMTC2 to it must not overwrite. + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr128(r_t0, 0xDEADBEEFCAFEBABEull, 0x0123456789ABCDEFull); + h.LoadProgram({QMTC2(r_t0, 0)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(0, 'x'), 0u); + EXPECT_EQ(h.GetVu0VfBitsJit(0, 'y'), 0u); + EXPECT_EQ(h.GetVu0VfBitsJit(0, 'z'), 0u); + EXPECT_EQ(h.GetVu0VfBitsJit(0, 'w'), FloatBits(1.0f)); +} + +TEST(EeVu0Qmfc2Qmtc2RoundTrip, QmtcThenQmfcReturnsOriginal) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SetGpr128(r_t0, 0xCAFEBABEDEADBEEFull, 0x1234567890ABCDEFull); + h.LoadProgram({ + QMTC2(r_t0, 7), + QMFC2(r_t1, 7), + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Jit(r_t1), 0xCAFEBABEDEADBEEFull); + EXPECT_EQ(h.GetGpr64Jit(r_t1), h.GetGpr64Interp(r_t1)); +} + +// ========================================================================= +// MFC2 / MTC2 — 32-bit single-lane transfer +// ========================================================================= + +TEST(EeVu0Mfc2, ReadsLaneXOfVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(3, 0x11111111, 0x22222222, 0x33333333, 0x44444444); + h.LoadProgram({MFC2(r_t0, 3)}); + h.Run(); + // MFC2 with sa=0 reads lane x. JIT and interp must agree on the + // 32-bit slice and on its sign-extension into the 64-bit GPR. + EXPECT_EQ(h.GetGpr64Jit(r_t0), h.GetGpr64Interp(r_t0)); +} + +TEST(EeVu0Mtc2, WritesLaneXOfVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(4, 0x11111111, 0x22222222, 0x33333333, 0x44444444); + h.SetGpr64(r_t0, 0xBEEFCAFE); + h.LoadProgram({MTC2(r_t0, 4)}); + h.Run(); + // Other lanes must remain untouched. Both engines must agree. + EXPECT_EQ(h.GetVu0VfBitsJit(4, 'y'), h.GetVu0VfBitsInterp(4, 'y')); + EXPECT_EQ(h.GetVu0VfBitsJit(4, 'z'), h.GetVu0VfBitsInterp(4, 'z')); + EXPECT_EQ(h.GetVu0VfBitsJit(4, 'w'), h.GetVu0VfBitsInterp(4, 'w')); + EXPECT_EQ(h.GetVu0VfBitsJit(4, 'x'), h.GetVu0VfBitsInterp(4, 'x')); +} + +// ========================================================================= +// LQC2 / SQC2 — quadword load/store between VU0.VF and EE memory +// ========================================================================= + +TEST(EeVu0Lqc2, LoadsQuadFromMemoryIntoVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + // EE main RAM at 0x100100. Write a quadword and load it via LQC2. + h.WriteU32(0x00100100, 0xAAAA1111); + h.WriteU32(0x00100104, 0xBBBB2222); + h.WriteU32(0x00100108, 0xCCCC3333); + h.WriteU32(0x0010010C, 0xDDDD4444); + h.SetGpr64(r_t0, 0x00100100); + h.LoadProgram({LQC2(/*ft*/6, /*base*/r_t0, /*offset*/0)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'x'), 0xAAAA1111u); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'y'), 0xBBBB2222u); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'z'), 0xCCCC3333u); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'w'), 0xDDDD4444u); + EXPECT_EQ(h.GetVu0VfBitsJit(6, 'x'), h.GetVu0VfBitsInterp(6, 'x')); +} + +TEST(EeVu0Lqc2, LoadsWithSignedOffset) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.WriteU32(0x00100200, 0x12121212); + h.WriteU32(0x00100204, 0x34343434); + h.WriteU32(0x00100208, 0x56565656); + h.WriteU32(0x0010020C, 0x78787878); + h.SetGpr64(r_t0, 0x00100210); + h.LoadProgram({LQC2(/*ft*/8, /*base*/r_t0, /*offset*/-16)}); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(8, 'x'), 0x12121212u); + EXPECT_EQ(h.GetVu0VfBitsJit(8, 'w'), 0x78787878u); + EXPECT_EQ(h.GetVu0VfBitsJit(8, 'x'), h.GetVu0VfBitsInterp(8, 'x')); +} + +TEST(EeVu0Sqc2, StoresQuadFromVfIntoMemory) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.TrackMemWindow(0x00100300, 16); + h.SeedVu0VfBits(9, 0xAAAA1111, 0xBBBB2222, 0xCCCC3333, 0xDDDD4444); + h.SetGpr64(r_t0, 0x00100300); + h.LoadProgram({SQC2(/*ft*/9, /*base*/r_t0, /*offset*/0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(0x00100300), 0xAAAA1111u); + EXPECT_EQ(h.ReadU32(0x00100304), 0xBBBB2222u); + EXPECT_EQ(h.ReadU32(0x00100308), 0xCCCC3333u); + EXPECT_EQ(h.ReadU32(0x0010030C), 0xDDDD4444u); +} + +TEST(EeVu0LqSqRoundTrip, SqThenLqReturnsOriginalVf) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(10, 0xCAFE0001, 0xCAFE0002, 0xCAFE0003, 0xCAFE0004); + h.SetGpr64(r_t0, 0x00100400); + h.LoadProgram({ + SQC2(/*ft*/10, /*base*/r_t0, 0), + LQC2(/*ft*/11, /*base*/r_t0, 0), + }); + h.Run(); + EXPECT_EQ(h.GetVu0VfBitsJit(11, 'x'), 0xCAFE0001u); + EXPECT_EQ(h.GetVu0VfBitsJit(11, 'y'), 0xCAFE0002u); + EXPECT_EQ(h.GetVu0VfBitsJit(11, 'z'), 0xCAFE0003u); + EXPECT_EQ(h.GetVu0VfBitsJit(11, 'w'), 0xCAFE0004u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/ee_vu1_vif_dispatch_tests.cpp b/tests/ctest/core/recompilers/ee_vu1_vif_dispatch_tests.cpp new file mode 100644 index 0000000000..d81b856e73 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_vu1_vif_dispatch_tests.cpp @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// EE↔VU1 handoff via VIF1 MSCAL/MSCALF/MSCNT. +// +// VU1 microprogram dispatch goes through VIF1: an EE program (or DMA tag +// chain) writes a VIF1 command into the FIFO, the VIF processor parses the +// command in `vifCode_MSCAL/MSCALF/MSCNT` (Vif_Codes.cpp:420-507), and +// `vu1ExecMicro` then kicks the microprogram on `CpuVU1`. +// +// The harness here uses EeRecTestHarness::EnableVu1VifCapture() to bypass +// the DMAC + FIFO and inject MSCAL-family commands directly: +// - the EE program is a no-op (LoadProgram({NOP})); +// - QueueVif1Mscal/Mscalf/Mscnt records the commands to fire; +// - SetGifPath1Busy / SetVif1WaitForVu / SetVif1Doublebuffer pre-stage +// the VIF1 state each pass should see; +// - Run() fires both passes — once with CpuVU1 = CpuMicroVU1 (JIT VU1), +// once with CpuVU1 = CpuIntVU1 — and snapshots VU1 + dispatch-side +// state on each side, asserting they agree. +// +// Path-1-busy is faked via gif_test_hooks::g_force_path1_busy, the same +// PCSX2_RECOMPILER_TESTS-gated stub the XGKICK suite uses. + +#include "harness/EeRecTestHarness.h" +#include "harness/VuTestHarness.h" + +#include "VU.h" +#include "Vif.h" + +#include + +namespace recompiler_tests { + +using namespace mips; +using namespace mips::ee; +using namespace vu; + +namespace { + +// VADD vf2, vf1, vf0 (xyzw) — the canonical "kick fired" assertion: vf2 +// holds vf1 + vf0 = (vf1.x, vf1.y, vf1.z, vf1.w + 1) after one program run. +constexpr u32 kVaddVf2Vf1Vf0 = VADD_U(/*mask*/ mask::xyzw, /*fd*/ 2, /*fs*/ 1, /*ft*/ 0); + +VuOp VaddPair() { return VuOp{VLitZero(), kVaddVf2Vf1Vf0}; } + +} // namespace + +// MSCAL with imm=0 kicks a VU1 microprogram starting at VU1.Micro byte 0. +// Verifies the dispatch path runs the program to E-bit termination on both +// the JIT and interp VU1 engines, and both arrive at the same vf2. +TEST(EeVu1Vif, Mscal_KickMicroProgramAtZero) +{ + EeRecTestHarness h; + h.EnableVu1VifCapture(); + h.SeedVu1Microprogram(0, {VaddPair(), EBitNopPair(), NopPair()}); + h.SeedVu1Vf(1, 5.0f, 7.0f, 11.0f, 0.0f); + h.LoadProgram({NOP}); + h.QueueVif1Mscal(0); + h.Run(); + EXPECT_TRUE(h.HasVu1TerminatedJit()); + EXPECT_TRUE(h.HasVu1TerminatedInterp()); + h.ExpectVu1Vf(2, 5.0f, 7.0f, 11.0f, 1.0f); // vf0.w = 1.0 hardware constant +} + +// MSCALF stalls if GIF path 1 is busy: vifCode_MSCALF sets vif1Regs.stat.VGW +// and DMA-stalls without dispatching the microprogram. Both engines must +// agree on the stall and leave vf2 untouched. +TEST(EeVu1Vif, Mscalf_StallsOnGifPath1Busy) +{ + EeRecTestHarness h; + h.EnableVu1VifCapture(); + h.SeedVu1Microprogram(0, {VaddPair(), EBitNopPair(), NopPair()}); + h.SeedVu1Vf(1, 5.0f, 7.0f, 11.0f, 0.0f); + h.SeedVu1Vf(2, 0.0f, 0.0f, 0.0f, 0.0f); + h.LoadProgram({NOP}); + h.SetGifPath1Busy(true); + h.QueueVif1Mscalf(0); + h.Run(); + // Microprogram never ran — vf2 holds its seeded zero value. + h.ExpectVu1Vf(2, 0.0f, 0.0f, 0.0f, 0.0f); + // VGW (bit 3 of stat) must be set on both sides. + EXPECT_NE(h.GetVif1StatJit() & VIF1_STAT_VGW, 0u); + EXPECT_NE(h.GetVif1StatInterp() & VIF1_STAT_VGW, 0u); +} + +// MSCNT (vu1ExecMicro with addr=-1) keeps VU1.VI[REG_TPC] from the previous +// kick. Inject MSCAL(0) followed by MSCNT against a 2-pair program; the +// program runs once for MSCAL (vf2 += vf0), terminates at TPC=2, then MSCNT +// resumes from TPC=2, walks the 0x7FE zero-pairs to wrap-around, and re-hits +// the program at byte 0 — vf2 ends up with two increments. +TEST(EeVu1Vif, Mscnt_ReusesPreviousMicroprogramPC) +{ + EeRecTestHarness h; + h.EnableVu1VifCapture(); + // vf2.xyzw += vf0.w (=1) in each lane via VADDw. Two passes → +2. + const u32 vaddw = VADDw_U(mask::xyzw, /*fd*/ 2, /*fs*/ 2, /*ft*/ 0); + h.SeedVu1Microprogram(0, {VuOp{VLitZero(), vaddw}, EBitNopPair(), NopPair()}); + h.SeedVu1Vf(2, 0.0f, 0.0f, 0.0f, 0.0f); + h.LoadProgram({NOP}); + h.QueueVif1Mscal(0); + h.QueueVif1Mscnt(); + h.Run(); + // Program ran twice — vf2 incremented from 0 to 2 in every lane. + h.ExpectVu1Vf(2, 2.0f, 2.0f, 2.0f, 2.0f); +} + +// vif1.waitforvu blocks subsequent VIF1 dispatch — vifCode_MSCAL pass1 sets +// DMASTALL and returns without queuing the microprogram. Both engines must +// agree: program never runs, waitforvu still latched. +TEST(EeVu1Vif, VifWaitForVuFromInflightMicroprogram) +{ + EeRecTestHarness h; + h.EnableVu1VifCapture(); + h.SeedVu1Microprogram(0, {VaddPair(), EBitNopPair(), NopPair()}); + h.SeedVu1Vf(1, 5.0f, 7.0f, 11.0f, 0.0f); + h.SeedVu1Vf(2, 0.0f, 0.0f, 0.0f, 0.0f); + h.LoadProgram({NOP}); + h.SetVif1WaitForVu(true); + h.QueueVif1Mscal(0); + h.Run(); + // Microprogram never ran — vf2 unchanged. + h.ExpectVu1Vf(2, 0.0f, 0.0f, 0.0f, 0.0f); +} + +// Double-buffered TOP: with stat.DBF clear and two MSCALs, vifRegs.tops +// alternates between (base+ofst) and base, while vifRegs.top latches the +// pre-swap tops on each kick. After two MSCALs: +// kick #1 — top = 0x000 (initial), tops = base+ofst, DBF = 1 +// kick #2 — top = base+ofst, tops = base, DBF = 0 +TEST(EeVu1Vif, DoubleBufferedTopAdvancesOnEachKick) +{ + EeRecTestHarness h; + h.EnableVu1VifCapture(); + h.SeedVu1Microprogram(0, {EBitNopPair(), NopPair()}); + h.LoadProgram({NOP}); + h.SetVif1Doublebuffer(/*base*/ 0x10, /*ofst*/ 0x20); + h.QueueVif1Mscal(0); + h.QueueVif1Mscal(0); + h.Run(); + EXPECT_EQ(h.GetVif1TopJit(), 0x30u); // base+ofst (latched at kick #2) + EXPECT_EQ(h.GetVif1TopInterp(), 0x30u); + EXPECT_EQ(h.GetVif1TopsJit(), 0x10u); // base (DBF flipped back) + EXPECT_EQ(h.GetVif1TopsInterp(), 0x10u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp new file mode 100644 index 0000000000..23afd7359f --- /dev/null +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp @@ -0,0 +1,885 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "EeRecTestHarness.h" + +#include "Config.h" +#include "Gif_Unit.h" +#include "Memory.h" +#include "R3000A.h" +#include "R5900.h" +#include "VU.h" +#include "VUmicro.h" +#include "Vif.h" +#include "Vif_Dma.h" + +#include +#include + +// Defined by the EE recompiler. Not part of R5900cpu (which has no +// ExecuteBlock member); called directly to drive the EE rec path for a +// bounded number of guest cycles. Returns cycles actually consumed. +extern s32 recEeExecuteBlock(s32 cycles, u32 park_pc); +extern void vifExecQueue(int idx); + +namespace recompiler_tests { + +namespace { + +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; +constexpr u32 kParkingPc = RecompilerTestEnvironment::kParkingPc; + +void ZeroCpuRegs() +{ + std::memset(&cpuRegs, 0, sizeof(cpuRegs)); + std::memset(&fpuRegs, 0, sizeof(fpuRegs)); +} + +} // namespace + +EeRecTestHarness::EeRecTestHarness() +{ + EXPECT_TRUE(RecompilerTestEnvironment::IsReady()) + << "RecompilerTestEnvironment was not set up — harness cannot run. " + "Make sure the binary's main.cpp registered the environment."; + ZeroCpuRegs(); +} + +EeRecTestHarness::~EeRecTestHarness() +{ + if (capture_vu1_) + { + gif_test_hooks::g_path1_sink = nullptr; + gif_test_hooks::g_force_path1_busy = false; + // Restore the env-default INSTANT_VU1 flag (matches the value + // RecompilerTestEnvironment sets at setup). + EmuConfig.Speedhacks.vu1Instant = false; + } + + if (fpu_full_mode_changed_) + EmuConfig.Cpu.Recompiler.fpuFullMode = prev_fpu_full_mode_; + + if (fpu_mul_hack_changed_) + EmuConfig.Gamefixes.FpuMulHack = prev_fpu_mul_hack_; +} + +void EeRecTestHarness::SetGpr64(u32 reg_idx, u64 value) +{ + if (reg_idx == 0) + return; + cpuRegs.GPR.r[reg_idx].UD[0] = value; + cpuRegs.GPR.r[reg_idx].UD[1] = 0; +} + +void EeRecTestHarness::SetGpr128(u32 reg_idx, u64 lo, u64 hi) +{ + if (reg_idx == 0) + return; + cpuRegs.GPR.r[reg_idx].UD[0] = lo; + cpuRegs.GPR.r[reg_idx].UD[1] = hi; +} + +void EeRecTestHarness::SetHi64(u64 value) { cpuRegs.HI.UD[0] = value; cpuRegs.HI.UD[1] = 0; } +void EeRecTestHarness::SetLo64(u64 value) { cpuRegs.LO.UD[0] = value; cpuRegs.LO.UD[1] = 0; } +void EeRecTestHarness::SetLoPair(u64 lo_qw, u64 hi_qw) { cpuRegs.LO.UD[0] = lo_qw; cpuRegs.LO.UD[1] = hi_qw; } +void EeRecTestHarness::SetHiPair(u64 lo_qw, u64 hi_qw) { cpuRegs.HI.UD[0] = lo_qw; cpuRegs.HI.UD[1] = hi_qw; } + +void EeRecTestHarness::SetCp0(u32 reg_idx, u32 value) { cpuRegs.CP0.r[reg_idx] = value; } + +void EeRecTestHarness::SetFpr(u32 reg_idx, float value) { fpuRegs.fpr[reg_idx].f = value; } +void EeRecTestHarness::SetFprBits(u32 reg_idx, u32 bits) { fpuRegs.fpr[reg_idx].UL = bits; } +void EeRecTestHarness::SetAcc(float value) { fpuRegs.ACC.f = value; } +void EeRecTestHarness::SetAccBits(u32 bits) { fpuRegs.ACC.UL = bits; } +void EeRecTestHarness::SetFcr31(u32 value) { fpuRegs.fprc[31] = value; } + +void EeRecTestHarness::EnableCop0() { cpuRegs.CP0.n.Status.val |= (1u << 28); /* CU0 */ } +void EeRecTestHarness::EnableCop1() { cpuRegs.CP0.n.Status.val |= (1u << 29); /* CU1 */ } + +void EeRecTestHarness::EnableFpuFullMode() +{ + if (!fpu_full_mode_changed_) + { + prev_fpu_full_mode_ = EmuConfig.Cpu.Recompiler.fpuFullMode; + fpu_full_mode_changed_ = true; + } + EmuConfig.Cpu.Recompiler.fpuFullMode = true; +} + +void EeRecTestHarness::EnableFpuMulHack() +{ + if (!fpu_mul_hack_changed_) + { + prev_fpu_mul_hack_ = EmuConfig.Gamefixes.FpuMulHack; + fpu_mul_hack_changed_ = true; + } + EmuConfig.Gamefixes.FpuMulHack = true; +} +void EeRecTestHarness::SetStatusBits(u32 mask) { cpuRegs.CP0.n.Status.val |= mask; } + +// EE vtlb_memWrite on a direct RAM hit bypasses Cpu->Clear — upstream relies +// on mmap-level write protection + a SIGSEGV handler to catch SMC, which +// isn't wired in the harness. Invalidate any compiled block covering the +// target address explicitly so multi-block tests that write into neighbour +// regions (e.g. kBlockBPc) don't execute a previous test's cached block. +// (IOP's iopMemWrite* sidesteps this by calling psxCpu->Clear itself.) +static void HarnessInvalidate(u32 addr, u32 size_words) +{ + if (recCpu.Clear) + recCpu.Clear(addr & ~0x3u, size_words); +} + +void EeRecTestHarness::WriteU8(u32 addr, u8 value) +{ + memWrite8(addr, value); + HarnessInvalidate(addr, 1); + MergeTrackedWindow(addr & ~0x3u, 4); +} +void EeRecTestHarness::WriteU16(u32 addr, u16 value) +{ + memWrite16(addr, value); + HarnessInvalidate(addr, 1); + MergeTrackedWindow(addr & ~0x3u, 4); +} +void EeRecTestHarness::WriteU32(u32 addr, u32 value) +{ + memWrite32(addr, value); + HarnessInvalidate(addr, 1); + MergeTrackedWindow(addr & ~0x3u, 4); +} +void EeRecTestHarness::WriteU64(u32 addr, u64 value) +{ + memWrite64(addr, value); + HarnessInvalidate(addr, 2); + MergeTrackedWindow(addr & ~0x7u, 8); +} +void EeRecTestHarness::WriteBytes(u32 addr, const void* src, size_t bytes) +{ + const u8* p = static_cast(src); + for (size_t i = 0; i < bytes; ++i) + memWrite8(addr + static_cast(i), p[i]); + HarnessInvalidate(addr, static_cast((bytes + 3) / 4)); + MergeTrackedWindow(addr, bytes); +} + +u8 EeRecTestHarness::ReadU8 (u32 addr) const { return memRead8 (addr); } +u16 EeRecTestHarness::ReadU16(u32 addr) const { return memRead16(addr); } +u32 EeRecTestHarness::ReadU32(u32 addr) const { return memRead32(addr); } +u64 EeRecTestHarness::ReadU64(u32 addr) const { return memRead64(addr); } + +void EeRecTestHarness::TrackMemWindow(u32 addr, size_t bytes) { MergeTrackedWindow(addr, bytes); } + +void EeRecTestHarness::TriggerSmc(u32 hw_addr, u32 value) +{ + // memWrite32 runs through the vtlb store path which calls Cpu->Clear on + // a hit — the SMC invalidation trigger. recRecompile compiles blocks + // (and caches them in recBlocks/recLUT), so recClear's range-invalidate + // removes those entries and re-seeds the BASEBLOCK slots with eeJITCompile + // so the next dispatch re-compiles. + memWrite32(hw_addr, value); + MergeTrackedWindow(hw_addr & ~0x3u, 4); +} + +void EeRecTestHarness::SimulateFastmemFault(u32 faulting_pc) +{ + // vtlb.cpp's SIGSEGV backpatch handler calls Cpu->Clear(faulting_pc, 1) + // on a fastmem store-into-code-page fault. This mirrors that single + // entry point — recClear should reset BLOCK->fnptr to JITCompile for + // every block whose extent covers `faulting_pc`, including straddlers + // (block startpc < faulting_pc < block endpc). This helper is the + // regression gate for straddler coverage. + if (recCpu.Clear) + recCpu.Clear(faulting_pc & ~0x3u, 1); +} + +void EeRecTestHarness::MergeTrackedWindow(u32 addr, size_t bytes) +{ + u32 start = addr & ~0x3u; + size_t end = ((addr + bytes + 3u) & ~0x3u); + size_t new_size = end - start; + + for (auto& w : mem_windows_) + { + const u32 w_end = w.addr + static_cast(w.bytes.size()); + if (start + new_size < w.addr || start > w_end) + continue; + const u32 merged_start = (start < w.addr) ? start : w.addr; + const u32 merged_end = (start + new_size > w_end) ? static_cast(start + new_size) : w_end; + w.addr = merged_start; + w.bytes.resize(merged_end - merged_start); + return; + } + + mem_windows_.push_back(MemWindow{start, std::vector(new_size)}); +} + +void EeRecTestHarness::LoadProgramImpl(std::initializer_list instructions, bool append_term) +{ + program_words_.assign(instructions.begin(), instructions.end()); + if (append_term) + { + program_words_.push_back(mips::JR(mips::reg::ra)); + program_words_.push_back(mips::NOP); + } + for (size_t i = 0; i < program_words_.size(); ++i) + memWrite32(kProgramPc + static_cast(i * 4), program_words_[i]); +} + +void EeRecTestHarness::LoadProgram(std::initializer_list instructions) +{ + LoadProgramImpl(instructions, /*append_term=*/true); +} + +void EeRecTestHarness::LoadProgramNoTerm(std::initializer_list instructions) +{ + LoadProgramImpl(instructions, /*append_term=*/false); +} + +void EeRecTestHarness::SeedEntryState() +{ + cpuRegs.GPR.n.ra.UD[0] = static_cast(static_cast(kParkingPc)); + cpuRegs.pc = kProgramPc; + cpuRegs.branch = 0; + + // An EE branch triggers cpuEventTest → psxCpu->ExecuteBlock(EEsCycle), + // which dispatches IOP code from psxRegs.pc. If that PC is 0 the IOP + // walks zero-bytes-as-NOPs forever. Park the IOP at its parking lot. + psxRegs.pc = RecompilerTestEnvironment::kParkingPc; +} + +void EeRecTestHarness::StepInterpUntilParkedOrTimeout() +{ + for (u32 i = 0; i < kMaxInstructions; ++i) + { + if (cpuRegs.pc == kParkingPc) + return; + intCpu.Step(); + } + ADD_FAILURE() << "EeRecTestHarness exhausted instruction budget (" + << kMaxInstructions << "); PC=0x" << std::hex << cpuRegs.pc + << " never reached parking lot 0x" << kParkingPc; +} + +void EeRecTestHarness::Run(RunMode mode) +{ + ASSERT_FALSE(program_words_.empty()) + << "LoadProgram() must be called before Run()"; + + SeedEntryState(); + pre_snapshot_ = EeSnapshot::Capture(mem_windows_); + + // Drop any cached block from a previous test that occupies kProgramPc + // or the parking lot. memWrite32 on harness program load *should* trip + // vtlb's Cpu->Clear path, but a belt-and-suspenders explicit Clear keeps + // the harness decoupled from that wiring (matches JitTestHarness::Run for IOP). + // Required once opcode handlers bake immediates into the emitted block, + // since a body that re-fetches from memory each dispatch is inadvertently + // SMC-immune. + // + // PreserveCache mode skips this — used by SMC tests that want a second + // dispatch against blocks left over from a previous Run(), with mid- + // block invalidation injected via SimulateFastmemFault() between calls. + if (mode == RunMode::FreshCache && recCpu.Clear) + { + recCpu.Clear(kProgramPc, static_cast(program_words_.size())); + recCpu.Clear(kParkingPc, 2); + } + + if (capture_vu0_) + vu0_pre_snapshot_ = VuSnapshot::Capture(0, {}); + if (capture_vu1_) + vu1_pre_snapshot_ = VuSnapshot::Capture(1, {}); + // A JIT block may legitimately leave host FPCR set to EmuConfig FPUFPCR + // (e.g. native DIV.S swaps to the div rounding mode and restores FPUFPCR, + // not the host default). The real EE thread holds FPUFPCR for its whole + // lifetime, but the harness never establishes that invariant, so contain + // the mutation to the JIT block: snapshot host FPCR and restore it before + // the interp oracle runs and before the next test. + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + recEeExecuteBlock(kCycleBudget, kParkingPc); + FPControlRegister::SetCurrent(saved_fpcr); + if (capture_vu1_) + FireVif1Pass(/*jit=*/true); + jit_snapshot_ = EeSnapshot::Capture(mem_windows_); + if (capture_vu0_) + vu0_jit_snapshot_ = VuSnapshot::Capture(0, {}); + if (capture_vu1_) + vu1_jit_snapshot_ = VuSnapshot::Capture(1, {}); + + // Restore pre-state and run interp from the same initial conditions. + pre_snapshot_.Restore(); + if (capture_vu0_) + vu0_pre_snapshot_.Restore(); + if (capture_vu1_) + vu1_pre_snapshot_.Restore(); + SeedEntryState(); + StepInterpUntilParkedOrTimeout(); + if (capture_vu1_) + FireVif1Pass(/*jit=*/false); + interp_snapshot_ = EeSnapshot::Capture(mem_windows_); + if (capture_vu0_) + vu0_interp_snapshot_ = VuSnapshot::Capture(0, {}); + if (capture_vu1_) + vu1_interp_snapshot_ = VuSnapshot::Capture(1, {}); + has_run_ = true; + + const auto diffs = DiffEe(jit_snapshot_, interp_snapshot_); + if (!diffs.empty()) + { + std::ostringstream ss; + ss << "EE JIT vs INTERP divergence (" << diffs.size() << "):\n"; + for (const auto& d : diffs) + ss << " " << d << "\n"; + ss << "Pre-state:\n"; + PrintEe(ss, pre_snapshot_); + ss << "JIT post-state:\n"; + PrintEe(ss, jit_snapshot_); + ss << "INTERP post-state:\n"; + PrintEe(ss, interp_snapshot_); + ADD_FAILURE() << ss.str(); + } + + if (capture_vu0_) + { + const auto vudiffs = DiffVu(vu0_jit_snapshot_, vu0_interp_snapshot_, + VuDiffMode::PipelinePermissive, vu0_ignored_vi_); + if (!vudiffs.empty()) + { + std::ostringstream ss; + ss << "VU0 JIT vs INTERP divergence (" << vudiffs.size() << "):\n"; + for (const auto& d : vudiffs) + ss << " " << d << "\n"; + ss << "VU0 JIT post-state:\n"; + PrintVu(ss, vu0_jit_snapshot_); + ss << "VU0 INTERP post-state:\n"; + PrintVu(ss, vu0_interp_snapshot_); + ADD_FAILURE() << ss.str(); + } + } + + if (capture_vu1_) + { + const auto vudiffs = DiffVu(vu1_jit_snapshot_, vu1_interp_snapshot_, + VuDiffMode::PipelinePermissive); + if (!vudiffs.empty()) + { + std::ostringstream ss; + ss << "VU1 JIT vs INTERP divergence (" << vudiffs.size() << "):\n"; + for (const auto& d : vudiffs) + ss << " " << d << "\n"; + ss << "VU1 JIT post-state:\n"; + PrintVu(ss, vu1_jit_snapshot_); + ss << "VU1 INTERP post-state:\n"; + PrintVu(ss, vu1_interp_snapshot_); + ADD_FAILURE() << ss.str(); + } + } +} + +void EeRecTestHarness::RunJitNoDiff(RunMode mode) +{ + ASSERT_FALSE(program_words_.empty()) + << "LoadProgram() must be called before RunJitNoDiff()"; + + SeedEntryState(); + pre_snapshot_ = EeSnapshot::Capture(mem_windows_); + + if (mode == RunMode::FreshCache && recCpu.Clear) + { + recCpu.Clear(kProgramPc, static_cast(program_words_.size())); + recCpu.Clear(kParkingPc, 2); + } + + // Contain any FPCR mutation the JIT block makes (see note in Run()). + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + recEeExecuteBlock(kCycleBudget, kParkingPc); + FPControlRegister::SetCurrent(saved_fpcr); + jit_snapshot_ = EeSnapshot::Capture(mem_windows_); + // Mirror the JIT post-state into the interp snapshot so accessors that read + // either side return the JIT value (there is no interp double-mode oracle). + interp_snapshot_ = jit_snapshot_; + has_run_ = true; +} + +void EeRecTestHarness::RunInterpOnly() +{ + ASSERT_FALSE(program_words_.empty()) + << "LoadProgram() must be called before RunInterpOnly()"; + + SeedEntryState(); + pre_snapshot_ = EeSnapshot::Capture(mem_windows_); + StepInterpUntilParkedOrTimeout(); + interp_snapshot_ = EeSnapshot::Capture(mem_windows_); + jit_snapshot_ = interp_snapshot_; + has_run_ = true; +} + +u64 EeRecTestHarness::GetGpr64Interp(u32 r) const { return interp_snapshot_.regs.GPR.r[r].UD[0]; } +u64 EeRecTestHarness::GetGpr64Jit (u32 r) const { return jit_snapshot_.regs.GPR.r[r].UD[0]; } +u64 EeRecTestHarness::GetHi64Interp() const { return interp_snapshot_.regs.HI.UD[0]; } +u64 EeRecTestHarness::GetLo64Interp() const { return interp_snapshot_.regs.LO.UD[0]; } +u32 EeRecTestHarness::GetFprBitsInterp(u32 r) const { return interp_snapshot_.fprs.fpr[r].UL; } +u32 EeRecTestHarness::GetFprBitsJit (u32 r) const { return jit_snapshot_.fprs.fpr[r].UL; } +u32 EeRecTestHarness::GetAccBitsInterp() const { return interp_snapshot_.fprs.ACC.UL; } +u32 EeRecTestHarness::GetAccBitsJit () const { return jit_snapshot_.fprs.ACC.UL; } +u32 EeRecTestHarness::GetCp0Interp(u32 r) const { return interp_snapshot_.regs.CP0.r[r]; } +u32 EeRecTestHarness::GetCp0Jit (u32 r) const { return jit_snapshot_.regs.CP0.r[r]; } + +void EeRecTestHarness::ExpectGpr64(u32 reg_idx, u64 expected) const +{ + EXPECT_EQ(interp_snapshot_.regs.GPR.r[reg_idx].UD[0], expected) + << "r" << reg_idx << ".lo (interp)"; + EXPECT_EQ(jit_snapshot_.regs.GPR.r[reg_idx].UD[0], expected) + << "r" << reg_idx << ".lo (jit)"; +} + +void EeRecTestHarness::ExpectGpr128(u32 reg_idx, u64 lo, u64 hi) const +{ + EXPECT_EQ(interp_snapshot_.regs.GPR.r[reg_idx].UD[0], lo) << "r" << reg_idx << ".lo (interp)"; + EXPECT_EQ(interp_snapshot_.regs.GPR.r[reg_idx].UD[1], hi) << "r" << reg_idx << ".hi (interp)"; + EXPECT_EQ(jit_snapshot_.regs.GPR.r[reg_idx].UD[0], lo) << "r" << reg_idx << ".lo (jit)"; + EXPECT_EQ(jit_snapshot_.regs.GPR.r[reg_idx].UD[1], hi) << "r" << reg_idx << ".hi (jit)"; +} + +void EeRecTestHarness::ExpectFpr(u32 reg_idx, u32 bits) const +{ + EXPECT_EQ(interp_snapshot_.fprs.fpr[reg_idx].UL, bits) << "fpr" << reg_idx << " (interp)"; + EXPECT_EQ(jit_snapshot_.fprs.fpr[reg_idx].UL, bits) << "fpr" << reg_idx << " (jit)"; +} + +void EeRecTestHarness::ExpectAcc(u32 bits) const +{ + EXPECT_EQ(interp_snapshot_.fprs.ACC.UL, bits) << "ACC (interp)"; + EXPECT_EQ(jit_snapshot_.fprs.ACC.UL, bits) << "ACC (jit)"; +} + +// ---- VU0 cross-tree handoff helpers ---- + +void EeRecTestHarness::EnableVu0Capture() +{ + capture_vu0_ = true; + // Mirror the VuTestHarness's first-touch invariant: VF[0] must read as + // (0,0,0,1.0). _vu0Exec asserts on drift via DbgCon.Error. The interp + // run will trigger that assertion on whatever stale state the previous + // test left behind unless this is reset here. + vuRegs[0].VF[0].f.x = 0.0f; + vuRegs[0].VF[0].f.y = 0.0f; + vuRegs[0].VF[0].f.z = 0.0f; + vuRegs[0].VF[0].f.w = 1.0f; + vuRegs[0].VI[0].UL = 0; +} + +void EeRecTestHarness::SeedVu0Vf(u32 reg_idx, float x, float y, float z, float w) +{ + if (reg_idx == 0) + return; + auto& vf = vuRegs[0].VF[reg_idx]; + vf.f.x = x; vf.f.y = y; vf.f.z = z; vf.f.w = w; +} + +void EeRecTestHarness::SeedVu0VfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w) +{ + if (reg_idx == 0) + return; + auto& vf = vuRegs[0].VF[reg_idx]; + vf.i.x = x; vf.i.y = y; vf.i.z = z; vf.i.w = w; +} + +void EeRecTestHarness::SeedVu0Acc(float x, float y, float z, float w) +{ + auto& acc = vuRegs[0].ACC; + acc.f.x = x; acc.f.y = y; acc.f.z = z; acc.f.w = w; +} + +void EeRecTestHarness::SeedVu0AccBits(u32 x, u32 y, u32 z, u32 w) +{ + auto& acc = vuRegs[0].ACC; + acc.i.x = x; acc.i.y = y; acc.i.z = z; acc.i.w = w; +} + +void EeRecTestHarness::SeedVu0Vi(u32 reg_idx, u32 value) +{ + if (reg_idx == 0) + return; + // REG_FBRST / REG_VPU_STAT / REG_TPC etc. hold full 32-bit values; the + // rest are 16-bit. Since callers know the register, just trust them. + vuRegs[0].VI[reg_idx].UL = value; +} + +void EeRecTestHarness::SeedVu0Microprogram(u32 byte_offset, std::initializer_list pairs) +{ + auto& vu = vuRegs[0]; + const u32 mask = VU0_PROGMASK; + u32 base = byte_offset & mask; + for (const auto& p : pairs) + { + std::memcpy(vu.Micro + ((base + 0) & mask), &p.lower, 4); + std::memcpy(vu.Micro + ((base + 4) & mask), &p.upper, 4); + base += 8; + } +} + +namespace { +inline u32 Vu0LaneIdx(char lane) +{ + switch (lane) { case 'x': return 0; case 'y': return 1; case 'z': return 2; default: return 3; } +} +inline u32 Vu0ViMask(u32 reg_idx) +{ + switch (reg_idx) + { + case REG_R: case REG_I: case REG_Q: case REG_P: + case REG_STATUS_FLAG: case REG_MAC_FLAG: case REG_CLIP_FLAG: + case REG_TPC: case REG_FBRST: case REG_VPU_STAT: + return 0xFFFFFFFFu; + default: + return 0x0000FFFFu; + } +} +} // namespace + +u32 EeRecTestHarness::GetVu0VfBitsJit(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu0_jit_snapshot_.regs.VF[reg_idx].UL[li]; +} +u32 EeRecTestHarness::GetVu0VfBitsInterp(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu0_interp_snapshot_.regs.VF[reg_idx].UL[li]; +} +float EeRecTestHarness::GetVu0VfJit(u32 reg_idx, char lane) const +{ + const u32 b = GetVu0VfBitsJit(reg_idx, lane); + float f; std::memcpy(&f, &b, sizeof(f)); return f; +} +float EeRecTestHarness::GetVu0VfInterp(u32 reg_idx, char lane) const +{ + const u32 b = GetVu0VfBitsInterp(reg_idx, lane); + float f; std::memcpy(&f, &b, sizeof(f)); return f; +} +u32 EeRecTestHarness::GetVu0AccBitsJit(char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu0_jit_snapshot_.regs.ACC.UL[li]; +} +u32 EeRecTestHarness::GetVu0AccBitsInterp(char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu0_interp_snapshot_.regs.ACC.UL[li]; +} +u32 EeRecTestHarness::GetVu0ViJit(u32 reg_idx) const +{ + return vu0_jit_snapshot_.regs.VI[reg_idx].UL & Vu0ViMask(reg_idx); +} +u32 EeRecTestHarness::GetVu0ViInterp(u32 reg_idx) const +{ + return vu0_interp_snapshot_.regs.VI[reg_idx].UL & Vu0ViMask(reg_idx); +} + +// ---- EE↔VU1 VIF dispatch helpers ---- + +void EeRecTestHarness::EnableVu1VifCapture() +{ + capture_vu1_ = true; + + // Cross-test isolation. A previous fixture (Vu1Xgkick, EeVu0*) may have + // left VU1 dirty: VPU_STAT.bit8 still set, ebit > 0, xgkickenable on, + // VU1.Mem holding GIF tags, etc. Without this, the JIT/interp passes + // here either hang in Execute (running bit never clears) or trigger + // MTGS::WaitGS during xgkick drain (MTGS thread isn't open in tests). + VuSnapshot::ZeroGlobals(1); + std::memset(vuRegs[1].Mem, 0, VU1_MEMSIZE); + std::memset(vuRegs[1].Micro, 0, VU1_PROGSIZE); + VU0.VI[REG_VPU_STAT].UL &= ~0xFF00u; // clear VU1 running/T/D bits + + // Install the path 1 sink so any incidental XGKICK during VU1 termination + // (the interp's vu1Exec drains xgkick on E-bit, even if the program never + // kicked) hits the test-only sink instead of MTGS::WaitGS. + gif_test_hooks::g_path1_sink = &vu1_path1_sink_; + + // vu1ExecMicro with INSTANT_VU1=true runs CpuVU1->Execute(vu1RunCycles) + // synchronously to completion (E-bit termination clears VPU_STAT bit 8). + // The test environment defaults this to false (RecompilerTestEnvironment. + // cpp:160) so cycle-driven tests stay deterministic; flip it on for the + // VIF dispatch tests where the program must fully execute inside + // vifExecQueue. The dtor restores the env default. + EmuConfig.Speedhacks.vu1Instant = true; +} + +void EeRecTestHarness::SeedVu1Microprogram(u32 byte_offset, std::initializer_list pairs) +{ + auto& vu = vuRegs[1]; + const u32 mask = VU1_PROGMASK; + u32 base = byte_offset & mask; + for (const auto& p : pairs) + { + std::memcpy(vu.Micro + ((base + 0) & mask), &p.lower, 4); + std::memcpy(vu.Micro + ((base + 4) & mask), &p.upper, 4); + base += 8; + } +} + +void EeRecTestHarness::SeedVu1Vf(u32 reg_idx, float x, float y, float z, float w) +{ + if (reg_idx == 0) + return; + auto& vf = vuRegs[1].VF[reg_idx]; + vf.f.x = x; vf.f.y = y; vf.f.z = z; vf.f.w = w; +} + +void EeRecTestHarness::SeedVu1VfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w) +{ + if (reg_idx == 0) + return; + auto& vf = vuRegs[1].VF[reg_idx]; + vf.i.x = x; vf.i.y = y; vf.i.z = z; vf.i.w = w; +} + +void EeRecTestHarness::SeedVu1Vi(u32 reg_idx, u32 value) +{ + if (reg_idx == 0) + return; + vuRegs[1].VI[reg_idx].UL = value; +} + +void EeRecTestHarness::QueueVif1Mscal(u16 microprogram_addr) +{ + vif1_queue_.push_back({PendingVifCmd::Mscal, 0x14000000u | (microprogram_addr & 0xFFFFu)}); +} + +void EeRecTestHarness::QueueVif1Mscalf(u16 microprogram_addr) +{ + vif1_queue_.push_back({PendingVifCmd::Mscalf, 0x15000000u | (microprogram_addr & 0xFFFFu)}); +} + +void EeRecTestHarness::QueueVif1Mscnt() +{ + vif1_queue_.push_back({PendingVifCmd::Mscnt, 0x17000000u}); +} + +void EeRecTestHarness::SetGifPath1Busy(bool busy) { vu1_state_path1_busy_ = busy; } +void EeRecTestHarness::SetVif1WaitForVu(bool wait) { vu1_state_waitforvu_ = wait; } +void EeRecTestHarness::SetVif1Doublebuffer(u16 base_qw, u16 ofst_qw) +{ + vu1_state_dbf_set_ = true; + vu1_state_base_qw_ = base_qw & 0x3ffu; + vu1_state_ofst_qw_ = ofst_qw & 0x3ffu; +} + +void EeRecTestHarness::FireVif1Pass(bool jit) +{ + // Reset vif1 + vif1Regs to a known baseline. vif1Reset() also calls + // resetNewVif(1) which clears the dynamic VIF unpack JIT cache — fine. + vif1Reset(); + + // Apply per-test state. + gif_test_hooks::g_force_path1_busy = vu1_state_path1_busy_; + if (vu1_state_waitforvu_) + vif1.waitforvu = true; + if (vu1_state_dbf_set_) + { + vif1Regs.base = vu1_state_base_qw_; + vif1Regs.ofst = vu1_state_ofst_qw_; + } + + // Swap the active VU1 engine for this pass. Restored on exit so the + // EE harness's default (CpuVU1 = CpuIntVU1 in RecompilerTestEnvironment) + // is preserved for any subsequent test run in this binary. + BaseVUmicroCPU* const saved_cpu_vu1 = CpuVU1; + CpuVU1 = jit ? static_cast(&CpuMicroVU1) + : static_cast(&CpuIntVU1); + + // Reset the JIT engine before each JIT pass so a previous test's + // compiled blocks can't be reused against this test's seeded state. + if (jit) + CpuMicroVU1.Reset(); + + for (const auto& cmd : vif1_queue_) + { + // Mimic vifCode_MSCAL/MSCALF/MSCNT pass1 — write the command and + // invoke the dispatch helpers directly. No vifFlush() because the + // FIFO is empty in the harness (no DMA tag chain). + vif1Regs.code = cmd.code; + + switch (cmd.kind) + { + case PendingVifCmd::Mscal: + { + if (vif1.waitforvu) + break; // Production sets DMASTALL and returns. + const u32 addr = static_cast(vif1Regs.code); + // vuExecMicro body — sets up itop/top/tops/DBF and queues the + // program. With INSTANT_VU1=true, vifExecQueue→vu1ExecMicro→ + // CpuVU1->Execute runs to completion synchronously. + vif1Regs.itop = vif1Regs.itops; + vif1Regs.top = vif1Regs.tops & 0x3ffu; + if (vif1Regs.stat.DBF) + { + vif1Regs.tops = vif1Regs.base; + vif1Regs.stat.DBF = false; + } + else + { + vif1Regs.tops = vif1Regs.base + vif1Regs.ofst; + vif1Regs.stat.DBF = true; + } + vif1.queued_program = true; + vif1.queued_pc = addr & 0x7ffu; + vif1.unpackcalls = 0; + vif1.queued_gif_wait = false; + vifExecQueue(1); + break; + } + case PendingVifCmd::Mscalf: + { + vif1Regs.stat.VGW = false; + if (gif_test_hooks::g_force_path1_busy) + { + vif1Regs.stat.VGW = true; + break; // Production stalls; no microprogram dispatch. + } + if (vif1.waitforvu) + break; + const u32 addr = static_cast(vif1Regs.code); + vif1Regs.itop = vif1Regs.itops; + vif1Regs.top = vif1Regs.tops & 0x3ffu; + if (vif1Regs.stat.DBF) + { + vif1Regs.tops = vif1Regs.base; + vif1Regs.stat.DBF = false; + } + else + { + vif1Regs.tops = vif1Regs.base + vif1Regs.ofst; + vif1Regs.stat.DBF = true; + } + vif1.queued_program = true; + vif1.queued_pc = addr & 0x7ffu; + vif1.unpackcalls = 0; + vif1.queued_gif_wait = true; + vifExecQueue(1); + break; + } + case PendingVifCmd::Mscnt: + { + if (vif1.waitforvu) + break; + vif1Regs.itop = vif1Regs.itops; + vif1Regs.top = vif1Regs.tops & 0x3ffu; + if (vif1Regs.stat.DBF) + { + vif1Regs.tops = vif1Regs.base; + vif1Regs.stat.DBF = false; + } + else + { + vif1Regs.tops = vif1Regs.base + vif1Regs.ofst; + vif1Regs.stat.DBF = true; + } + vif1.queued_program = true; + vif1.queued_pc = static_cast(-1); // MSCNT keeps last PC + vif1.unpackcalls = 0; + vif1.queued_gif_wait = false; + vifExecQueue(1); + break; + } + } + } + + // Snapshot VIF1 dispatch-side post-state for this pass before any + // later pass overwrites the globals. Architectural fields only — + // vif1.fifo etc. are dispatcher bookkeeping, not captured here. + Vif1PostState& post = jit ? vif1_jit_post_ : vif1_interp_post_; + post.stat = vif1Regs.stat._u32; + post.tops = vif1Regs.tops; + post.top = vif1Regs.top; + + CpuVU1 = saved_cpu_vu1; + gif_test_hooks::g_force_path1_busy = false; +} + +float EeRecTestHarness::GetVu1VfJit(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + const u32 b = vu1_jit_snapshot_.regs.VF[reg_idx].UL[li]; + float f; std::memcpy(&f, &b, sizeof(f)); return f; +} +float EeRecTestHarness::GetVu1VfInterp(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + const u32 b = vu1_interp_snapshot_.regs.VF[reg_idx].UL[li]; + float f; std::memcpy(&f, &b, sizeof(f)); return f; +} +u32 EeRecTestHarness::GetVu1VfBitsJit(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu1_jit_snapshot_.regs.VF[reg_idx].UL[li]; +} +u32 EeRecTestHarness::GetVu1VfBitsInterp(u32 reg_idx, char lane) const +{ + const u32 li = Vu0LaneIdx(lane); + return vu1_interp_snapshot_.regs.VF[reg_idx].UL[li]; +} +u32 EeRecTestHarness::GetVu1ViJit(u32 reg_idx) const +{ + return vu1_jit_snapshot_.regs.VI[reg_idx].UL & Vu0ViMask(reg_idx); +} +u32 EeRecTestHarness::GetVu1ViInterp(u32 reg_idx) const +{ + return vu1_interp_snapshot_.regs.VI[reg_idx].UL & Vu0ViMask(reg_idx); +} + +bool EeRecTestHarness::HasVu1TerminatedJit() const +{ + return (vu1_jit_snapshot_.regs.VI[REG_VPU_STAT].UL & 0x100u) == 0; +} +bool EeRecTestHarness::HasVu1TerminatedInterp() const +{ + return (vu1_interp_snapshot_.regs.VI[REG_VPU_STAT].UL & 0x100u) == 0; +} + +u32 EeRecTestHarness::GetVif1StatJit() const { return vif1_jit_post_.stat; } +u32 EeRecTestHarness::GetVif1StatInterp() const { return vif1_interp_post_.stat; } +u32 EeRecTestHarness::GetVif1TopsJit() const { return vif1_jit_post_.tops; } +u32 EeRecTestHarness::GetVif1TopsInterp() const { return vif1_interp_post_.tops; } +u32 EeRecTestHarness::GetVif1TopJit() const { return vif1_jit_post_.top; } +u32 EeRecTestHarness::GetVif1TopInterp() const { return vif1_interp_post_.top; } + +void EeRecTestHarness::ExpectVu1Vf(u32 reg_idx, float x, float y, float z, float w) const +{ + auto check = [&](float ex, char lane, const char* side, float v) { + EXPECT_EQ(v, ex) << "vf" << reg_idx << "." << lane << " (" << side << ")"; + }; + check(x, 'x', "jit", GetVu1VfJit (reg_idx, 'x')); + check(y, 'y', "jit", GetVu1VfJit (reg_idx, 'y')); + check(z, 'z', "jit", GetVu1VfJit (reg_idx, 'z')); + check(w, 'w', "jit", GetVu1VfJit (reg_idx, 'w')); + check(x, 'x', "interp", GetVu1VfInterp(reg_idx, 'x')); + check(y, 'y', "interp", GetVu1VfInterp(reg_idx, 'y')); + check(z, 'z', "interp", GetVu1VfInterp(reg_idx, 'z')); + check(w, 'w', "interp", GetVu1VfInterp(reg_idx, 'w')); +} + +void EeRecTestHarness::ExpectVu1Vi(u32 reg_idx, u32 expected) const +{ + EXPECT_EQ(GetVu1ViJit (reg_idx), expected) << "vi" << reg_idx << " (jit)"; + EXPECT_EQ(GetVu1ViInterp(reg_idx), expected) << "vi" << reg_idx << " (interp)"; +} + +} // namespace recompiler_tests + +extern bool recEeIsBlockLinked(u32 src_pc, u32 dst_pc); + +namespace recompiler_tests { + +void EeRecTestHarness::ExpectBlockLinked(u32 src_pc, u32 dst_pc) const +{ + // The live LinkArm64 consumer is the page-boundary / pre-compiled-neighbor + // fall-through tail in recRecompile. Branch-opcode handlers (J/JAL/BEQ/BNE/...) + // add more link sites via their SetBranchImm callers. + EXPECT_TRUE(recEeIsBlockLinked(src_pc, dst_pc)) + << "no LinkArm64 patch site from block containing src_pc=" << std::hex << src_pc + << " to dst_pc=" << dst_pc; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.h b/tests/ctest/core/recompilers/harness/EeRecTestHarness.h new file mode 100644 index 0000000000..7f5a047ec2 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.h @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "MipsEncode.h" +#include "RecompilerTestEnvironment.h" +#include "StateSnapshot.h" +#include "VuEncode.h" +#include "VuSnapshot.h" + +#include "common/Pcsx2Defs.h" + +#include +#include +#include + +namespace recompiler_tests { + +// EE recompiler test harness. Always executes both JIT and interpreter +// paths and diffs architectural state. +class EeRecTestHarness +{ +public: + EeRecTestHarness(); + ~EeRecTestHarness(); + + EeRecTestHarness(const EeRecTestHarness&) = delete; + EeRecTestHarness& operator=(const EeRecTestHarness&) = delete; + + // ---- Pre-state setters ---- + + void SetGpr64(u32 reg_idx, u64 value); + void SetGpr128(u32 reg_idx, u64 lo, u64 hi); + void SetGpr(u32 reg_idx, u32 value) { SetGpr64(reg_idx, static_cast(static_cast(value))); } + void SetHi64(u64 value); + void SetLo64(u64 value); + // Full 128-bit LO/HI setters — PMFHL reads HI.UD[1] / LO.UD[1] (the + // "MULT1/DIV1 upper half") in addition to the lower 64 bits, so tests + // for that op need to seed both halves independently. + void SetLoPair(u64 lo_qw, u64 hi_qw); + void SetHiPair(u64 lo_qw, u64 hi_qw); + void SetCp0(u32 reg_idx, u32 value); + void SetFpr(u32 reg_idx, float value); + void SetFprSingle(u32 reg_idx, float value) { SetFpr(reg_idx, value); } + void SetFprBits(u32 reg_idx, u32 bits); + void SetAcc(float value); + void SetAccBits(u32 bits); + void SetFcr31(u32 value); + + // MMI alias — the PS2's 128-bit paired-word MMI ops address the same GPR + // file as the scalar ops; SetMmiPair is purely a documentation hint at + // call-sites that the test is exercising the upper 64 bits. + void SetMmiPair(u32 reg_idx, u64 lo_qw, u64 hi_qw) { SetGpr128(reg_idx, lo_qw, hi_qw); } + + // Privileged-mode bringup. Tests that exercise MTC0/MFC0/ERET/trap + // delivery want CU[0]=1 so the opcode doesn't trap on a coprocessor- + // unusable exception; FPU tests want CU[1]=1 for the same reason. + // Both are no-ops if Status is already configured. + void EnableCop0(); + void EnableCop1(); + void SetStatusBits(u32 mask); + + // Enables the PS2 "Full" FPU clamp mode (CHECK_FPU_FULL / GameDB eeClampMode:3) + // for the JIT recompile. The shared interpreter has no double-precision path, + // so full-mode tests must use RunJitNoDiff() and assert GetFprBitsJit() against + // a hand-computed double-mode value rather than the auto-diffing Run(). Restored + // to its previous value in the dtor. + void EnableFpuFullMode(); + void EnableFpuMulHack(); + + // ---- Memory ---- + + void WriteU8 (u32 addr, u8 value); + void WriteU16(u32 addr, u16 value); + void WriteU32(u32 addr, u32 value); + void WriteU64(u32 addr, u64 value); + void WriteBytes(u32 addr, const void* src, size_t bytes); + + u8 ReadU8 (u32 addr) const; + u16 ReadU16(u32 addr) const; + u32 ReadU32(u32 addr) const; + u64 ReadU64(u32 addr) const; + + void TrackMemWindow(u32 addr, size_t bytes); + + // Exercises the SMC invalidation path directly — stores one word at + // `hw_addr` via the memWrite/vtlb path that triggers `Cpu->Clear()` on + // any compiled EE block covering that address. + void TriggerSmc(u32 hw_addr, u32 value); + + // Mimics vtlb.cpp's `Cpu->Clear(faulting_pc, 1)` call from the SIGSEGV + // fastmem-backpatch handler — the production entry point for mid-block + // SMC invalidation. Tests that need to assert the recClear straddler + // behavior call this between a Run(PreserveCache) pair so the second + // dispatch must re-recompile rather than reuse the stale block.fnptr + // from the first run. + void SimulateFastmemFault(u32 faulting_pc); + + // ---- Program load ---- + + void LoadProgram(std::initializer_list instructions); + void LoadProgramNoTerm(std::initializer_list instructions); + + // ---- Execute ---- + + // FreshCache (default) — Run() invalidates any cached block at kProgramPc / + // kParkingPc before the JIT pass. Standard mode for one-shot tests where + // the JIT must compile the program from scratch. + // + // PreserveCache — skips the pre-JIT Cpu->Clear calls. Used by SMC tests + // that want a *second* Run() after SimulateFastmemFault() to re-dispatch + // against partially-invalidated blocks (verifying that recClear correctly + // resets straddler block.fnptr so the second dispatch re-compiles cleanly, + // rather than jumping into removed compiled code). + enum class RunMode { FreshCache, PreserveCache }; + + // Runs both interpreter and JIT paths from the same pre-state, + // captures both post-states, and fails the test via gtest if any + // architecturally-significant field diverges. + void Run(RunMode mode = RunMode::FreshCache); + + // JIT-only execution with no interp run and no JIT-vs-interp diff. For tests + // whose JIT path legitimately diverges from the single-precision interpreter + // (the FPU full-mode DOUBLE path) — assert GetFprBitsJit()/GetAccBitsJit() + // against an independently-computed expected value. + void RunJitNoDiff(RunMode mode = RunMode::FreshCache); + + // Authoring mode — one-sided execution, useful when drafting a new + // test before the corresponding JIT opcode handler exists. The JIT + // snapshot is set to equal the interp snapshot so `GetGprJit*()` + // calls return the expected value regardless. + void RunInterpOnly(); + + // ---- Post-run accessors ---- + + u64 GetGpr64Interp(u32 reg_idx) const; + u64 GetGpr64Jit (u32 reg_idx) const; + u32 GetGprInterp (u32 reg_idx) const { return static_cast(GetGpr64Interp(reg_idx)); } + u32 GetGprJit (u32 reg_idx) const { return static_cast(GetGpr64Jit(reg_idx)); } + u64 GetHi64Interp() const; + u64 GetLo64Interp() const; + u32 GetFprBitsInterp(u32 reg_idx) const; + u32 GetFprBitsJit (u32 reg_idx) const; + u32 GetAccBitsInterp() const; + u32 GetAccBitsJit () const; + u32 GetCp0Interp(u32 reg_idx) const; + u32 GetCp0Jit (u32 reg_idx) const; + + // ---- Expect helpers (paired-side architectural assertions) ---- + // + // Each helper asserts that *both* the JIT and interp snapshots hold the + // expected value. Divergence between the two is already caught by Run()'s + // internal diff; these helpers go further and require that both sides + // match the *expected* value — catches cases where the test and the real + // behavior agree with each other but disagree with the spec. + + void ExpectGpr64(u32 reg_idx, u64 expected) const; + void ExpectGpr128(u32 reg_idx, u64 lo, u64 hi) const; + void ExpectMmiPair(u32 reg_idx, u64 lo_qw, u64 hi_qw) const { ExpectGpr128(reg_idx, lo_qw, hi_qw); } + void ExpectFpr(u32 reg_idx, u32 bits) const; + void ExpectAcc(u32 bits) const; + + // Introspects the `recBlocks` BaseBlocks map after Run() and asserts that + // a LinkArm64 patch site within the block containing `src_pc` targets + // `dst_pc`. Backed by the `recEeIsBlockLinked(src_pc, dst_pc)` query + // using BaseBlocks::Arm64Links(). + void ExpectBlockLinked(u32 src_pc, u32 dst_pc) const; + + const EeSnapshot& JitSnapshot() const { return jit_snapshot_; } + const EeSnapshot& InterpSnapshot() const { return interp_snapshot_; } + + // ---- VU0 cross-tree handoff support ---- + // + // Off by default. Tests that exercise COP2 macro mode, VCALLMS, + // CFC2/CTC2/QMFC2/QMTC2/LQC2/SQC2 must call EnableVu0Capture() once + // before LoadProgram() so Run() snapshots VU0 state on both sides + // and includes any divergence in the diff. The VU0 snapshot is + // scoped to the architectural register file; pipeline flags are + // compared in PipelinePermissive mode (the JIT and interp legitimately + // reorder writes within a stage — see VuSnapshot.h). + void EnableVu0Capture(); + + // Opt a specific VU0 VI index out of Run()'s JIT-vs-interp auto-diff. + // Used when the JIT and interp legitimately disagree on a register by + // design (e.g. REG_STATUS_FLAG: the microVU JIT path masks the write to + // the 0xFC0 sticky field + denormalizes into micro_statusflags, while the + // shared interpreter CTC2 does a plain full-width VI store). The test then + // asserts the JIT post-state directly via Vu0JitSnapshot(). Forwarded to + // DiffVu's ignored_vi parameter. + void IgnoreVu0Vi(u32 reg_idx) { vu0_ignored_vi_.push_back(static_cast(reg_idx)); } + + void SeedVu0Vf(u32 reg_idx, float x, float y, float z, float w); + void SeedVu0VfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w); + void SeedVu0Acc(float x, float y, float z, float w); + void SeedVu0AccBits(u32 x, u32 y, u32 z, u32 w); + void SeedVu0Vi(u32 reg_idx, u32 value); + void SeedVu0Microprogram(u32 byte_offset, std::initializer_list pairs); + + u32 GetVu0VfBitsJit (u32 reg_idx, char lane) const; + u32 GetVu0VfBitsInterp(u32 reg_idx, char lane) const; + float GetVu0VfJit (u32 reg_idx, char lane) const; + float GetVu0VfInterp (u32 reg_idx, char lane) const; + u32 GetVu0AccBitsJit (char lane) const; + u32 GetVu0AccBitsInterp(char lane) const; + u32 GetVu0ViJit (u32 reg_idx) const; + u32 GetVu0ViInterp (u32 reg_idx) const; + + const VuSnapshot& Vu0JitSnapshot() const { return vu0_jit_snapshot_; } + const VuSnapshot& Vu0InterpSnapshot() const { return vu0_interp_snapshot_; } + + // ---- EE↔VU1 VIF dispatch support ---- + // + // Off by default. Tests that exercise MSCAL / MSCALF / MSCNT call + // EnableVu1VifCapture() once before LoadProgram() so Run() will: + // (1) reset vif1 + vif1Regs to a known baseline, + // (2) seed VIF1 state set via SetVif1*/SetGifPath1Busy, + // (3) after each EE pass, swap CpuVU1 to the matching engine + // (CpuMicroVU1 for the JIT pass, CpuIntVU1 for the interp pass) + // and fire each queued VIF1 command via vifExecQueue(1) under + // INSTANT_VU1=true so the microprogram runs to completion, + // (4) snapshot VU1 (architectural register file + a 1 KiB scratchpad + // window) on both sides and diff in PipelinePermissive mode. + // + // vif1Reset() + path-1-busy stub run between passes so the second pass + // fires from the same baseline as the first. + void EnableVu1VifCapture(); + + void SeedVu1Microprogram(u32 byte_offset, std::initializer_list pairs); + void SeedVu1Vf(u32 reg_idx, float x, float y, float z, float w); + void SeedVu1VfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w); + void SeedVu1Vi(u32 reg_idx, u32 value); + + // VIF1 command queue. Each command is fired in the order queued during + // each pass (JIT and interp) — direct injection bypasses the DMAC + FIFO, + // mimicking the post-tag state that vif1Transfer would land in. + void QueueVif1Mscal(u16 microprogram_addr); // 0x14XXXXXX + void QueueVif1Mscalf(u16 microprogram_addr); // 0x15XXXXXX (path-1 wait) + void QueueVif1Mscnt(); // 0x17000000 + + // State applied to vif1Regs / vif1 / gif_test_hooks at the start of each + // VIF1 pass (after vif1Reset, before any queued command fires). + void SetGifPath1Busy(bool busy); + void SetVif1WaitForVu(bool wait); + void SetVif1Doublebuffer(u16 base_qw, u16 ofst_qw); + + // Post-Run() VU1 register accessors. *Jit / *Interp variants return the + // snapshot taken at the end of the matching pass. Use ExpectVu1* to + // assert both sides agree on a specific value. + float GetVu1VfJit (u32 reg_idx, char lane) const; + float GetVu1VfInterp (u32 reg_idx, char lane) const; + u32 GetVu1VfBitsJit (u32 reg_idx, char lane) const; + u32 GetVu1VfBitsInterp(u32 reg_idx, char lane) const; + u32 GetVu1ViJit (u32 reg_idx) const; + u32 GetVu1ViInterp (u32 reg_idx) const; + + bool HasVu1TerminatedJit() const; + bool HasVu1TerminatedInterp() const; + + // Post-Run() VIF1 register accessors — the dispatch-side state both + // engines should agree on (stat.VGW after a path-1-busy MSCALF, the + // double-buffered tops/top values, etc.). + u32 GetVif1StatJit() const; + u32 GetVif1StatInterp() const; + u32 GetVif1TopsJit() const; + u32 GetVif1TopsInterp() const; + u32 GetVif1TopJit() const; + u32 GetVif1TopInterp() const; + + // Convenience — both sides must equal `expected`. + void ExpectVu1Vf(u32 reg_idx, float x, float y, float z, float w) const; + void ExpectVu1Vi(u32 reg_idx, u32 expected) const; + + const VuSnapshot& Vu1JitSnapshot() const { return vu1_jit_snapshot_; } + const VuSnapshot& Vu1InterpSnapshot() const { return vu1_interp_snapshot_; } + +private: + static constexpr s32 kCycleBudget = 1024; + static constexpr u32 kMaxInstructions = 2048; + + void LoadProgramImpl(std::initializer_list instructions, bool append_term); + void SeedEntryState(); + void StepInterpUntilParkedOrTimeout(); + void MergeTrackedWindow(u32 addr, size_t bytes); + + // Fire one VIF1 dispatch pass (JIT or interp). Resets vif1 state, + // applies SetVif1*/SetGifPath1Busy values, swaps CpuVU1 to the + // matching engine, and runs each queued command via vifExecQueue(1). + void FireVif1Pass(bool jit); + + std::vector program_words_; + std::vector mem_windows_; + + EeSnapshot pre_snapshot_; + EeSnapshot jit_snapshot_; + EeSnapshot interp_snapshot_; + + bool capture_vu0_ = false; + std::vector vu0_ignored_vi_; + VuSnapshot vu0_pre_snapshot_; + VuSnapshot vu0_jit_snapshot_; + VuSnapshot vu0_interp_snapshot_; + + // VIF1 dispatch capture state. + struct PendingVifCmd + { + enum Kind { Mscal, Mscalf, Mscnt }; + Kind kind; + u32 code; // full vif1Regs.code value to install + }; + bool capture_vu1_ = false; + bool vu1_state_path1_busy_ = false; + bool vu1_state_waitforvu_ = false; + bool vu1_state_dbf_set_ = false; + u16 vu1_state_base_qw_ = 0; + u16 vu1_state_ofst_qw_ = 0; + std::vector vif1_queue_; + VuSnapshot vu1_pre_snapshot_; + VuSnapshot vu1_jit_snapshot_; + VuSnapshot vu1_interp_snapshot_; + + // VIF1-side post-state captured at the end of each FireVif1Pass — kept + // as raw u32 to avoid pulling Vif.h into this header. + struct Vif1PostState + { + u32 stat; + u32 tops; + u32 top; + }; + Vif1PostState vif1_jit_post_{}; + Vif1PostState vif1_interp_post_{}; + + // Owned storage for the GIF path-1 sink while capture_vu1_ is on. Drains + // any incidental XGKICK during VU1 termination so MTGS::WaitGS doesn't + // fire. Cleared in the dtor. + std::vector vu1_path1_sink_; + + bool has_run_ = false; + + bool fpu_full_mode_changed_ = false; + bool prev_fpu_full_mode_ = false; + bool fpu_mul_hack_changed_ = false; + bool prev_fpu_mul_hack_ = false; +}; + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/JitTestHarness.cpp b/tests/ctest/core/recompilers/harness/JitTestHarness.cpp new file mode 100644 index 0000000000..e79c1c518c --- /dev/null +++ b/tests/ctest/core/recompilers/harness/JitTestHarness.cpp @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "JitTestHarness.h" + +#include "IopMem.h" +#include "R3000A.h" + +#include +#include + +namespace recompiler_tests { + +namespace { + +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; +constexpr u32 kParkingPc = RecompilerTestEnvironment::kParkingPc; + +// Writes a single 32-bit word to IOP RAM via the memory system. +void WriteIopU32(u32 addr, u32 value) +{ + iopMemWrite32(addr, value); +} + +// Zero-init every user-visible field of psxRegs. The harness owns the IOP +// register file for the duration of Run(), so any pre-existing contents are +// the previous test's post-state and must be wiped before setting up the +// new pre-state. (cycle / interrupt / event bookkeeping is also zeroed; +// psxInt.ExecuteBlock re-derives iopCycleEE from its parameter.) +void ZeroPsxRegs() +{ + std::memset(&psxRegs, 0, sizeof(psxRegisters)); +} + +} // namespace + +JitTestHarness::JitTestHarness(Mode mode) + : mode_(mode) +{ + EXPECT_TRUE(RecompilerTestEnvironment::IsReady()) + << "RecompilerTestEnvironment was not set up — harness cannot run. " + "Make sure the binary's main.cpp registered the environment."; + ZeroPsxRegs(); + + // The test env installs `psxCpu = &psxInt` to avoid dragging IOP-rec + // compilation into every EE test (see RecompilerTestEnvironment.cpp). + // For the IOP harness, guest-level stores via iopMemWrite* must + // cascade through `psxCpu->Clear` → psxRecClearMem → LUT invalidation, + // which is the real SMC path. Flip psxCpu to &psxRec for the harness + // lifetime in DiffJitVsInterp mode; restore on destruction. + saved_psxCpu_ = psxCpu; + if (mode_ == Mode::DiffJitVsInterp) + psxCpu = &psxRec; +} + +JitTestHarness::~JitTestHarness() +{ + psxCpu = saved_psxCpu_; +} + +void JitTestHarness::SetGpr(u32 reg_idx, u32 value) +{ + if (reg_idx == 0) + return; // r0 is hardwired zero + psxRegs.GPR.r[reg_idx] = value; +} + +void JitTestHarness::SetHi(u32 value) { psxRegs.GPR.r[32] = value; } +void JitTestHarness::SetLo(u32 value) { psxRegs.GPR.r[33] = value; } + +void JitTestHarness::SetCp0(u32 reg_idx, u32 value) +{ + psxRegs.CP0.r[reg_idx] = value; +} + +void JitTestHarness::WriteU8(u32 addr, u8 value) +{ + iopMemWrite8(addr, value); + MergeTrackedWindow(addr & ~0x3u, 4); +} + +void JitTestHarness::WriteU16(u32 addr, u16 value) +{ + iopMemWrite16(addr, value); + MergeTrackedWindow(addr & ~0x3u, 4); +} + +void JitTestHarness::WriteU32(u32 addr, u32 value) +{ + iopMemWrite32(addr, value); + MergeTrackedWindow(addr & ~0x3u, 4); +} + +void JitTestHarness::WriteBytes(u32 addr, const void* src, size_t bytes) +{ + const u8* p = static_cast(src); + for (size_t i = 0; i < bytes; ++i) + iopMemWrite8(addr + static_cast(i), p[i]); + MergeTrackedWindow(addr, bytes); +} + +void JitTestHarness::TrackMemWindow(u32 addr, size_t bytes) +{ + MergeTrackedWindow(addr, bytes); +} + +u8 JitTestHarness::ReadU8 (u32 addr) const { return iopMemRead8(addr); } +u16 JitTestHarness::ReadU16(u32 addr) const { return iopMemRead16(addr); } +u32 JitTestHarness::ReadU32(u32 addr) const { return iopMemRead32(addr); } + +void JitTestHarness::MergeTrackedWindow(u32 addr, size_t bytes) +{ + // Round to 4-byte boundaries and merge contiguous windows. + u32 start = addr & ~0x3u; + size_t end = ((addr + bytes + 3u) & ~0x3u); + size_t new_size = end - start; + + for (auto& w : mem_windows_) + { + const u32 w_end = w.addr + static_cast(w.bytes.size()); + if (start + new_size < w.addr || start > w_end) + continue; // disjoint + // Overlap or touch — merge. + const u32 merged_start = (start < w.addr) ? start : w.addr; + const u32 merged_end = (start + new_size > w_end) ? static_cast(start + new_size) : w_end; + w.addr = merged_start; + w.bytes.resize(merged_end - merged_start); + return; + } + + mem_windows_.push_back(MemWindow{start, std::vector(new_size)}); +} + +void JitTestHarness::LoadProgram(std::initializer_list instructions) +{ + LoadProgramAt(kProgramPc, instructions, /*append_jr_ra_term=*/true); +} + +void JitTestHarness::LoadProgramNoTerm(std::initializer_list instructions) +{ + LoadProgramAt(kProgramPc, instructions, /*append_jr_ra_term=*/false); +} + +void JitTestHarness::LoadProgramAt(u32 pc, + std::initializer_list instructions, + bool append_jr_ra_term) +{ + LoadProgramAt(pc, instructions.begin(), instructions.size(), append_jr_ra_term); +} + +void JitTestHarness::LoadProgramAt(u32 pc, + const u32* instructions, + size_t count, + bool append_jr_ra_term) +{ + const size_t total = count + (append_jr_ra_term ? 2 : 0); + + for (size_t i = 0; i < count; ++i) + WriteIopU32(pc + static_cast(i * 4), instructions[i]); + + if (append_jr_ra_term) + { + WriteIopU32(pc + static_cast(count * 4), mips::JR(mips::reg::ra)); + WriteIopU32(pc + static_cast((count + 1) * 4), mips::NOP); + } + + program_regions_.push_back({pc, static_cast(total)}); +} + +void JitTestHarness::SetPc(u32 pc) +{ + pc_override_ = true; + pc_override_value_ = pc; +} + +void JitTestHarness::SetRa(u32 ra) +{ + ra_override_ = true; + ra_override_value_ = ra; +} + +void JitTestHarness::InvalidateProgramRegions() +{ + if (!psxRec.Clear) + return; + for (const auto& r : program_regions_) + psxRec.Clear(r.pc, r.size_words); + // Parking lot — belt-and-suspenders; iopMemWrite32 at env setup + // should have cleared, but a previous test's block may linger. + psxRec.Clear(kParkingPc, 2); +} + +void JitTestHarness::Run() +{ + ASSERT_FALSE(program_regions_.empty()) + << "LoadProgram*() must be called before Run()"; + + // Starting PC and return address — entry point defaults to kProgramPc + // (the typical single-block test layout); ra lands in the parking-lot + // self-loop so `jr ra` terminators exit cleanly. Either can be + // overridden via SetPc() / SetRa() for multi-block or resume tests. + psxRegs.pc = pc_override_ ? pc_override_value_ : kProgramPc; + psxRegs.GPR.n.ra = ra_override_ ? ra_override_value_ : kParkingPc; + + InvalidateProgramRegions(); + + ExecuteAndDiff(); +} + +void JitTestHarness::RunResume() +{ + ASSERT_FALSE(program_regions_.empty()) + << "LoadProgram*() must be called before RunResume()"; + + // RunResume picks up from whatever state the caller left behind. If + // the caller wants to re-enter from a specific PC, they do + // `h.SetPc(...)` before RunResume. Override fields are respected + // if they've been set; otherwise psxRegs.pc / ra are left alone. + if (pc_override_) + psxRegs.pc = pc_override_value_; + if (ra_override_) + psxRegs.GPR.n.ra = ra_override_value_; + + // No invalidation — the point of Resume is to exercise the cache's + // behavior as of now. Guest stores to program memory via + // iopMemWrite32 already invalidate through psxCpu->Clear, which is + // exactly the SMC path under test. + ExecuteAndDiff(); +} + +void JitTestHarness::ExecuteAndDiff() +{ + // Capture pre-state AFTER any PC/RA seeding so the pre-snapshot used + // for the interpreter side's restore matches what the JIT saw at entry. + pre_snapshot_ = IopSnapshot::Capture(mem_windows_); + + if (mode_ == Mode::DiffJitVsInterp) + { + psxRec.ExecuteBlock(kCycleBudget); + jit_snapshot_ = IopSnapshot::Capture(mem_windows_); + + // Restore pre-state, then run interpreter from the same initial + // conditions. + pre_snapshot_.Restore(); + } + + psxInt.ExecuteBlock(kCycleBudget); + interp_snapshot_ = IopSnapshot::Capture(mem_windows_); + + // In InterpOnly mode, lock the interpreter output as the "spec" for + // GetGprJit() / JitSnapshot() accessors. That way tests that compare + // against specific architectural values pass symmetrically regardless + // of whether the JIT is wired or not. + if (mode_ == Mode::InterpOnly) + jit_snapshot_ = interp_snapshot_; + + has_run_ = true; + + // Overrides apply to ONE run only — consume them. + pc_override_ = false; + ra_override_ = false; + + if (mode_ == Mode::DiffJitVsInterp) + { + const auto diffs = DiffIop(jit_snapshot_, interp_snapshot_); + if (!diffs.empty()) + { + std::ostringstream ss; + ss << "JIT vs INTERP divergence (" << diffs.size() << "):\n"; + for (const auto& d : diffs) + ss << " " << d << "\n"; + ss << "Pre-state:\n"; + PrintIop(ss, pre_snapshot_); + ss << "JIT post-state:\n"; + PrintIop(ss, jit_snapshot_); + ss << "INTERP post-state:\n"; + PrintIop(ss, interp_snapshot_); + ADD_FAILURE() << ss.str(); + } + } +} + +u32 JitTestHarness::GetGprJit(u32 reg_idx) const +{ + return jit_snapshot_.regs.GPR.r[reg_idx]; +} + +u32 JitTestHarness::GetGprInterp(u32 reg_idx) const +{ + return interp_snapshot_.regs.GPR.r[reg_idx]; +} + +u32 JitTestHarness::GetHiJit() const { return jit_snapshot_.regs.GPR.r[32]; } +u32 JitTestHarness::GetLoJit() const { return jit_snapshot_.regs.GPR.r[33]; } +u32 JitTestHarness::GetHiInterp() const { return interp_snapshot_.regs.GPR.r[32]; } +u32 JitTestHarness::GetLoInterp() const { return interp_snapshot_.regs.GPR.r[33]; } + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/JitTestHarness.h b/tests/ctest/core/recompilers/harness/JitTestHarness.h new file mode 100644 index 0000000000..fa8100980d --- /dev/null +++ b/tests/ctest/core/recompilers/harness/JitTestHarness.h @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "MipsEncode.h" +#include "RecompilerTestEnvironment.h" +#include "StateSnapshot.h" + +#include "common/Pcsx2Defs.h" + +#include +#include +#include + +namespace recompiler_tests { + +// In-process differential test harness for the IOP recompiler. +// +// Each test instance owns a fresh pre-state. The usage contract is: +// +// JitTestHarness h; +// h.SetGpr(reg::a0, 100); // seed register state +// h.WriteU32(0x00020000, 0xDEADBEEF); // seed memory +// h.LoadProgram({ // test program +// ADDIU(reg::v0, reg::a0, 7), +// JR(reg::ra), +// NOP, // branch delay slot +// }); +// h.Run(); // runs BOTH paths, gtest-diffs +// EXPECT_EQ(h.GetGprJit(reg::v0), 107u); // optional spec-lock check +// +// Run() executes the program twice: +// 1. Through the IOP recompiler (psxRec.ExecuteBlock). +// 2. From the same pre-state, through the interpreter (psxInt.ExecuteBlock). +// It captures architectural state after each and fails the enclosing gtest +// assertion on any field-level divergence (GPR / HI / LO / CP0 / PC / mem). +// +// When the IOP recompiler is not available on the host, set `mode` to +// HarnessMode::InterpOnly — the JIT path is skipped and the interpreter +// output is locked as the spec. +class JitTestHarness +{ +public: + enum class Mode + { + DiffJitVsInterp, // run both JIT + interp, gtest-diff + InterpOnly, // skip JIT; only captures interpreter post-state + }; + + explicit JitTestHarness(Mode mode = Mode::DiffJitVsInterp); + ~JitTestHarness(); + + JitTestHarness(const JitTestHarness&) = delete; + JitTestHarness& operator=(const JitTestHarness&) = delete; + + // ---- Pre-state setters (mutate psxRegs directly) ---- + + void SetGpr(u32 reg_idx, u32 value); + void SetHi(u32 value); + void SetLo(u32 value); + void SetCp0(u32 reg_idx, u32 value); + + // Write to IOP guest memory at the given byte address. Automatically + // registers the write's address range as a "mem window" that Run() will + // snapshot + diff after execution. + void WriteU8 (u32 addr, u8 value); + void WriteU16(u32 addr, u16 value); + void WriteU32(u32 addr, u32 value); + void WriteBytes(u32 addr, const void* src, size_t bytes); + + // Read back guest memory after Run(). For SW / SB / SH verification. + u8 ReadU8 (u32 addr) const; + u16 ReadU16(u32 addr) const; + u32 ReadU32(u32 addr) const; + + // Declare a memory window that should be compared after Run(). Useful + // for tests that expect a *store* to write something — this lets the + // diff notice the write. + void TrackMemWindow(u32 addr, size_t bytes); + + // ---- Program load ---- + + // Writes `instructions` to IOP RAM at kProgramPc, then appends a + // terminator (`jr ra; nop`). The caller is responsible for any in-block + // branches / delay slots; the terminator's delay slot is a NOP. + // + // Before Run(), `ra` will be set to kParkingPc so the terminator lands + // in the parking-lot tight loop. + void LoadProgram(std::initializer_list instructions); + + // Writes `instructions` verbatim with no terminator. Use for branch / + // jump tests where the author needs to control the exact program + // layout — typically by ending each path with an explicit `JR(ra)` + // or `J(kParkingPc)` + NOP. + void LoadProgramNoTerm(std::initializer_list instructions); + + // Writes `instructions` to arbitrary IOP RAM address `pc`. Appends the + // `jr ra; nop` terminator when `append_jr_ra_term` is true. + // + // Use for multi-block tests that lay blocks out at distinct addresses + // and for SMC tests that want to mutate a specific region. Caller picks + // the mirror (physical `0x00000000`, kseg0 `0x80000000`, kseg1 + // `0xa0000000` — the JIT dispatches via the same RAM either way). + // + // Each call adds a region to the harness's invalidation list; Run() + // invalidates every registered region before compiling. + void LoadProgramAt(u32 pc, + std::initializer_list instructions, + bool append_jr_ra_term = false); + + // Same, but takes a runtime-sized buffer — used by tests that compose + // long programs via std::vector. + void LoadProgramAt(u32 pc, + const u32* instructions, + size_t count, + bool append_jr_ra_term = false); + + // ---- Entry-point overrides ---- + + // Override the PC / RA seeded by Run(). If not set, Run() defaults to + // `pc = kProgramPc` and `ra = kParkingPc`. Multi-block tests that enter + // at block 1's address use the default for pc; SMC tests that jump back + // through the just-mutated program after `RunResume()` call `SetPc()` + // explicitly. Cleared on every Run/RunResume. + void SetPc(u32 pc); + void SetRa(u32 ra); + + // ---- Execution ---- + + // Runs the program through both JIT and interpreter (or just interpreter + // if `mode == InterpOnly`), captures post-state, and issues a gtest + // non-fatal failure (EXPECT_*) if the two paths diverge. + // + // After Run(), the global psxRegs contains the *interpreter* post-state + // (so GetGprInterp() and direct psxRegs reads agree). + void Run(); + + // Like Run(), but does NOT reset pc / ra or invalidate program regions. + // Used after mutating program memory via WriteU32/iopMemWrite32 (SMC + // tests) or as a follow-on after a previous Run() to drive the same + // state through another dispatch. `SetPc()` before RunResume() is the + // intended way to re-enter the program. + void RunResume(); + + // ---- Post-run accessors ---- + // Valid only after Run(). + + u32 GetGprJit(u32 reg_idx) const; + u32 GetGprInterp(u32 reg_idx) const; + u32 GetHiJit() const; + u32 GetLoJit() const; + u32 GetHiInterp() const; + u32 GetLoInterp() const; + + const IopSnapshot& JitSnapshot() const { return jit_snapshot_; } + const IopSnapshot& InterpSnapshot() const { return interp_snapshot_; } + +private: + // EE-cycle budget passed to ExecuteBlock. The interpreter ALWAYS runs + // at least one branch-delimited block to completion before checking the + // budget (the inner loop exits only on a taken branch). For branch + // tests where the taken path lands in a second block terminated by + // `J kParkingPc`, we need the budget to allow a second iteration. + // + // 200 EE cycles covers ~25 IOP instructions — plenty for any unit test + // layout without running the parking-lot self-loop forever. Any block + // that lands in the parking lot hits `j self; nop`, a 2-instruction + // IOP block that burns 16 EE cycles per iteration — the budget still + // runs out in ~12 parking iterations, bounded at sub-microsecond. + static constexpr s32 kCycleBudget = 200; + + struct ProgramRegion + { + u32 pc; + u32 size_words; // recClearIOP / psxRec.Clear take a word count, not bytes + }; + + void InvalidateProgramRegions(); + void MergeTrackedWindow(u32 addr, size_t bytes); + void ExecuteAndDiff(); + + Mode mode_; + R3000Acpu* saved_psxCpu_ = nullptr; // restored in dtor + std::vector program_regions_; // every LoadProgramAt call + std::vector mem_windows_; // windows the diff watches + + bool pc_override_ = false; + u32 pc_override_value_ = 0; + bool ra_override_ = false; + u32 ra_override_value_ = 0; + + IopSnapshot pre_snapshot_; // captured at Run() entry + IopSnapshot jit_snapshot_; // post-JIT + IopSnapshot interp_snapshot_; // post-interp + bool has_run_ = false; +}; + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/MipsEncode.h b/tests/ctest/core/recompilers/harness/MipsEncode.h new file mode 100644 index 0000000000..3459f64908 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/MipsEncode.h @@ -0,0 +1,573 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +// Constexpr MIPS I / II / IOP-specific instruction encoders. +// Used by the recompiler test harness to build tiny programs in C++ without +// a cross-toolchain. Matches MIPS-I plus a few MIPS-II additions used by +// the IOP (R3000A variant) and EE (R5900 variant). +namespace mips { + +namespace reg { +constexpr u32 zero = 0, at = 1, v0 = 2, v1 = 3; +constexpr u32 a0 = 4, a1 = 5, a2 = 6, a3 = 7; +constexpr u32 t0 = 8, t1 = 9, t2 = 10, t3 = 11; +constexpr u32 t4 = 12, t5 = 13, t6 = 14, t7 = 15; +constexpr u32 s0 = 16, s1 = 17, s2 = 18, s3 = 19; +constexpr u32 s4 = 20, s5 = 21, s6 = 22, s7 = 23; +constexpr u32 t8 = 24, t9 = 25, k0 = 26, k1 = 27; +constexpr u32 gp = 28, sp = 29, s8 = 30, ra = 31; +} + +// Low-level formats. +constexpr u32 RType(u32 op, u32 rs, u32 rt, u32 rd, u32 sa, u32 funct) +{ + return (op << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | + ((rd & 0x1F) << 11) | ((sa & 0x1F) << 6) | (funct & 0x3F); +} +constexpr u32 IType(u32 op, u32 rs, u32 rt, u32 imm16) +{ + return (op << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | (imm16 & 0xFFFF); +} +constexpr u32 JType(u32 op, u32 target_word) +{ + return (op << 26) | (target_word & 0x03FFFFFF); +} + +// SPECIAL (op=0) — funct selects. +constexpr u32 SLL (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x00); } +constexpr u32 SRL (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x02); } +constexpr u32 SRA (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x03); } +constexpr u32 SLLV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x04); } +constexpr u32 SRLV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x06); } +constexpr u32 SRAV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x07); } +constexpr u32 JR (u32 rs) { return RType(0, rs, 0, 0, 0, 0x08); } +constexpr u32 JALR(u32 rd, u32 rs) { return RType(0, rs, 0, rd, 0, 0x09); } +constexpr u32 SYSCALL_(u32 code = 0) { return RType(0, 0, 0, 0, 0, 0x0C) | ((code & 0xFFFFF) << 6); } +constexpr u32 BREAK = RType(0, 0, 0, 0, 0, 0x0D); +constexpr u32 MFHI(u32 rd) { return RType(0, 0, 0, rd, 0, 0x10); } +constexpr u32 MTHI(u32 rs) { return RType(0, rs, 0, 0, 0, 0x11); } +constexpr u32 MFLO(u32 rd) { return RType(0, 0, 0, rd, 0, 0x12); } +constexpr u32 MTLO(u32 rs) { return RType(0, rs, 0, 0, 0, 0x13); } +constexpr u32 MULT (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x18); } +constexpr u32 MULTU(u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x19); } +constexpr u32 DIV (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1A); } +constexpr u32 DIVU (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1B); } +constexpr u32 ADD (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x20); } +constexpr u32 ADDU(u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x21); } +constexpr u32 SUB (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x22); } +constexpr u32 SUBU(u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x23); } +constexpr u32 AND (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x24); } +constexpr u32 OR (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x25); } +constexpr u32 XOR (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x26); } +constexpr u32 NOR (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x27); } +constexpr u32 SLT (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2A); } +constexpr u32 SLTU(u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2B); } + +// REGIMM (op=1) — rt selects sub-op. +constexpr u32 BLTZ (u32 rs, s16 off) { return IType(0x01, rs, 0x00, (u16)off); } +constexpr u32 BGEZ (u32 rs, s16 off) { return IType(0x01, rs, 0x01, (u16)off); } +constexpr u32 BLTZAL(u32 rs, s16 off) { return IType(0x01, rs, 0x10, (u16)off); } +constexpr u32 BGEZAL(u32 rs, s16 off) { return IType(0x01, rs, 0x11, (u16)off); } + +// I-type arith/logic. +constexpr u32 ADDI (u32 rt, u32 rs, s16 imm) { return IType(0x08, rs, rt, (u16)imm); } +constexpr u32 ADDIU(u32 rt, u32 rs, s16 imm) { return IType(0x09, rs, rt, (u16)imm); } +constexpr u32 SLTI (u32 rt, u32 rs, s16 imm) { return IType(0x0A, rs, rt, (u16)imm); } +constexpr u32 SLTIU(u32 rt, u32 rs, s16 imm) { return IType(0x0B, rs, rt, (u16)imm); } +constexpr u32 ANDI (u32 rt, u32 rs, u16 imm) { return IType(0x0C, rs, rt, imm); } +constexpr u32 ORI (u32 rt, u32 rs, u16 imm) { return IType(0x0D, rs, rt, imm); } +constexpr u32 XORI (u32 rt, u32 rs, u16 imm) { return IType(0x0E, rs, rt, imm); } +constexpr u32 LUI (u32 rt, u16 imm) { return IType(0x0F, 0, rt, imm); } + +// I-type branches. +constexpr u32 BEQ (u32 rs, u32 rt, s16 off) { return IType(0x04, rs, rt, (u16)off); } +constexpr u32 BNE (u32 rs, u32 rt, s16 off) { return IType(0x05, rs, rt, (u16)off); } +constexpr u32 BLEZ(u32 rs, s16 off) { return IType(0x06, rs, 0, (u16)off); } +constexpr u32 BGTZ(u32 rs, s16 off) { return IType(0x07, rs, 0, (u16)off); } + +// Jumps (absolute, target is byte address). +constexpr u32 J (u32 target) { return JType(0x02, target >> 2); } +constexpr u32 JAL(u32 target) { return JType(0x03, target >> 2); } + +// Loads / stores. `offset` is sign-extended, `base` is the base GPR index. +constexpr u32 LB (u32 rt, s16 off, u32 base) { return IType(0x20, base, rt, (u16)off); } +constexpr u32 LH (u32 rt, s16 off, u32 base) { return IType(0x21, base, rt, (u16)off); } +constexpr u32 LWL(u32 rt, s16 off, u32 base) { return IType(0x22, base, rt, (u16)off); } +constexpr u32 LW (u32 rt, s16 off, u32 base) { return IType(0x23, base, rt, (u16)off); } +constexpr u32 LBU(u32 rt, s16 off, u32 base) { return IType(0x24, base, rt, (u16)off); } +constexpr u32 LHU(u32 rt, s16 off, u32 base) { return IType(0x25, base, rt, (u16)off); } +constexpr u32 LWR(u32 rt, s16 off, u32 base) { return IType(0x26, base, rt, (u16)off); } +constexpr u32 SB (u32 rt, s16 off, u32 base) { return IType(0x28, base, rt, (u16)off); } +constexpr u32 SH (u32 rt, s16 off, u32 base) { return IType(0x29, base, rt, (u16)off); } +constexpr u32 SWL(u32 rt, s16 off, u32 base) { return IType(0x2A, base, rt, (u16)off); } +constexpr u32 SW (u32 rt, s16 off, u32 base) { return IType(0x2B, base, rt, (u16)off); } +constexpr u32 SWR(u32 rt, s16 off, u32 base) { return IType(0x2E, base, rt, (u16)off); } + +// COP0 (op=0x10). +constexpr u32 MFC0(u32 rt, u32 rd) { return (0x10u << 26) | (0x00u << 21) | ((rt & 0x1F) << 16) | ((rd & 0x1F) << 11); } +constexpr u32 MTC0(u32 rt, u32 rd) { return (0x10u << 26) | (0x04u << 21) | ((rt & 0x1F) << 16) | ((rd & 0x1F) << 11); } +constexpr u32 RFE = (0x10u << 26) | (0x10u << 21) | 0x10u; + +// COP0 branch on DMAC condition (op=0x10, rs=BC=0x08; rt selects F/T/FL/TL). +constexpr u32 BC0_(u32 brt, s16 ofs) { return (0x10u << 26) | (0x08u << 21) | ((brt & 0x1F) << 16) | (static_cast(ofs)); } +constexpr u32 BC0F (s16 ofs) { return BC0_(0, ofs); } +constexpr u32 BC0T (s16 ofs) { return BC0_(1, ofs); } +constexpr u32 BC0FL(s16 ofs) { return BC0_(2, ofs); } +constexpr u32 BC0TL(s16 ofs) { return BC0_(3, ofs); } + +constexpr u32 NOP = 0u; + +// --------------------------------------------------------------------------- +// EE / R5900 extensions +// --------------------------------------------------------------------------- +// The EE reuses all of the MIPS-I encoders above (ADDU, AND, BEQ, ...) and +// adds the following families. 64-bit variants share MIPS-III encodings; +// MMI is PS2-specific (op=0x1C) with sub-op selection via `funct`/sub-field. +namespace ee { + +// ---- 64-bit arith/logic (MIPS-III; SPECIAL, op=0) ---- +constexpr u32 DADD (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2C); } +constexpr u32 DADDU (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2D); } +constexpr u32 DSUB (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2E); } +constexpr u32 DSUBU (u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x2F); } + +// 64-bit shifts by immediate (SPECIAL, op=0; sa field) +constexpr u32 DSLL (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x38); } +constexpr u32 DSRL (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x3A); } +constexpr u32 DSRA (u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x3B); } +constexpr u32 DSLL32(u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x3C); } +constexpr u32 DSRL32(u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x3E); } +constexpr u32 DSRA32(u32 rd, u32 rt, u32 sa) { return RType(0, 0, rt, rd, sa, 0x3F); } + +// 64-bit variable shifts (SPECIAL, op=0) +constexpr u32 DSLLV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x14); } +constexpr u32 DSRLV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x16); } +constexpr u32 DSRAV(u32 rd, u32 rt, u32 rs) { return RType(0, rs, rt, rd, 0, 0x17); } + +// 64-bit mul/div (SPECIAL) +constexpr u32 DMULT (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1C); } +constexpr u32 DMULTU(u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1D); } +constexpr u32 DDIV (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1E); } +constexpr u32 DDIVU (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x1F); } + +// 64-bit I-type (MIPS-III; top-level opcodes) +constexpr u32 DADDI (u32 rt, u32 rs, s16 imm) { return IType(0x18, rs, rt, (u16)imm); } +constexpr u32 DADDIU(u32 rt, u32 rs, s16 imm) { return IType(0x19, rs, rt, (u16)imm); } +constexpr u32 LDL (u32 rt, s16 off, u32 base) { return IType(0x1A, base, rt, (u16)off); } +constexpr u32 LDR (u32 rt, s16 off, u32 base) { return IType(0x1B, base, rt, (u16)off); } +constexpr u32 LD (u32 rt, s16 off, u32 base) { return IType(0x37, base, rt, (u16)off); } +constexpr u32 SD (u32 rt, s16 off, u32 base) { return IType(0x3F, base, rt, (u16)off); } +constexpr u32 SDL (u32 rt, s16 off, u32 base) { return IType(0x2C, base, rt, (u16)off); } +constexpr u32 SDR (u32 rt, s16 off, u32 base) { return IType(0x2D, base, rt, (u16)off); } +constexpr u32 LWU (u32 rt, s16 off, u32 base) { return IType(0x27, base, rt, (u16)off); } + +// 128-bit quad load/store (PS2-specific) +constexpr u32 LQ (u32 rt, s16 off, u32 base) { return IType(0x1E, base, rt, (u16)off); } +constexpr u32 SQ (u32 rt, s16 off, u32 base) { return IType(0x1F, base, rt, (u16)off); } + +// Likely branches (MIPS-II/III additions; EE has these) +constexpr u32 BEQL (u32 rs, u32 rt, s16 off) { return IType(0x14, rs, rt, (u16)off); } +constexpr u32 BNEL (u32 rs, u32 rt, s16 off) { return IType(0x15, rs, rt, (u16)off); } +constexpr u32 BLEZL (u32 rs, s16 off) { return IType(0x16, rs, 0, (u16)off); } +constexpr u32 BGTZL (u32 rs, s16 off) { return IType(0x17, rs, 0, (u16)off); } +constexpr u32 BLTZL (u32 rs, s16 off) { return IType(0x01, rs, 0x02, (u16)off); } +constexpr u32 BGEZL (u32 rs, s16 off) { return IType(0x01, rs, 0x03, (u16)off); } + +// MOVZ / MOVN (MIPS-IV; EE has them) +constexpr u32 MOVZ(u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x0A); } +constexpr u32 MOVN(u32 rd, u32 rs, u32 rt) { return RType(0, rs, rt, rd, 0, 0x0B); } + +// MFHI1 / MTHI1 / MFLO1 / MTLO1 use MMI1 encodings +// MULT1 / DIV1 etc. — MMI table (op=0x1C) +constexpr u32 MMI(u32 rs, u32 rt, u32 rd, u32 sa, u32 funct) +{ + return (0x1Cu << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | + ((rd & 0x1F) << 11) | ((sa & 0x1F) << 6) | (funct & 0x3F); +} +// Common MMI-direct funct codes (PS2 programming manual §3 table). +constexpr u32 MADD (u32 rd, u32 rs, u32 rt) { return MMI(rs, rt, rd, 0, 0x00); } +constexpr u32 MADDU (u32 rd, u32 rs, u32 rt) { return MMI(rs, rt, rd, 0, 0x01); } +constexpr u32 PLZCW (u32 rd, u32 rs) { return MMI(rs, 0, rd, 0, 0x04); } +constexpr u32 MFHI1 (u32 rd) { return MMI(0, 0, rd, 0, 0x10); } +constexpr u32 MTHI1 (u32 rs) { return MMI(rs, 0, 0, 0, 0x11); } +constexpr u32 MFLO1 (u32 rd) { return MMI(0, 0, rd, 0, 0x12); } +constexpr u32 MTLO1 (u32 rs) { return MMI(rs, 0, 0, 0, 0x13); } +constexpr u32 MULT1 (u32 rd, u32 rs, u32 rt) { return MMI(rs, rt, rd, 0, 0x18); } +constexpr u32 MULTU1(u32 rd, u32 rs, u32 rt) { return MMI(rs, rt, rd, 0, 0x19); } +constexpr u32 DIV1 (u32 rs, u32 rt) { return MMI(rs, rt, 0, 0, 0x1A); } +constexpr u32 DIVU1 (u32 rs, u32 rt) { return MMI(rs, rt, 0, 0, 0x1B); } + +// ---- MMI SIMD sub-tables (MMI0/1/2/3 via funct-field selector) ---- +// The sub-table is chosen by the funct field (bits 5:0): +// MMI0 = funct 0x08, MMI2 = funct 0x09, MMI1 = funct 0x28, MMI3 = funct 0x29. +// The second-level op (the index into the chosen sub-table) goes in the sa +// field (bits 10:6). Encoded here as one helper per sub-table. +constexpr u32 MMI0(u32 rs, u32 rt, u32 rd, u32 sub) { return MMI(rs, rt, rd, sub, 0x08); } +constexpr u32 MMI2(u32 rs, u32 rt, u32 rd, u32 sub) { return MMI(rs, rt, rd, sub, 0x09); } +constexpr u32 MMI1(u32 rs, u32 rt, u32 rd, u32 sub) { return MMI(rs, rt, rd, sub, 0x28); } +constexpr u32 MMI3(u32 rs, u32 rt, u32 rd, u32 sub) { return MMI(rs, rt, rd, sub, 0x29); } + +// Sub-op indexes verified against pcsx2/R5900OpcodeTables.cpp tbl_MMI*. +// MMI0 — parallel add/sub/compare-greater-than (selected sub-ops) +constexpr u32 PADDW (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x00); } +constexpr u32 PADDH (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x04); } +constexpr u32 PADDB (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x08); } +constexpr u32 PSUBW (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x01); } +constexpr u32 PSUBH (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x05); } +constexpr u32 PSUBB (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x09); } +constexpr u32 PCGTW (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x02); } +constexpr u32 PCGTH (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x06); } +constexpr u32 PCGTB (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x0A); } +constexpr u32 PADDSW(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x10); } +constexpr u32 PSUBSW(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x11); } +constexpr u32 PADDSH(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x14); } +constexpr u32 PSUBSH(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x15); } +constexpr u32 PADDSB(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x18); } +constexpr u32 PSUBSB(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x19); } + +// MMI1 — parallel compare-equal + extensions (selected sub-ops) +constexpr u32 PCEQW (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x02); } +constexpr u32 PCEQH (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x06); } +constexpr u32 PCEQB (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x0A); } +constexpr u32 PADDUW(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x10); } +constexpr u32 PSUBUW(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x11); } +constexpr u32 PADDUH(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x14); } +constexpr u32 PSUBUH(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x15); } +constexpr u32 PADDUB(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x18); } +constexpr u32 PSUBUB(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x19); } + +// MMI0 pack/unpack and min/max — sub-op indices from R5900OpcodeTables.cpp:536-546. +constexpr u32 PMAXW (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x03); } +constexpr u32 PMAXH (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x07); } +constexpr u32 PEXTLW(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x12); } +constexpr u32 PPACW (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x13); } +constexpr u32 PEXTLH(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x16); } +constexpr u32 PPACH (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x17); } +constexpr u32 PEXTLB(u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x1A); } +constexpr u32 PPACB (u32 rd, u32 rs, u32 rt) { return MMI0(rs, rt, rd, 0x1B); } + +// MMI1 absolute-value / min — sub-op indices from R5900OpcodeTables.cpp:548-558. +constexpr u32 PABSW (u32 rd, u32 rt) { return MMI1(0, rt, rd, 0x01); } +constexpr u32 PMINW (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x03); } +constexpr u32 PADSBH(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x04); } +constexpr u32 PABSH (u32 rd, u32 rt) { return MMI1(0, rt, rd, 0x05); } +constexpr u32 PMINH (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x07); } +constexpr u32 PEXTUW(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x12); } +constexpr u32 PEXTUH(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x16); } +constexpr u32 PEXTUB(u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x1A); } +// QFSRV — funnel-shift {Rs:Rt} right by cpuRegs.sa bytes (MMI1 sub 0x1B). +constexpr u32 QFSRV (u32 rd, u32 rs, u32 rt) { return MMI1(rs, rt, rd, 0x1B); } + +// MMI2 — PAND, PXOR, paired copy-low (selected sub-ops) +constexpr u32 PAND (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x12); } +constexpr u32 PXOR (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x13); } +constexpr u32 PCPYLD(u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x0E); } + +// MMI3 — POR, PNOR, paired copy-high, PCPYH (selected sub-ops) +constexpr u32 POR (u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x12); } +constexpr u32 PNOR (u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x13); } +constexpr u32 PCPYUD(u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x0E); } +constexpr u32 PCPYH (u32 rd, u32 rt) { return MMI3(0, rt, rd, 0x1B); } + +// MMI2 / MMI3 — pack/unpack/exchange + HI/LO transfer. +// Sub-op indices verified against tbl_MMI2 / tbl_MMI3 in +// pcsx2/R5900OpcodeTables.cpp:561-583. +constexpr u32 PMFHI (u32 rd) { return MMI2(0, 0, rd, 0x08); } +constexpr u32 PMFLO (u32 rd) { return MMI2(0, 0, rd, 0x09); } +constexpr u32 PMTHI (u32 rs) { return MMI3(rs, 0, 0, 0x08); } +constexpr u32 PMTLO (u32 rs) { return MMI3(rs, 0, 0, 0x09); } +constexpr u32 PINTH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x0A); } + +// MMI2 parallel-mul / mul-acc subops (h-word lanes) — sub-op indices from +// R5900OpcodeTables.cpp tbl_MMI2. +constexpr u32 PMADDH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x10); } +constexpr u32 PMSUBH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x14); } +constexpr u32 PMULTH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x1C); } +constexpr u32 PMULTW (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x0C); } +constexpr u32 PMULTUW(u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x0C); } +constexpr u32 PMADDUW(u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x00); } +constexpr u32 PHMADH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x11); } +constexpr u32 PHMSBH (u32 rd, u32 rs, u32 rt) { return MMI2(rs, rt, rd, 0x15); } +constexpr u32 PEXT5 (u32 rd, u32 rt) { return MMI0(0, rt, rd, 0x1E); } +constexpr u32 PPAC5 (u32 rd, u32 rt) { return MMI0(0, rt, rd, 0x1F); } +constexpr u32 PINTEH (u32 rd, u32 rs, u32 rt) { return MMI3(rs, rt, rd, 0x0A); } +constexpr u32 PEXEH (u32 rd, u32 rt) { return MMI2(0, rt, rd, 0x1A); } +constexpr u32 PREVH (u32 rd, u32 rt) { return MMI2(0, rt, rd, 0x1B); } +constexpr u32 PEXEW (u32 rd, u32 rt) { return MMI2(0, rt, rd, 0x1E); } +constexpr u32 PROT3W(u32 rd, u32 rt) { return MMI2(0, rt, rd, 0x1F); } +constexpr u32 PEXCH (u32 rd, u32 rt) { return MMI3(0, rt, rd, 0x1A); } +constexpr u32 PEXCW (u32 rd, u32 rt) { return MMI3(0, rt, rd, 0x1E); } + +// PMFHL — top-level MMI (funct=0x30) with sa selecting one of 5 sub-formats: +// 0=LW, 1=UW, 2=SLW, 3=LH, 4=SH (see MMI.cpp PMFHL()). +constexpr u32 PMFHL (u32 rd, u32 sub) { return MMI(0, 0, rd, sub, 0x30); } + +// PMTHL — top-level MMI (funct=0x31). Only sa=0 (PMTHL.LW) is defined; +// other sa values are reserved (interp early-exits in MMI.cpp:218). +constexpr u32 PMTHL (u32 rs) { return MMI(rs, 0, 0, 0, 0x31); } + +// Parallel shift immediates — top-level MMI table (funct = 0x34..0x3F). +// Uses sa for shift amount; rs is unused. See tbl_MMI[64] in +// pcsx2/R5900OpcodeTables.cpp:524-534. +constexpr u32 PSLLH (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x34); } +constexpr u32 PSRLH (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x36); } +constexpr u32 PSRAH (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x37); } +constexpr u32 PSLLW (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x3C); } +constexpr u32 PSRLW (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x3E); } +constexpr u32 PSRAW (u32 rd, u32 rt, u32 sa) { return MMI(0, rt, rd, sa, 0x3F); } + +// ---- Trap instructions (SPECIAL, op=0) ---- +constexpr u32 TGE (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x30); } +constexpr u32 TGEU (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x31); } +constexpr u32 TLT (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x32); } +constexpr u32 TLTU (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x33); } +constexpr u32 TEQ (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x34); } +constexpr u32 TNE (u32 rs, u32 rt) { return RType(0, rs, rt, 0, 0, 0x36); } + +// Trap-immediate (REGIMM, op=1) +constexpr u32 TGEI (u32 rs, s16 imm) { return IType(0x01, rs, 0x08, (u16)imm); } +constexpr u32 TGEIU (u32 rs, s16 imm) { return IType(0x01, rs, 0x09, (u16)imm); } +constexpr u32 TLTI (u32 rs, s16 imm) { return IType(0x01, rs, 0x0A, (u16)imm); } +constexpr u32 TLTIU (u32 rs, s16 imm) { return IType(0x01, rs, 0x0B, (u16)imm); } +constexpr u32 TEQI (u32 rs, s16 imm) { return IType(0x01, rs, 0x0C, (u16)imm); } +constexpr u32 TNEI (u32 rs, s16 imm) { return IType(0x01, rs, 0x0E, (u16)imm); } + +// ---- Shift-amount register (PS2-specific) ---- +// MFSA / MTSA live in the SPECIAL table at funct 0x28 / 0x29. +// MTSAB / MTSAH live in the REGIMM table at rt = 0x18 / 0x19. +constexpr u32 MFSA (u32 rd) { return RType(0, 0, 0, rd, 0, 0x28); } +constexpr u32 MTSA (u32 rs) { return RType(0, rs, 0, 0, 0, 0x29); } +constexpr u32 MTSAB (u32 rs, s16 imm) { return IType(0x01, rs, 0x18, (u16)imm); } +constexpr u32 MTSAH (u32 rs, s16 imm) { return IType(0x01, rs, 0x19, (u16)imm); } + +// ---- SYNC (SPECIAL, funct=0x0F) ---- +constexpr u32 SYNC = (0u << 26) | 0x0Fu; + +// ---- COP0 privileged helpers ---- +constexpr u32 ERET = (0x10u << 26) | (0x10u << 21) | 0x18u; +constexpr u32 EI = (0x10u << 26) | (0x10u << 21) | 0x38u; +constexpr u32 DI = (0x10u << 26) | (0x10u << 21) | 0x39u; + +// ---- COP1 (FPU, op=0x11). Single-precision variants (fmt=0x10). ---- +constexpr u32 COP1(u32 rs, u32 ft, u32 fs, u32 fd, u32 funct) +{ + return (0x11u << 26) | ((rs & 0x1F) << 21) | ((ft & 0x1F) << 16) | + ((fs & 0x1F) << 11) | ((fd & 0x1F) << 6) | (funct & 0x3F); +} +constexpr u32 MFC1 (u32 rt, u32 fs) { return COP1(0x00, rt, fs, 0, 0); } +constexpr u32 MTC1 (u32 rt, u32 fs) { return COP1(0x04, rt, fs, 0, 0); } +constexpr u32 CFC1 (u32 rt, u32 fs) { return COP1(0x02, rt, fs, 0, 0); } +constexpr u32 CTC1 (u32 rt, u32 fs) { return COP1(0x06, rt, fs, 0, 0); } +constexpr u32 ADD_S(u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x00); } +constexpr u32 SUB_S(u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x01); } +constexpr u32 MUL_S(u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x02); } +constexpr u32 DIV_S(u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x03); } +// PS2 SQRT.S reads ft (rt slot, bits 20:16), NOT fs. Both PCSX2 interp +// (FPU.cpp _FtValUl_) and the JIT (XMMINFO_READT) follow that quirk; the +// encoder must place the source in the rt slot to match. +constexpr u32 SQRT_S(u32 fd, u32 ft) { return COP1(0x10, ft, 0, fd, 0x04); } +// RSQRT.S: fd = fs / sqrt(ft). funct 0x16; reads fs (dividend) and ft (divisor). +constexpr u32 RSQRT_S(u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x16); } +constexpr u32 ABS_S(u32 fd, u32 fs) { return COP1(0x10, 0, fs, fd, 0x05); } +constexpr u32 MOV_S(u32 fd, u32 fs) { return COP1(0x10, 0, fs, fd, 0x06); } +constexpr u32 NEG_S(u32 fd, u32 fs) { return COP1(0x10, 0, fs, fd, 0x07); } +constexpr u32 CVT_W_S(u32 fd, u32 fs) { return COP1(0x10, 0, fs, fd, 0x24); } +// PS2-specific FPU accumulator family. ADDA/SUBA/MULA write to ACC (no Fd +// field in the op); MADD/MSUB/MADDA/MSUBA are multiply-then-add/sub forms. +constexpr u32 ADDA_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x18); } +constexpr u32 SUBA_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x19); } +constexpr u32 MULA_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x1A); } +constexpr u32 MADD_S (u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x1C); } +constexpr u32 MSUB_S (u32 fd, u32 fs, u32 ft) { return COP1(0x10, ft, fs, fd, 0x1D); } +constexpr u32 MADDA_S(u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x1E); } +constexpr u32 MSUBA_S(u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x1F); } +// Compare — PS2 FPU implements four conditions: F (0x30), EQ (0x32), +// LT (0x34), LE (0x36). NOT the standard MIPS layout that puts LT/LE at +// 0x3C/0x3E — the PS2 table at R5900OpcodeTables.cpp tbl_COP1_S[0x34/0x36] +// is what dispatches recC_LT/recC_LE. +constexpr u32 C_F_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x30); } +constexpr u32 C_EQ_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x32); } +constexpr u32 C_LT_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x34); } +constexpr u32 C_LE_S (u32 fs, u32 ft) { return COP1(0x10, ft, fs, 0, 0x36); } +// Branch on COP1 condition — BC1F (cond=0) / BC1T (cond=1), rt field 0/1 +constexpr u32 BC1F(s16 off) { return (0x11u << 26) | (0x08u << 21) | (0x00u << 16) | (u16)off; } +constexpr u32 BC1T(s16 off) { return (0x11u << 26) | (0x08u << 21) | (0x01u << 16) | (u16)off; } +// FPU 32-bit load/store — top-level opcodes LWC1 (0x31) / SWC1 (0x39). +// ft = FPU register, off(base) = EA. ft occupies the rt slot. +constexpr u32 LWC1(u32 ft, s16 off, u32 base) { return IType(0x31, base, ft, (u16)off); } +constexpr u32 SWC1(u32 ft, s16 off, u32 base) { return IType(0x39, base, ft, (u16)off); } + +// ---- COP2 (VU0 macro mode + microprogram kick + register transfer) ---- +// +// Layout: bits[31:26] = 0x12 (COP2 primary). bits[25:21] = rs sub-selector. +// rs=0x00 MFC2 rs=0x01 QMFC2 rs=0x02 CFC2 +// rs=0x04 MTC2 rs=0x05 QMTC2 rs=0x06 CTC2 +// rs=0x08 BC2 (branches; not used by handoff tests) +// rs>=0x10 (CO=1, bit[25] set): macro-mode VU op or VCALLMS +// +// For COP2-CO ops (CO=1), bits[5:0] = funct (mirror of VU upper-pipe primary +// table for VADDx..VMINIw and the SPECIAL2 trampolines 0x3C..0x3F). Macro +// VADDx fd, fs, ft is then identical in shape to a VU upper-word VADDx +// except the top byte is 0x4A rather than the bare VU upper bits. +// +// Field reuse: _Ft_ = bits[20:16] (rt), _Fs_ = bits[15:11] (rd), _Fd_ = +// bits[10:6] (sa). The XYZW destination mask occupies bits[24:21]. +constexpr u32 COP2(u32 rs, u32 rt, u32 rd, u32 sa, u32 funct) +{ + return (0x12u << 26) | ((rs & 0x1Fu) << 21) + | ((rt & 0x1Fu) << 16) | ((rd & 0x1Fu) << 11) + | ((sa & 0x1Fu) << 6) | (funct & 0x3Fu); +} + +// Register-transfer ops — rt is an EE GPR index, fs is a VU0 register index. +// CFC2/CTC2 access VI[fs]; QMFC2/QMTC2/MFC2/MTC2 access VF[fs]. +constexpr u32 CFC2 (u32 rt, u32 fs) { return COP2(0x02, rt, fs, 0, 0); } +constexpr u32 CTC2 (u32 rt, u32 fs) { return COP2(0x06, rt, fs, 0, 0); } +constexpr u32 MFC2 (u32 rt, u32 fs) { return COP2(0x00, rt, fs, 0, 0); } +constexpr u32 MTC2 (u32 rt, u32 fs) { return COP2(0x04, rt, fs, 0, 0); } +constexpr u32 QMFC2(u32 rt, u32 fs) { return COP2(0x01, rt, fs, 0, 0); } +constexpr u32 QMTC2(u32 rt, u32 fs) { return COP2(0x05, rt, fs, 0, 0); } + +// COP2 condition branches — rs=0x08 (BC2), rt selects the variant; imm16 is the +// signed branch offset (NOT the CO-op rd/sa/funct layout). CP2COND = bit 8 of +// VU0.VI[REG_VPU_STAT]: BC2F taken when clear, BC2T taken when set; FL/TL are +// the likely (delay-slot-squashing) variants. +constexpr u32 BC2F (s16 off) { return (0x12u << 26) | (0x08u << 21) | (0x00u << 16) | (u16)off; } +constexpr u32 BC2T (s16 off) { return (0x12u << 26) | (0x08u << 21) | (0x01u << 16) | (u16)off; } +constexpr u32 BC2FL(s16 off) { return (0x12u << 26) | (0x08u << 21) | (0x02u << 16) | (u16)off; } +constexpr u32 BC2TL(s16 off) { return (0x12u << 26) | (0x08u << 21) | (0x03u << 16) | (u16)off; } + +// LQC2 / SQC2 — top-level opcode 0x36 / 0x3E. Base+offset addressing. +constexpr u32 LQC2(u32 ft, u32 base, s16 offset) { return IType(0x36, base, ft, (u16)offset); } +constexpr u32 SQC2(u32 ft, u32 base, s16 offset) { return IType(0x3E, base, ft, (u16)offset); } + +// VCALLMS / VCALLMSR — kick a VU0 microprogram. COP2-CO funct 0x38 / 0x39. +// VCALLMS encodes the start-PC/8 in bits[20:6] (15-bit imm). +constexpr u32 VCALLMS (u32 startpc_div8) +{ + return (0x12u << 26) | (1u << 25) | ((startpc_div8 & 0x7FFFu) << 6) | 0x38u; +} +constexpr u32 VCALLMSR() +{ + return (0x12u << 26) | (1u << 25) | 0x39u; +} + +// COP2-CO macro-mode VU ops. The funct field mirrors the VU upper-pipe primary +// opcode (0x28 = VADD, 0x29 = VMADD, 0x2A = VMUL, 0x2B = VMAX, 0x2C = VSUB, +// 0x2D = VMSUB, 0x2E = VOPMSUB, 0x2F = VMINI). All take an XYZW dest mask. +// +// Operand convention matches EE bit layout: rt = ft (bits 20-16), rd = fs +// (bits 15-11), sa = fd (bits 10-6). +constexpr u32 COP2_FMAC(u32 mask_xyzw, u32 fd, u32 fs, u32 ft, u32 funct) +{ + return (0x12u << 26) | (1u << 25) + | ((mask_xyzw & 0xFu) << 21) + | ((ft & 0x1Fu) << 16) | ((fs & 0x1Fu) << 11) + | ((fd & 0x1Fu) << 6) | (funct & 0x3Fu); +} +constexpr u32 VADD_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x28); } +constexpr u32 VMADD_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x29); } + +// VMADDx/y/z/w — broadcast MADD (fd = ACC + fs * ft.bc). COP2 SPECIAL1 funct +// 0x08-0x0B (row 1 of Int_COP2SPECIAL1PrintTable). +constexpr u32 VMADDx_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x08); } +constexpr u32 VMADDy_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x09); } +constexpr u32 VMADDz_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x0A); } +constexpr u32 VMADDw_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x0B); } +// VMULx/y/z/w — broadcast MUL (fd = fs * ft.bc). COP2 SPECIAL1 funct 0x18-0x1B +// (row 3 of Int_COP2SPECIAL1PrintTable). +constexpr u32 VMULx_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x18); } +constexpr u32 VMULy_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x19); } +constexpr u32 VMULz_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x1A); } +constexpr u32 VMULw_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x1B); } +constexpr u32 VMUL_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2A); } +constexpr u32 VMAX_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2B); } +constexpr u32 VSUB_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2C); } +constexpr u32 VMSUB_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2D); } +constexpr u32 VOPMSUB_C2(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2E); } +constexpr u32 VMINI_C2 (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, fd, fs, ft, 0x2F); } + +// Integer-ALU macro ops (funct 0x30/0x31/0x32/0x34/0x35). Same field layout +// as the VU LowerOP integer table but issued as EE COP2-CO. The SA field is +// the VU integer destination index (4-bit valid). +constexpr u32 VIADD_C2 (u32 id, u32 is, u32 it) { return COP2_FMAC(0, id, is, it, 0x30); } +constexpr u32 VISUB_C2 (u32 id, u32 is, u32 it) { return COP2_FMAC(0, id, is, it, 0x31); } +constexpr u32 VIAND_C2 (u32 id, u32 is, u32 it) { return COP2_FMAC(0, id, is, it, 0x34); } +constexpr u32 VIOR_C2 (u32 id, u32 is, u32 it) { return COP2_FMAC(0, id, is, it, 0x35); } + +// Fixed-point conversion macro ops (COP2-CO SPECIAL2 indices 16-23). FTOI/ITOF +// share funct 0x3C..0x3F (the SPECIAL2 escape) with the sub-op selector in the +// _Sa_/fd field (bits 10-6): 0x04 = ITOFx, 0x05 = FTOIx. The funct low 2 bits +// pick the fixed-point fraction (0x3C/3D/3E/3F = 0/4/12/15). ft (bits 20-16) is +// the destination, fs (bits 15-11) the source. +constexpr u32 VFTOI0_C2 (u32 mask_xyzw, u32 ft, u32 fs) { return COP2_FMAC(mask_xyzw, 0x05, fs, ft, 0x3C); } +constexpr u32 VFTOI4_C2 (u32 mask_xyzw, u32 ft, u32 fs) { return COP2_FMAC(mask_xyzw, 0x05, fs, ft, 0x3D); } +constexpr u32 VFTOI12_C2(u32 mask_xyzw, u32 ft, u32 fs) { return COP2_FMAC(mask_xyzw, 0x05, fs, ft, 0x3E); } +constexpr u32 VFTOI15_C2(u32 mask_xyzw, u32 ft, u32 fs) { return COP2_FMAC(mask_xyzw, 0x05, fs, ft, 0x3F); } +constexpr u32 VITOF0_C2 (u32 mask_xyzw, u32 ft, u32 fs) { return COP2_FMAC(mask_xyzw, 0x04, fs, ft, 0x3C); } + +// VCLIP — COP2-CO SPECIAL2 index 31 (sub-op 0x07, funct 0x3F). Tests fs.xyz +// against |ft.w|; folds a 6-bit result into VI[REG_CLIP_FLAG] (no FD write). +constexpr u32 VCLIP_C2 (u32 ft, u32 fs) { return COP2_FMAC(0, 0x07, fs, ft, 0x3F); } + +// VMULAw — broadcast-accumulator MUL (ACC = fs * ft.w). COP2-CO SPECIAL2 index +// 27 (sub-op 0x06, funct 0x3F). VMULAx/y/z are indices 24-26 (funct 0x3C/3D/3E). +constexpr u32 VMULAw_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x06, fs, ft, 0x3F); } +constexpr u32 VMULAx_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x06, fs, ft, 0x3C); } +constexpr u32 VMULAy_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x06, fs, ft, 0x3D); } +constexpr u32 VMULAz_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x06, fs, ft, 0x3E); } + +// COP2-CO SPECIAL2 (LowerOP2 trampolines via SPEC1 funct 0x3C..0x3F). +// The SPEC2 dispatch index inside recCOP2SPECIAL2t is +// (code & 0x3) | ((code >> 4) & 0x7c) +// = (sa << 2) | (funct & 3); i.e. sa selects the 4-op group and the low 2 +// bits of funct pick within the group. funct's high nibble is always 0x3C. +constexpr u32 COP2_SPEC2(u32 mask_xyzw, u32 ft, u32 fs, u32 sa, u32 funct) +{ + return (0x12u << 26) | (1u << 25) + | ((mask_xyzw & 0xFu) << 21) + | ((ft & 0x1Fu) << 16) | ((fs & 0x1Fu) << 11) + | ((sa & 0x1Fu) << 6) | (funct & 0x3Fu); +} + +// VF load/store with VI post/pre-inc/dec (sa group = 0x0D). +// VLQI / VLQD: dest VF[ft] at bits[20:16], addr VI[is] at bits[15:11]. +// VSQI / VSQD: addr VI[it] at bits[20:16], src VF[fs] at bits[15:11]. +// (Load and store ops have the VF/VI positions swapped — load's dest VF +// shares the slot that store's addr VI uses.) +constexpr u32 VLQI_C2(u32 mask_xyzw, u32 ft, u32 is) { return COP2_SPEC2(mask_xyzw, ft, is, 0x0D, 0x3C); } +constexpr u32 VSQI_C2(u32 mask_xyzw, u32 fs, u32 it) { return COP2_SPEC2(mask_xyzw, it, fs, 0x0D, 0x3D); } +constexpr u32 VLQD_C2(u32 mask_xyzw, u32 ft, u32 is) { return COP2_SPEC2(mask_xyzw, ft, is, 0x0D, 0x3E); } +constexpr u32 VSQD_C2(u32 mask_xyzw, u32 fs, u32 it) { return COP2_SPEC2(mask_xyzw, it, fs, 0x0D, 0x3F); } + +// VI ↔ VF transfer + VU memory ops on VI bank (sa group = 0x0F). +// VMTIR : VI[it] = VF[fs].lane(fsf). fsf occupies bits[22:21] = mask[1:0]. +// VMFIR : VF[ft].mask = sign_extend(VI[is]). +// VILWR : VI[it] = Mem[VI[is] * 16].lane (mask picks lane). +// VISWR : Mem[VI[is] * 16].lane = VI[it] (interp _Is_ = addr base, _It_ = value). +constexpr u32 VMTIR_C2(u32 fsf, u32 it, u32 fs) { return COP2_SPEC2(fsf & 0x3, it, fs, 0x0F, 0x3C); } +constexpr u32 VMFIR_C2(u32 mask_xyzw, u32 ft, u32 is) { return COP2_SPEC2(mask_xyzw, ft, is, 0x0F, 0x3D); } +constexpr u32 VILWR_C2(u32 mask_xyzw, u32 it, u32 is) { return COP2_SPEC2(mask_xyzw, it, is, 0x0F, 0x3E); } +constexpr u32 VISWR_C2(u32 mask_xyzw, u32 it, u32 is) { return COP2_SPEC2(mask_xyzw, it, is, 0x0F, 0x3F); } + +// VOPMULA — SPEC2 group, sa=0xB, funct=0x3E. Result written to ACC, not VF[fd]. +// PS2 hardware always writes XYZ lanes of ACC; W is preserved regardless of mask. +constexpr u32 VOPMULA_C2(u32 mask_xyzw, u32 fs, u32 ft) { return COP2_SPEC2(mask_xyzw, ft, fs, 0xB, 0x3E); } + +// R-register / LFSR ops (sa group = 0x10). +// VRNEXT : advance LFSR; VF[ft].mask = R | 0x3F800000. +// VRGET : VF[ft].mask = R | 0x3F800000. +// VRINIT : R = (VF[fs].lane(fsf) & 0x007FFFFF) | 0x3F800000. +// VRXOR : R ^= VF[fs].lane(fsf). +constexpr u32 VRNEXT_C2(u32 mask_xyzw, u32 ft) { return COP2_SPEC2(mask_xyzw, ft, 0, 0x10, 0x3C); } +constexpr u32 VRGET_C2 (u32 mask_xyzw, u32 ft) { return COP2_SPEC2(mask_xyzw, ft, 0, 0x10, 0x3D); } +constexpr u32 VRINIT_C2(u32 fsf, u32 fs){ return COP2_SPEC2(fsf & 0x3, 0, fs, 0x10, 0x3E); } +constexpr u32 VRXOR_C2 (u32 fsf, u32 fs){ return COP2_SPEC2(fsf & 0x3, 0, fs, 0x10, 0x3F); } + +} // namespace ee + +} // namespace mips diff --git a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp new file mode 100644 index 0000000000..c57b3ab385 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "RecompilerTestEnvironment.h" +#include "MipsEncode.h" + +#include "Config.h" +#include "IopCounters.h" +#include "IopHw.h" +#include "IopMem.h" +#include "Memory.h" +#include "R3000A.h" +#include "R5900.h" +#include "VMManager.h" +#include "VUmicro.h" +#include "arm64/microVU_Persist-arm64.h" +#include "common/FPControl.h" + +#include "cpuinfo.h" + +#include + +extern s32 psxNextDeltaCounter; +extern u64 psxNextStartCounter; +extern s32 nextDeltaCounter; +extern u64 nextStartCounter; + +namespace recompiler_tests { + +namespace { + +bool s_ready = false; + +void InstallParkingLot() +{ + // `j kParkingPc; nop` — the harness's `jr ra` sentinel target. Any + // block the JIT compiles here is an infinite self-loop that will run + // until the ExecuteBlock cycle budget is exhausted. + iopMemWrite32(RecompilerTestEnvironment::kParkingPc + 0, mips::J(RecompilerTestEnvironment::kParkingPc)); + iopMemWrite32(RecompilerTestEnvironment::kParkingPc + 4, mips::NOP); + + // Same convention for EE — parking lot at the same guest address + // (different physical memory). EE harness loops intCpu.Step() until + // `cpuRegs.pc == kParkingPc`, so the J is never actually executed; it's + // here so that if any path accidentally falls through, it still self- + // loops rather than escaping into uninitialized memory. + memWrite32(RecompilerTestEnvironment::kParkingPc + 0, mips::J(RecompilerTestEnvironment::kParkingPc)); + memWrite32(RecompilerTestEnvironment::kParkingPc + 4, mips::NOP); +} + +void InstallEeExceptionVectorStubs() +{ + // EE trap-bearing opcodes (TEQ/TGE/TLT/TNE + immediate variants + + // SYSCALL/BREAK) call cpuException, which with BEV=0 / EXL=0 lands PC + // at 0x80000000+offset (R5900.cpp:94). Without a handler, the interp + // step-loop walks zero-bytes-as-NOPs forever and wedges the harness. + // + // Both EeRecTestHarness and JitTestHarness seed ra=kParkingPc on entry + // and traps don't clobber ra, so a `jr ra; nop` stub at each vector + // returns cleanly to the parking lot — giving trap-taken tests a + // well-defined post-state (architecturally-correct Cause/EPC/Status.EXL + // + PC parked). + // + // Cover all five BEV=0 vectors (not just 0x80000180) so tests + // exercising interrupt/TLB/L2 paths don't each need to re-install. BEV=1 + // mirrors at 0xBFC00200+offset are BIOS-ROM territory and stay + // uninstalled; tests that intentionally set BEV can install their own. + static constexpr u32 kBev0Vectors[] = { + 0x80000000, // TLB refill + 0x80000080, // Level-2 (perf counter) + 0x80000100, // Level-2 (debug) + 0x80000180, // Common (trap/syscall/break + general exceptions) + 0x80000200, // Interrupt (when Cause.IV=0; IV=1 diverts to +0x200 as well) + }; + for (const u32 va : kBev0Vectors) + { + memWrite32(va + 0, mips::JR(mips::reg::ra)); + memWrite32(va + 4, mips::NOP); + } +} + +} // namespace + +bool RecompilerTestEnvironment::Initialize() +{ + if (s_ready) + return true; + + // 1. FPU default rounding mode — upstream does this in + // VMManager::Internal::CPUThreadInitialize before any JIT setup. + FPControlRegister::SetCurrent(FPControlRegister::GetDefault()); + + // 2. cpuinfo. Used by emitter ISA checks; harmless if already done. + if (!cpuinfo_initialize()) + std::fprintf(stderr, "[recompiler_tests] cpuinfo_initialize() failed\n"); + + // 3. VM memory regions (EE / IOP / VU RAM + all rec code buffers). + if (!SysMemory::Allocate()) + return false; + + // 4-5. CPU providers. Reserve allocates BASEBLOCK tables and the JIT + // dispatcher's code buffer; Reset initializes them + emits the + // dispatcher prologue. + psxRec.Reserve(); + psxInt.Reserve(); + recCpu.Reserve(); + intCpu.Reserve(); + // Persisted-JIT cache: take manual control of recording so the + // production mVUinit/mVUreset SyncRecordingFromConfig calls don't drive + // it from EmuConfig — the persist/abi/disk tests set recording (and the + // on-disk cache) explicitly. Must precede the Reserve calls below, since + // CpuMicroVU0.Reserve runs mVUinit (which calls SyncRecordingFromConfig). + mVUPersist::SetTestManualRecording(true); + + // microVU JITs. mVUinit allocates the per-VU regAlloc + sets cache + // pointers; the corresponding Reset call below emits dispatchers. + // CpuMicroVU1::Reserve also opens vu1Thread — with THREAD_VU1=false it + // parks waiting for work that never comes; TearDown joins it via + // CpuMicroVU1.Shutdown(). + CpuMicroVU0.Reserve(); + CpuMicroVU1.Reserve(); + + // 6. SysMemory::Reset zeroes + remaps IOP LUT + EE mem + VU mem. + SysMemory::Reset(); + + // 7-8. Per-CPU reset. + psxRec.Reset(); + psxInt.Reset(); + recCpu.Reset(); + intCpu.Reset(); + CpuMicroVU0.Reset(); + CpuMicroVU1.Reset(); + + // iopMemWrite32 calls `psxCpu->Clear(addr, 1)` after every store to invalidate + // the JIT's block cache for self-modifying code. Must be non-null before any + // memory write. Default to the IOP interpreter; the IOP JIT harness flips + // this to psxRec in its ctor when DiffJitVsInterp mode is active. + // + // Why not default to psxRec? EE branches call cpuEventTest → + // `psxCpu->ExecuteBlock` on every branch — so using the JIT here would drag + // IOP-rec compilation into every EE test, needlessly. The EE tests want a + // benign IOP that just burns a few cycles, which is exactly what psxInt + // provides. + psxCpu = &psxInt; + + // EE: wire the interpreter as the active Cpu. memWrite* → vtlb store paths + // eventually call `Cpu->Clear(...)` to invalidate compiled blocks; Cpu must + // be non-null before any EE memory write. intCpu.Clear is a no-op, which is + // fine for InterpOnly mode. + Cpu = &intCpu; + + // VU interpreters. _cpuEventTest_Shared calls CpuVU0/CpuVU1->ExecuteBlock + // at the end of every EE branch; these pointers default to null and would + // segfault on first branch. CpuIntVU0 / CpuIntVU1 are always-linked + // interpreter instances (no VU rec required). VU tests reuse these as + // the diff baseline. + CpuVU0 = &CpuIntVU0; + CpuVU1 = &CpuIntVU1; + + // Pin VU-related EmuConfig flags off for determinism. THREAD_VU1 forks + // VU1 dispatch into a separate thread (parallel-universe code paths); + // vu1Instant short-circuits VU1 cycle accounting; XgKickHack alters + // XGKICK cycle accumulation per-game. None of these belong in a unit + // test where determinism is the contract. + EmuConfig.Speedhacks.vuThread = false; + EmuConfig.Speedhacks.vu1Instant = false; + EmuConfig.Gamefixes.XgKickHack = false; + + // 9. Parking lot for test programs' `jr ra` sentinel + EE exception + // vector `jr ra; nop` stubs that trap-bearing opcode tests return + // through. + InstallParkingLot(); + InstallEeExceptionVectorStubs(); + + // 10. Initialize IOP counters to a state where the IOP event test is a + // no-op for the duration of any single-block test. Without this, + // `psxTestCycle(psxNextStartCounter, psxNextDeltaCounter)` fires + // after 1 cycle (both values default to 0), which calls + // psxRcntUpdate() → DEV9 ATA::Async on an uninitialized `this`. + // Setting the delta to INT32_MAX makes the counter check return + // false until a test runs for 2^31 IOP cycles — effectively never. + psxRcntInit(); + psxNextDeltaCounter = 0x7FFFFFFF; + psxNextStartCounter = 0; + + // 11. Same story for the EE. On any EE branch, `intEventTest` → + // `_cpuEventTest_Shared` → + // `if (cpuTestCycle(nextStartCounter, nextDeltaCounter)) rcntUpdate()` + // → `rcntUpdate_vSync` → `VSyncStart` → input-poll (null deref on the + // uninitialized InputManager). Setting the delta to INT32_MAX prevents + // cpuTestCycle from ever firing during a test-length run. + nextDeltaCounter = 0x7FFFFFFF; + nextStartCounter = 0; + + s_ready = true; + return true; +} + +void RecompilerTestEnvironment::Shutdown() +{ + if (!s_ready) + return; + + psxRec.Shutdown(); + psxInt.Shutdown(); + recCpu.Shutdown(); + intCpu.Shutdown(); + CpuMicroVU0.Shutdown(); + CpuMicroVU1.Shutdown(); // joins vu1Thread + SysMemory::Release(); + + s_ready = false; +} + +bool RecompilerTestEnvironment::IsReady() +{ + return s_ready; +} + +void RecompilerTestEnvironment::ResetVuBlockCache(int vu_index) +{ + // Reset() re-emits the dispatcher and zeroes mVU.prog.lpState, which + // invalidates every compiled block — the cheapest way to ensure a fresh + // test cannot inherit a compiled variant whose entry pState happens to + // match the new test's seeded entry state. + if (vu_index == 0) + CpuMicroVU0.Reset(); + else + CpuMicroVU1.Reset(); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.h b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.h new file mode 100644 index 0000000000..bbae70a923 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Types.h" + +namespace recompiler_tests { + +// Stand-up logic for a headless PCSX2 core suitable for in-process +// recompiler work — no Qt, no VMManager, no BIOS. Used by both the gtest +// recompiler suite (via RecompilerTestGtestEnvironment) and the +// pcsx2-vurunner binary, which is gtest-free. +// +// Lifecycle (Initialize): +// 1. FPU default rounding mode. +// 2. cpuinfo_initialize(). +// 3. SysMemory::Allocate() — host VM memory (EE/IOP/VU RAM + rec buffers). +// 4. psxRec.Reserve() — BASEBLOCK tables + dispatcher. +// 5. psxInt.Reserve() — no-op, for API symmetry. +// 6. SysMemory::Reset() — zero + remap mirrors. +// 7. psxRec.Reset() — compile dispatcher, clear LUT. +// 8. psxInt.Reset() — no-op. +// 9. Install a parking-lot (infinite NOP loop) at kParkingPc so any test +// program's `jr ra` with ra=kParkingPc settles in predictable code. +// +// Shutdown reverses 4-8 and releases SysMemory. +class RecompilerTestEnvironment +{ +public: + // Guest addresses reserved for the harness. + // [kProgramPc, kProgramPc + 4KB) test program emit region + // [kScratchPc, kScratchPc + 4KB) scratch data memory for load/store tests + // [kParkingPc, kParkingPc + 8) `j self; nop` — safe `ra` target + static constexpr u32 kProgramPc = 0x00010000; + static constexpr u32 kScratchAddr = 0x00020000; + static constexpr u32 kParkingPc = 0x001F0000; + + // Idempotent: returns true if already initialized. Returns false if + // SysMemory::Allocate or other one-shot setup failed; the caller must + // not invoke any harness/replay primitives in that case. + static bool Initialize(); + static void Shutdown(); + + // Returns true once Initialize() has completed successfully. + static bool IsReady(); + + // Invalidate microVU's per-VU block cache via mVUreset so a test's JIT + // compile cannot inherit a cached block from a prior test that happened + // to land at the same start_pc with a matching microRegInfo. Call from + // the VU harness's SeedEntryState() (i.e. once per Run()). + static void ResetVuBlockCache(int vu_index); +}; + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/StateSnapshot.cpp b/tests/ctest/core/recompilers/harness/StateSnapshot.cpp new file mode 100644 index 0000000000..ff00525cf0 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/StateSnapshot.cpp @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "StateSnapshot.h" + +#include "IopMem.h" +#include "Memory.h" +#include "R3000A.h" +#include "R5900.h" + +#include +#include + +namespace recompiler_tests { + +namespace { + +// Copy `count` bytes of IOP RAM starting at guest address `addr` to `out`. +// Uses iopMemRead8 so mirrored addresses (0x0000/0x8000/0xA000 prefixes) all +// work. Missing mappings read as zero, matching how the harness initializes +// memory — tests should only capture windows inside 2 MB main RAM. +void CopyFromIopMem(u32 addr, size_t count, u8* out) +{ + for (size_t i = 0; i < count; ++i) + out[i] = iopMemRead8(addr + static_cast(i)); +} + +void CopyToIopMem(u32 addr, size_t count, const u8* src) +{ + for (size_t i = 0; i < count; ++i) + iopMemWrite8(addr + static_cast(i), src[i]); +} + +} // namespace + +IopSnapshot IopSnapshot::Capture(const std::vector& windows_to_capture) +{ + IopSnapshot s; + std::memcpy(&s.regs, &psxRegs, sizeof(psxRegisters)); + s.mem_windows.reserve(windows_to_capture.size()); + for (const auto& w : windows_to_capture) + { + MemWindow copy{w.addr, std::vector(w.bytes.size())}; + CopyFromIopMem(w.addr, copy.bytes.size(), copy.bytes.data()); + s.mem_windows.push_back(std::move(copy)); + } + return s; +} + +void IopSnapshot::Restore() const +{ + std::memcpy(&psxRegs, ®s, sizeof(psxRegisters)); + for (const auto& w : mem_windows) + CopyToIopMem(w.addr, w.bytes.size(), w.bytes.data()); +} + +void IopSnapshot::ZeroGlobals() +{ + std::memset(&psxRegs, 0, sizeof(psxRegisters)); +} + +std::vector DiffIop(const IopSnapshot& a, const IopSnapshot& b) +{ + std::vector diffs; + auto emit = [&](const char* name, u32 lhs, u32 rhs) { + if (lhs != rhs) + { + std::ostringstream ss; + ss << name << ": JIT=0x" << std::hex << lhs + << " INTERP=0x" << rhs; + diffs.push_back(ss.str()); + } + }; + + static const char* const k_gpr_names[32] = { + "zero","at","v0","v1","a0","a1","a2","a3", + "t0","t1","t2","t3","t4","t5","t6","t7", + "s0","s1","s2","s3","s4","s5","s6","s7", + "t8","t9","k0","k1","gp","sp","s8","ra"}; + + for (int i = 0; i < 32; ++i) + emit(k_gpr_names[i], a.regs.GPR.r[i], b.regs.GPR.r[i]); + emit("hi", a.regs.GPR.r[32], b.regs.GPR.r[32]); + emit("lo", a.regs.GPR.r[33], b.regs.GPR.r[33]); + + // CP0 registers — only user-visible ones. Index 12 is Status, 13 Cause, + // 14 EPC, 15 PRid, others not typically touched by single-op tests but + // included for completeness. + for (int i = 0; i < 32; ++i) + { + std::string name = "cp0[" + std::to_string(i) + "]"; + emit(name.c_str(), a.regs.CP0.r[i], b.regs.CP0.r[i]); + } + + emit("pc", a.regs.pc, b.regs.pc); + + // Memory windows — compare by address. + for (size_t i = 0; i < a.mem_windows.size() && i < b.mem_windows.size(); ++i) + { + const auto& aw = a.mem_windows[i]; + const auto& bw = b.mem_windows[i]; + if (aw.addr != bw.addr || aw.bytes.size() != bw.bytes.size()) + { + diffs.push_back("mem window[" + std::to_string(i) + "] geometry mismatch"); + continue; + } + for (size_t j = 0; j < aw.bytes.size(); ++j) + { + if (aw.bytes[j] != bw.bytes[j]) + { + std::ostringstream ss; + ss << "mem[0x" << std::hex << (aw.addr + static_cast(j)) + << "]: JIT=0x" << static_cast(aw.bytes[j]) + << " INTERP=0x" << static_cast(bw.bytes[j]); + diffs.push_back(ss.str()); + } + } + } + + return diffs; +} + +// --------------------------------------------------------------------------- +// EE / R5900 snapshot +// --------------------------------------------------------------------------- + +namespace { + +// Read/write EE RAM via memRead8/memWrite8. EE guest addresses passed in +// are KSEG0-style physical offsets into main RAM. +void CopyFromEeMem(u32 addr, size_t count, u8* out) +{ + for (size_t i = 0; i < count; ++i) + out[i] = memRead8(addr + static_cast(i)); +} + +void CopyToEeMem(u32 addr, size_t count, const u8* src) +{ + for (size_t i = 0; i < count; ++i) + memWrite8(addr + static_cast(i), src[i]); +} + +} // namespace + +EeSnapshot EeSnapshot::Capture(const std::vector& windows_to_capture) +{ + EeSnapshot s; + std::memcpy(&s.regs, &cpuRegs, sizeof(cpuRegisters)); + std::memcpy(&s.fprs, &fpuRegs, sizeof(fpuRegisters)); + s.mem_windows.reserve(windows_to_capture.size()); + for (const auto& w : windows_to_capture) + { + MemWindow copy{w.addr, std::vector(w.bytes.size())}; + CopyFromEeMem(w.addr, copy.bytes.size(), copy.bytes.data()); + s.mem_windows.push_back(std::move(copy)); + } + return s; +} + +void EeSnapshot::Restore() const +{ + std::memcpy(&cpuRegs, ®s, sizeof(cpuRegisters)); + std::memcpy(&fpuRegs, &fprs, sizeof(fpuRegisters)); + for (const auto& w : mem_windows) + CopyToEeMem(w.addr, w.bytes.size(), w.bytes.data()); +} + +void EeSnapshot::ZeroGlobals() +{ + std::memset(&cpuRegs, 0, sizeof(cpuRegisters)); + std::memset(&fpuRegs, 0, sizeof(fpuRegisters)); +} + +std::vector DiffEe(const EeSnapshot& a, const EeSnapshot& b) +{ + std::vector diffs; + auto emit64 = [&](const char* name, u64 lhs, u64 rhs) { + if (lhs != rhs) + { + std::ostringstream ss; + ss << name << ": JIT=0x" << std::hex << lhs + << " INTERP=0x" << rhs; + diffs.push_back(ss.str()); + } + }; + auto emit32 = [&](const char* name, u32 lhs, u32 rhs) { + if (lhs != rhs) + { + std::ostringstream ss; + ss << name << ": JIT=0x" << std::hex << lhs + << " INTERP=0x" << rhs; + diffs.push_back(ss.str()); + } + }; + + static const char* const k_gpr_names[32] = { + "zero","at","v0","v1","a0","a1","a2","a3", + "t0","t1","t2","t3","t4","t5","t6","t7", + "s0","s1","s2","s3","s4","s5","s6","s7", + "t8","t9","k0","k1","gp","sp","s8","ra"}; + + for (int i = 0; i < 32; ++i) + { + std::string lo = std::string(k_gpr_names[i]) + ".lo"; + std::string hi = std::string(k_gpr_names[i]) + ".hi"; + emit64(lo.c_str(), a.regs.GPR.r[i].UD[0], b.regs.GPR.r[i].UD[0]); + emit64(hi.c_str(), a.regs.GPR.r[i].UD[1], b.regs.GPR.r[i].UD[1]); + } + emit64("hi.lo", a.regs.HI.UD[0], b.regs.HI.UD[0]); + emit64("hi.hi", a.regs.HI.UD[1], b.regs.HI.UD[1]); + emit64("lo.lo", a.regs.LO.UD[0], b.regs.LO.UD[0]); + emit64("lo.hi", a.regs.LO.UD[1], b.regs.LO.UD[1]); + + for (int i = 0; i < 32; ++i) + { + // CP0[1] Random — TLB pseudo-random index, dispatcher-driven + // CP0[9] Count — EE cycle counter, dispatcher-driven + // CP0[11] Compare — Count interrupt target, moves with Count + // These differ between JIT and interp for reasons unrelated to + // ISA semantics; same discipline DiffIop uses for cycle / + // iopNextEventCycle / sCycle / eCycle bookkeeping. + if (i == 1 || i == 9 || i == 11) + continue; + std::string name = "cp0[" + std::to_string(i) + "]"; + emit32(name.c_str(), a.regs.CP0.r[i], b.regs.CP0.r[i]); + } + + for (int i = 0; i < 32; ++i) + { + std::string name = "fpr[" + std::to_string(i) + "]"; + emit32(name.c_str(), a.fprs.fpr[i].UL, b.fprs.fpr[i].UL); + } + + // PS2 FPU accumulator — written by ADDA/SUBA/MULA/MADDA/MSUBA and read + // by MADD/MSUB. Diverging ACC corrupts geometry/lighting silently if + // not included in the diff. + emit32("ACC", a.fprs.ACC.UL, b.fprs.ACC.UL); + + emit32("pc", a.regs.pc, b.regs.pc); + emit32("sa", a.regs.sa, b.regs.sa); + + for (size_t i = 0; i < a.mem_windows.size() && i < b.mem_windows.size(); ++i) + { + const auto& aw = a.mem_windows[i]; + const auto& bw = b.mem_windows[i]; + if (aw.addr != bw.addr || aw.bytes.size() != bw.bytes.size()) + { + diffs.push_back("mem window[" + std::to_string(i) + "] geometry mismatch"); + continue; + } + for (size_t j = 0; j < aw.bytes.size(); ++j) + { + if (aw.bytes[j] != bw.bytes[j]) + { + std::ostringstream ss; + ss << "mem[0x" << std::hex << (aw.addr + static_cast(j)) + << "]: JIT=0x" << static_cast(aw.bytes[j]) + << " INTERP=0x" << static_cast(bw.bytes[j]); + diffs.push_back(ss.str()); + } + } + } + + return diffs; +} + +void PrintEe(std::ostream& os, const EeSnapshot& s) +{ + os << std::hex; + os << " pc=0x" << s.regs.pc << "\n"; + for (int i = 0; i < 32; i += 2) + { + os << " r" << std::dec << i << ": 0x" << std::hex + << s.regs.GPR.r[i].UD[1] << "_" << s.regs.GPR.r[i].UD[0] + << " r" << std::dec << i+1 << ": 0x" << std::hex + << s.regs.GPR.r[i+1].UD[1] << "_" << s.regs.GPR.r[i+1].UD[0] << "\n"; + } + os << " hi=0x" << s.regs.HI.UD[1] << "_" << s.regs.HI.UD[0] + << " lo=0x" << s.regs.LO.UD[1] << "_" << s.regs.LO.UD[0] << "\n"; + os << std::dec; +} + +void PrintIop(std::ostream& os, const IopSnapshot& s) +{ + os << std::hex; + os << " pc=0x" << s.regs.pc << "\n"; + for (int i = 0; i < 32; i += 4) + { + os << " r" << std::dec << i << "-" << i + 3 << ": "; + for (int j = 0; j < 4; ++j) + os << "0x" << std::hex << s.regs.GPR.r[i + j] << " "; + os << "\n"; + } + os << " hi=0x" << s.regs.GPR.r[32] << " lo=0x" << s.regs.GPR.r[33] << "\n"; + os << std::dec; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/StateSnapshot.h b/tests/ctest/core/recompilers/harness/StateSnapshot.h new file mode 100644 index 0000000000..9ff8e23e13 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/StateSnapshot.h @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" +#include "R3000A.h" +#include "R5900.h" + +#include +#include +#include +#include +#include + +namespace recompiler_tests { + +// Fixed-size memory window captured alongside register state. +struct MemWindow +{ + u32 addr = 0; + std::vector bytes; +}; + +// Captures everything a diff between JIT and interp needs to compare for the +// IOP. +struct IopSnapshot +{ + psxRegisters regs{}; + std::vector mem_windows; + + // Fills `regs` from the global psxRegs and copies the given windows of + // IOP RAM into `mem_windows`. + static IopSnapshot Capture(const std::vector& windows_to_capture); + + // Writes this snapshot's `regs` back to global psxRegs and restores the + // memory windows. + void Restore() const; + + // Zeroes every field and clears captured memory. + static void ZeroGlobals(); +}; + +// Produces a human-readable list of field-level differences between two IOP +// snapshots. Empty when the snapshots match across every field the harness +// considers architecturally significant (GPRs, HI/LO, CP0, PC, memory windows). +// Specifically ignores: `cycle`, `interrupt`, `pcWriteback`, +// `iopNextEventCycle`, `iopBreak`, `iopCycleEE`, `iopCycleEECarry`, `sCycle`, +// `eCycle` — these are dispatcher bookkeeping that differs between interp and +// JIT for reasons unrelated to ISA semantics. +std::vector DiffIop(const IopSnapshot& a, const IopSnapshot& b); + +void PrintIop(std::ostream& os, const IopSnapshot& s); + +// --------------------------------------------------------------------------- +// EE / R5900 snapshot +// --------------------------------------------------------------------------- +// Captures cpuRegs (128-bit GPRs, HI/LO, 32-bit CP0, PC, SA) and fpuRegs +// (32 FPR + 32 FPR control). Does NOT capture dispatcher bookkeeping +// (cycle, nextEventCycle, sCycle, eCycle, interrupt, dmastall) — those +// differ between JIT and interp for reasons unrelated to ISA semantics. +struct EeSnapshot +{ + cpuRegisters regs{}; + fpuRegisters fprs{}; + std::vector mem_windows; + + static EeSnapshot Capture(const std::vector& windows_to_capture); + void Restore() const; + static void ZeroGlobals(); +}; + +std::vector DiffEe(const EeSnapshot& a, const EeSnapshot& b); +void PrintEe(std::ostream& os, const EeSnapshot& s); + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuEncode.h b/tests/ctest/core/recompilers/harness/VuEncode.h new file mode 100644 index 0000000000..a225d777a8 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuEncode.h @@ -0,0 +1,484 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +// Constexpr VU instruction encoders for the recompiler test harness. +// +// VU programs are 64-bit instruction pairs at byte offsets `pc, pc+8, pc+16, +// ...`. Each pair consists of a 32-bit lower word (at offset +0) and a 32-bit +// upper word (at offset +4). The interpreter and recompilers both fetch +// `ptr[0]` (lower) and `ptr[1]` (upper) per pair (see VU0microInterp.cpp:75 +// and VU1microInterp.cpp:79 for the reference fetch pattern). +// +// Upper-word layout (per VUops.cpp `_Ft_` / `_Fs_` / `_Fd_` / `_XYZW`): +// bits[31:27] — special bits I, E, M, D, T (one each, msb-first) +// bits[26:25] — unused +// bits[24:21] — XYZW destination mask (one bit per lane) +// bits[20:16] — FT (5-bit register index) +// bits[15:11] — FS +// bits[10: 6] — FD (or sub-op selector for the UPPER_FD_xx tables) +// bits[ 5: 0] — primary opcode (selects from VU*_UPPER_OPCODE[64]) +// +// Lower-word layout: +// bits[31:25] — primary opcode (selects from VU*_LOWER_OPCODE[128]) +// bits[24: 0] — opcode-specific (typically FT/FS/FD/imm fields) +// +// Coverage in this file is intentionally minimal: enough to write +// harness-validation tests (NOP pair with E-bit, integer ALU, simple +// FMAC) and lay down the encoder shape so additional suites can +// extend per-suite without churn. +namespace vu { + +// VU register indices. VF and VI both have 32 entries; VF[0] is hardwired +// to (0, 0, 0, 1.0) and VI[0] is hardwired to 0. Names match the PS2 VU +// programming manual. +namespace vf { +constexpr u32 vf0 = 0, vf1 = 1, vf2 = 2, vf3 = 3; +constexpr u32 vf4 = 4, vf5 = 5, vf6 = 6, vf7 = 7; +constexpr u32 vf8 = 8, vf9 = 9, vf10 = 10, vf11 = 11; +constexpr u32 vf12 = 12, vf13 = 13, vf14 = 14, vf15 = 15; +constexpr u32 vf16 = 16, vf17 = 17, vf18 = 18, vf19 = 19; +constexpr u32 vf20 = 20, vf21 = 21, vf22 = 22, vf23 = 23; +constexpr u32 vf24 = 24, vf25 = 25, vf26 = 26, vf27 = 27; +constexpr u32 vf28 = 28, vf29 = 29, vf30 = 30, vf31 = 31; +} + +namespace vi { +constexpr u32 vi0 = 0, vi1 = 1, vi2 = 2, vi3 = 3; +constexpr u32 vi4 = 4, vi5 = 5, vi6 = 6, vi7 = 7; +constexpr u32 vi8 = 8, vi9 = 9, vi10 = 10, vi11 = 11; +constexpr u32 vi12 = 12, vi13 = 13, vi14 = 14, vi15 = 15; +} + +// Destination-mask bits for the upper instruction word. +namespace mask { +constexpr u32 x = 1u << 24; +constexpr u32 y = 1u << 23; +constexpr u32 z = 1u << 22; +constexpr u32 w = 1u << 21; +constexpr u32 xyzw = x | y | z | w; +constexpr u32 xyz = x | y | z; +constexpr u32 none = 0; +} + +// Special bits in the upper word. The interpreter checks these via +// `ptr[1] & 0x40000000` (E), `& 0x10000000` (D), `& 0x08000000` (T), +// `& 0x80000000` (I), `& 0x20000000` (M, VU0 only). See VU0microInterp.cpp +// _vu0Exec / VU1microInterp.cpp _vu1Exec for the reference checks. +namespace bits { +constexpr u32 I = 1u << 31; // lower word becomes 32-bit float immediate (VI[REG_I]) +constexpr u32 E = 1u << 30; // end of microprogram (one delay-slot pair after) +constexpr u32 M = 1u << 29; // VU0 only — sets VUFLAG_MFLAGSET, breaks Execute loop +constexpr u32 D = 1u << 28; // INTC interrupt if FBRST.D-stop bit set +constexpr u32 T = 1u << 27; // INTC interrupt if FBRST.T-stop bit set +} + +// One instruction pair. `lower` ends up at `pc+0`, `upper` at `pc+4`. +struct VuOp +{ + u32 lower = 0; + u32 upper = 0; +}; + +// Bit-flag composers — return a copy of the pair with the requested bit +// OR'd into the upper word. Composable: `EBit(IBit(NopPair()))`. +constexpr VuOp WithBits(VuOp op, u32 mask) { op.upper |= mask; return op; } +constexpr VuOp EBit(VuOp op) { return WithBits(op, bits::E); } +constexpr VuOp MBit(VuOp op) { return WithBits(op, bits::M); } +constexpr VuOp DBit(VuOp op) { return WithBits(op, bits::D); } +constexpr VuOp TBit(VuOp op) { return WithBits(op, bits::T); } +constexpr VuOp IBit(VuOp op) { return WithBits(op, bits::I); } + +// --------------------------------------------------------------------------- +// Upper instruction encoders +// --------------------------------------------------------------------------- + +// Generic upper-pipe builder for primary-table opcodes (bits[5:0]). +constexpr u32 Upper(u32 mask_xyzw, u32 ft, u32 fs, u32 fd, u32 op) +{ + return (mask_xyzw & 0x1E00000u) + | ((ft & 0x1Fu) << 16) + | ((fs & 0x1Fu) << 11) + | ((fd & 0x1Fu) << 6) + | (op & 0x3Fu); +} + +// FMAC primary-table ops: ADD/SUB/MADD/MSUB/MAX/MINI/MUL with full xyzw +// operands (FD ← FS op FT). Primary opcodes 0x28..0x2F. +constexpr u32 VADD_U (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x28); } +constexpr u32 VMADD_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x29); } +constexpr u32 VMUL_U (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x2A); } +constexpr u32 VMAX_U (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x2B); } +constexpr u32 VSUB_U (u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x2C); } +constexpr u32 VMSUB_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x2D); } +constexpr u32 VMINI_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x2F); } + +// "i" broadcasts: read VI[REG_I] (the I-bit immediate) as a scalar and +// broadcast across xyzw. Per the UPPER_OPCODE table: 0x1D=MAXi, 0x1E=MULi, +// 0x1F=MINIi, 0x22=ADDi, 0x23=MADDi, 0x26=SUBi, 0x27=MSUBi. +constexpr u32 VMAXi_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x1D); } +constexpr u32 VMULi_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x1E); } +constexpr u32 VMINIi_U(u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x1F); } +constexpr u32 VADDi_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x22); } +constexpr u32 VMADDi_U(u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x23); } +constexpr u32 VSUBi_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x26); } +constexpr u32 VMSUBi_U(u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x27); } + +// "q" broadcasts: read Q-pipe scalar and broadcast across xyzw. +// 0x1C=MULq, 0x20=ADDq, 0x21=MADDq, 0x24=SUBq, 0x25=MSUBq. +constexpr u32 VMULq_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x1C); } +constexpr u32 VADDq_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x20); } +constexpr u32 VMADDq_U(u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x21); } +constexpr u32 VSUBq_U (u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x24); } +constexpr u32 VMSUBq_U(u32 mask_xyzw, u32 fd, u32 fs) { return Upper(mask_xyzw, 0, fs, fd, 0x25); } + +// Broadcast variants: ADDx/y/z/w (FD ← FS + FT.bc). Primary opcodes 0x00..0x03. +constexpr u32 VADDx_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x00); } +constexpr u32 VADDy_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x01); } +constexpr u32 VADDz_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x02); } +constexpr u32 VADDw_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x03); } +constexpr u32 VSUBx_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x04); } +constexpr u32 VSUBy_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x05); } +constexpr u32 VSUBz_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x06); } +constexpr u32 VSUBw_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x07); } +constexpr u32 VMULx_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x18); } +constexpr u32 VMULy_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x19); } +constexpr u32 VMULz_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x1A); } +constexpr u32 VMULw_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x1B); } +// MADD / MSUB broadcast variants (FD ← ACC + FS * FT.bc / FD ← ACC - FS * FT.bc). +// Primary opcodes 0x08..0x0F per PREFIX##_UPPER_OPCODE (VUops.cpp:3749). +constexpr u32 VMADDx_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x08); } +constexpr u32 VMADDy_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x09); } +constexpr u32 VMADDz_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0A); } +constexpr u32 VMADDw_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0B); } +constexpr u32 VMSUBx_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0C); } +constexpr u32 VMSUBy_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0D); } +constexpr u32 VMSUBz_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0E); } +constexpr u32 VMSUBw_U(u32 mask_xyzw, u32 fd, u32 fs, u32 ft) { return Upper(mask_xyzw, ft, fs, fd, 0x0F); } + +// Upper NOP. Lives in UPPER_FD_11_TABLE[0x0B], reached by primary opcode +// 0x3F + sub-op 0x0B in the FD field. Result: 0x000002FF. +constexpr u32 VNOP_U() +{ + return (0x3Fu) | (0x0Bu << 6); +} + +// --------------------------------------------------------------------------- +// Lower instruction encoders +// --------------------------------------------------------------------------- + +// Generic lower-pipe builder for primary-table opcodes (bits[31:25]). +constexpr u32 Lower(u32 op7, u32 ft, u32 fs, u32 fd_or_imm, u32 fields_24_to_0_extra = 0) +{ + return ((op7 & 0x7Fu) << 25) + | ((ft & 0x1Fu) << 16) + | ((fs & 0x1Fu) << 11) + | ((fd_or_imm & 0x1Fu) << 6) + | (fields_24_to_0_extra & 0x3Fu); +} + +// IADDIU — `it = is + uimm15`. Primary 0x08. Operand layout (per VUops.cpp +// _vuIADDIU at line 1033): `imm15 = ((code >> 10) & 0x7800) | (code & 0x7FF)` +// — split between bits[14:11] (high 4 bits) and bits[10:0] (low 11 bits). +constexpr u32 VIADDIU_L(u32 it, u32 is, u32 uimm15) +{ + const u32 hi4 = (uimm15 >> 11) & 0xFu; + const u32 lo11 = uimm15 & 0x7FFu; + return (0x08u << 25) | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | (hi4 << 21) | lo11; +} + +// ILW — load from VU mem to VI register. Primary 0x04, imm11 in bits[10:0] +// with sign extension via bit[10]. Operand layout per _vuILW VUops.cpp:1150. +constexpr u32 VILW_L(u32 mask_xyzw, u32 it, u32 is, s16 imm11) +{ + const u32 imm = static_cast(imm11) & 0x7FFu; + return (0x04u << 25) | (mask_xyzw & 0x1E00000u) + | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | imm; +} + +// ISW — store VI register to VU mem. Primary 0x05. +constexpr u32 VISW_L(u32 mask_xyzw, u32 it, u32 is, s16 imm11) +{ + const u32 imm = static_cast(imm11) & 0x7FFu; + return (0x05u << 25) | (mask_xyzw & 0x1E00000u) + | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | imm; +} + +// LQ / SQ — 128-bit load/store between VU mem and VF register. Primary 0x00 / 0x01. +constexpr u32 VLQ_L(u32 mask_xyzw, u32 ft, u32 is, s16 imm11) +{ + const u32 imm = static_cast(imm11) & 0x7FFu; + return (0x00u << 25) | (mask_xyzw & 0x1E00000u) + | ((ft & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | imm; +} +constexpr u32 VSQ_L(u32 mask_xyzw, u32 fs, u32 it, s16 imm11) +{ + const u32 imm = static_cast(imm11) & 0x7FFu; + return (0x01u << 25) | (mask_xyzw & 0x1E00000u) + | ((it & 0x1Fu) << 16) | ((fs & 0x1Fu) << 11) | imm; +} + +// LowerOP family — primary opcode 0x40, sub-op in bits[5:0] (LowerOP_OPCODE), +// and for sub-ops 0x3C..0x3F a secondary in bits[10:6] (LowerOP_T3_xx_OPCODE). +// +// LowerOP_OPCODE (bits[5:0]): +// 0x30 IADD 0x31 ISUB 0x32 IADDI 0x34 IAND 0x35 IOR +// 0x3C LowerOP_T3_00 0x3D _T3_01 0x3E _T3_10 0x3F _T3_11 +// +// LowerOP_T3_xx (sub-op in bits[10:6]) — VMOVE/VMR32, VLQI/VSQI, VDIV/VSQRT/VRSQRT/VWAITQ +// VMTIR/VMFIR, VMFP, VITOFx/VFTOIx, RAND/REGS, EFU ops, XGKICK/XITOP/XTOP. See +// VUops.cpp:3607-3650 for the full table. + +// Direct sub-op assembler — pass the secondary in `sec5` (bits[10:6]) and the +// T3 selector (0x3C..0x3F) explicitly. +constexpr u32 BuildLowerT3(u32 t3_sel, u32 sec5, u32 ft, u32 fs, u32 mask_xyzw_or_zero, + u32 fsf = 0, u32 ftf = 0) +{ + return (0x40u << 25) + | (mask_xyzw_or_zero & 0x1E00000u) + | ((ftf & 0x3u) << 23) + | ((fsf & 0x3u) << 21) + | ((ft & 0x1Fu) << 16) + | ((fs & 0x1Fu) << 11) + | ((sec5 & 0x1Fu) << 6) + | (t3_sel & 0x3Fu); +} + +// VMOVE: ft = fs (per-lane, masked). T3_00 sub 0x0C. +constexpr u32 VMOVE_L(u32 mask_xyzw, u32 ft, u32 fs) +{ + return BuildLowerT3(0x3C, 0x0C, ft, fs, mask_xyzw); +} + +// VMR32: rotates fs xyzw -> ft yzwx (lanes shifted by 1). T3_01 sub 0x0C. +constexpr u32 VMR32_L(u32 mask_xyzw, u32 ft, u32 fs) +{ + return BuildLowerT3(0x3D, 0x0C, ft, fs, mask_xyzw); +} + +// VMFIR: ft.{xyzw masked} = sign_extend_s32(VI[is]). T3_01 sub 0x0F. +constexpr u32 VMFIR_L(u32 mask_xyzw, u32 ft, u32 is) +{ + return BuildLowerT3(0x3D, 0x0F, ft, is, mask_xyzw); +} + +// VMTIR: VI[it].US[0] = u16(fs.f[fsf]). T3_00 sub 0x0F. Uses Fsf. +constexpr u32 VMTIR_L(u32 it, u32 fs, u32 fsf) +{ + return BuildLowerT3(0x3C, 0x0F, it, fs, /*mask*/0, fsf); +} + +// Q-pipe primary scalar ops: +// VDIV T3_00 sub 0x0E — q = fs[fsf] / ft[ftf] +// VSQRT T3_01 sub 0x0E — q = sqrt(|ft[ftf]|) +// VRSQRT T3_10 sub 0x0E — q = fs[fsf] / sqrt(|ft[ftf]|) +// VWAITQ T3_11 sub 0x0E — stall until Q-pipe completes +constexpr u32 VDIV_L (u32 fs, u32 fsf, u32 ft, u32 ftf) { return BuildLowerT3(0x3C, 0x0E, ft, fs, /*mask*/0, fsf, ftf); } +constexpr u32 VSQRT_L (u32 ft, u32 ftf) { return BuildLowerT3(0x3D, 0x0E, ft, /*fs*/0, /*mask*/0, /*fsf*/0, ftf); } +constexpr u32 VRSQRT_L(u32 fs, u32 fsf, u32 ft, u32 ftf) { return BuildLowerT3(0x3E, 0x0E, ft, fs, /*mask*/0, fsf, ftf); } +constexpr u32 VWAITQ_L() { return BuildLowerT3(0x3F, 0x0E, 0, 0, 0); } + +// VLQI: ft = MEM[VI[is]*16]; VI[is]++. T3_00 sub 0x0D. Mask diffs the loaded lanes. +constexpr u32 VLQI_L(u32 mask_xyzw, u32 ft, u32 is) { return BuildLowerT3(0x3C, 0x0D, ft, is, mask_xyzw); } +constexpr u32 VSQI_L(u32 mask_xyzw, u32 fs, u32 it) { return BuildLowerT3(0x3D, 0x0D, it, fs, mask_xyzw); } +constexpr u32 VLQD_L(u32 mask_xyzw, u32 ft, u32 is) { return BuildLowerT3(0x3E, 0x0D, ft, is, mask_xyzw); } +constexpr u32 VSQD_L(u32 mask_xyzw, u32 fs, u32 it) { return BuildLowerT3(0x3F, 0x0D, it, fs, mask_xyzw); } + +// EFU P-pipeline ops — VU1-only (EFU not present on VU0). All write to the +// architectural P scalar, which `mVUendProgram` commits to VI[REG_P] at +// E-bit. VWAITP stalls until the P-pipeline drains. Per VUops.cpp:1652-1805, +// EVERY EFU op reads from VF[_Fs_] (lane indexed by _Fsf_ where applicable). +// Dispatch table encodings: +// T3_00 sec 0x1C VESADD (Fs xyz) +// T3_00 sec 0x1D VEATANxy (Fs xy) +// T3_00 sec 0x1E VESQRT (Fs + Fsf) +// T3_00 sec 0x1F VESIN (Fs + Fsf) +// T3_01 sec 0x1C VERSADD (Fs xyz) +// T3_01 sec 0x1D VEATANxz (Fs xz) +// T3_01 sec 0x1E VERSQRT (Fs + Fsf) ← only Fsf, despite the function reading Ft fields too +// T3_01 sec 0x1F VEATAN (Fs + Fsf) +// T3_10 sec 0x1C VELENG (Fs xyz) +// T3_10 sec 0x1D VESUM (Fs xyzw) +// T3_10 sec 0x1E VERCPR (Fs + Fsf) +// T3_10 sec 0x1F VEEXP (Fs + Fsf) +// T3_11 sec 0x1C VERLENG (Fs xyz) +// T3_11 sec 0x1E VWAITP (no operands) +constexpr u32 VESADD_L (u32 fs) { return BuildLowerT3(0x3C, 0x1C, 0, fs, 0); } +constexpr u32 VEATANXY_L(u32 fs) { return BuildLowerT3(0x3C, 0x1D, 0, fs, 0); } +constexpr u32 VESQRT_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3C, 0x1E, 0, fs, 0, fsf, 0); } +constexpr u32 VESIN_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3C, 0x1F, 0, fs, 0, fsf, 0); } +constexpr u32 VERSADD_L (u32 fs) { return BuildLowerT3(0x3D, 0x1C, 0, fs, 0); } +constexpr u32 VEATANXZ_L(u32 fs) { return BuildLowerT3(0x3D, 0x1D, 0, fs, 0); } +constexpr u32 VERSQRT_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3D, 0x1E, 0, fs, 0, fsf, 0); } +constexpr u32 VEATAN_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3D, 0x1F, 0, fs, 0, fsf, 0); } +constexpr u32 VELENG_L (u32 fs) { return BuildLowerT3(0x3E, 0x1C, 0, fs, 0); } +constexpr u32 VESUM_L (u32 fs) { return BuildLowerT3(0x3E, 0x1D, 0, fs, 0); } +constexpr u32 VERCPR_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3E, 0x1E, 0, fs, 0, fsf, 0); } +constexpr u32 VEEXP_L (u32 fs, u32 fsf) { return BuildLowerT3(0x3E, 0x1F, 0, fs, 0, fsf, 0); } +constexpr u32 VERLENG_L (u32 fs) { return BuildLowerT3(0x3F, 0x1C, 0, fs, 0); } +constexpr u32 VWAITP_L () { return BuildLowerT3(0x3F, 0x1E, 0, 0, 0); } + +// VU1-only XGKICK — T3_00 sub 0x1B (per VUops.cpp:3614, table index 27). +// Reads the current GIF-target address from VI[is], drains it into the GIF +// Path 1 stream. +constexpr u32 VXGKICK_L(u32 is) { return BuildLowerT3(0x3C, 0x1B, 0, is, 0); } + +// LowerOP integer ALU — primary 0x40, sub-op in bits[5:0]. Uses Id/Is/It (low +// 4 bits of the FD/FS/FT fields, picking VI[0..15]). +constexpr u32 BuildLowerInt(u32 sub6, u32 id, u32 is, u32 it) +{ + return (0x40u << 25) + | ((it & 0x1Fu) << 16) + | ((is & 0x1Fu) << 11) + | ((id & 0x1Fu) << 6) + | (sub6 & 0x3Fu); +} +constexpr u32 VIADD_L(u32 id, u32 is, u32 it) { return BuildLowerInt(0x30, id, is, it); } +constexpr u32 VISUB_L(u32 id, u32 is, u32 it) { return BuildLowerInt(0x31, id, is, it); } +constexpr u32 VIAND_L(u32 id, u32 is, u32 it) { return BuildLowerInt(0x34, id, is, it); } +constexpr u32 VIOR_L (u32 id, u32 is, u32 it) { return BuildLowerInt(0x35, id, is, it); } + +// VIADDI: it = is + sext5(imm5). Imm in bits[10:6] (FD field), sign-extended via bit[10]. +// Per _vuIADDI VUops.cpp:1014. +constexpr u32 VIADDI_L(u32 it, u32 is, s32 simm5) +{ + const u32 imm5 = static_cast(simm5) & 0x1Fu; + return (0x40u << 25) | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | (imm5 << 6) | 0x32u; +} + +// VISUBIU: it = is - imm15. Primary 0x09. Same imm15 split as VIADDIU. +constexpr u32 VISUBIU_L(u32 it, u32 is, u32 uimm15) +{ + const u32 hi4 = (uimm15 >> 11) & 0xFu; + const u32 lo11 = uimm15 & 0x7FFu; + return (0x09u << 25) | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | (hi4 << 21) | lo11; +} + +// Branches — primary opcode in bits[31:25]. Imm11 in bits[10:0] is signed, +// units of pairs (8 bytes), relative to VI[REG_TPC]+8 per `_branchAddr`. +constexpr u32 BuildBranch11(u32 op7, u32 it, u32 is, s16 imm11_pairs) +{ + const u32 imm = static_cast(imm11_pairs) & 0x7FFu; + return ((op7 & 0x7Fu) << 25) + | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11) | imm; +} +constexpr u32 VB_L (s16 imm11) { return BuildBranch11(0x20, 0, 0, imm11); } +constexpr u32 VBAL_L (u32 it, s16 imm11) { return BuildBranch11(0x21, it, 0, imm11); } +constexpr u32 VJR_L (u32 is) { return BuildBranch11(0x24, 0, is, 0); } +constexpr u32 VJALR_L (u32 it, u32 is) { return BuildBranch11(0x25, it, is, 0); } +constexpr u32 VIBEQ_L (u32 it, u32 is, s16 imm11){ return BuildBranch11(0x28, it, is, imm11); } +constexpr u32 VIBNE_L (u32 it, u32 is, s16 imm11){ return BuildBranch11(0x29, it, is, imm11); } +constexpr u32 VIBLTZ_L (u32 is, s16 imm11) { return BuildBranch11(0x2C, 0, is, imm11); } +constexpr u32 VIBGTZ_L (u32 is, s16 imm11) { return BuildBranch11(0x2D, 0, is, imm11); } +constexpr u32 VIBLEZ_L (u32 is, s16 imm11) { return BuildBranch11(0x2E, 0, is, imm11); } +constexpr u32 VIBGEZ_L (u32 is, s16 imm11) { return BuildBranch11(0x2F, 0, is, imm11); } + +// Flag ops — primary opcode in bits[31:25]. +// FCEQ/FCSET/FCAND/FCOR — 24-bit imm in bits[23:0]. +// FSEQ/FSAND/FSOR/FSSET — 12-bit imm: bits[10:0] | (bit[21] << 11). VUops.cpp:1352. +// FMEQ/FMAND/FMOR — register-form (it, is). FCGET — register-form (it). +constexpr u32 BuildFlagImm24(u32 op7, u32 imm24) +{ + return ((op7 & 0x7Fu) << 25) | (imm24 & 0xFFFFFFu); +} +constexpr u32 BuildFlagImm12(u32 op7, u32 it, u32 imm12) +{ + const u32 lo11 = imm12 & 0x7FFu; + const u32 bit11 = (imm12 >> 11) & 0x1u; + return ((op7 & 0x7Fu) << 25) | ((it & 0x1Fu) << 16) | (bit11 << 21) | lo11; +} +constexpr u32 BuildFlagReg(u32 op7, u32 it, u32 is) +{ + return ((op7 & 0x7Fu) << 25) | ((it & 0x1Fu) << 16) | ((is & 0x1Fu) << 11); +} +constexpr u32 VFCEQ_L (u32 imm24) { return BuildFlagImm24(0x10, imm24); } +constexpr u32 VFCSET_L(u32 imm24) { return BuildFlagImm24(0x11, imm24); } +constexpr u32 VFCAND_L(u32 imm24) { return BuildFlagImm24(0x12, imm24); } +constexpr u32 VFCOR_L (u32 imm24) { return BuildFlagImm24(0x13, imm24); } +constexpr u32 VFSEQ_L (u32 it, u32 imm12) { return BuildFlagImm12(0x14, it, imm12); } +constexpr u32 VFSSET_L(u32 imm12) { return BuildFlagImm12(0x15, 0, imm12); } +constexpr u32 VFSAND_L(u32 it, u32 imm12) { return BuildFlagImm12(0x16, it, imm12); } +constexpr u32 VFSOR_L (u32 it, u32 imm12) { return BuildFlagImm12(0x17, it, imm12); } +constexpr u32 VFMEQ_L (u32 it, u32 is) { return BuildFlagReg (0x18, it, is); } +constexpr u32 VFMAND_L(u32 it, u32 is) { return BuildFlagReg (0x1A, it, is); } +constexpr u32 VFMOR_L (u32 it, u32 is) { return BuildFlagReg (0x1B, it, is); } +constexpr u32 VFCGET_L(u32 it) { return BuildFlagReg (0x1C, it, 0); } + +// --------------------------------------------------------------------------- +// Upper accumulator-target ops — UPPER_FD_xx tables. Used for flag-pipeline +// and ACC chaining tests. Encoded as primary 0x3F+T3_xx via the FD field +// acting as a sub-op selector. +// --------------------------------------------------------------------------- +constexpr u32 UpperFD(u32 mask_xyzw, u32 ft, u32 fs, u32 fd_subop, u32 fd_table_sel) +{ + // Primary opcodes 0x3C..0x3F = UPPER_FD_00..11. fd_subop occupies bits[10:6]. + return (mask_xyzw & 0x1E00000u) + | ((ft & 0x1Fu) << 16) + | ((fs & 0x1Fu) << 11) + | ((fd_subop & 0x1Fu) << 6) + | (0x3Cu | (fd_table_sel & 0x3u)); +} + +// ACC-target FMACs (write to ACC, not FD). Per VUops.cpp:3705-3747 the four +// FD_xx tables hold: +// FD_00 0x0A=ADDA 0x0B=SUBA 0x04=ITOF0 0x05=FTOI0 0x06=MULAx … +// FD_01 0x0A=MADDA 0x0B=MSUBA 0x04=ITOF4 0x05=FTOI4 0x07=ABS … +// FD_10 0x0A=MULA 0x0B=OPMULA 0x04=ITOF12 0x05=FTOI12 … +// FD_11 0x0A=unkn 0x0B=NOP 0x04=ITOF15 0x05=FTOI15 0x07=CLIP +constexpr u32 VADDA_U (u32 mask_xyzw, u32 fs, u32 ft) { return UpperFD(mask_xyzw, ft, fs, 0x0A, 0); } +constexpr u32 VSUBA_U (u32 mask_xyzw, u32 fs, u32 ft) { return UpperFD(mask_xyzw, ft, fs, 0x0B, 0); } +constexpr u32 VMADDA_U (u32 mask_xyzw, u32 fs, u32 ft) { return UpperFD(mask_xyzw, ft, fs, 0x0A, 1); } +constexpr u32 VMSUBA_U (u32 mask_xyzw, u32 fs, u32 ft) { return UpperFD(mask_xyzw, ft, fs, 0x0B, 1); } +constexpr u32 VMULA_U (u32 mask_xyzw, u32 fs, u32 ft) { return UpperFD(mask_xyzw, ft, fs, 0x0A, 2); } +constexpr u32 VOPMULA_U(u32 fs, u32 ft) { return UpperFD(mask::xyz, ft, fs, 0x0B, 2); } + +// Fixed-point conversion families — VFTOI{0,4,12,15} truncate fs * 2^N → s32; +// VITOF{0,4,12,15} convert s32 → fs / 2^N. All masked per dest lane. +constexpr u32 VITOF0_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x04, 0); } +constexpr u32 VITOF4_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x04, 1); } +constexpr u32 VITOF12_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x04, 2); } +constexpr u32 VITOF15_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x04, 3); } +constexpr u32 VFTOI0_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x05, 0); } +constexpr u32 VFTOI4_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x05, 1); } +constexpr u32 VFTOI12_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x05, 2); } +constexpr u32 VFTOI15_U (u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x05, 3); } + +// VABS — FD_01 sub 0x07. +constexpr u32 VABS_U(u32 mask_xyzw, u32 ft, u32 fs) { return UpperFD(mask_xyzw, ft, fs, 0x07, 1); } + +// VCLIP — UPPER_FD_11 sub 0x07. Operands fs (xyz) tested against ft.w; result +// folded into VI[REG_CLIP_FLAG] as a 24-bit rolling history (no FD writeback). +constexpr u32 VCLIP_U(u32 fs, u32 ft) { return UpperFD(0, ft, fs, 0x07, 3); } + +// Lower NOP isn't a discrete opcode. Instead, set the I-bit on the upper word +// and the lower word becomes a 32-bit float immediate that loads into +// VI[REG_I] (see VU0microInterp.cpp _vu0Exec lines 113-127). Use `VLitI(value)` +// when you want a known I value, or `VLitZero()` for a zero filler. +constexpr u32 VLitI(u32 imm32) { return imm32; } +constexpr u32 VLitZero() { return 0u; } + +// --------------------------------------------------------------------------- +// Convenience pair builders +// --------------------------------------------------------------------------- + +// Pure NOP pair. Sets I-bit so the lower is interpreted as a float immediate +// (VI[REG_I] ← 0); upper is the architectural NOP from UPPER_FD_11_TABLE. +constexpr VuOp NopPair() +{ + return IBit(VuOp{VLitZero(), VNOP_U()}); +} + +// E-bit-terminated NOP pair. The interpreter's E-bit cleanup runs one +// instruction-pair AFTER the E-bit pair, so any E-bit-terminated test +// program must include a follow-up pair as the architectural delay slot. +// The harness's `LoadProgram` appends `NopPair()` automatically when the +// last user-supplied pair carries the E bit. +constexpr VuOp EBitNopPair() +{ + return EBit(NopPair()); +} + +} // namespace vu diff --git a/tests/ctest/core/recompilers/harness/VuReplay.cpp b/tests/ctest/core/recompilers/harness/VuReplay.cpp new file mode 100644 index 0000000000..d609604400 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuReplay.cpp @@ -0,0 +1,354 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "VuReplay.h" + +#include "RecompilerTestEnvironment.h" + +#include "Config.h" +#include "Gif_Unit.h" +#include "VU.h" +#include "VUmicro.h" +#include "common/FPControl.h" + +#if defined(_M_ARM64) || defined(__aarch64__) +#include "vixl/aarch64/decoder-aarch64.h" +#include "vixl/aarch64/disasm-aarch64.h" + +// Avoid including microVU-arm64.h directly: it carries __fi function defs +// without `inline`, which create ODR collisions when pulled into more than +// one TU. Just declare the needed accessor. +namespace vu_capture_internal { + void GetCompiledRange(int vu_index, const u8** out_start, const u8** out_end); +} +#endif + +#include +#include +#include + +namespace recompiler_tests { + +namespace +{ + +VURegs& Regs(int idx) { return vuRegs[idx]; } +u32 MemSize(int idx) { return idx == 0 ? VU0_MEMSIZE : VU1_MEMSIZE; } +u32 RunningBit(int idx) { return idx == 0 ? 0x1u : 0x100u; } + +BaseVUmicroCPU* InterpCpu(int idx) +{ + return idx == 0 ? static_cast(&CpuIntVU0) + : static_cast(&CpuIntVU1); +} + +BaseVUmicroCPU* JitCpu(int idx) +{ + return idx == 0 ? static_cast(&CpuMicroVU0) + : static_cast(&CpuMicroVU1); +} + +// Whole-VuMem window so the diff covers any byte the program touches. +std::vector WholeMemWindow(int vu_index) +{ + return {VuMemWindow{0u, std::vector(MemSize(vu_index))}}; +} + +// Restore microcode + data memory + registers from a CaptureRecord. Sets up +// the VPU_STAT running bit + start_pc the same way VuTestHarness does, and +// drops the JIT block cache so the capture's start_pc compiles fresh. +void PrimeFromCapture(const vu_capture::CaptureRecord& rec, + bool reset_block_cache = true) +{ + const int idx = static_cast(rec.vu_index); + auto& vu = Regs(idx); + + std::memcpy(vu.Micro, rec.microcode.data(), rec.microcode.size()); + std::memcpy(vu.Mem, rec.vumem.data(), rec.vumem.size()); + + vu_capture::RestoreState(rec.state, vu); + + vu.start_pc = rec.start_pc; + vu.VI[REG_TPC].UL = rec.start_pc / 8u; + + vuRegs[0].VI[REG_VPU_STAT].UL = + (vuRegs[0].VI[REG_VPU_STAT].UL & ~0xFFFu) | RunningBit(idx); + vu.VI[REG_VPU_STAT].UL = vuRegs[0].VI[REG_VPU_STAT].UL; + + vu.cycle = 0; + vu.ebit = 0; + vu.branch = 0; + vu.branchpc = 0; + vu.delaybranchpc = 0; + vu.takedelaybranch = false; + vu.flags = 0; + vu.fmacreadpos = vu.fmacwritepos = vu.fmaccount = 0; + vu.ialureadpos = vu.ialuwritepos = vu.ialucount = 0; + std::memset(&vu.fdiv, 0, sizeof(vu.fdiv)); + std::memset(&vu.efu, 0, sizeof(vu.efu)); + std::memset(&vu.fmac, 0, sizeof(vu.fmac)); + std::memset(&vu.ialu, 0, sizeof(vu.ialu)); + + InterpCpu(idx)->SetStartPC(rec.start_pc); + JitCpu(idx)->SetStartPC(rec.start_pc); + + // Reset drops every compiled VU block so the capture's start_pc compiles + // fresh. Callers that re-seed the architectural + pipeline state between + // timed bench iterations pass reset_block_cache=false: they want the + // already-compiled block kept so the measured Execute is steady-state + // (no recompile) rather than paying the one-time compile every iter. + if (reset_block_cache) + RecompilerTestEnvironment::ResetVuBlockCache(idx); +} + +void RunInterpFromSeeded(int idx, u32 cycles) +{ + // Same REC_VU1 / EnableVU1 dance VuTestHarness does — see VuTestHarness.cpp + // `RunInterpFromSeeded` for the rationale (Gif_Unit GetGSPacketSize gates + // its bit-31 EOP signal on `(CHECK_XGKICKHACK || !REC_VU1)`). + const bool saved = EmuConfig.Cpu.Recompiler.EnableVU1; + EmuConfig.Cpu.Recompiler.EnableVU1 = false; + InterpCpu(idx)->Execute(cycles); + EmuConfig.Cpu.Recompiler.EnableVU1 = saved; +} + +void RunJitFromSeeded(int idx, u32 cycles) +{ + JitCpu(idx)->Execute(cycles); +} + +} // namespace + +void ReseedFromCapture(const vu_capture::CaptureRecord& rec) +{ + // Re-seed the architectural + pipeline state from the capture WITHOUT + // dropping the compiled VU block cache. Used between timed bench iters so + // each iteration re-runs the whole program from its captured entry state + // (no E-bit drift-to-halt) while still measuring already-compiled code + // (no per-iter recompile). + PrimeFromCapture(rec, /*reset_block_cache=*/false); +} + +std::vector BenchJitSteady(const vu_capture::CaptureRecord& rec, + u32 iters) +{ + std::vector samples; + if (rec.vu_index > 1 || iters == 0) + return samples; + if (!RecompilerTestEnvironment::IsReady()) + return samples; + + const int idx = static_cast(rec.vu_index); + const u32 cycles = rec.cycle_budget; + + PmuCounters::Group g; + if (!g.Open()) + return samples; + + std::vector path1_buf; + gif_test_hooks::g_path1_sink = &path1_buf; + + // Prime once WITH a cache reset so iter 0 compiles the block; subsequent + // iters re-seed without a reset (block stays compiled) so the measured + // Execute is honest steady-state program execution. iter 0 (which pays + // the compile) is dropped as warmup by the caller. + PrimeFromCapture(rec); + samples.reserve(iters); + for (u32 i = 0; i < iters; ++i) + { + if (i > 0) + ReseedFromCapture(rec); + path1_buf.clear(); + samples.push_back(g.Measure([&]() { + RunJitFromSeeded(idx, cycles); + })); + } + + gif_test_hooks::g_path1_sink = nullptr; + return samples; +} + +VuReplayResult ReplayCapture(const vu_capture::CaptureRecord& rec, + VuDiffMode diff_mode, + u32 cycle_budget_override) +{ + VuReplayResult out; + + if (rec.vu_index > 1) + return out; + if (!RecompilerTestEnvironment::IsReady()) + return out; + + const int idx = static_cast(rec.vu_index); + const u32 cycles = cycle_budget_override ? cycle_budget_override : rec.cycle_budget; + + const auto windows = WholeMemWindow(idx); + + // Set host FPCR to the VU's FPCR (FZ + ChopZero) for BOTH passes. + // The interpreter computes via host scalar FP (`fs / ft` in `_vuDIV`, + // host `sqrt`/`sqrtf` in `_vuSQRT`, etc.), which uses the host thread + // FPCR. The JIT switches FPCR to VU FPCR at dispatcher entry. If the + // harness sets vu_fpcr only around the JIT pass and lets interp run + // with default round-to-nearest, the two engines compute different + // FP results for the same operands — a 1-ULP false-positive divergence + // on any DIV/SQRT/MUL with non-trivial operands. Both passes use + // vu_fpcr so any divergence is a real codegen difference. + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + const FPControlRegister vu_fpcr = (idx == 0) + ? EmuConfig.Cpu.VU0FPCR + : EmuConfig.Cpu.VU1FPCR; + FPControlRegister::SetCurrent(vu_fpcr); + + std::vector path1_buf; + gif_test_hooks::g_path1_sink = &path1_buf; + PrimeFromCapture(rec); + const VuSnapshot pre = VuSnapshot::Capture(idx, windows); + path1_buf.clear(); + RunJitFromSeeded(idx, cycles); + out.jit_snapshot = VuSnapshot::Capture(idx, windows); + out.path1_packets_jit = path1_buf; + // Authoritative running bit lives in vuRegs[0] for both VUs (see header). + out.jit_ebit = (vuRegs[0].VI[REG_VPU_STAT].UL & RunningBit(idx)) == 0; + + // Interp pass — restore the JIT-pass pre-state so the engines see + // identical inputs. + pre.Restore(); + std::memcpy(Regs(idx).Micro, rec.microcode.data(), rec.microcode.size()); + std::memcpy(Regs(idx).Mem, rec.vumem.data(), rec.vumem.size()); + Regs(idx).start_pc = rec.start_pc; + Regs(idx).VI[REG_TPC].UL = rec.start_pc / 8u; + vuRegs[0].VI[REG_VPU_STAT].UL = + (vuRegs[0].VI[REG_VPU_STAT].UL & ~0xFFFu) | RunningBit(idx); + Regs(idx).VI[REG_VPU_STAT].UL = vuRegs[0].VI[REG_VPU_STAT].UL; + InterpCpu(idx)->SetStartPC(rec.start_pc); + JitCpu(idx)->SetStartPC(rec.start_pc); + + path1_buf.clear(); + RunInterpFromSeeded(idx, cycles); + out.interp_snapshot = VuSnapshot::Capture(idx, windows); + out.path1_packets_interp = path1_buf; + out.interp_ebit = (vuRegs[0].VI[REG_VPU_STAT].UL & RunningBit(idx)) == 0; + + gif_test_hooks::g_path1_sink = nullptr; + FPControlRegister::SetCurrent(saved_fpcr); + + out.diff_lines = DiffVu(out.jit_snapshot, out.interp_snapshot, diff_mode); + out.diverged = !out.diff_lines.empty(); + out.ok = true; + return out; +} + +VuReplayResult LoadAndReplay(const std::string& path, + VuDiffMode diff_mode, + u32 cycle_budget_override) +{ + vu_capture::CaptureRecord rec; + if (!vu_capture::ReadFromFile(path, rec)) + return {}; + return ReplayCapture(rec, diff_mode, cycle_budget_override); +} + +bool DumpJitAsm(const vu_capture::CaptureRecord& rec, const std::string& out_path) +{ +#if defined(_M_ARM64) || defined(__aarch64__) + if (rec.vu_index > 1) + return false; + if (!RecompilerTestEnvironment::IsReady()) + return false; + + const int idx = static_cast(rec.vu_index); + const u32 cycles = rec.cycle_budget; + + // Compile once via the standard prime+execute path. + std::vector path1_buf; + gif_test_hooks::g_path1_sink = &path1_buf; + PrimeFromCapture(rec); + RunJitFromSeeded(idx, cycles); + gif_test_hooks::g_path1_sink = nullptr; + + const u8* code_start = nullptr; + const u8* code_end = nullptr; + vu_capture_internal::GetCompiledRange(idx, &code_start, &code_end); + if (!code_start || code_end <= code_start) + return false; + + std::FILE* f = std::fopen(out_path.c_str(), "w"); + if (!f) + return false; + + std::fprintf(f, "// vu_capture codegen dump\n"); + std::fprintf(f, "// vu_index = %d\n", idx); + std::fprintf(f, "// start_pc = 0x%08X\n", rec.start_pc); + std::fprintf(f, "// host bytes = %zu\n", static_cast(code_end - code_start)); + std::fprintf(f, "// host range = [%p, %p)\n", code_start, code_end); + std::fprintf(f, "\n"); + + vixl::aarch64::Decoder decoder; + vixl::aarch64::Disassembler disasm; + decoder.AppendVisitor(&disasm); + + for (const u8* p = code_start; p + 4 <= code_end; p += 4) + { + u32 word; + std::memcpy(&word, p, 4); + const auto* instr = reinterpret_cast(p); + decoder.Decode(instr); + std::fprintf(f, " %p %08x %s\n", + static_cast(p), word, disasm.GetOutput()); + } + + std::fclose(f); + return true; +#else + (void)rec; + std::FILE* f = std::fopen(out_path.c_str(), "w"); + if (!f) + return false; + std::fprintf(f, "// DumpJitAsm: not wired on non-ARM64 hosts.\n"); + std::fclose(f); + return false; +#endif +} + +std::vector BenchJit(const vu_capture::CaptureRecord& rec, + u32 iters, + u32 cycle_budget_override, + bool reprime_per_iter) +{ + std::vector samples; + if (rec.vu_index > 1 || iters == 0) + return samples; + if (!RecompilerTestEnvironment::IsReady()) + return samples; + + const int idx = static_cast(rec.vu_index); + const u32 cycles = cycle_budget_override ? cycle_budget_override : rec.cycle_budget; + + PmuCounters::Group g; + if (!g.Open()) + return samples; + + // Path1 sink installed but unused — the bench cares about cycles, not + // XGKICK packets. The sink is still installed because some captures may emit + // path-1 packets and asserting when no sink is present must be avoided. + std::vector path1_buf; + gif_test_hooks::g_path1_sink = &path1_buf; + + samples.reserve(iters); + if (!reprime_per_iter) + PrimeFromCapture(rec); + for (u32 i = 0; i < iters; ++i) + { + if (reprime_per_iter) + PrimeFromCapture(rec); + path1_buf.clear(); + samples.push_back(g.Measure([&]() { + RunJitFromSeeded(idx, cycles); + })); + } + + gif_test_hooks::g_path1_sink = nullptr; + return samples; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuReplay.h b/tests/ctest/core/recompilers/harness/VuReplay.h new file mode 100644 index 0000000000..ed301b908c --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuReplay.h @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "VuSnapshot.h" + +#include "common/Pcsx2Defs.h" +#include "common/PmuCounters.h" +#include "vu_capture.h" + +#include +#include + +namespace recompiler_tests { + +// Result of replaying one captured VU microprogram through both engines. +// Populated in full whether or not the JIT and interp diverged — the diff +// list is empty on a clean replay. +struct VuReplayResult +{ + // True if both engines ran to completion without setup error. False on + // load failure / unsupported vu_index / cycle exhaustion in either engine. + bool ok = false; + + // True iff `diff_lines` is non-empty. + bool diverged = false; + + // Architectural divergences between JIT and interp post-state. Same + // format as DiffVu() in VuSnapshot.h. + std::vector diff_lines; + + // Full post-state from each engine's run. Always populated when ok=true. + VuSnapshot jit_snapshot; + VuSnapshot interp_snapshot; + + // XGKICK Path 1 packet bytes captured during each engine's run. + std::vector path1_packets_jit; + std::vector path1_packets_interp; + + // Termination signal: true if the run cleared its VPU_STAT running bit + // (E-bit terminated naturally), false if it was truncated by the cycle + // budget. Read from the authoritative vuRegs[0].VI[REG_VPU_STAT] right + // after each pass — the running bit lives in VU0's VI for both VUs, so a + // VU1 run's own VI[REG_VPU_STAT] mirror is stale and can't be used. + // + // Why it matters: a budget-truncated looping program runs a different + // number of iterations under JIT vs interp, producing large memory/reg + // diff counts that are NOT codegen bugs. interp_ebit==false ⇒ deprioritize + // the divergence (it's loop/budget noise, not a mis-emitted op). + bool jit_ebit = false; + bool interp_ebit = false; +}; + +// Drives the JIT and interpreter against one captured program. Restores the +// captured microcode + VU memory + register state, runs the JIT, snapshots, +// restores pre-state, runs the interpreter, snapshots, diffs. +// +// `cycle_budget_override` of 0 means "use the budget from the capture". The +// pcsx2-vurunner --bench mode sets a tight per-iteration budget to avoid +// running the program multiple times when it's a long loop. +// +// Caller must ensure RecompilerTestEnvironment::IsReady() — the SysMemory +// allocations and CpuMicroVU0/1 reservations need to be in place. +VuReplayResult ReplayCapture(const vu_capture::CaptureRecord& rec, + VuDiffMode diff_mode = VuDiffMode::PipelinePermissive, + u32 cycle_budget_override = 0); + +// Convenience: load + replay in one call. Returns a result with ok=false +// if the file can't be read or has bad magic/version/sizes. +VuReplayResult LoadAndReplay(const std::string& path, + VuDiffMode diff_mode = VuDiffMode::PipelinePermissive, + u32 cycle_budget_override = 0); + +// Run the JIT against the capture `iters` times, measuring PMU counters +// per iteration. The first iter pays a one-time JIT compile cost; the +// per-iter cycles will drop after that. Caller is expected to throw out +// the warmup iter when computing summary stats. +// +// Returns one Values entry per iter. Empty on setup failure (caller should +// check RecompilerTestEnvironment::IsReady() and PmuCounters::Group::Open() +// availability — the vector is empty if either fails). +std::vector BenchJit(const vu_capture::CaptureRecord& rec, + u32 iters, + u32 cycle_budget_override = 0, + bool reprime_per_iter = true); + +// Re-seed the architectural + pipeline state from a capture WITHOUT dropping +// the compiled VU block cache (unlike the implicit prime BenchJit does, which +// resets the cache). Lets a bench loop re-run the whole program from its +// captured entry every iteration with no E-bit drift and no recompile. +void ReseedFromCapture(const vu_capture::CaptureRecord& rec); + +// Steady-state JIT bench: prime+compile once on iter 0 (warmup), then re-seed +// (no cache reset) and measure Execute each subsequent iter, so every timed +// iter runs the full program through the already-compiled block plus the real +// mVU dispatch/search/exit envelope. Drop iter 0 when summarizing. +// +// Re-seeding each iter is what keeps an E-bit-terminating program measurable: +// without it the program halts at its E-bit, leaving the VU stopped so every +// later Execute returns having run nothing (~tens of insns of dispatch), which +// badly under-reports the real cost. +std::vector BenchJitSteady(const vu_capture::CaptureRecord& rec, + u32 iters); + +// Compile the captured program once and dump the host code (ARM64) emitted +// for it as a textual disassembly, written to `out_path`. Intended for +// codegen-iteration A/B diffs: run --dump-asm, change an emitter, run +// --dump-asm again, diff the .codegen.s files. +// +// The dump covers `mVU.prog.x86start` to `mVU.prog.x86ptr` after a fresh +// Reset and one execute call — i.e. the bytes that this single program's +// compile placed in the cache. The first few entries before the program +// proper are typically the per-program dispatcher epilogue; everything +// after is real microcode codegen. ARM64 only. +// +// Returns false on file-open failure or if the recompiler environment +// isn't ready. On non-ARM64 hosts, the dump is a placeholder note +// indicating the disassembler isn't wired for this architecture. +bool DumpJitAsm(const vu_capture::CaptureRecord& rec, const std::string& out_path); + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuSnapshot.cpp b/tests/ctest/core/recompilers/harness/VuSnapshot.cpp new file mode 100644 index 0000000000..c92352b779 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuSnapshot.cpp @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "VuSnapshot.h" + +#include "VUmicro.h" + +#include +#include +#include + +namespace recompiler_tests { + +namespace { + +void CopyFromVuMem(const VURegs& vu, u32 addr, size_t count, u8* out) +{ + const u32 mask = (vu.idx == 0) ? VU0_MEMMASK : VU1_MEMMASK; + for (size_t i = 0; i < count; ++i) + out[i] = vu.Mem[(addr + static_cast(i)) & mask]; +} + +void CopyToVuMem(VURegs& vu, u32 addr, size_t count, const u8* src) +{ + const u32 mask = (vu.idx == 0) ? VU0_MEMMASK : VU1_MEMMASK; + for (size_t i = 0; i < count; ++i) + vu.Mem[(addr + static_cast(i)) & mask] = src[i]; +} + +} // namespace + +VuSnapshot VuSnapshot::Capture(int index, const std::vector& windows_to_capture) +{ + VuSnapshot s; + s.index = index; + std::memcpy(&s.regs, &vuRegs[index], sizeof(VURegs)); + + s.mem_windows.reserve(windows_to_capture.size()); + for (const auto& w : windows_to_capture) + { + VuMemWindow copy{w.addr, std::vector(w.bytes.size())}; + CopyFromVuMem(vuRegs[index], copy.addr, copy.bytes.size(), copy.bytes.data()); + s.mem_windows.push_back(std::move(copy)); + } + return s; +} + +void VuSnapshot::Restore() const +{ + u8* live_mem = vuRegs[index].Mem; + u8* live_micro = vuRegs[index].Micro; + std::memcpy(&vuRegs[index], ®s, sizeof(VURegs)); + vuRegs[index].Mem = live_mem; + vuRegs[index].Micro = live_micro; + + for (const auto& w : mem_windows) + CopyToVuMem(vuRegs[index], w.addr, w.bytes.size(), w.bytes.data()); +} + +void VuSnapshot::ZeroGlobals(int index) +{ + u8* live_mem = vuRegs[index].Mem; + u8* live_micro = vuRegs[index].Micro; + std::memset(&vuRegs[index], 0, sizeof(VURegs)); + vuRegs[index].Mem = live_mem; + vuRegs[index].Micro = live_micro; + vuRegs[index].idx = static_cast(index); + // VF[0] is hardwired { 0, 0, 0, 1.0f } on real hardware; the interpreter + // asserts on any drift via DbgCon.Error. Restore the canonical value here + // so a fresh ZeroGlobals leaves the bank in a runnable state. + vuRegs[index].VF[0].f.x = 0.0f; + vuRegs[index].VF[0].f.y = 0.0f; + vuRegs[index].VF[0].f.z = 0.0f; + vuRegs[index].VF[0].f.w = 1.0f; +} + +namespace { + +void EmitU32(std::vector& out, const char* name, u32 a, u32 b) +{ + if (a == b) + return; + std::ostringstream ss; + ss << name << ": JIT=0x" << std::hex << a << " INTERP=0x" << b; + out.push_back(ss.str()); +} + +void EmitVfLane(std::vector& out, int reg, char lane, u32 a, u32 b) +{ + if (a == b) + return; + std::ostringstream ss; + ss << "vf" << reg << "." << lane + << ": JIT=0x" << std::hex << a << " INTERP=0x" << b; + out.push_back(ss.str()); +} + +void DiffArchitectural(std::vector& diffs, const VURegs& a, const VURegs& b, + const std::vector& ignored_vi) +{ + // VF[32] — 4 lanes per register, compared as raw bits to catch + // NaN-payload divergences that would silently match under float compare. + for (int i = 0; i < 32; ++i) + { + EmitVfLane(diffs, i, 'x', a.VF[i].i.x, b.VF[i].i.x); + EmitVfLane(diffs, i, 'y', a.VF[i].i.y, b.VF[i].i.y); + EmitVfLane(diffs, i, 'z', a.VF[i].i.z, b.VF[i].i.z); + EmitVfLane(diffs, i, 'w', a.VF[i].i.w, b.VF[i].i.w); + } + // VI[32] — most are 16-bit (low 16 valid; upper 112 hardwired zero on HW). + // The "special" VIs (REG_R/I/Q/P/STATUS/MAC/CLIP/TPC/ + // FBRST/VPU_STAT) hold full 32-bit values: REG_Q/P/I/R store IEEE-754 + // floats, the *_FLAG triplet stores microVU's normalized flag layout, + // VPU_STAT/FBRST hold control bits in the upper byte. mVUendProgram + // writes the JIT's architectural Q to VI[REG_Q] in full, so the diff + // must compare the full word for those slots. + auto isFullWidthVi = [](int i) { + return i == REG_R || i == REG_I || i == REG_Q || i == REG_P + || i == REG_STATUS_FLAG || i == REG_MAC_FLAG || i == REG_CLIP_FLAG + || i == REG_TPC || i == REG_FBRST || i == REG_VPU_STAT; + }; + for (int i = 0; i < 32; ++i) + { + if (std::find(ignored_vi.begin(), ignored_vi.end(), i) != ignored_vi.end()) + continue; + std::string name = "vi" + std::to_string(i); + const u32 mask = isFullWidthVi(i) ? 0xFFFFFFFFu : 0x0000FFFFu; + EmitU32(diffs, name.c_str(), a.VI[i].UL & mask, b.VI[i].UL & mask); + } + // ACC — 4 lanes. + EmitVfLane(diffs, -1, 'x', a.ACC.i.x, b.ACC.i.x); + EmitVfLane(diffs, -1, 'y', a.ACC.i.y, b.ACC.i.y); + EmitVfLane(diffs, -1, 'z', a.ACC.i.z, b.ACC.i.z); + EmitVfLane(diffs, -1, 'w', a.ACC.i.w, b.ACC.i.w); + // q.UL / p.UL are interpreter-internal staging slots that the interp + // updates per-instruction inside Q/EFU ops. The JIT only commits to + // VI[REG_Q] / VI[REG_P] at end-of-program. Architectural state is the + // VI cell, already covered above — don't double-diff. +} + +void DiffPipeline(std::vector& diffs, const VURegs& a, const VURegs& b) +{ + for (int i = 0; i < 4; ++i) + { + std::string mname = "micro_macflags[" + std::to_string(i) + "]"; + std::string cname = "micro_clipflags[" + std::to_string(i) + "]"; + std::string sname = "micro_statusflags[" + std::to_string(i) + "]"; + EmitU32(diffs, mname.c_str(), a.micro_macflags[i], b.micro_macflags[i]); + EmitU32(diffs, cname.c_str(), a.micro_clipflags[i], b.micro_clipflags[i]); + EmitU32(diffs, sname.c_str(), a.micro_statusflags[i], b.micro_statusflags[i]); + } + EmitU32(diffs, "pending_q", a.pending_q, b.pending_q); + EmitU32(diffs, "pending_p", a.pending_p, b.pending_p); +} + +void DiffXgkick(std::vector& diffs, const VURegs& a, const VURegs& b) +{ + EmitU32(diffs, "xgkickaddr", a.xgkickaddr, b.xgkickaddr); + EmitU32(diffs, "xgkickdiff", a.xgkickdiff, b.xgkickdiff); + EmitU32(diffs, "xgkicksizeremaining", a.xgkicksizeremaining, b.xgkicksizeremaining); + EmitU32(diffs, "xgkickcyclecount", a.xgkickcyclecount, b.xgkickcyclecount); + EmitU32(diffs, "xgkickenable", a.xgkickenable, b.xgkickenable); + EmitU32(diffs, "xgkickendpacket", a.xgkickendpacket, b.xgkickendpacket); +} + +void DiffMemWindows(std::vector& diffs, + const std::vector& a, const std::vector& b) +{ + for (size_t i = 0; i < a.size() && i < b.size(); ++i) + { + const auto& aw = a[i]; + const auto& bw = b[i]; + if (aw.addr != bw.addr || aw.bytes.size() != bw.bytes.size()) + { + diffs.push_back("vumem window[" + std::to_string(i) + "] geometry mismatch"); + continue; + } + for (size_t j = 0; j < aw.bytes.size(); ++j) + { + if (aw.bytes[j] == bw.bytes[j]) + continue; + std::ostringstream ss; + ss << "vumem[0x" << std::hex << (aw.addr + static_cast(j)) + << "]: JIT=0x" << static_cast(aw.bytes[j]) + << " INTERP=0x" << static_cast(bw.bytes[j]); + diffs.push_back(ss.str()); + } + } +} + +} // namespace + +std::vector DiffVu(const VuSnapshot& a, const VuSnapshot& b, VuDiffMode mode, + const std::vector& ignored_vi) +{ + std::vector diffs; + if (a.index != b.index) + { + diffs.push_back("index mismatch"); + return diffs; + } + + DiffArchitectural(diffs, a.regs, b.regs, ignored_vi); + if (mode == VuDiffMode::Strict) + DiffPipeline(diffs, a.regs, b.regs); + if (a.index == 1 && mode != VuDiffMode::XgkickPacketEquivalent) + DiffXgkick(diffs, a.regs, b.regs); + DiffMemWindows(diffs, a.mem_windows, b.mem_windows); + + return diffs; +} + +void PrintVu(std::ostream& os, const VuSnapshot& s) +{ + os << std::hex; + os << " VU" << s.index << " TPC=0x" << s.regs.VI[REG_TPC].UL + << " VPU_STAT=0x" << s.regs.VI[REG_VPU_STAT].UL + << " STATUS=0x" << s.regs.VI[REG_STATUS_FLAG].UL + << " MAC=0x" << s.regs.VI[REG_MAC_FLAG].UL + << " CLIP=0x" << s.regs.VI[REG_CLIP_FLAG].UL << "\n"; + for (int i = 0; i < 32; i += 2) + { + os << " vf" << std::dec << i << ": " + << s.regs.VF[i].f.x << "," << s.regs.VF[i].f.y << "," + << s.regs.VF[i].f.z << "," << s.regs.VF[i].f.w + << " vf" << i + 1 << ": " + << s.regs.VF[i + 1].f.x << "," << s.regs.VF[i + 1].f.y << "," + << s.regs.VF[i + 1].f.z << "," << s.regs.VF[i + 1].f.w << "\n"; + } + for (int i = 0; i < 32; i += 8) + { + os << " vi" << std::dec << i << "-" << i + 7 << ":"; + for (int j = 0; j < 8; ++j) + os << " 0x" << std::hex << (s.regs.VI[i + j].UL & 0xFFFF); + os << "\n"; + } + os << " q=0x" << std::hex << s.regs.q.UL << " p=0x" << s.regs.p.UL << "\n"; + os << std::dec; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuSnapshot.h b/tests/ctest/core/recompilers/harness/VuSnapshot.h new file mode 100644 index 0000000000..88b4b35a24 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuSnapshot.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "VU.h" +#include "common/Pcsx2Defs.h" + +#include +#include +#include +#include +#include + +namespace recompiler_tests { + +struct VuMemWindow +{ + u32 addr = 0; + std::vector bytes; +}; + +// Captures everything a diff between JIT and interp needs to compare for one +// VU instance. The architectural surface is `VF[32]`, `VI[32]` (low 16 bits), +// `ACC`, the spilled `q`/`p` scalars, the flag pipeline arrays, XGKICK +// state (VU1 only), and tracked windows of VU data memory. +// +// Fields explicitly excluded from `Diff*` (captured but never compared): +// * `Mem`, `Micro` pointers — preserved on Restore(), never compared. +// * `cycle`, `nextBlockCycles`, `start_pc`, `code` — dispatcher bookkeeping. +// * `branch`, `branchpc`, `delaybranchpc`, `takedelaybranch`, `ebit` — +// interpreter mid-execution sentinels; meaningless after E-bit termination. +// * `fmac[4]`, `fdiv`, `efu`, `ialu[4]`, fmac/ialu r/w positions, counts — +// interpreter pipeline modeling that microVU represents differently. +// * `macflag`, `statusflag`, `clipflag` — `VURegs.h` calls these "kind of +// hacky"; the architecturally-meaningful values live in `VI[REG_*_FLAG]`. +// * `idx`, `flags`, `VIBackupCycles`, `VIOldValue`, `VIRegNumber` — +// internal bookkeeping. +struct VuSnapshot +{ + int index = 0; + VURegs regs{}; + std::vector mem_windows; + + // Snapshots `vuRegs[index]` plus the requested data-memory windows from + // `VU.Mem`. Captures the full struct verbatim; the diff function decides + // which fields are architecturally significant. + static VuSnapshot Capture(int index, const std::vector& windows_to_capture); + + // Writes architectural state back to `vuRegs[index]` and restores any + // captured memory windows. Preserves the live `Mem` / `Micro` pointers + // so the restored struct keeps pointing at SysMemory-allocated VU memory. + void Restore() const; + + // Zeroes the architectural state of one VU instance while preserving the + // `Mem` / `Micro` pointers. Pipeline / dispatcher bookkeeping is reset. + static void ZeroGlobals(int index); +}; + +// Diff mode controls strictness on the microVU-specific pipeline-state arrays +// (`micro_macflags[4]`, `micro_clipflags[4]`, `micro_statusflags[4]`) and on +// the interpreter's pending_q / pending_p shadows. +// * Strict — bit-exact compare on every captured field. Use for +// interp-vs-interp round-trip tests where divergence is impossible. +// * PipelinePermissive — architectural fields strict (VF/VI/ACC/q/p/MAC/ +// STATUS/CLIP/XGKICK/memory), pipeline state ignored. Use for +// JIT-vs-interp where the two disagree on internal pipeline modeling but +// converge on architectural state at E-bit termination. +// * XgkickPacketEquivalent — PipelinePermissive minus the xgkick scratch +// fields (xgkickaddr/diff/sizeremaining/cyclecount/enable/endpacket). +// The non-XGKICKHACK microVU emit path computes addr/size in locals and +// fires the GIF transfer directly, without writing back to the +// VU1.xgkick* fields — so the JIT and interp legitimately disagree on +// those scratch fields. Tests using +// this mode are expected to assert architectural equivalence by +// comparing the GIF Path 1 packet bytes (VuTestHarness:: +// Path1PacketBytesJit/Interp). +enum class VuDiffMode +{ + Strict, + PipelinePermissive, + XgkickPacketEquivalent, +}; + +// Returns a list of human-readable field-level differences, empty when the +// snapshots match across every field considered architecturally significant +// for the chosen mode. `ignored_vi` opts specific VI indices out of the diff +// — used by tests that probe a termination path where the JIT and interp +// legitimately disagree on bookkeeping registers (e.g. REG_TPC after an +// M-bit break) without touching the architectural state under test. +std::vector DiffVu(const VuSnapshot& a, const VuSnapshot& b, + VuDiffMode mode = VuDiffMode::PipelinePermissive, + const std::vector& ignored_vi = {}); + +void PrintVu(std::ostream& os, const VuSnapshot& s); + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuTestHarness.cpp b/tests/ctest/core/recompilers/harness/VuTestHarness.cpp new file mode 100644 index 0000000000..ac97e9902e --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuTestHarness.cpp @@ -0,0 +1,466 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "VuTestHarness.h" + +#include "Config.h" +#include "Gif_Unit.h" +#include "VU.h" +#include "VUmicro.h" +#include "common/FPControl.h" + +#include +#include + +namespace recompiler_tests { + +namespace { + +VURegs& Regs(int idx) { return vuRegs[idx]; } + +u32 MemMask(int idx) { return idx == 0 ? VU0_MEMMASK : VU1_MEMMASK; } +u32 MemSize(int idx) { return idx == 0 ? VU0_MEMSIZE : VU1_MEMSIZE; } +u32 ProgMask(int idx) { return idx == 0 ? VU0_PROGMASK : VU1_PROGMASK; } +u32 ProgSize(int idx) { return idx == 0 ? VU0_PROGSIZE : VU1_PROGSIZE; } + +BaseVUmicroCPU* InterpCpu(int idx) +{ + return idx == 0 ? static_cast(&CpuIntVU0) + : static_cast(&CpuIntVU1); +} + +BaseVUmicroCPU* JitCpu(int idx) +{ + return idx == 0 ? static_cast(&CpuMicroVU0) + : static_cast(&CpuMicroVU1); +} + +// VPU_STAT bit indicating "VU running". The interpreter's Execute loop +// breaks once this clears, which the E-bit cleanup does automatically. +// Both VU0 and VU1's running-bits live in VU0.VI[REG_VPU_STAT] — the VU1 +// interpreter's Execute loop explicitly checks +// `VU0.VI[REG_VPU_STAT].UL & 0x100`, not VU1's own VI. Same wiring on the +// cleanup side. +u32 RunningBit(int idx) { return idx == 0 ? 0x1u : 0x100u; } + +// Special VIs (REG_R/I/Q/P/STATUS/MAC/CLIP/TPC/FBRST/VPU_STAT) hold full +// 32-bit values; the others use only the low 16 (per VURegs.h). Mask only +// the 16-bit ones so callers reading/writing REG_Q etc. see the full bits, +// not just the low half. +inline u32 ViMaskFor(u32 reg_idx) +{ + switch (reg_idx) + { + case REG_R: case REG_I: case REG_Q: case REG_P: + case REG_STATUS_FLAG: case REG_MAC_FLAG: case REG_CLIP_FLAG: + case REG_TPC: case REG_FBRST: case REG_VPU_STAT: + return 0xFFFFFFFFu; + default: return 0x0000FFFFu; + } +} + +} // namespace + +VuTestHarness::VuTestHarness(int vu_index) + : vu_index_(vu_index) +{ + EXPECT_TRUE(RecompilerTestEnvironment::IsReady()) + << "RecompilerTestEnvironment was not set up — VU harness cannot run."; + EXPECT_TRUE(vu_index == 0 || vu_index == 1) + << "VuTestHarness: vu_index must be 0 or 1, got " << vu_index; + VuSnapshot::ZeroGlobals(vu_index_); + // Cross-test isolation: a previous fixture may have left VU.Mem dirty + // at addresses the new test doesn't touch via WriteMemU32 but does read + // (via VU loads). VuSnapshot::ZeroGlobals only resets the register file. + std::memset(Regs(vu_index_).Mem, 0, MemSize(vu_index_)); + gif_test_hooks::g_path1_sink = &path1_packets_; +} + +VuTestHarness::~VuTestHarness() +{ + gif_test_hooks::g_path1_sink = nullptr; +} + +void VuTestHarness::SetVf(u32 reg_idx, float x, float y, float z, float w) +{ + if (reg_idx == 0) // VF[0] is hardwired (0,0,0,1) + return; + auto& vf = Regs(vu_index_).VF[reg_idx]; + vf.f.x = x; vf.f.y = y; vf.f.z = z; vf.f.w = w; +} + +void VuTestHarness::SetVfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w) +{ + if (reg_idx == 0) + return; + auto& vf = Regs(vu_index_).VF[reg_idx]; + vf.i.x = x; vf.i.y = y; vf.i.z = z; vf.i.w = w; +} + +void VuTestHarness::SetVi(u32 reg_idx, u32 value) +{ + if (reg_idx == 0) // VI[0] is hardwired zero + return; + // The special VIs (REG_R/I/Q/P/flags/TPC/FBRST/VPU_STAT) are full-width; + // the rest are 16-bit. Mask to match, mirroring GetViInterp/GetViJit. + Regs(vu_index_).VI[reg_idx].UL = value & ViMaskFor(reg_idx); +} + +void VuTestHarness::SetQ(u32 bits) +{ + Regs(vu_index_).q.UL = bits; + Regs(vu_index_).VI[REG_Q].UL = bits; +} + +void VuTestHarness::SetP(u32 bits) +{ + Regs(vu_index_).p.UL = bits; + Regs(vu_index_).VI[REG_P].UL = bits; +} + +void VuTestHarness::WriteMemU32(u32 addr, u32 value) +{ + const u32 a = addr & MemMask(vu_index_); + std::memcpy(Regs(vu_index_).Mem + a, &value, sizeof(value)); + MergeTrackedWindow(a & ~0x3u, 4); +} + +void VuTestHarness::WriteMemU128(u32 addr, u32 x, u32 y, u32 z, u32 w) +{ + const u32 a = addr & MemMask(vu_index_); + const u32 quad[4] = {x, y, z, w}; + std::memcpy(Regs(vu_index_).Mem + a, quad, sizeof(quad)); + MergeTrackedWindow(a & ~0xFu, 16); +} + +u32 VuTestHarness::ReadMemU32(u32 addr) const +{ + u32 value = 0; + const u32 a = addr & MemMask(vu_index_); + std::memcpy(&value, Regs(vu_index_).Mem + a, sizeof(value)); + return value; +} + +void VuTestHarness::TrackMemWindow(u32 addr, size_t bytes) +{ + MergeTrackedWindow(addr, bytes); +} + +void VuTestHarness::MergeTrackedWindow(u32 addr, size_t bytes) +{ + const u32 start = addr & ~0x3u; + const size_t end = ((addr + bytes + 3u) & ~0x3u); + const size_t new_size = end - start; + + for (auto& w : mem_windows_) + { + const u32 w_end = w.addr + static_cast(w.bytes.size()); + if (start + new_size < w.addr || start > w_end) + continue; + const u32 merged_start = (start < w.addr) ? start : w.addr; + const u32 merged_end = (start + new_size > w_end) + ? static_cast(start + new_size) : w_end; + w.addr = merged_start; + w.bytes.resize(merged_end - merged_start); + return; + } + mem_windows_.push_back(VuMemWindow{start, std::vector(new_size)}); +} + +void VuTestHarness::LoadProgram(std::initializer_list pairs) +{ + program_pairs_.assign(pairs.begin(), pairs.end()); + ASSERT_FALSE(program_pairs_.empty()) + << "VuTestHarness::LoadProgram requires at least one instruction pair"; + ASSERT_TRUE((program_pairs_.back().upper & vu::bits::E) != 0) + << "VuTestHarness::LoadProgram requires E-bit on the final user-supplied " + "pair so the interpreter terminates deterministically."; + + // Architectural E-bit cleanup runs on the *next* exec step (one delay-slot + // pair after the E-bit pair). Append a NOP pair so the test program is + // self-terminating without each test having to remember. + program_pairs_.push_back(vu::NopPair()); + + const u32 prog_bytes = static_cast(program_pairs_.size() * 8u); + ASSERT_LE(prog_bytes, ProgSize(vu_index_)) + << "VuTestHarness program (" << prog_bytes << " bytes) exceeds VU" + << vu_index_ << " micro memory (" << ProgSize(vu_index_) << " bytes)"; + + WriteProgramToMicro(); +} + +void VuTestHarness::WriteProgramToMicro() +{ + auto& vu = Regs(vu_index_); + // Zero the entire micro memory first so a shorter program in this test + // cannot read leftover instruction bytes from a previous longer test + // running through the same fixture. + std::memset(vu.Micro, 0, ProgSize(vu_index_)); + for (size_t i = 0; i < program_pairs_.size(); ++i) + { + const u32 base = static_cast(i * 8u) & ProgMask(vu_index_); + std::memcpy(vu.Micro + base + 0, &program_pairs_[i].lower, 4); + std::memcpy(vu.Micro + base + 4, &program_pairs_[i].upper, 4); + } +} + +void VuTestHarness::SeedEntryState(bool reset_block_cache) +{ + auto& vu = Regs(vu_index_); + + // Start at pair 0 (kProgramPc / 8 = 0). Stored as a pair index in + // REG_TPC; InterpVU::Execute will internally `<<= 3` it to a + // byte address before fetching from VU.Micro. + vu.VI[REG_TPC].UL = kProgramPc / 8u; + vu.start_pc = kProgramPc; + + // Mark VU as "running" so Execute's loop predicate fires. The + // running-bit lives in VU0.VI[REG_VPU_STAT] for *both* VUs (bit 0x1 + // for VU0, bit 0x100 for VU1) — always poke VU0's copy here, then + // also stamp the local instance for snapshot consistency on VU1. + vuRegs[0].VI[REG_VPU_STAT].UL = (vuRegs[0].VI[REG_VPU_STAT].UL & ~0xFFFu) | RunningBit(vu_index_); + vu.VI[REG_VPU_STAT].UL = vuRegs[0].VI[REG_VPU_STAT].UL; + + // Cycle counter / pipeline state — start fresh so the first test in + // a fixture doesn't inherit decay from a prior test. + vu.cycle = 0; + vu.ebit = 0; + vu.branch = 0; + vu.branchpc = 0; + vu.delaybranchpc = 0; + vu.takedelaybranch = false; + vu.flags = 0; + vu.fmacreadpos = vu.fmacwritepos = vu.fmaccount = 0; + vu.ialureadpos = vu.ialuwritepos = vu.ialucount = 0; + std::memset(&vu.fdiv, 0, sizeof(vu.fdiv)); + std::memset(&vu.efu, 0, sizeof(vu.efu)); + std::memset(&vu.fmac, 0, sizeof(vu.fmac)); + std::memset(&vu.ialu, 0, sizeof(vu.ialu)); + + InterpCpu(vu_index_)->SetStartPC(kProgramPc); + JitCpu(vu_index_)->SetStartPC(kProgramPc); + + // Drop any compiled block left by a prior test. mVUreset re-emits the + // dispatcher and zeroes mVU.prog.lpState — cheapest correct invalidation. + // RunJitPreserveBlockCache skips this so pre-seeded (hydrated) blocks + // survive into the execution. + if (reset_block_cache) + RecompilerTestEnvironment::ResetVuBlockCache(vu_index_); +} + +void VuTestHarness::RunInterpFromSeeded() +{ + // Match production's REC_VU1 ↔ CpuVU1 invariant (the CpuVU1 = EnableVU1 ? + // rec : interp selection made when CPU providers are initialized): the + // interp's `_vuXGKICKTransfer` is only reachable in production when + // EnableVU1=false (interp is the active VU1 engine), and `GetGSPacketSize` + // only emits its bit-31 EOP signal when `(CHECK_XGKICKHACK || !REC_VU1)` + // holds. The harness violates the + // invariant by calling CpuIntVU1::Execute while EnableVU1=true to diff + // against the JIT — which leaves `_vuXGKICKTransfer` looping forever on + // any back-to-back kick because xgkickendpacket never gets set. Flip the + // flag for the duration of the interp pass so the production guard fires + // and the loop terminates correctly. + const bool saved = EmuConfig.Cpu.Recompiler.EnableVU1; + EmuConfig.Cpu.Recompiler.EnableVU1 = false; + InterpCpu(vu_index_)->Execute(kCycleBudget); + EmuConfig.Cpu.Recompiler.EnableVU1 = saved; +} + +void VuTestHarness::RunJitFromSeeded() +{ + // recMicroVU0/1::Execute already implements a bounded-cycle entry — + // mVU.cycles is the budget and the per-block cycle test exits to + // the dispatcher when it exhausts. No new entry point on the microVU + // side is required for E-bit-terminated programs. + JitCpu(vu_index_)->Execute(kCycleBudget); +} + +void VuTestHarness::Run() +{ + ASSERT_FALSE(program_pairs_.empty()) + << "LoadProgram() must be called before Run()"; + + // Set host FPCR to the VU's FPCR (FZ + ChopZero) for BOTH passes. + // The interpreter's `_vuDIV`/`_vuSQRT` etc. compute via host scalar + // FP (`fs / ft`), which uses the host thread FPCR. The JIT switches + // FPCR to VU FPCR at dispatcher entry. If the harness leaves host + // FPCR at default (round-to-nearest, no flush) for the interp pass, + // interp produces IEEE-rounded results while JIT produces VU-rounded + // (round-toward-zero) results — a 1-ULP false-positive divergence on + // any FMAC/DIV with non-trivial operands. Setting both passes to + // vu_fpcr removes the asymmetry; any divergence is then a real + // codegen difference. Mirrors VuReplay.cpp's pattern. + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + const FPControlRegister vu_fpcr = (vu_index_ == 0) + ? EmuConfig.Cpu.VU0FPCR + : EmuConfig.Cpu.VU1FPCR; + FPControlRegister::SetCurrent(vu_fpcr); + + // JIT side first. SeedEntryState resets the block cache, so the JIT + // compiles a fresh variant against this test's seeded entry pState. + SeedEntryState(); + pre_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + path1_packets_.clear(); + RunJitFromSeeded(); + jit_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + path1_packets_jit_ = path1_packets_; + + // Restore registers + tracked memory windows, re-seed the dispatch + // state, then run the interpreter from the same pre-state. Capture + // the interp packet stream separately so XGKICK tests can compare. + pre_snapshot_.Restore(); + SeedEntryState(); + path1_packets_.clear(); + RunInterpFromSeeded(); + interp_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + path1_packets_interp_ = path1_packets_; + has_run_ = true; + + FPControlRegister::SetCurrent(saved_fpcr); + + // PipelinePermissive (default): the interpreter doesn't populate the + // 4-stage micro_*flags pipeline arrays (those are microVU's shadow), + // so a strict diff would fire on every run. The architectural snapshot + // of MAC/STATUS/CLIP in VI is still strict. XgkickPacketEquivalent — + // further skips xgkickaddr/diff/cyclecount/enable/endpacket because + // the non-XGKICKHACK microVU path doesn't write VU1.xgkick* (see + // VuDiffMode docstring). + const auto diffs = DiffVu(jit_snapshot_, interp_snapshot_, + diff_mode_, ignored_vi_); + if (!diffs.empty()) + { + std::ostringstream ss; + ss << "VU" << vu_index_ << " JIT-vs-interp divergence (" + << diffs.size() << "):\n"; + for (const auto& d : diffs) + ss << " " << d << "\n"; + ss << "Pre-state:\n"; + PrintVu(ss, pre_snapshot_); + ss << "JIT post-state:\n"; + PrintVu(ss, jit_snapshot_); + ss << "Interp post-state:\n"; + PrintVu(ss, interp_snapshot_); + ADD_FAILURE() << ss.str(); + } +} + +void VuTestHarness::RunJitPreserveBlockCache() +{ + ASSERT_FALSE(program_pairs_.empty()) + << "LoadProgram() must be called before RunJitPreserveBlockCache()"; + ASSERT_TRUE(has_run_) + << "RunJitPreserveBlockCache() replays the pre-state captured by a " + "prior Run() — call Run() first."; + + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + const FPControlRegister vu_fpcr = (vu_index_ == 0) + ? EmuConfig.Cpu.VU0FPCR + : EmuConfig.Cpu.VU1FPCR; + FPControlRegister::SetCurrent(vu_fpcr); + + pre_snapshot_.Restore(); + SeedEntryState(/*reset_block_cache=*/false); + path1_packets_.clear(); + RunJitFromSeeded(); + jit_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + path1_packets_jit_ = path1_packets_; + + FPControlRegister::SetCurrent(saved_fpcr); +} + +void VuTestHarness::RunInterpOnly() +{ + ASSERT_FALSE(program_pairs_.empty()) + << "LoadProgram() must be called before RunInterpOnly()"; + + const FPControlRegister saved_fpcr = FPControlRegister::GetCurrent(); + const FPControlRegister vu_fpcr = (vu_index_ == 0) + ? EmuConfig.Cpu.VU0FPCR + : EmuConfig.Cpu.VU1FPCR; + FPControlRegister::SetCurrent(vu_fpcr); + + SeedEntryState(); + pre_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + path1_packets_.clear(); + RunInterpFromSeeded(); + interp_snapshot_ = VuSnapshot::Capture(vu_index_, mem_windows_); + jit_snapshot_ = interp_snapshot_; + path1_packets_interp_ = path1_packets_; + path1_packets_jit_ = path1_packets_; + has_run_ = true; + + FPControlRegister::SetCurrent(saved_fpcr); +} + +u32 VuTestHarness::GetVfBitsInterp(u32 reg_idx, char lane) const +{ + const auto& vf = interp_snapshot_.regs.VF[reg_idx]; + switch (lane) { case 'x': return vf.i.x; case 'y': return vf.i.y; + case 'z': return vf.i.z; case 'w': return vf.i.w; } + return 0; +} +u32 VuTestHarness::GetVfBitsJit(u32 reg_idx, char lane) const +{ + const auto& vf = jit_snapshot_.regs.VF[reg_idx]; + switch (lane) { case 'x': return vf.i.x; case 'y': return vf.i.y; + case 'z': return vf.i.z; case 'w': return vf.i.w; } + return 0; +} +float VuTestHarness::GetVfInterp(u32 reg_idx, char lane) const +{ + const auto& vf = interp_snapshot_.regs.VF[reg_idx]; + switch (lane) { case 'x': return vf.f.x; case 'y': return vf.f.y; + case 'z': return vf.f.z; case 'w': return vf.f.w; } + return 0.0f; +} +float VuTestHarness::GetVfJit(u32 reg_idx, char lane) const +{ + const auto& vf = jit_snapshot_.regs.VF[reg_idx]; + switch (lane) { case 'x': return vf.f.x; case 'y': return vf.f.y; + case 'z': return vf.f.z; case 'w': return vf.f.w; } + return 0.0f; +} +u32 VuTestHarness::GetViInterp(u32 reg_idx) const +{ + return interp_snapshot_.regs.VI[reg_idx].UL & ViMaskFor(reg_idx); +} +u32 VuTestHarness::GetViJit(u32 reg_idx) const +{ + return jit_snapshot_.regs.VI[reg_idx].UL & ViMaskFor(reg_idx); +} +u32 VuTestHarness::GetMemU32Interp(u32 addr) const +{ + const u32 a = addr & MemMask(vu_index_); + for (const auto& w : interp_snapshot_.mem_windows) + { + if (a >= w.addr && a + 4 <= w.addr + w.bytes.size()) + { + u32 value = 0; + std::memcpy(&value, w.bytes.data() + (a - w.addr), 4); + return value; + } + } + return 0; +} +bool VuTestHarness::HasTerminated() const +{ + return (vuRegs[0].VI[REG_VPU_STAT].UL & RunningBit(vu_index_)) == 0; +} + +u32 VuTestHarness::GetMemU32Jit(u32 addr) const +{ + const u32 a = addr & MemMask(vu_index_); + for (const auto& w : jit_snapshot_.mem_windows) + { + if (a >= w.addr && a + 4 <= w.addr + w.bytes.size()) + { + u32 value = 0; + std::memcpy(&value, w.bytes.data() + (a - w.addr), 4); + return value; + } + } + return 0; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/VuTestHarness.h b/tests/ctest/core/recompilers/harness/VuTestHarness.h new file mode 100644 index 0000000000..c3cc174148 --- /dev/null +++ b/tests/ctest/core/recompilers/harness/VuTestHarness.h @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "RecompilerTestEnvironment.h" +#include "VuEncode.h" +#include "VuSnapshot.h" +// VuDiffMode is defined in VuSnapshot.h. + +#include "common/Pcsx2Defs.h" + +#include +#include +#include + +namespace recompiler_tests { + +// VU recompiler test harness. Drives one VU instance (0 or 1) through a short +// instruction-pair program and captures architectural state for diff. Run() +// executes the program through both the JIT and the interpreter and diffs the +// post-states; RunInterpOnly() drives the interpreter alone. +// +// Termination: the harness expects the user-supplied program to have its +// final pair tagged with the E-bit. `LoadProgram` validates this and appends +// a single NOP pair to honour the architectural one-pair delay slot after +// E-bit (see VU0microInterp.cpp:225-234 for the `VU->ebit-- == 1` cleanup +// cadence). +class VuTestHarness +{ +public: + explicit VuTestHarness(int vu_index); + ~VuTestHarness(); + + VuTestHarness(const VuTestHarness&) = delete; + VuTestHarness& operator=(const VuTestHarness&) = delete; + + // ---- Pre-state setters ---- + void SetVf(u32 reg_idx, float x, float y, float z, float w); + void SetVfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w); + // Masks to the register's architectural width: 32-bit for the special VIs + // (REG_R/I/Q/P/flags/TPC/FBRST/VPU_STAT), low 16 for the rest. For REG_Q/P + // prefer SetQ/SetP, which also write the q.UL/p.UL float slot. + void SetVi(u32 reg_idx, u32 value); + void SetQ(u32 bits); + void SetP(u32 bits); + + // VU data memory access — addr is a byte offset into VU.Mem and is + // masked by VU0_MEMMASK / VU1_MEMMASK. Tracked windows are diffed + // after Run(). + void WriteMemU32(u32 addr, u32 value); + void WriteMemU128(u32 addr, u32 x, u32 y, u32 z, u32 w); + u32 ReadMemU32(u32 addr) const; + void TrackMemWindow(u32 addr, size_t bytes); + + // Skip a VI register in the JIT-vs-interp auto-diff. Use sparingly: this + // is the right tool when JIT and interp legitimately disagree on a + // bookkeeping register (e.g. REG_TPC after an M-bit mid-program break) + // but the test's architectural assertions are still worth running. + void IgnoreViInDiff(int reg_idx) { ignored_vi_.push_back(reg_idx); } + + // Override the JIT-vs-interp diff mode used by Run(). Default is + // PipelinePermissive — XGKICK tests use XgkickPacketEquivalent to + // silence the legitimately-divergent xgkick scratch fields and assert + // architectural equivalence on the captured Path 1 packet stream. + void SetDiffMode(VuDiffMode mode) { diff_mode_ = mode; } + + // ---- Program load ---- + // Each VuOp is a {lower, upper} pair written to VU.Micro at byte + // offsets `pc, pc+8, ...` starting at `kProgramPc = 0`. The final + // user-supplied pair must have the E-bit set; a follow-up NOP pair + // is appended automatically as the architectural E-bit delay slot. + void LoadProgram(std::initializer_list pairs); + + // ---- Execute ---- + // Runs the program through the interpreter and through the JIT, + // captures both post-states, and gtest-fails on any architectural + // divergence. + void Run(); + + // One-sided execution against the interpreter only. Both `JitSnapshot()` + // and `InterpSnapshot()` reflect the interpreter result. Use when + // authoring a new test before the JIT path is ready. + void RunInterpOnly(); + + // JIT-only re-run from the SAME pre-state as the last Run(), WITHOUT + // resetting the VU block cache first — compiled (or hydrated) blocks + // survive into this execution. Updates JitSnapshot(); performs no diff. + // Used by the persisted-JIT round-trip tests, where the whole point is + // asserting the pre-seeded block graph runs without a recompile. + void RunJitPreserveBlockCache(); + + // ---- Post-run accessors ---- + u32 GetVfBitsInterp(u32 reg_idx, char lane) const; + u32 GetVfBitsJit(u32 reg_idx, char lane) const; + float GetVfInterp(u32 reg_idx, char lane) const; + float GetVfJit(u32 reg_idx, char lane) const; + u32 GetViInterp(u32 reg_idx) const; + u32 GetViJit(u32 reg_idx) const; + u32 GetMemU32Interp(u32 addr) const; + u32 GetMemU32Jit(u32 addr) const; + + // Returns true once the most recent Run() / RunInterpOnly() has cleared + // the VU's running-bit in VU0.VI[REG_VPU_STAT]. The running-bit lives + // in VU0's VI for both VU0 and VU1 (see VU1microInterp.cpp:309), so a + // per-VU snapshot diff cannot reach it directly. + bool HasTerminated() const; + + // Bytes of every GIF Path 1 packet emitted via XGKICK during the most + // recent Run() / RunInterpOnly(). Captured by the test-only sink wired + // into Gif_Unit::TransferGSPacketData (see Gif_Unit.h gif_test_hooks). + // Packets are appended back-to-back; tests that care about boundaries + // can re-parse the GIF tags. + // + // Path1PacketBytesJit/Interp() return the per-pass capture that Run() + // builds — used by XGKICK tests to assert "JIT and interp emit the same + // architectural GIF packet stream" even when their internal scratch + // state legitimately diverges (see VuDiffMode::XgkickPacketEquivalent). + const std::vector& Path1PacketBytes() const { return path1_packets_; } + const std::vector& Path1PacketBytesJit() const { return path1_packets_jit_; } + const std::vector& Path1PacketBytesInterp() const { return path1_packets_interp_; } + + const VuSnapshot& JitSnapshot() const { return jit_snapshot_; } + const VuSnapshot& InterpSnapshot() const { return interp_snapshot_; } + + int VuIndex() const { return vu_index_; } + + // Program memory base — pair 0 lives here, pair N at +8N. Always 0 + // for the harness (start of VU.Micro). + static constexpr u32 kProgramPc = 0; + +private: + void SeedEntryState(bool reset_block_cache = true); + void RunInterpFromSeeded(); + void RunJitFromSeeded(); + void WriteProgramToMicro(); + void MergeTrackedWindow(u32 addr, size_t bytes); + + int vu_index_; + std::vector program_pairs_; + std::vector mem_windows_; + std::vector ignored_vi_; + VuDiffMode diff_mode_ = VuDiffMode::PipelinePermissive; + std::vector path1_packets_; + std::vector path1_packets_jit_; + std::vector path1_packets_interp_; + + VuSnapshot pre_snapshot_; + VuSnapshot jit_snapshot_; + VuSnapshot interp_snapshot_; + bool has_run_ = false; + + // Cycle budget — generous for short 2-256-instruction test programs. + // Real microprograms can run thousands of cycles but the harness's + // E-bit terminator caps every test deterministically. + static constexpr u32 kCycleBudget = 4096; +}; + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/iop_alu_tests.cpp b/tests/ctest/core/recompilers/iop_alu_tests.cpp new file mode 100644 index 0000000000..5066c28b3a --- /dev/null +++ b/tests/ctest/core/recompilers/iop_alu_tests.cpp @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +#include + +using namespace recompiler_tests; +using namespace mips; + +// Sanity: JIT and interp agree on addiu semantics including sign-extension +// of the 16-bit immediate. The go-to "does the pipeline work end-to-end?" +// test. +TEST(IopAlu, AddiuSignExtend) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 100); + h.LoadProgram({ + ADDIU(reg::v0, reg::a0, -7), // immediate sign-extended to 0xFFFFFFF9 + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 93u); + EXPECT_EQ(h.GetGprJit(reg::v0), 93u); +} + +TEST(IopAlu, AddiuWrapPositive) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x7FFFFFFFu); + h.LoadProgram({ADDIU(reg::v0, reg::a0, 1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x80000000u); +} + +TEST(IopAlu, AddiuZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.LoadProgram({ADDIU(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 42u); +} + +TEST(IopAlu, AdduBasic) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 10); + h.SetGpr(reg::a1, 20); + h.LoadProgram({ADDU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 30u); +} + +TEST(IopAlu, SubuUnderflow) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 5); + h.SetGpr(reg::a1, 10); + h.LoadProgram({SUBU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), static_cast(-5)); +} + +// SUBU with Rs==Rt is the zero fast-path: the JIT materializes 0 directly +// instead of subtracting a register from itself. Result must still be 0 and +// match interp. +TEST(IopAlu, SubuSelfIsZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xDEADBEEFu); + h.LoadProgram({SUBU(reg::v0, reg::a0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0u); + EXPECT_EQ(h.GetGprJit(reg::v0), 0u); +} + +// Regression: SUB/SUBU with a CONST minuend (Rs const, e.g. from a preceding +// LUI) and the destination aliasing the subtrahend (Rd == Rt). A naive const +// path that materializes the constant into Rd before subtracting clobbers Rt +// with the constant first when Rd and Rt share a host register, so the +// subtract degenerates to cv - cv = 0 instead of cv - rt_old. Route the +// constant through a scratch register. `x = CONST - y` written back over y is +// a common idiom. +TEST(IopAlu, SubConstMinuendDestAliasesSubtrahend) +{ + JitTestHarness h; + h.SetGpr(reg::s6, 0xFFFFAC45u); // r22 = subtrahend AND dest (Rd == Rt) + h.LoadProgram({ + LUI(reg::t5, 0x0FE7), // r13 = 0x0FE70000 (const minuend) + SUB(reg::s6, reg::t5, reg::s6), // r22 = r13 - r22 = 0x0FE753BB (not 0) + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::s6), 0x0FE753BBu); + EXPECT_EQ(h.GetGprJit(reg::s6), 0x0FE753BBu); +} + +TEST(IopAlu, SubuConstMinuendDestAliasesSubtrahend) +{ + JitTestHarness h; + h.SetGpr(reg::s6, 0xFFFFAC45u); + h.LoadProgram({ + LUI(reg::t5, 0x0FE7), + SUBU(reg::s6, reg::t5, reg::s6), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::s6), 0x0FE753BBu); + EXPECT_EQ(h.GetGprJit(reg::s6), 0x0FE753BBu); +} + +TEST(IopAlu, AndAllBits) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFFFFFFu); + h.SetGpr(reg::a1, 0xAAAAAAAAu); + h.LoadProgram({AND(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xAAAAAAAAu); +} + +TEST(IopAlu, OrCombine) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00FF00FFu); + h.SetGpr(reg::a1, 0xFF00FF00u); + h.LoadProgram({OR(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFFFFFFu); +} + +TEST(IopAlu, XorSelfIsZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x12345678u); + h.LoadProgram({XOR(reg::v0, reg::a0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0u); +} + +TEST(IopAlu, LuiShifts) +{ + JitTestHarness h; + h.LoadProgram({LUI(reg::v0, 0x1234)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x12340000u); +} + +TEST(IopAlu, SltSigned) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-5)); + h.SetGpr(reg::a1, 3); + h.LoadProgram({SLT(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopAlu, SltuUnsignedMaxIsNotLess) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFFFFFFu); + h.SetGpr(reg::a1, 3u); + h.LoadProgram({SLTU(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0u); +} + +TEST(IopAlu, AndiZeroExtends) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFFFFFFu); + h.LoadProgram({ANDI(reg::v0, reg::a0, 0x1234)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00001234u); +} + +TEST(IopAlu, OriMergesImmediate) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFF0000u); + h.LoadProgram({ORI(reg::v0, reg::a0, 0xABCD)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFFABCDu); +} + +TEST(IopAlu, XoriToggleLowBits) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x0000FFFFu); + h.LoadProgram({XORI(reg::v0, reg::a0, 0xAAAA)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00005555u); +} + +TEST(IopAlu, SltiPositiveLess) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 3); + h.LoadProgram({SLTI(reg::v0, reg::a0, 10)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopAlu, SltiNegativeCompare) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-1)); + h.LoadProgram({SLTI(reg::v0, reg::a0, 0)}); // -1 < 0 → 1 + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopAlu, SltiuSignExtendedImmediate) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0u); + // imm=-1 sign-extends to 0xFFFFFFFF; 0 prog; + prog.push_back(ADDIU(reg::s0, reg::a0, static_cast(kImmDelta))); // s0 = imm result + for (u32 i = 0; i < 14; ++i) + prog.push_back(LW(ld[i], static_cast(i * 4), reg::a1)); + // Sum all 14 loads, then add s0 last so s0 is the stale LRU candidate. + prog.push_back(ADDU(reg::v0, ld[0], reg::zero)); + for (u32 i = 1; i < 14; ++i) + prog.push_back(ADDU(reg::v0, reg::v0, ld[i])); + prog.push_back(ADDU(reg::v0, reg::v0, reg::s0)); + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), expected); + EXPECT_EQ(h.GetGprJit(reg::v0), expected); + EXPECT_EQ(h.GetGprInterp(reg::s0), s0_val); +} diff --git a/tests/ctest/core/recompilers/iop_branch_tests.cpp b/tests/ctest/core/recompilers/iop_branch_tests.cpp new file mode 100644 index 0000000000..410f2e969f --- /dev/null +++ b/tests/ctest/core/recompilers/iop_branch_tests.cpp @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; + +// A shared program layout for conditional-branch tests: +// +// 0x00: if taken, skip 4 instrs (target = 0x18) +// 0x04: NOP delay slot (always executed) +// 0x08: ADDIU v0, zero, 1 NOT-TAKEN marker +// 0x0C: J kParkingPc sink +// 0x10: NOP +// 0x14: NOP (unused padding) +// 0x18: ADDIU v0, zero, 2 TAKEN marker +// 0x1C: J kParkingPc sink +// 0x20: NOP +// +// Offset from the branch (at 0x00) is `(0x18 - (0x04)) / 4` = 5 (signed, fits in 16 bits). +// Loads a conditional-branch test program directly into the harness. +inline void LoadBranchLayout(JitTestHarness& h, u32 branch_instr) +{ + h.LoadProgramNoTerm({ + branch_instr, NOP, + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); +} + +// Branch offset for the taken path in MakeBranchLayout. The branch is at +// program PC + 0. MIPS branch target = (pc of branch + 4) + (offset << 2). +// Target = program PC + 0x18 → offset = (0x18 - 0x04) / 4 = 5. +constexpr s16 kTakenOffset = 5; +} // namespace + +TEST(IopBranch, BeqTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 42); + LoadBranchLayout(h, BEQ(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +TEST(IopBranch, BeqNotTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 43); + LoadBranchLayout(h, BEQ(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopBranch, BneTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 43); + LoadBranchLayout(h, BNE(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +TEST(IopBranch, BneNotTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 42); + LoadBranchLayout(h, BNE(reg::a0, reg::a1, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopBranch, BgezTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + LoadBranchLayout(h, BGEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +TEST(IopBranch, BgezNotTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-1)); + LoadBranchLayout(h, BGEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopBranch, BltzTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-5)); + LoadBranchLayout(h, BLTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +// Const-folded Rs path in rpsxBranchZero: no explicit _psxFlushAllDirty() +// is needed. Rs is const-folded by a preceding immediate load, so the branch +// resolves statically. The delay slot dirties t0 — proving the delay slot +// still executes and commits without the explicit flush. +// +// 0x00: ADDIU a0, zero, imm const-fold Rs +// 0x04: BLTZ a0, +4 → target 0x18 +// 0x08: ADDIU t0, zero, 7 delay slot (dirties t0) +// 0x0C: ADDIU v0, zero, 1 not-taken marker +// 0x10: J park; 0x14: NOP +// 0x18: ADDIU v0, zero, 2 taken marker +// 0x1C: J park; 0x20: NOP +TEST(IopBranch, BltzConstRsTakenDelaySlotCommits) +{ + JitTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::a0, reg::zero, static_cast(-1)), // a0 = const -1 + BLTZ(reg::a0, 4), // -1 < 0 → taken + ADDIU(reg::t0, reg::zero, 7), // delay slot + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); // taken + EXPECT_EQ(h.GetGprInterp(reg::t0), 7u); // delay slot ran +} + +TEST(IopBranch, BltzConstRsNotTakenDelaySlotCommits) +{ + JitTestHarness h; + h.LoadProgramNoTerm({ + ADDIU(reg::a0, reg::zero, 1), // a0 = const +1 + BLTZ(reg::a0, 4), // 1 >= 0 → not taken + ADDIU(reg::t0, reg::zero, 7), // delay slot + ADDIU(reg::v0, reg::zero, 1), J(kPark), NOP, + ADDIU(reg::v0, reg::zero, 2), J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); // not taken + EXPECT_EQ(h.GetGprInterp(reg::t0), 7u); // delay slot ran +} + +TEST(IopBranch, BgtzTakenZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0); + LoadBranchLayout(h, BGTZ(reg::a0, kTakenOffset)); + h.Run(); + // 0 is not > 0 — BGTZ NOT taken. + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); +} + +TEST(IopBranch, BgtzTakenPositive) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 7); + LoadBranchLayout(h, BGTZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +TEST(IopBranch, BlezZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0); + LoadBranchLayout(h, BLEZ(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); +} + +TEST(IopBranch, BgezalLinkRegSet) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 42); + LoadBranchLayout(h, BGEZAL(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); + // BGEZAL writes pc+8 to ra *regardless* of whether the branch is taken. + EXPECT_EQ(h.GetGprInterp(reg::ra), RecompilerTestEnvironment::kProgramPc + 8); +} + +TEST(IopBranch, BltzalTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-7)); + LoadBranchLayout(h, BLTZAL(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); + EXPECT_EQ(h.GetGprInterp(reg::ra), RecompilerTestEnvironment::kProgramPc + 8); +} + +TEST(IopBranch, BltzalNotTaken) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 5); + LoadBranchLayout(h, BLTZAL(reg::a0, kTakenOffset)); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 1u); + // BLTZAL unconditionally writes ra, even when branch isn't taken. + EXPECT_EQ(h.GetGprInterp(reg::ra), RecompilerTestEnvironment::kProgramPc + 8); +} + +TEST(IopBranch, DelaySlotAlwaysExecutes) +{ + // Build a layout where the delay slot (always executed) writes v0 = 42. + // Whether or not the branch is taken, v0 must equal 42. + JitTestHarness h; + h.LoadProgramNoTerm({ + BEQ(reg::zero, reg::zero, kTakenOffset), + ADDIU(reg::v0, reg::zero, 42), // delay slot + ADDIU(reg::a0, reg::zero, 99), // not-taken body (skipped) + J(kPark), NOP, NOP, + ADDIU(reg::a0, reg::zero, 11), // taken body + J(kPark), NOP, + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 42u); + EXPECT_EQ(h.GetGprInterp(reg::a0), 11u); // branch was taken +} diff --git a/tests/ctest/core/recompilers/iop_cop0_exception_tests.cpp b/tests/ctest/core/recompilers/iop_cop0_exception_tests.cpp new file mode 100644 index 0000000000..b0a612a54a --- /dev/null +++ b/tests/ctest/core/recompilers/iop_cop0_exception_tests.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// COP0 + RFE coverage for the IOP recompiler. RFE emits +// Status = (Status & 0xfffffff0) | ((Status & 0x3c) >> 2) +// matching the interpreter at pcsx2/R3000AOpcodeTables.cpp:160. +// +// Full exception dispatch (psxException → jump to 0x80000080, Status stack +// push) is NOT tested here — the harness suppresses the event scheduler so +// a manually-poked Cause/Status combo won't fire. + +#include "harness/JitTestHarness.h" + +#include "R3000A.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(IopCop0Exception, RfeShiftsStackedBitsIntoCurrent) +{ + // Status = 0x0000003C (bits 5:2 = 0b1111, bits 1:0 = 0). + // RFE result expected: + // bits 3:0 ← (Status & 0x3c) >> 2 = 0xF + // bits 5:4 unchanged = 0b11 + // → Status = 0x3F + JitTestHarness h; + h.SetCp0(12, 0x0000003Cu); + h.LoadProgram({RFE}); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[12], 0x0000003Fu); + EXPECT_EQ(h.JitSnapshot().regs.CP0.r[12], 0x0000003Fu); +} + +TEST(IopCop0Exception, RfeOnlySinglePositionShift) +{ + // Check that RFE shifts one position, not two. Status=0x10 (only + // bit 4 = IEo set). + // (Status & 0x3c) >> 2 = 0x10 >> 2 = 0x04 → bit 2 set + // bits 5:4 unchanged → 0x10 + // → Status = 0x14 + JitTestHarness h; + h.SetCp0(12, 0x00000010u); + h.LoadProgram({RFE}); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[12], 0x00000014u); +} + +TEST(IopCop0Exception, RfePreservesHighStatusBits) +{ + // Status = 0x12340030 — bits 31:6 should be entirely untouched by RFE. + // bits 5:2 = 0b1100 → bits 3:0 = 0b1100 = 0xC; bits 5:4 unchanged. + // → 0x1234003C + JitTestHarness h; + h.SetCp0(12, 0x12340030u); + h.LoadProgram({RFE}); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[12], 0x1234003Cu); + EXPECT_EQ(h.JitSnapshot().regs.CP0.r[12], 0x1234003Cu); +} + +TEST(IopCop0Exception, MtcThenRfePipelinesThroughGpr) +{ + // MTC0 a0 -> Status; RFE; MFC0 v0 <- Status. End-to-end through the + // whole COP0 pipeline. Pre-state a0 = 0x3C, expected v0 = 0x3F. + JitTestHarness h; + h.SetGpr(reg::a0, 0x0000003Cu); + h.LoadProgram({ + MTC0(reg::a0, 12), // Status <- a0 (0x3C) + RFE, // Status = (Status & ~0xf) | ((Status & 0x3c)>>2) = 0x3F + MFC0(reg::v0, 12), // v0 <- Status + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x0000003Fu); + EXPECT_EQ(h.GetGprJit(reg::v0), 0x0000003Fu); +} + +TEST(IopCop0Exception, CauseRegisterIndependentOfRfe) +{ + // RFE operates on Status only. Cause must be untouched. Seed Cause + // with an arbitrary pattern and verify it survives RFE. + JitTestHarness h; + h.SetCp0(12, 0x0000003Cu); + h.SetCp0(13, 0xCAFEBABEu); // Cause + h.SetCp0(14, 0x12345678u); // EPC + h.LoadProgram({RFE}); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[12], 0x0000003Fu) << "Status"; + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[13], 0xCAFEBABEu) << "Cause unchanged"; + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[14], 0x12345678u) << "EPC unchanged"; +} + +TEST(IopCop0Exception, TwoBackToBackRfesStackAgain) +{ + // Run RFE twice in one block. Each shifts (Status & 0x3c) >> 2 into + // bits 3:0. Starting from 0x3C: + // After RFE #1: 0x3F (bits 3:0 = 0xF copied from 5:2) + // After RFE #2: 0x3F (bits 3:0 still 0xF copied from 5:2 = 0xF) + // The second RFE is idempotent given this starting Status because the + // stacked bits don't clear. Worth pinning as spec. + JitTestHarness h; + h.SetCp0(12, 0x0000003Cu); + h.LoadProgram({RFE, RFE}); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[12], 0x0000003Fu); + EXPECT_EQ(h.JitSnapshot().regs.CP0.r[12], 0x0000003Fu); +} diff --git a/tests/ctest/core/recompilers/iop_cop0_tests.cpp b/tests/ctest/core/recompilers/iop_cop0_tests.cpp new file mode 100644 index 0000000000..adff2fe7f6 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_cop0_tests.cpp @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(IopCop0, Mfc0ReadsStatus) +{ + JitTestHarness h; + // Bit 16 (IsolateCache / IsC) in CP0.Status *must be clear* — otherwise + // iopMemWrite32 silently discards the LoadProgram bytes and the program executes + // zeros (nops). 0x12340EEF has bit 16 clear. + h.SetCp0(12, 0x12340EEFu); // CP0.Status + h.LoadProgram({MFC0(reg::v0, 12)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x12340EEFu); +} + +TEST(IopCop0, Mfc0ReadsEpc) +{ + JitTestHarness h; + h.SetCp0(14, 0xDEADBEEFu); // EPC has no IsC interaction + h.LoadProgram({MFC0(reg::v0, 14)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xDEADBEEFu); +} + +TEST(IopCop0, Mfc0ReadsToA0) +{ + JitTestHarness h; + h.SetCp0(13, 0x77777777u); // Cause register — no IsC interaction + h.LoadProgram({MFC0(reg::a0, 13)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::a0), 0x77777777u); +} + +TEST(IopCop0, Mtc0WritesEpc) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x12345678u); + h.LoadProgram({MTC0(reg::a0, 14)}); // CP0.EPC + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[14], 0x12345678u); +} + +TEST(IopCop0, MtcThenMfcRoundtrip) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xABCDEF01u); + h.LoadProgram({ + MTC0(reg::a0, 13), // CP0.Cause + MFC0(reg::v0, 13), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xABCDEF01u); +} + +// Register-pressure regression guards for COP0 moves: rpsxMFC0/MTC0 use +// allocator-aware Rt access (MODE_WRITE alloc / source read) rather than +// flushing the whole host-reg pool. JitTestHarness::Run() diffs JIT vs interp +// across all fields including CP0, so the failure mode — MTC0 reading a +// dirty-allocated Rt from stale GPR memory — surfaces as a divergence here. +// The earlier MTC0 tests set Rt via SetGpr (memory already current), so they +// cannot catch this. + +TEST(IopCop0, Mtc0ReadsDirtyAllocatedRt) +{ + // LW writes its result straight to GPR memory, so to get a dirty-in-host-reg + // Rt, the program follows it with an ALU op (non-const operands): ADDU writes its result + // MODE_WRITE into a host reg and leaves psxRegs.GPR.r[t0] memory stale. MTC0 + // must read that host reg, not memory. + JitTestHarness h; + constexpr u32 kData = RecompilerTestEnvironment::kScratchAddr; + h.SetGpr(reg::a0, kData); + h.WriteU32(kData, 0x10000001u); + h.LoadProgram({ + LW(reg::t1, 0, reg::a0), // t1 = 0x10000001 (in memory) + ADDU(reg::t0, reg::t1, reg::t1), // t0 = 0x20000002, dirty in host reg + MTC0(reg::t0, 14), // CP0.EPC = t0 + MFC0(reg::v0, 14), // v0 = CP0.EPC + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x20000002u); + EXPECT_EQ(h.InterpSnapshot().regs.CP0.r[14], 0x20000002u); +} + +TEST(IopCop0, Mfc0ResultFeedsLaterUse) +{ + // MFC0 writes v0 (MODE_WRITE alloc); a following ADDIU consumes it. Confirms + // the written value propagates to later uses without a host-reg flush. + JitTestHarness h; + h.SetCp0(14, 0x0000002Au); // EPC = 42 + h.LoadProgram({ + MFC0(reg::v0, 14), // v0 = CP0.EPC = 42 + ADDIU(reg::v1, reg::v0, 1), // v1 = v0 + 1 = 43 + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x2Au); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0x2Bu); +} diff --git a/tests/ctest/core/recompilers/iop_jit_fuzz_tests.cpp b/tests/ctest/core/recompilers/iop_jit_fuzz_tests.cpp new file mode 100644 index 0000000000..cf4be3e9cb --- /dev/null +++ b/tests/ctest/core/recompilers/iop_jit_fuzz_tests.cpp @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Comprehensive IOP JIT-vs-interp fuzzer (straight-line, full opcode set). +// +// Covers the FULL IOP opcode set rather than the imm subset already covered by +// iop_alu_tests. Broad randomized coverage like this catches aliasing +// miscompiles such as the const-minuend SUB/SUBU case where the destination +// aliased the subtrahend (Rd == Rt): a naive materialization of the constant +// into Rd clobbers Rt before the subtract, yielding cv - cv = 0. The minimal +// repro lives as a targeted regression in iop_alu_tests.cpp +// (Sub*ConstMinuendDestAliasesSubtrahend). This fuzzer stays as permanent broad +// coverage. +// +// Why straight-line only: the harness diffs JIT vs interp at a fixed EE-cycle +// budget, and the two runners debit that budget at different granularities (the +// rec uses static per-block cycle estimates, the interp counts dynamically). A +// single straight-line block runs to its `jr ra` terminator in one block +// regardless of budget, so both runners reach the identical terminal state — +// the comparison is sound. Multi-block programs straddle the budget boundary and +// get cut off at different points between the two runners (a cycle-accounting +// artifact, not a miscompile); control-flow coverage lives in the hand-written +// iop_branch_tests / iop_jump_tests / iop_multiblock_tests instead. + +#include "harness/JitTestHarness.h" + +#include + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { + +constexpr u32 kPc = RecompilerTestEnvironment::kProgramPc; // 0x00010000 +constexpr u32 kData = RecompilerTestEnvironment::kScratchAddr; // 0x00020000 +constexpr u32 kWindowWords = 60; // 240-byte scratch window + +// Dest registers: exclude $zero(0), $k0(26)/$k1(27) (memory bases), $ra(31). +// 28 dests > host pool (~14) → sustained eviction. +constexpr u32 kDest[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30}; +constexpr u32 kNumDest = sizeof(kDest) / sizeof(kDest[0]); + +struct Lcg +{ + u32 s; + u32 next() { s = s * 1103515245u + 12345u; return s; } + u32 range(u32 n) { return (next() >> 8) % n; } +}; + +u32 baseReg(Lcg& r) { return (r.next() & 1) ? reg::k0 : reg::k1; } +u32 destReg(Lcg& r) { return kDest[r.range(kNumDest)]; } +u32 srcReg(Lcg& r) { return r.range(31); } // 0..30 (any, incl. bases/zero) + +s16 fuzzImm(Lcg& r) +{ + const u32 v = r.next(); + switch ((v >> 4) & 3) + { + case 0: return static_cast(v & 0x7F); + case 1: return static_cast(0x8000u | (v & 0x7FFF)); // sign bit set + default: return static_cast(v & 0xFFFF); + } +} + +// In-window offset for memory ops. `align` allows an unaligned skew for ops +// that legally take unaligned addresses (LWL/LWR/SWL/SWR, byte/half ops). +s16 memOff(Lcg& r, u32 align) +{ + u32 off = r.range(kWindowWords - 1) * 4; + if (align < 4) + off += r.range(4) & ~(align - 1); + return static_cast(off); +} + +// Generate one non-control-flow IOP instruction. +u32 genOp(Lcg& r) +{ + const u32 d = destReg(r), a = srcReg(r), b = srcReg(r); + const s16 imm = fuzzImm(r); + switch (r.range(34)) + { + case 0: return ADD(d, a, b); + case 1: return ADDU(d, a, b); + case 2: return SUB(d, a, b); + case 3: return SUBU(d, a, b); + case 4: return AND(d, a, b); + case 5: return OR(d, a, b); + case 6: return XOR(d, a, b); + case 7: return NOR(d, a, b); + case 8: return SLT(d, a, b); + case 9: return SLTU(d, a, b); + case 10: return SLL(d, a, r.range(32)); + case 11: return SRL(d, a, r.range(32)); + case 12: return SRA(d, a, r.range(32)); + case 13: return SLLV(d, a, b); + case 14: return SRLV(d, a, b); + case 15: return SRAV(d, a, b); + case 16: return MULT(a, b); + case 17: return MULTU(a, b); + case 18: return DIV(a, b); // includes div-by-zero (canonical result) + case 19: return DIVU(a, b); + case 20: return MFHI(d); + case 21: return MFLO(d); + case 22: return MTHI(a); + case 23: return MTLO(a); + case 24: return ADDIU(d, a, imm); + case 25: return ANDI(d, a, static_cast(imm)); + case 26: return ORI(d, a, static_cast(imm)); + case 27: return LUI(d, static_cast(imm)); + case 28: return LW(d, memOff(r, 4), baseReg(r)); + case 29: return LB(d, memOff(r, 1), baseReg(r)); + case 30: return LHU(d, memOff(r, 2), baseReg(r)); + case 31: return LWL(d, memOff(r, 1), baseReg(r)); + case 32: return SW(a, memOff(r, 4), baseReg(r)); + default: return SWL(a, memOff(r, 1), baseReg(r)); + } +} + +void SeedState(JitTestHarness& h, Lcg& r) +{ + for (u32 i = 1; i < 31; ++i) + h.SetGpr(i, r.next()); + h.SetGpr(reg::k0, kData); + h.SetGpr(reg::k1, kData); // same valid base, distinct allocator entry + h.SetHi(r.next()); + h.SetLo(r.next()); + for (u32 i = 0; i < kWindowWords; ++i) + h.WriteU32(kData + i * 4, r.next()); + h.TrackMemWindow(kData, kWindowWords * 4); +} + +} // namespace + +// ── Straight-line: full op set, heavy register pressure, no branches ───────── +TEST(IopFuzz, StraightLineAllOps) +{ + for (u32 seed = 0; seed < 1500; ++seed) + { + SCOPED_TRACE(::testing::Message() << "seed=" << seed); + Lcg r{seed * 2654435761u + 0x1234567u}; + JitTestHarness h; + SeedState(h, r); + + std::vector prog; + for (u32 i = 0; i < 80; ++i) + prog.push_back(genOp(r)); + + h.LoadProgramAt(kPc, prog.data(), prog.size(), /*append_jr_ra_term=*/true); + h.Run(); + if (::testing::Test::HasFailure()) + return; // stop at first failing seed for a clean repro + } +} diff --git a/tests/ctest/core/recompilers/iop_jump_tests.cpp b/tests/ctest/core/recompilers/iop_jump_tests.cpp new file mode 100644 index 0000000000..9ebfd03572 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_jump_tests.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc; +constexpr u32 kProg = RecompilerTestEnvironment::kProgramPc; +} + +// A block that ends with `jr ra` where ra was pre-set to the parking lot. +// Uses the harness's auto-appended terminator; primary purpose is to +// confirm the test-lifecycle plumbing works without the harness's own +// synthetic terminator getting in the way. +TEST(IopJump, JrToParkingLot) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xCAFEu); + h.LoadProgram({ORI(reg::v0, reg::a0, 0)}); // simple copy + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xCAFEu); + // After the JR ra; nop terminator, pc should sit inside the parking + // lot's own `j kPark; nop` infinite loop. + EXPECT_EQ(h.InterpSnapshot().regs.pc, kPark); +} + +TEST(IopJump, JalSetsLinkRegister) +{ + JitTestHarness h; + // jal writes PC+8 (delay slot return) into r31 = ra. + // Jump to kPark (the parking lot's `j self` instruction) so execution + // sticks in a controlled loop instead of drifting into uninitialized + // memory past the parking lot. + h.LoadProgramNoTerm({ + JAL(kPark), // target = parking lot head; returns kProg+8 into ra + NOP, // delay slot (always executed) + NOP, NOP, NOP, // filler (not reached) + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::ra), kProg + 8); +} + +TEST(IopJump, JalrReturnLinkCustomRd) +{ + JitTestHarness h; + h.SetGpr(reg::a0, kPark); // target — parking lot head + h.LoadProgramNoTerm({ + JALR(reg::v0, reg::a0), // link into v0 + NOP, // delay slot + }); + h.Run(); + // v0 should hold the return address = instruction after delay slot. + EXPECT_EQ(h.GetGprInterp(reg::v0), kProg + 8); +} + +TEST(IopJump, JExactTarget) +{ + JitTestHarness h; + h.LoadProgramNoTerm({ + J(kPark), + NOP, + }); + h.Run(); + EXPECT_EQ(h.InterpSnapshot().regs.pc, kPark); +} diff --git a/tests/ctest/core/recompilers/iop_loadstore_tests.cpp b/tests/ctest/core/recompilers/iop_loadstore_tests.cpp new file mode 100644 index 0000000000..89401cc2e1 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_loadstore_tests.cpp @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kDataAddr = RecompilerTestEnvironment::kScratchAddr; +} + +TEST(IopLoadStore, LwBasic) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0x12345678u); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LW(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x12345678u); +} + +TEST(IopLoadStore, LwPositiveOffset) +{ + JitTestHarness h; + h.WriteU32(kDataAddr + 8, 0xDEADBEEFu); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LW(reg::v0, 8, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xDEADBEEFu); +} + +TEST(IopLoadStore, LwNegativeOffset) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xCAFEF00Du); + h.SetGpr(reg::a0, kDataAddr + 16); + h.LoadProgram({LW(reg::v0, -16, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xCAFEF00Du); +} + +TEST(IopLoadStore, LbSignExtendsNegative) +{ + JitTestHarness h; + h.WriteU8(kDataAddr, 0xFFu); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LB(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFFFFFFu); +} + +TEST(IopLoadStore, LbuZeroExtends) +{ + JitTestHarness h; + h.WriteU8(kDataAddr, 0xFFu); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LBU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x000000FFu); +} + +TEST(IopLoadStore, LhSignExtends) +{ + JitTestHarness h; + h.WriteU16(kDataAddr, 0x8001u); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LH(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFF8001u); +} + +TEST(IopLoadStore, LhuZeroExtends) +{ + JitTestHarness h; + h.WriteU16(kDataAddr, 0x8001u); + h.SetGpr(reg::a0, kDataAddr); + h.LoadProgram({LHU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00008001u); +} + +TEST(IopLoadStore, SwBasic) +{ + JitTestHarness h; + h.TrackMemWindow(kDataAddr, 4); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0xFEEDFACEu); + h.LoadProgram({SW(reg::a1, 0, reg::a0)}); + h.Run(); + // The diff between pre and post mem state is what asserts correctness; + // the stored value can also be read back directly. + EXPECT_EQ(h.ReadU32(kDataAddr), 0xFEEDFACEu); +} + +TEST(IopLoadStore, SbLowByteOnly) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xAABBCCDDu); + h.TrackMemWindow(kDataAddr, 4); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x55u); + h.LoadProgram({SB(reg::a1, 0, reg::a0)}); + h.Run(); + // Stored only the low byte; upper three bytes are unaffected. + EXPECT_EQ(h.ReadU32(kDataAddr), 0xAABBCC55u); +} + +TEST(IopLoadStore, ShLowHalf) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xAABBCCDDu); + h.TrackMemWindow(kDataAddr, 4); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x1234u); + h.LoadProgram({SH(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kDataAddr), 0xAABB1234u); +} + +TEST(IopLoadStore, StoreThenLoadRoundtrip) +{ + JitTestHarness h; + h.TrackMemWindow(kDataAddr, 4); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x01020304u); + h.LoadProgram({ + SW(reg::a1, 0, reg::a0), + LW(reg::v0, 0, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x01020304u); +} + +// =========================================================================== +// IOP unaligned word loads/stores — LWL / LWR / SWL / SWR +// +// Same LE merge semantics as the EE 32-bit ops (see +// R3000AOpcodeTables.cpp:214-288). IOP recompiler uses REC_FUNC fallback +// to the interpreter for these — tests lock in the architectural result +// and the JIT call-sequence around the fallback. +// =========================================================================== + +TEST(IopLoadStore, LwlShift0PartialLoad) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0x44332211u); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::v0, 0xCAFEBABEu); + h.LoadProgram({LWL(reg::v0, 0, reg::a0)}); + h.Run(); + // (rt & 0x00FFFFFF) | (mem << 24) = 0x00FEBABE | 0x11000000 + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x11FEBABEu); +} + +TEST(IopLoadStore, LwlShift3FullWord) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xFFEEDDCCu); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::v0, 0xCAFEBABEu); + h.LoadProgram({LWL(reg::v0, 3, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFEEDDCCu); +} + +TEST(IopLoadStore, LwrShift0FullWord) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0x44332211u); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::v0, 0xCAFEBABEu); + h.LoadProgram({LWR(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x44332211u); +} + +TEST(IopLoadStore, LwrShift3SingleByte) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0x44332211u); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::v0, 0xCAFEBABEu); + h.LoadProgram({LWR(reg::v0, 3, reg::a0)}); + h.Run(); + // (rt & 0xFFFFFF00) | (mem >> 24) = 0xCAFEBA00 | 0x44 + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xCAFEBA44u); +} + +TEST(IopLoadStore, LwrLwlPairUnalignedWordLoad) +{ + // Bytes [0..7] = 99 11 22 33 44 AA BB CC. Unaligned word at byte 1 + // = LE 0x44332211. Canonical IOP unaligned-load pair. + JitTestHarness h; + h.WriteU32(kDataAddr + 0, 0x33221199u); + h.WriteU32(kDataAddr + 4, 0xCCBBAA44u); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::v0, 0xCAFEBABEu); + h.LoadProgram({ + LWR(reg::v0, 1, reg::a0), + LWL(reg::v0, 4, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x44332211u); +} + +TEST(IopLoadStore, SwlShift0SingleByte) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xAABBCCDDu); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kDataAddr, 4); + h.LoadProgram({SWL(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kDataAddr), 0xAABBCC12u); +} + +TEST(IopLoadStore, SwrShift3SingleByte) +{ + JitTestHarness h; + h.WriteU32(kDataAddr, 0xAABBCCDDu); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kDataAddr, 4); + h.LoadProgram({SWR(reg::a1, 3, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kDataAddr), 0x78BBCCDDu); +} + +TEST(IopLoadStore, SwrSwlPairUnalignedWordStore) +{ + JitTestHarness h; + h.WriteU32(kDataAddr + 0, 0xAAAAAAAAu); + h.WriteU32(kDataAddr + 4, 0xBBBBBBBBu); + h.SetGpr(reg::a0, kDataAddr); + h.SetGpr(reg::a1, 0x12345678u); + h.TrackMemWindow(kDataAddr, 8); + h.LoadProgram({ + SWR(reg::a1, 1, reg::a0), + SWL(reg::a1, 4, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.ReadU32(kDataAddr + 0), 0x345678AAu); + EXPECT_EQ(h.ReadU32(kDataAddr + 4), 0xBBBBBB12u); +} diff --git a/tests/ctest/core/recompilers/iop_memory_access_tests.cpp b/tests/ctest/core/recompilers/iop_memory_access_tests.cpp new file mode 100644 index 0000000000..4aeb634fad --- /dev/null +++ b/tests/ctest/core/recompilers/iop_memory_access_tests.cpp @@ -0,0 +1,287 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Coverage expansion for the IOP load/store JIT path. Basic width / +// sign-extension coverage lives in iop_loadstore_tests.cpp; this file picks up +// what that file doesn't: +// +// * RAM mirror aliasing (physical 0x00000000 / kseg0 0x80000000 / kseg1 +// 0xa0000000 all land in the same 2MB iopMem->Main through the fast +// path's 21-bit mask). +// * Helper-path dispatch — any effective address with bit 28 set bypasses +// the fast path and calls iopMemRead*/iopMemWrite* directly. +// * Unaligned load/store (LWL/LWR/SWL/SWR) via the REC_FUNC interpreter +// fallback — a different JIT code path from aligned loads/stores. +// * `lw $0, ...` short-circuit (the _Rt_==0 branch in rpsxLW). + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kPhysBase = RecompilerTestEnvironment::kScratchAddr; // 0x00020000 +constexpr u32 kKseg0Base = 0x80000000u | kPhysBase; // 0x80020000 +constexpr u32 kKseg1Base = 0xA0000000u | kPhysBase; // 0xA0020000 + +// A bit-28-set address that routes through the helper path. Chosen in the +// IOP hardware register window (0x1F801xxx); iopHw is zero-filled by the +// test env, so reads from unassigned registers return whatever the helper +// dispatches to. No values are asserted — the implicit JIT-vs-interp diff +// in Run() locks the two paths to the same result. +constexpr u32 kHwHelperAddr = 0x1F801078u; +} // namespace + +// --------------------------------------------------------------------------- +// RAM mirror aliasing — physical / kseg0 / kseg1 all address the same byte. +// --------------------------------------------------------------------------- + +TEST(IopMemoryAccess, LwThroughKseg0Mirror) +{ + JitTestHarness h; + h.WriteU32(kPhysBase, 0xABCD1234u); + h.SetGpr(reg::a0, kKseg0Base); + h.LoadProgram({LW(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xABCD1234u); +} + +TEST(IopMemoryAccess, LwThroughKseg1Mirror) +{ + JitTestHarness h; + h.WriteU32(kPhysBase, 0xFEEDFACEu); + h.SetGpr(reg::a0, kKseg1Base); + h.LoadProgram({LW(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFEEDFACEu); +} + +TEST(IopMemoryAccess, SwViaKseg0ReadableViaPhysical) +{ + JitTestHarness h; + h.TrackMemWindow(kPhysBase, 4); + h.SetGpr(reg::a0, kKseg0Base); + h.SetGpr(reg::a1, 0xDEADBEEFu); + h.LoadProgram({SW(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kPhysBase), 0xDEADBEEFu); +} + +TEST(IopMemoryAccess, SwViaKseg1ReadableViaPhysical) +{ + JitTestHarness h; + h.TrackMemWindow(kPhysBase, 4); + h.SetGpr(reg::a0, kKseg1Base); + h.SetGpr(reg::a1, 0x55AA55AAu); + h.LoadProgram({SW(reg::a1, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.ReadU32(kPhysBase), 0x55AA55AAu); +} + +TEST(IopMemoryAccess, LbuThroughKseg1Mirror) +{ + // Byte load through the uncached-mirror address. + JitTestHarness h; + h.WriteU8(kPhysBase, 0x42u); + h.SetGpr(reg::a0, kKseg1Base); + h.LoadProgram({LBU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x42u); +} + +TEST(IopMemoryAccess, LhuThroughKseg0Mirror) +{ + JitTestHarness h; + h.WriteU16(kPhysBase, 0xBEEFu); + h.SetGpr(reg::a0, kKseg0Base); + h.LoadProgram({LHU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xBEEFu); +} + +TEST(IopMemoryAccess, MirroredWritesAliasSamePhysicalWord) +{ + // Sequence that bounces through all three mirrors. + // SW $a1, 0($a0) ; a0 = phys, a1 = 0x11111111 → phys[0] = 0x11111111 + // LW $v0, 0($a2) ; a2 = kseg0, should see 0x11111111 + // SW $a1, 0($a3) ; a3 = kseg1, overwrite with 0x22222222 + // LW $v1, 0($a0) ; phys, should see 0x22222222 + JitTestHarness h; + h.TrackMemWindow(kPhysBase, 4); + h.SetGpr(reg::a0, kPhysBase); + h.SetGpr(reg::a1, 0x11111111u); + h.SetGpr(reg::a2, kKseg0Base); + h.SetGpr(reg::a3, kKseg1Base); + h.LoadProgram({ + SW(reg::a1, 0, reg::a0), + LW(reg::v0, 0, reg::a2), + ORI(reg::a1, reg::zero, 0x2222u), // a1 = 0x00002222 + LUI(reg::t0, 0x2222), // t0 = 0x22220000 + ADDU(reg::a1, reg::a1, reg::t0), // a1 = 0x22222222 + SW(reg::a1, 0, reg::a3), + LW(reg::v1, 0, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x11111111u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0x22222222u); + EXPECT_EQ(h.ReadU32(kPhysBase), 0x22222222u); +} + +// --------------------------------------------------------------------------- +// Helper-path dispatch — bit 28 of the effective address routes to the +// iopMemRead*/iopMemWrite* C helpers instead of the RAM fast path. +// --------------------------------------------------------------------------- + +TEST(IopMemoryAccess, LwViaHelperPathMatchesInterp) +{ + // Read from an IOP HW register window address. No concrete assertion — + // the test relies on the harness's implicit JIT-vs-interp diff in Run() to lock + // both paths to the same value. If the JIT's helper call emits a + // different ABI or clobbers a reg the interp didn't, the diff surfaces. + JitTestHarness h; + h.SetGpr(reg::a0, kHwHelperAddr); + h.LoadProgram({LW(reg::v0, 0, reg::a0)}); + h.Run(); + // Lock the observed value as spec (whatever interp returned). + EXPECT_EQ(h.GetGprJit(reg::v0), h.GetGprInterp(reg::v0)); +} + +TEST(IopMemoryAccess, LbuViaHelperPathMatchesInterp) +{ + JitTestHarness h; + h.SetGpr(reg::a0, kHwHelperAddr); + h.LoadProgram({LBU(reg::v0, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprJit(reg::v0), h.GetGprInterp(reg::v0)); +} + +// --------------------------------------------------------------------------- +// _Rt_==0 short-circuit in rpsxLoad. The fast path body early-exits +// without writing a result, but the helper path still runs so device +// side-effects fire. +// --------------------------------------------------------------------------- + +TEST(IopMemoryAccess, LoadIntoZeroDoesNotDisturbR0) +{ + // r0 is hardwired zero and must stay that way even when named as the + // destination of a load. Both JIT and interp uphold this; the diff + // locks them together, and the direct check pins the architectural + // guarantee. + JitTestHarness h; + h.WriteU32(kPhysBase, 0xAAAAAAAAu); + h.SetGpr(reg::a0, kPhysBase); + h.LoadProgram({LW(reg::zero, 0, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::zero), 0u); + EXPECT_EQ(h.GetGprJit(reg::zero), 0u); +} + +// --------------------------------------------------------------------------- +// LWL/LWR/SWL/SWR — REC_FUNC interpreter fallback. These spill the opcode, +// flush live regs, and dispatch to the interpreter. Different JIT code path +// from aligned loads/stores; worth smoke-testing. +// --------------------------------------------------------------------------- + +TEST(IopMemoryAccess, LwlLwrAssembleAlignedWord) +{ + // `lwl rt, 3(base); lwr rt, 0(base)` — the canonical unaligned-load + // pattern, here used with naturally-aligned base = 0x20000. Expected + // result: the word at 0x20000 (little-endian). + JitTestHarness h; + h.WriteU32(kPhysBase, 0x44332211u); + h.SetGpr(reg::a0, kPhysBase); + h.LoadProgram({ + LWL(reg::v0, 3, reg::a0), + LWR(reg::v0, 0, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x44332211u); +} + +TEST(IopMemoryAccess, LwlLwrAssembleUnalignedWord) +{ + // Load 4 bytes starting at an unaligned boundary (offset 1). Seed two + // adjacent words so the unaligned read crosses the boundary. Lock + // whatever the interpreter produces as spec. + JitTestHarness h; + h.WriteU32(kPhysBase + 0, 0x44332211u); + h.WriteU32(kPhysBase + 4, 0x88776655u); + h.SetGpr(reg::a0, kPhysBase); + h.LoadProgram({ + // lwl rt, 4(a0) -> loads high bytes from word at 0x20004 + // lwr rt, 1(a0) -> loads low bytes starting at 0x20001 + LWL(reg::v0, 4, reg::a0), + LWR(reg::v0, 1, reg::a0), + }); + h.Run(); + // Don't pin a concrete value; surface only if JIT diverges from interp. + EXPECT_EQ(h.GetGprJit(reg::v0), h.GetGprInterp(reg::v0)); +} + +TEST(IopMemoryAccess, SwlSwrWriteAlignedWord) +{ + // Mirror of the LWL/LWR round-trip for stores. + JitTestHarness h; + h.WriteU32(kPhysBase, 0u); + h.TrackMemWindow(kPhysBase, 4); + h.SetGpr(reg::a0, kPhysBase); + h.SetGpr(reg::a1, 0x44332211u); + h.LoadProgram({ + SWL(reg::a1, 3, reg::a0), + SWR(reg::a1, 0, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.ReadU32(kPhysBase), 0x44332211u); +} + +TEST(IopMemoryAccess, LwlAfterSwSeesFlushedData) +{ + // Sequence: pre-state word → SW overwrites it → LWL+LWR reads it back. + // Exercises the flush between rec opcodes: the SW leaves its value in + // memory (helper-dispatched), and the subsequent LWL — which falls back to + // the interpreter after flushing all live guest registers to memory — must + // see the updated word. + JitTestHarness h; + h.WriteU32(kPhysBase, 0xDEADDEADu); + h.TrackMemWindow(kPhysBase, 4); + h.SetGpr(reg::a0, kPhysBase); + h.SetGpr(reg::a1, 0xCAFEF00Du); + h.LoadProgram({ + SW(reg::a1, 0, reg::a0), + LWL(reg::v0, 3, reg::a0), + LWR(reg::v0, 0, reg::a0), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xCAFEF00Du); +} + +// --------------------------------------------------------------------------- +// Flush semantics under pressure — a helper-dispatched store must flush +// all live guest regs so the post-helper state is coherent. Existing ALU +// tests don't stress this because none follow an SW with a read-from- +// register sequence long enough to see a bad flush. +// --------------------------------------------------------------------------- + +TEST(IopMemoryAccess, ManyLiveRegsSurviveStoreFlush) +{ + // Load several regs, issue an SW (which calls _psxFlushCall), then + // consume those regs afterward. A botched flush would show as one of + // the post-SW reads returning a stale value. + JitTestHarness h; + h.SetGpr(reg::a0, kPhysBase); + h.SetGpr(reg::t0, 0x11111111u); + h.SetGpr(reg::t1, 0x22222222u); + h.SetGpr(reg::t2, 0x33333333u); + h.SetGpr(reg::t3, 0x44444444u); + h.LoadProgram({ + SW(reg::t0, 0, reg::a0), // flush happens here + ADDU(reg::v0, reg::t1, reg::t2), // uses t1+t2 post-flush + ADDU(reg::v1, reg::t2, reg::t3), // uses t2+t3 post-flush + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x55555555u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0x77777777u); + EXPECT_EQ(h.ReadU32(kPhysBase), 0x11111111u); +} diff --git a/tests/ctest/core/recompilers/iop_muldiv_tests.cpp b/tests/ctest/core/recompilers/iop_muldiv_tests.cpp new file mode 100644 index 0000000000..664dc37da8 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_muldiv_tests.cpp @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(IopMulDiv, MultPositive) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 7); + h.SetGpr(reg::a1, 11); + h.LoadProgram({ + MULT(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0u); // hi = 0 for small product + EXPECT_EQ(h.GetGprInterp(reg::v1), 77u); // lo = 77 +} + +TEST(IopMulDiv, MultLargeGoesToHi) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x10000); + h.SetGpr(reg::a1, 0x10000); + h.LoadProgram({ + MULT(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000001u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0x00000000u); +} + +TEST(IopMulDiv, MultSigned) +{ + JitTestHarness h; + h.SetGpr(reg::a0, static_cast(-2)); + h.SetGpr(reg::a1, 3); + h.LoadProgram({ + MULT(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + // -2 * 3 = -6: 64-bit signed = 0xFFFF_FFFF_FFFF_FFFA + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFFFFFFu); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0xFFFFFFFAu); +} + +TEST(IopMulDiv, MultuUnsignedMax) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFFFFFFu); + h.SetGpr(reg::a1, 2); + h.LoadProgram({ + MULTU(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + // 0xFFFFFFFF * 2 = 0x1_FFFFFFFE + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000001u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0xFFFFFFFEu); +} + +TEST(IopMulDiv, DivSigned) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 23); + h.SetGpr(reg::a1, 4); + h.LoadProgram({ + DIV(reg::a0, reg::a1), + MFHI(reg::v0), // remainder + MFLO(reg::v1), // quotient + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 3u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 5u); +} + +TEST(IopMulDiv, DivuUnsigned) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 100); + h.SetGpr(reg::a1, 7); + h.LoadProgram({ + DIVU(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 2u); // 100 % 7 = 2 + EXPECT_EQ(h.GetGprInterp(reg::v1), 14u); // 100 / 7 = 14 +} + +// Divide-by-zero canonical results — psxDIV / psxDIVU at +// R3000AOpcodeTables.cpp:63-92 specify: +// psxDIV Rt==0: LO = (i32(Rs) < 0) ? 1 : 0xFFFFFFFF; HI = Rs +// psxDIVU Rt==0: LO = 0xFFFFFFFF; HI = Rs +// Run() auto-diffs JIT vs interp post-state, so reaching an interp value +// that matches the spec also confirms the JIT side. + +TEST(IopMulDiv, DivByZeroPositiveRs) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x12345678u); + h.SetGpr(reg::a1, 0u); + h.LoadProgram({ + DIV(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x12345678u); // HI = Rs + EXPECT_EQ(h.GetGprInterp(reg::v1), 0xFFFFFFFFu); // LO = -1 (Rs>=0) +} + +TEST(IopMulDiv, DivByZeroNegativeRs) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x80000001u); // sign bit set + h.SetGpr(reg::a1, 0u); + h.LoadProgram({ + DIV(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x80000001u); // HI = Rs + EXPECT_EQ(h.GetGprInterp(reg::v1), 1u); // LO = 1 (Rs<0) +} + +TEST(IopMulDiv, DivuByZero) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xCAFEBABEu); + h.SetGpr(reg::a1, 0u); + h.LoadProgram({ + DIVU(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xCAFEBABEu); // HI = Rs + EXPECT_EQ(h.GetGprInterp(reg::v1), 0xFFFFFFFFu); // LO = 0xFFFFFFFF +} + +TEST(IopMulDiv, DivOverflowMinIntByMinusOne) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x80000000u); + h.SetGpr(reg::a1, 0xFFFFFFFFu); + h.LoadProgram({ + DIV(reg::a0, reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + // psxDIV() at R3000AOpcodeTables.cpp:69 hardcodes LO=INT_MIN, HI=0 for + // this overflow case; aarch64 SDIV produces the same values natively, + // so the JIT needs no explicit overflow branch. + EXPECT_EQ(h.GetGprInterp(reg::v0), 0u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0x80000000u); +} + +TEST(IopMulDiv, MthiMtloMfhiMflo) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x12345678u); + h.SetGpr(reg::a1, 0xABCDEF01u); + h.LoadProgram({ + MTHI(reg::a0), + MTLO(reg::a1), + MFHI(reg::v0), + MFLO(reg::v1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x12345678u); + EXPECT_EQ(h.GetGprInterp(reg::v1), 0xABCDEF01u); +} diff --git a/tests/ctest/core/recompilers/iop_multiblock_tests.cpp b/tests/ctest/core/recompilers/iop_multiblock_tests.cpp new file mode 100644 index 0000000000..135fa678f4 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_multiblock_tests.cpp @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Multi-block dispatch tests for the IOP recompiler. A block ends with a +// branch, ExecuteBlock returns, the dispatcher looks up the next block via +// psxRecLUT, and compiles it on first hit. These tests exercise that +// cross-block dispatch path. +// +// The branch tests in iop_branch_tests.cpp exercise cross-block flow within +// a single LoadProgramNoTerm() layout. These tests load each block at a +// distinct address via LoadProgramAt(), so the dispatcher re-dispatch is +// obvious and distinct recBlocks entries are created. + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; // 0x00010000 +constexpr u32 kParkingPc = RecompilerTestEnvironment::kParkingPc; // 0x001F0000 + +// Second / third blocks at distinct pages from the primary program region. +// Chosen well outside [kProgramPc, kProgramPc + 4KB) and inside legal +// 16-bit BEQ range from the primary block (offset ≤ 0x3FFF words). +constexpr u32 kBlock2Pc = 0x00014000; +constexpr u32 kBlock3Pc = 0x00018000; + +constexpr s16 ShortBranchOffset(u32 branch_pc, u32 target_pc) +{ + // MIPS target = (branch_pc + 4) + (offset << 2). + return static_cast((static_cast(target_pc) - static_cast(branch_pc + 4)) / 4); +} +} // namespace + +TEST(IopMultiBlock, JAcrossBlocksPropagatesState) +{ + // Block 1 sets v0, jumps to block 2; block 2 adds to v0 and returns. + JitTestHarness h; + h.LoadProgramAt(kProgramPc, { + ADDIU(reg::v0, reg::zero, 10), // v0 = 10 + J(kBlock2Pc), + NOP, // delay slot + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::v0, 100), // v0 = v0 + 100 + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 110u); +} + +TEST(IopMultiBlock, DelaySlotAcrossBlockBoundary) +{ + // The J in block 1 has a non-trivial delay slot that mutates v0. The + // MIPS arch guarantees the delay-slot instruction executes before the + // control transfer finishes, so v0 is 15 when block 2 begins. + JitTestHarness h; + h.LoadProgramAt(kProgramPc, { + ADDIU(reg::v0, reg::zero, 10), + J(kBlock2Pc), + ADDIU(reg::v0, reg::v0, 5), // delay slot: v0 = 15 + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::v0, 100), // v0 = 115 + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 115u); +} + +TEST(IopMultiBlock, JalLinksToReturnAddress) +{ + // JAL stores PC_of_JAL + 8 into ra. Block 2 reads ra into v0 and + // returns. Pre-set s0 = kParkingPc so block 1's continuation can + // jr to the parking lot without clobbering the JAL's linkage. + JitTestHarness h; + h.SetGpr(reg::s0, kParkingPc); + h.LoadProgramAt(kProgramPc, { + JAL(kBlock2Pc), // 0x10000: ra = 0x10008 + NOP, // 0x10004: delay slot + // 0x10008: block 2 returns here + ADDU(reg::v1, reg::ra, reg::zero), // v1 = ra (= 0x10008) + JR(reg::s0), // jr parking + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDU(reg::v0, reg::ra, reg::zero), // v0 = ra (captured) + JR(reg::ra), + NOP, + }, /*append_jr_ra_term=*/false); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), kProgramPc + 8); + EXPECT_EQ(h.GetGprInterp(reg::v1), kProgramPc + 8); +} + +TEST(IopMultiBlock, BeqAcrossBlocksTaken) +{ + // BEQ within legal offset range to block 2. + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 42); + const s16 off = ShortBranchOffset(kProgramPc, kBlock2Pc); + h.LoadProgramAt(kProgramPc, { + BEQ(reg::a0, reg::a1, off), // taken — go to block 2 + NOP, // delay slot + ADDIU(reg::v0, reg::zero, 0xBAD), // fall-through (should not run) + J(kParkingPc), + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::zero, 0x600D), // taken marker + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x600Du); +} + +TEST(IopMultiBlock, BeqAcrossBlocksNotTaken) +{ + // BEQ with mismatched inputs — fall through. Block 2 is compiled only + // if the branch fires, so in this test block 2 is never entered. + JitTestHarness h; + h.SetGpr(reg::a0, 42); + h.SetGpr(reg::a1, 43); + const s16 off = ShortBranchOffset(kProgramPc, kBlock2Pc); + h.LoadProgramAt(kProgramPc, { + BEQ(reg::a0, reg::a1, off), // not taken + NOP, // delay slot + ADDIU(reg::v0, reg::zero, 0x0A11), // fall-through marker (positive 16-bit, no sign extend) + J(kParkingPc), + NOP, + }, /*append_jr_ra_term=*/false); + // Block 2 still laid down for safety (poison value so an accidental + // dispatch is obvious). + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::zero, 0xBAD), + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x0A11u); +} + +TEST(IopMultiBlock, ThreeBlockChain) +{ + // Block 1 → block 2 → block 3 → parking. Each block adds to v0. + JitTestHarness h; + h.LoadProgramAt(kProgramPc, { + ADDIU(reg::v0, reg::zero, 1), + J(kBlock2Pc), + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::v0, 10), + J(kBlock3Pc), + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock3Pc, { + ADDIU(reg::v0, reg::v0, 100), + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 111u); +} + +TEST(IopMultiBlock, ReEnterFirstBlockAfterReturn) +{ + // Block 1 JALs block 2; block 2 returns; block 1's continuation runs + // more instructions; block 1 exits via an explicit jump to parking. + // Proves that after dispatcher re-enters block 1, the cached block is + // still valid (or is recompiled from the correct address). + JitTestHarness h; + h.SetGpr(reg::s0, kParkingPc); + h.SetGpr(reg::v0, 0); + h.LoadProgramAt(kProgramPc, { + ADDIU(reg::v0, reg::zero, 1), // 0x10000: v0 = 1 + JAL(kBlock2Pc), // 0x10004: ra = 0x1000C + NOP, // 0x10008: delay slot + // 0x1000C — block 2 returns here + ADDIU(reg::v0, reg::v0, 100), // v0 += 100 + JR(reg::s0), // parking + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::v0, 10), // v0 += 10 + JR(reg::ra), + NOP, + }, /*append_jr_ra_term=*/false); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 111u); +} + +TEST(IopMultiBlock, JalrThenReturn) +{ + // JALR variant (register-indirect jump + link). s0 holds block 2's + // address; JALR jumps there and links ra. + JitTestHarness h; + h.SetGpr(reg::s0, kBlock2Pc); + h.SetGpr(reg::s1, kParkingPc); + h.LoadProgramAt(kProgramPc, { + ADDIU(reg::v0, reg::zero, 5), + JALR(reg::ra, reg::s0), // ra = 0x1000C, jumps to s0 + NOP, // delay slot + // 0x1000C — return continuation + ADDIU(reg::v0, reg::v0, 20), + JR(reg::s1), + NOP, + }, /*append_jr_ra_term=*/false); + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::v0, 300), + JR(reg::ra), + NOP, + }, /*append_jr_ra_term=*/false); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 325u); +} diff --git a/tests/ctest/core/recompilers/iop_regalloc_pressure_tests.cpp b/tests/ctest/core/recompilers/iop_regalloc_pressure_tests.cpp new file mode 100644 index 0000000000..8fb5603fec --- /dev/null +++ b/tests/ctest/core/recompilers/iop_regalloc_pressure_tests.cpp @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Pressure tests for the IOP recompiler's host-register allocator. The +// allocatable pool for normal PSX GPR work holds 14 host registers; once +// the live set exceeds that, the allocator must evict a slot — choose a +// victim that isn't currently needed, flush it back to psxRegs.GPR.r[], and +// reuse it for the new value. +// +// LUI+ORI sequences don't work as pressure: PSX const-prop folds them and the +// allocator never materializes a host reg. Guest loads (LW) do materialize — +// rpsxLoad always allocates rt with MODE_WRITE — so enough back-to-back LWs +// force the pool to exhaust and exercise the eviction path. + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kData = RecompilerTestEnvironment::kScratchAddr; // 0x00020000 + +// Distinct seed values small enough that their sum fits in u32 comfortably. +// Each slot is `0x01010101 * (i+1)` to make sums unambiguous. 15 slots: +// sum = 0x01010101 * (1+2+...+15) = 0x01010101 * 120 = 0x78787878. +constexpr u32 PatternFor(u32 i) { return 0x01010101u * (i + 1); } +} // namespace + +TEST(IopRegallocPressure, FifteenLiveGprsTriggerEviction) +{ + // 15 LWs into distinct guest regs → forces at least one eviction (pool + // is 14). Then an ADDU chain sums all 15 into v0. A botched + // eviction/reload shows as a wrong sum. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + + u32 expected = 0; + for (u32 i = 0; i < 15; ++i) + { + const u32 v = PatternFor(i); + h.WriteU32(kData + i * 4, v); + expected += v; + } + + // Guest regs t0..t9, s0..s4 — 15 regs, disjoint from a0 (base) and v0. + const u32 rr[15] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, reg::s4, + }; + + std::vector prog; + for (u32 i = 0; i < 15; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + // v0 = t0 + prog.push_back(ADDU(reg::v0, rr[0], reg::zero)); + for (u32 i = 1; i < 15; ++i) + prog.push_back(ADDU(reg::v0, reg::v0, rr[i])); + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), expected); + // Every individual reg also carried its loaded value at the final ADDU: + for (u32 i = 0; i < 15; ++i) + EXPECT_EQ(h.GetGprInterp(rr[i]), PatternFor(i)) + << "guest reg index " << i << " (mips reg " << rr[i] << ")"; +} + +TEST(IopRegallocPressure, TwentyLiveGprsCascadeEviction) +{ + // 20 LWs → at minimum 6 evictions required to finish the block. + // Sums should still land correctly. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + + u32 expected = 0; + for (u32 i = 0; i < 20; ++i) + { + const u32 v = PatternFor(i); + h.WriteU32(kData + i * 4, v); + expected += v; + } + + // Use t0..t9, s0..s7, v1, a1, a2 — 20 regs, disjoint from a0 + v0. + const u32 rr[20] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, reg::s4, + reg::s5, reg::s6, reg::s7, reg::v1, reg::a1, + }; + + std::vector prog; + for (u32 i = 0; i < 20; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + prog.push_back(ADDU(reg::v0, rr[0], reg::zero)); + for (u32 i = 1; i < 20; ++i) + prog.push_back(ADDU(reg::v0, reg::v0, rr[i])); + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), expected); +} + +TEST(IopRegallocPressure, WritesOnlyWithoutSubsequentReadStillWritesBack) +{ + // 15 LWs without a subsequent ADDU chain. Each load still allocates + // and must eventually write back to psxRegs.GPR.r[] on block exit — + // so the final snapshot should reflect each loaded value. Regression + // guard: the eviction path must not lose write-only loads. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + + for (u32 i = 0; i < 15; ++i) + h.WriteU32(kData + i * 4, PatternFor(i)); + + const u32 rr[15] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, reg::s4, + }; + + std::vector prog; + for (u32 i = 0; i < 15; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + for (u32 i = 0; i < 15; ++i) + EXPECT_EQ(h.GetGprInterp(rr[i]), PatternFor(i)) + << "guest reg index " << i; +} + +TEST(IopRegallocPressure, ReadAfterEvictionRoundsTripsThroughMemory) +{ + // Interleave LWs and reads: load the first reg, then 13 more; then + // read the very first reg back into v0. By the time it is read back, + // the allocator must have evicted and reloaded it (because 13 more + // LWs fill the pool). A broken spill target address would show as v0 + // holding garbage instead of the first pattern. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + + for (u32 i = 0; i < 14; ++i) + h.WriteU32(kData + i * 4, PatternFor(i)); + + const u32 rr[14] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, + }; + + std::vector prog; + for (u32 i = 0; i < 14; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + // After 14 loads + base reg a0 + potential scratch, the allocator is + // exhausted. The next read of t0 forces eviction of something else + // and a reload of t0 from psxRegs (or the cache). + prog.push_back(ADDU(reg::v0, rr[0], reg::zero)); // reads t0 late + prog.push_back(ADDU(reg::v1, rr[13], reg::zero)); // reads s3 late + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), PatternFor(0)); + EXPECT_EQ(h.GetGprInterp(reg::v1), PatternFor(13)); +} + +TEST(IopRegallocPressure, PairedHiLoStayLiveDuringPoolBurn) +{ + // MULT sets both HI and LO. A follow-up MFHI + MFLO reads them. In + // between, 14 unrelated LWs exhaust the pool and force eviction of + // whatever slots HI/LO chose. HI/LO writebacks must survive the + // eviction and the MFHI/MFLO reads must see the correct values. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + h.SetGpr(reg::a1, 0x10000); // multiplicand + h.SetGpr(reg::a2, 0x10001); // multiplier → 64-bit product 0x1'00010000 + + for (u32 i = 0; i < 14; ++i) + h.WriteU32(kData + i * 4, PatternFor(i)); + + // 0x10000 * 0x10001 = 0x1_00010000. So LO = 0x00010000, HI = 0x00000001. + constexpr u32 kExpectLo = 0x00010000u; + constexpr u32 kExpectHi = 0x00000001u; + + const u32 rr[14] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, + }; + + std::vector prog; + // RType encoder for MULT: funct 0x18, rs, rt, rd=0, sa=0 + prog.push_back(mips::RType(0, reg::a1, reg::a2, 0, 0, 0x18)); // mult a1, a2 + for (u32 i = 0; i < 14; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + // MFHI rd: funct 0x10, rs=0, rt=0, rd=rd + prog.push_back(mips::RType(0, 0, 0, reg::v0, 0, 0x10)); // mfhi v0 + // MFLO rd: funct 0x12 + prog.push_back(mips::RType(0, 0, 0, reg::v1, 0, 0x12)); // mflo v1 + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), kExpectHi) << "MFHI after pool burn"; + EXPECT_EQ(h.GetGprInterp(reg::v1), kExpectLo) << "MFLO after pool burn"; +} + +TEST(IopRegallocPressure, EvictedRegIsReaderObservable) +{ + // Cascade: 15 LWs (forces the first eviction), then read ALL of + // them. A silent miscopy of the evicted reg's spill destination + // would show as ONE of the GPR reads holding wrong data, without + // affecting the others. + JitTestHarness h; + h.SetGpr(reg::a0, kData); + + for (u32 i = 0; i < 15; ++i) + h.WriteU32(kData + i * 4, PatternFor(i)); + + const u32 rr[15] = { + reg::t0, reg::t1, reg::t2, reg::t3, reg::t4, + reg::t5, reg::t6, reg::t7, reg::t8, reg::t9, + reg::s0, reg::s1, reg::s2, reg::s3, reg::s4, + }; + + std::vector prog; + for (u32 i = 0; i < 15; ++i) + prog.push_back(LW(rr[i], static_cast(i * 4), reg::a0)); + // A single ADDU chain "touches" each reg in order, promoting a read + // of each. If any eviction mis-wrote to psxRegs, the individual + // GetGprInterp checks below catch it by reg-name. + prog.push_back(ADDU(reg::v0, rr[0], rr[1])); + for (u32 i = 2; i < 15; ++i) + prog.push_back(ADDU(reg::v0, reg::v0, rr[i])); + + h.LoadProgramAt(RecompilerTestEnvironment::kProgramPc, + prog.data(), prog.size(), + /*append_jr_ra_term=*/true); + h.Run(); + for (u32 i = 0; i < 15; ++i) + EXPECT_EQ(h.GetGprInterp(rr[i]), PatternFor(i)) + << "reg index " << i; +} diff --git a/tests/ctest/core/recompilers/iop_shift_tests.cpp b/tests/ctest/core/recompilers/iop_shift_tests.cpp new file mode 100644 index 0000000000..c04db63ede --- /dev/null +++ b/tests/ctest/core/recompilers/iop_shift_tests.cpp @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/JitTestHarness.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +TEST(IopShift, SllBasic) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00000001u); + h.LoadProgram({SLL(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000010u); +} + +TEST(IopShift, SllBy0IsMove) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xDEADBEEFu); + h.LoadProgram({SLL(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xDEADBEEFu); +} + +TEST(IopShift, SllBy31) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00000001u); + h.LoadProgram({SLL(reg::v0, reg::a0, 31)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x80000000u); +} + +TEST(IopShift, SrlBasic) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x80000000u); + h.LoadProgram({SRL(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x08000000u); +} + +TEST(IopShift, SraNegativeKeepsSign) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x80000000u); // -2147483648 + h.LoadProgram({SRA(reg::v0, reg::a0, 1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xC0000000u); // arithmetic: propagates 1 +} + +TEST(IopShift, SraPositive) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x0000FF00u); + h.LoadProgram({SRA(reg::v0, reg::a0, 4)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000FF0u); +} + +TEST(IopShift, Sllv) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00000001u); + h.SetGpr(reg::a1, 16); + h.LoadProgram({SLLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00010000u); +} + +TEST(IopShift, SllvMasksShiftToLow5Bits) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00000001u); + h.SetGpr(reg::a1, 33); // low 5 bits = 1 + h.LoadProgram({SLLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000002u); +} + +TEST(IopShift, Srlv) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00010000u); + h.SetGpr(reg::a1, 8); + h.LoadProgram({SRLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x00000100u); +} + +TEST(IopShift, Srav) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFF0000u); + h.SetGpr(reg::a1, 4); + h.LoadProgram({SRAV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xFFFFF000u); +} + +TEST(IopShift, SraArithmeticOnPositive) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x40000000u); + h.LoadProgram({SRA(reg::v0, reg::a0, 2)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x10000000u); +} + +TEST(IopShift, SrlBy0IsCopy) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xABCDEF01u); + h.LoadProgram({SRL(reg::v0, reg::a0, 0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xABCDEF01u); +} + +TEST(IopShift, SrlvLosesHighBits) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xFFFFFFFFu); + h.SetGpr(reg::a1, 16); + h.LoadProgram({SRLV(reg::v0, reg::a0, reg::a1)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x0000FFFFu); +} + +// --------------------------------------------------------------------------- +// Variable-shift Rd == Rs aliasing. Both Rd and Rs are the same GPR +// (e.g. `SLLV $a0, $a1, $a0`). Allocator-order regression: if Rd is +// allocated MODE_WRITE before Rs is allocated MODE_READ, the slot tracking +// Rd/Rs gets a fresh write-only slot with no memory load, then Rs's alloc +// reuses that slot without loading — so the shift count is garbage. +// --------------------------------------------------------------------------- + +TEST(IopShift, SllvRdEqualsRsAliasing) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 4u); // shift count (also Rd) + h.SetGpr(reg::a1, 0xFFu); + h.LoadProgram({SLLV(reg::a0, reg::a1, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::a0), 0xFFu << 4); // 0xFF0 + EXPECT_EQ(h.GetGprJit(reg::a0), 0xFFu << 4); +} + +TEST(IopShift, SrlvRdEqualsRsAliasing) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 4u); + h.SetGpr(reg::a1, 0xFF00u); + h.LoadProgram({SRLV(reg::a0, reg::a1, reg::a0)}); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::a0), 0xFF00u >> 4); // 0xFF0 + EXPECT_EQ(h.GetGprJit(reg::a0), 0xFF00u >> 4); +} + +TEST(IopShift, SravRdEqualsRsAliasing) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 4u); + h.SetGpr(reg::a1, 0xF0000000u); // negative + h.LoadProgram({SRAV(reg::a0, reg::a1, reg::a0)}); + h.Run(); + const u32 expected = static_cast(static_cast(0xF0000000u) >> 4); + EXPECT_EQ(h.GetGprInterp(reg::a0), expected); + EXPECT_EQ(h.GetGprJit(reg::a0), expected); +} + +// --------------------------------------------------------------------------- +// Variable-shift Rs-const fast path. Prepend an `ORI rs, $zero, k` so the +// JIT propagates a const value into Rs at block-compile time, then the +// variable shift folds to an immediate shift. +// --------------------------------------------------------------------------- + +TEST(IopShift, SllvWithConstRsFolds) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x0000FF00u); + h.LoadProgram({ + ORI(reg::a1, reg::zero, 4), + SLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x000FF000u); + EXPECT_EQ(h.GetGprJit(reg::v0), 0x000FF000u); +} + +TEST(IopShift, SrlvWithConstRsFolds) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0x00FF0000u); + h.LoadProgram({ + ORI(reg::a1, reg::zero, 8), + SRLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x0000FF00u); + EXPECT_EQ(h.GetGprJit(reg::v0), 0x0000FF00u); +} + +TEST(IopShift, SravWithConstRsHighBitsMasked) +{ + // Rs = 0x21 → low 5 bits = 1. Verifies the 0x1F mask on the const-prop + // emit (otherwise vixl would assert or emit a shift > 31). + JitTestHarness h; + h.SetGpr(reg::a0, 0xF0000000u); + h.LoadProgram({ + ORI(reg::a1, reg::zero, 0x21), + SRAV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + const u32 expected = static_cast(static_cast(0xF0000000u) >> 1); + EXPECT_EQ(h.GetGprInterp(reg::v0), expected); + EXPECT_EQ(h.GetGprJit(reg::v0), expected); +} + +TEST(IopShift, SllvWithConstRsZeroIsMove) +{ + JitTestHarness h; + h.SetGpr(reg::a0, 0xDEADBEEFu); + h.LoadProgram({ + ORI(reg::a1, reg::zero, 0), + SLLV(reg::v0, reg::a0, reg::a1), + }); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xDEADBEEFu); + EXPECT_EQ(h.GetGprJit(reg::v0), 0xDEADBEEFu); +} diff --git a/tests/ctest/core/recompilers/iop_smc_tests.cpp b/tests/ctest/core/recompilers/iop_smc_tests.cpp new file mode 100644 index 0000000000..291cc33720 --- /dev/null +++ b/tests/ctest/core/recompilers/iop_smc_tests.cpp @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Self-modifying code coverage for the IOP recompiler. +// +// The chain: `iopMemWrite*` → `psxCpu->Clear(addr, 1)` → `recClearIOP` → +// `psxRecClearMem(pc)` → walk recBlocks, merge overlapping BASEBLOCKEX +// entries, `iopClearRecLUT` zeroes the LUT slots for the cleared range. The +// next dispatcher lookup misses and compilation re-fires via the JIT-compile +// trampoline. +// +// The SMC invalidation path requires tests that overwrite and re-dispatch; +// a single immutable program would not cover this. + +#include "harness/JitTestHarness.h" + +#include "IopMem.h" +#include "R3000A.h" + +#include + +using namespace recompiler_tests; +using namespace mips; + +namespace { +constexpr u32 kProgramPc = RecompilerTestEnvironment::kProgramPc; // 0x00010000 +constexpr u32 kParkingPc = RecompilerTestEnvironment::kParkingPc; // 0x001F0000 + +// Block 2 well outside block 1's 4KB page so merging logic doesn't +// touch it across the tests that stay in block 1. +constexpr u32 kBlock2Pc = 0x00014000; +} // namespace + +TEST(IopSmc, HarnessOverwriteThenRunProducesNewResult) +{ + // 1) Compile a 100-producing program. 2) Rewrite the first word in + // place with a 200-producing ADDIU. 3) Re-run and verify the new + // opcode executes. + JitTestHarness h; + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, 100), + }); + h.Run(); + ASSERT_EQ(h.GetGprInterp(reg::v0), 100u); + + // Overwrite the ADDIU in place. iopMemWrite32 calls psxCpu->Clear, + // which invalidates the cached block at kProgramPc. + iopMemWrite32(kProgramPc, ADDIU(reg::v0, reg::zero, 200)); + + // Re-enter. SetPc restores the program's entry point; SetRa keeps + // the `jr ra; nop` terminator going to the parking lot. + h.SetPc(kProgramPc); + h.SetRa(kParkingPc); + h.RunResume(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 200u); +} + +TEST(IopSmc, GuestSwIntoOwnProgramRegionTriggersRecompile) +{ + // Block 1 builds an ADDIU instruction into a GPR (lui+ori), stores + // it to block 2's address via SW (which calls psxCpu->Clear), then + // jumps to block 2. Block 2 is pre-loaded with a different ADDIU + // but should execute the just-written one. + // + // MIPS ADDIU opcode: 0x09 in top 6 bits. + // ADDIU v0, zero, 0x1337 + // = (0x09 << 26) | (0 << 21) | (2 << 16) | 0x1337 + // = 0x24020000 | 0x1337 + // = 0x24021337 + JitTestHarness h; + constexpr u32 kNewInstr = ADDIU(reg::v0, reg::zero, 0x1337); + h.SetGpr(reg::a0, kBlock2Pc); + // Pre-state: a1 holds the new ADDIU encoding. LUI+ORI to materialize + // the 32-bit constant into a1. + const u16 hi = static_cast(kNewInstr >> 16); + const u16 lo = static_cast(kNewInstr & 0xFFFF); + h.LoadProgramAt(kProgramPc, { + LUI(reg::a1, hi), + ORI(reg::a1, reg::a1, lo), + SW(reg::a1, 0, reg::a0), // overwrites block 2's 1st word + J(kBlock2Pc), + NOP, // delay slot + }, /*append_jr_ra_term=*/false); + // Pre-load block 2 with a POISON opcode that the SW should replace. + h.LoadProgramAt(kBlock2Pc, { + ADDIU(reg::v0, reg::zero, 0x0BAD), // should not execute + }, /*append_jr_ra_term=*/true); + h.Run(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0x1337u); +} + +TEST(IopSmc, OverlappingBlockClearInvalidatesBoth) +{ + // Two blocks in the same 4KB page: kProgramPc + 0x000 and + // kProgramPc + 0x100. Run each once so both are compiled. Then + // overwrite a word inside the first block's range. `psxRecClearMem` + // will merge overlapping entries; both should be invalidated and + // recompiled on the next dispatch. + JitTestHarness h; + constexpr u32 kProgA = kProgramPc; + constexpr u32 kProgB = kProgramPc + 0x100; + + h.LoadProgramAt(kProgA, { + ADDIU(reg::v0, reg::zero, 1), + }, /*append_jr_ra_term=*/true); + h.LoadProgramAt(kProgB, { + ADDIU(reg::v1, reg::zero, 2), + }, /*append_jr_ra_term=*/true); + + // First run enters at block A (default kProgramPc). + h.Run(); + ASSERT_EQ(h.GetGprInterp(reg::v0), 1u); + + // Second run entered at block B — proves block B was compiled too. + h.SetPc(kProgB); + h.SetRa(kParkingPc); + h.RunResume(); + ASSERT_EQ(h.GetGprInterp(reg::v1), 2u); + + // Now overwrite block A's first word with a different instruction. + // The SMC clear may merge-and-invalidate block B as well (depends + // on the block's size tracking). Regardless, re-entering block A + // should execute the NEW instruction. + iopMemWrite32(kProgA, ADDIU(reg::v0, reg::zero, 99)); + h.SetPc(kProgA); + h.SetRa(kParkingPc); + h.RunResume(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 99u); + + // Block B should still work — whether it was silently recompiled + // or left cached, the correct instruction must run. Poison v1 first so + // the assertion forces block B to actively WRITE 2 (RunResume does not + // reset GPRs; without this, a silently-skipped block B would leave the + // stale 2 from the second run and pass vacuously). + h.SetGpr(reg::v1, 0xDEADBEEFu); + h.SetPc(kProgB); + h.SetRa(kParkingPc); + h.RunResume(); + EXPECT_EQ(h.GetGprInterp(reg::v1), 2u); +} + +TEST(IopSmc, ClearOutsideCodeRegionLeavesActiveBlockIntact) +{ + // Compile a block, then store at an address far from the code. The + // store triggers a Clear(addr, 1) but the block's LUT slot is + // unaffected, so a resume uses the cached block. + JitTestHarness h; + h.LoadProgram({ + ADDIU(reg::v0, reg::zero, 42), + }); + h.Run(); + ASSERT_EQ(h.GetGprInterp(reg::v0), 42u); + + // Far-from-code store. kScratchAddr is in a different page from + // kProgramPc, so Clear(kScratchAddr, 1) can't touch the program's + // BASEBLOCK entry. + iopMemWrite32(RecompilerTestEnvironment::kScratchAddr, 0xDEADBEEF); + + h.SetPc(kProgramPc); + h.SetRa(kParkingPc); + h.RunResume(); + EXPECT_EQ(h.GetGprInterp(reg::v0), 42u); +} + +TEST(IopSmc, OverwriteLastWordOfBlockBeforeTerminator) +{ + // Edge case: write at the exact last instruction of a block's body + // (just before the `jr ra; nop` terminator). The entire block should + // re-compile with the new instruction, without the terminator being + // disturbed. Regression coverage for the LUT-fnptr early-exit in + // psxRecClearMem (mid-block words still hold iopJITCompile so a + // fnptr-based check would silently leak the SMC through to stale + // compiled code). + JitTestHarness h; + h.LoadProgram({ + ADDIU(reg::t0, reg::zero, 10), + ADDIU(reg::t1, reg::zero, 20), + ADDU(reg::v0, reg::t0, reg::t1), // last body word: v0 = 30 + }); + h.Run(); + ASSERT_EQ(h.GetGprInterp(reg::v0), 30u); + + // The 3rd body word sits at kProgramPc + 8. Replace with ADDU that + // sums t0+t1 and leaves it in v1 instead. + iopMemWrite32(kProgramPc + 8, ADDU(reg::v1, reg::t0, reg::t1)); + h.SetPc(kProgramPc); + h.SetRa(kParkingPc); + h.SetGpr(reg::v0, 0xDEAD); // sentinel — should stay 0xDEAD since the + // new opcode writes v1, not v0. + h.SetGpr(reg::v1, 0); + h.RunResume(); + EXPECT_EQ(h.GetGprInterp(reg::v1), 30u); + EXPECT_EQ(h.GetGprInterp(reg::v0), 0xDEADu); +} diff --git a/tests/ctest/core/recompilers/main.cpp b/tests/ctest/core/recompilers/main.cpp new file mode 100644 index 0000000000..f1b4aa1054 --- /dev/null +++ b/tests/ctest/core/recompilers/main.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "harness/RecompilerTestEnvironment.h" + +#include + +namespace +{ + +// gtest-side wrapper that delegates to the shared (gtest-free) +// RecompilerTestEnvironment::Initialize/Shutdown so pcsx2-vurunner can use +// the same setup logic without linking gtest. +class GtestEnvironment : public ::testing::Environment +{ +public: + void SetUp() override + { + if (!recompiler_tests::RecompilerTestEnvironment::Initialize()) + ADD_FAILURE() << "RecompilerTestEnvironment::Initialize() failed (likely SysMemory::Allocate)"; + } + void TearDown() override + { + recompiler_tests::RecompilerTestEnvironment::Shutdown(); + } +}; + +} // namespace + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(new GtestEnvironment()); + return RUN_ALL_TESTS(); +} diff --git a/tests/ctest/core/recompilers/vu0_alu_lower_tests.cpp b/tests/ctest/core/recompilers/vu0_alu_lower_tests.cpp new file mode 100644 index 0000000000..a2f1edac8a --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_alu_lower_tests.cpp @@ -0,0 +1,666 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 lower-pipe non-Q ALU DiffJitVsInterp suite. Covers VMOVE, +// VMR32, VMTIR, VMFIR; the eight VFTOIx / VITOFx fixed-point conversions; +// and VLQI / VSQI / VLQD / VSQD pre/post-increment loads/stores. Q-pipe +// ops (VDIV / VSQRT / VRSQRT + VWAITQ) live in the dedicated Q-pipeline +// suite so the timing-fragile cycle-distance permutations stay isolated. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// Pair the supplied lower-word op with an upper-word NOP (FD_11 sub 0x0B). +// No I-bit needed since the lower already carries a real opcode. +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } + +} // namespace + +// -------- VMOVE (lower-pipe register copy) -------- + +TEST(Vu0AluLower, VmoveXyzwCopiesAllLanes) +{ + VuTestHarness h(0); + h.SetVf(1, 1.5f, 2.5f, 3.5f, 4.5f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 3.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 4.5f); +} + +TEST(Vu0AluLower, VmoveMaskedLeavesUnmaskedLanes) +{ + VuTestHarness h(0); + h.SetVf(1, 11.0f, 22.0f, 33.0f, 44.0f); + h.SetVf(2, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::y | mask::w, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), -3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 44.0f); +} + +TEST(Vu0AluLower, VmoveSelfIsIdentity) +{ + VuTestHarness h(0); + h.SetVf(5, 7.0f, -8.0f, 9.0f, -10.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::xyzw, vf::vf5, vf::vf5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'y'), -8.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'z'), 9.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'w'), -10.0f); +} + +// -------- VMR32 (rotate xyzw → yzwx) -------- + +TEST(Vu0AluLower, VmR32RotatesByOneLane) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + // Per _vuMR32: ft.x = fs.y, ft.y = fs.z, ft.z = fs.w, ft.w = fs.x. + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 1.0f); +} + +TEST(Vu0AluLower, VmR32MaskedZWritesOnlyZ) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 100.0f, 200.0f, 300.0f, 400.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::z, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 100.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 200.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); // pre-rotation src lane = w + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 400.0f); +} + +TEST(Vu0AluLower, VmR32SelfAliasingRotatesIntoSelf) +{ + // fs == ft: the rotate must fully complete from the saved input — + // regalloc has to either spill or use a temp, otherwise lane writes + // stomp later lane reads. Classic aliasing-bug shape. + VuTestHarness h(0); + h.SetVf(3, 10.0f, 20.0f, 30.0f, 40.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::xyzw, vf::vf3, vf::vf3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 20.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 30.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 40.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 10.0f); +} + +// -------- VMTIR / VMFIR (VF ↔ VI scalar transfer) -------- + +TEST(Vu0AluLower, VmtirCopiesLaneToVi) +{ + // Per _vuMTIR: VI[it].US[0] = u16(fs.f[fsf]) — the *low 16 bits of the + // IEEE-754 representation* of the chosen lane. NOT a float→int cast. + VuTestHarness h(0); + h.SetVfBits(1, 0xCAFEBABE, 0xDEADBEEF, 0x12345678, 0xAAAA5555); + // Pick fsf=2 (z lane) → VI[1] should get low 16 bits of 0x12345678 = 0x5678. + h.LoadProgram({ + LowerOnly(VMTIR_L(vi::vi1, vf::vf1, /*fsf=*/2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(1), 0x5678u); +} + +TEST(Vu0AluLower, VmfirSignExtendsViIntoAllLanes) +{ + // Per _vuMFIR: ft.SL[lane] = (s32)VI[is].SS[0] for each masked lane. + // VI is 16-bit signed; result populates the s32 slot of each VF lane. + VuTestHarness h(0); + h.SetVi(2, 0xFFFFu); // -1 as s16 + h.SetVfBits(3, 0u, 0u, 0u, 0u); + h.LoadProgram({ + LowerOnly(VMFIR_L(mask::xyzw, vf::vf3, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + // VI[2] = -1 (s16) → (s32)-1 = 0xFFFFFFFF stored as 32-bit pattern in VF. + EXPECT_EQ(h.GetVfBitsJit(3, 'x'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'y'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'z'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'w'), 0xFFFFFFFFu); +} + +TEST(Vu0AluLower, VmfirMaskedOnlyTouchesSelectedLanes) +{ + VuTestHarness h(0); + h.SetVi(4, 0x1234u); + h.SetVfBits(5, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDDDD4444u); + h.LoadProgram({ + LowerOnly(VMFIR_L(mask::y, vf::vf5, vi::vi4)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(5, 'x'), 0xAAAA1111u); + EXPECT_EQ(h.GetVfBitsJit(5, 'y'), 0x00001234u); + EXPECT_EQ(h.GetVfBitsJit(5, 'z'), 0xCCCC3333u); + EXPECT_EQ(h.GetVfBitsJit(5, 'w'), 0xDDDD4444u); +} + +// -------- VITOFx / VFTOIx (fixed-point conversions; UPPER pipe) -------- + +namespace { +inline VuOp UpperOnlyPair(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } +} // namespace + +TEST(Vu0AluLower, VitofZeroMatchesIntegerCast) +{ + // VITOF0: just (float)(s32)bits — no scaling. + VuTestHarness h(0); + h.SetVfBits(1, 0u, 1u, static_cast(-1), 0x80000000u); + h.LoadProgram({ + UpperOnlyPair(VITOF0_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + // Diff already polices the architectural answer; sanity-check the JIT side. + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), h.GetVfBitsInterp(2, 'x')); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), h.GetVfBitsInterp(2, 'y')); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), h.GetVfBitsInterp(2, 'z')); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), h.GetVfBitsInterp(2, 'w')); +} + +TEST(Vu0AluLower, VitofFourScalesByExp4) +{ + // VITOF4: (float)bits / 2^4 — used for 28.4 fixed-point reads. + VuTestHarness h(0); + h.SetVfBits(1, 16u, 32u, 64u, 256u); + h.LoadProgram({ + UpperOnlyPair(VITOF4_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 16.0f); +} + +TEST(Vu0AluLower, Vitof12ScalesByExp12) +{ + VuTestHarness h(0); + h.SetVfBits(1, 4096u, 8192u, 16384u, 32768u); + h.LoadProgram({ + UpperOnlyPair(VITOF12_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 8.0f); +} + +TEST(Vu0AluLower, Vitof15ScalesByExp15) +{ + VuTestHarness h(0); + h.SetVfBits(1, 32768u, 65536u, 131072u, 262144u); + h.LoadProgram({ + UpperOnlyPair(VITOF15_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 8.0f); +} + +TEST(Vu0AluLower, Vftoi0RoundsTowardZero) +{ + // VFTOI0: (s32)f — truncate toward zero. + VuTestHarness h(0); + h.SetVf(1, 1.7f, -1.7f, 2.999f, -0.5f); + h.LoadProgram({ + UpperOnlyPair(VFTOI0_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), -1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 2); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), 0); +} + +TEST(Vu0AluLower, Vftoi4ScalesUp16XThenTruncates) +{ + // VFTOI4: (s32)(f * 16). Used for 28.4 fixed-point writes. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 0.5f, 0.0625f, -2.5f); + h.LoadProgram({ + UpperOnlyPair(VFTOI4_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 16); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 8); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -40); +} + +TEST(Vu0AluLower, Vftoi12ScalesUp4096X) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 0.5f, 0.0f, -1.0f); + h.LoadProgram({ + UpperOnlyPair(VFTOI12_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 4096); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 2048); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 0); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -4096); +} + +TEST(Vu0AluLower, Vftoi15ScalesUp32768X) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 0.5f, 0.25f, -1.0f); + h.LoadProgram({ + UpperOnlyPair(VFTOI15_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 32768); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 16384); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 8192); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -32768); +} + +TEST(Vu0AluLower, Vftoi0SaturatesAtIntMaxOnHugeFloat) +{ + // Per floatToInt<>: any float whose exponent is ≥ 0x4F (≈ 2^31) saturates + // to ±INT_MAX/INT_MIN. A bug here would be the runaway int-cast undefined + // behaviour seen on the EE side (MTSA float-mul cast). + VuTestHarness h(0); + h.SetVfBits(1, 0x7F7FFFFFu, 0xFF7FFFFFu, 0u, 0u); // ±FLT_MAX, 0 + h.LoadProgram({ + UpperOnlyPair(VFTOI0_U(mask::x | mask::y, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0x7FFFFFFFu); // INT_MAX + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0x80000000u); // INT_MIN +} + +// -------- VABS (UPPER, FD_01 sub 0x07) — included here since it's a unary +// ALU op alongside VITOF/VFTOI. Bit-strip-the-sign-bit semantics. -- + +TEST(Vu0AluLower, VabsClearsSignBitPerLane) +{ + VuTestHarness h(0); + h.SetVf(1, -1.0f, 2.0f, -3.5f, 0.0f); + h.LoadProgram({ + UpperOnlyPair(VABS_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 3.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 0.0f); +} + +TEST(Vu0AluLower, VabsOnNegativeZeroProducesPositiveZero) +{ + VuTestHarness h(0); + h.SetVfBits(1, 0x80000000u, 0u, 0x80000000u, 0u); + h.LoadProgram({ + UpperOnlyPair(VABS_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), 0u); +} + +// -------- VLQI / VSQI / VLQD / VSQD (auto-increment loads/stores) -------- + +TEST(Vu0AluLower, VlqiLoadsThenIncrementsViPointer) +{ + VuTestHarness h(0); + h.WriteMemU128(0x40, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.SetVi(3, 0x40 / 16); // VI in units of 16-byte quads + h.SetVfBits(2, 0u, 0u, 0u, 0u); + h.TrackMemWindow(0x40, 16); + h.LoadProgram({ + LowerOnly(VLQI_L(mask::xyzw, vf::vf2, vi::vi3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0x11111111u); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0x22222222u); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), 0x33333333u); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), 0x44444444u); + EXPECT_EQ(h.GetViJit(3), (0x40 / 16) + 1); +} + +TEST(Vu0AluLower, VsqiStoresThenIncrementsViPointer) +{ + VuTestHarness h(0); + h.SetVfBits(5, 0xDEADBEEFu, 0xCAFEBABEu, 0xFEEDFACEu, 0x12345678u); + h.SetVi(4, 0x80 / 16); + h.TrackMemWindow(0x80, 16); + h.LoadProgram({ + LowerOnly(VSQI_L(mask::xyzw, vf::vf5, vi::vi4)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0x80), 0xDEADBEEFu); + EXPECT_EQ(h.GetMemU32Jit(0x84), 0xCAFEBABEu); + EXPECT_EQ(h.GetMemU32Jit(0x88), 0xFEEDFACEu); + EXPECT_EQ(h.GetMemU32Jit(0x8C), 0x12345678u); + EXPECT_EQ(h.GetViJit(4), (0x80 / 16) + 1); +} + +TEST(Vu0AluLower, VlqdPredecrementsThenLoads) +{ + VuTestHarness h(0); + h.WriteMemU128(0x60, 0xAAAA0000u, 0xBBBB0000u, 0xCCCC0000u, 0xDDDD0000u); + h.SetVi(2, (0x60 / 16) + 1); // pre-decrement → load from 0x60 + h.SetVfBits(7, 0u, 0u, 0u, 0u); + h.TrackMemWindow(0x60, 16); + h.LoadProgram({ + LowerOnly(VLQD_L(mask::xyzw, vf::vf7, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(7, 'x'), 0xAAAA0000u); + EXPECT_EQ(h.GetViJit(2), (0x60 / 16)); +} + +TEST(Vu0AluLower, VsqdPredecrementsThenStores) +{ + VuTestHarness h(0); + h.SetVfBits(8, 0xEEEE1111u, 0xEEEE2222u, 0xEEEE3333u, 0xEEEE4444u); + h.SetVi(6, (0xA0 / 16) + 1); // pre-decrement → store at 0xA0 + h.TrackMemWindow(0xA0, 16); + h.LoadProgram({ + LowerOnly(VSQD_L(mask::xyzw, vf::vf8, vi::vi6)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0xA0), 0xEEEE1111u); + EXPECT_EQ(h.GetMemU32Jit(0xAC), 0xEEEE4444u); + EXPECT_EQ(h.GetViJit(6), 0xA0 / 16); +} + +// -------- VLQ / VSQ + LQI/SQI/LQD/SQD partial-mask coverage -------- +// +// A partial-mask LQ writes exactly the masked lanes of the destination VF, +// placing the correct memory word into each — e.g. mem[3] (the W word) into +// the W lane for a .w mask — and leaves every unmasked lane untouched. These +// tests pin that single-lane and multi-lane masking behavior. +// +// `Run()`'s auto-diff between JIT and interp is the primary gate; the +// explicit `EXPECT_EQ(GetVfBitsJit(...))` lines pin down the architectural +// intent and survive even on the unlikely chance JIT and interp happen +// to agree on a wrong value. + +TEST(Vu0AluLower, VlqWritesOnlyWLane) +{ + VuTestHarness h(0); + h.WriteMemU128(0, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDEADBEEFu); + h.SetVfBits(14, 0xFEEDFACEu, 0xFEEDFACEu, 0xFEEDFACEu, 0xFEEDFACEu); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::w, vf::vf14, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(14, 'x'), 0xFEEDFACEu); + EXPECT_EQ(h.GetVfBitsJit(14, 'y'), 0xFEEDFACEu); + EXPECT_EQ(h.GetVfBitsJit(14, 'z'), 0xFEEDFACEu); + EXPECT_EQ(h.GetVfBitsJit(14, 'w'), 0xDEADBEEFu); +} + +TEST(Vu0AluLower, VlqWritesOnlyZLane) +{ + VuTestHarness h(0); + h.WriteMemU128(0, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDEADBEEFu); + h.SetVfBits(7, 0u, 0u, 0u, 0u); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::z, vf::vf7, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(7, 'x'), 0u); + EXPECT_EQ(h.GetVfBitsJit(7, 'y'), 0u); + EXPECT_EQ(h.GetVfBitsJit(7, 'z'), 0xCCCC3333u); + EXPECT_EQ(h.GetVfBitsJit(7, 'w'), 0u); +} + +TEST(Vu0AluLower, VlqWritesOnlyYLane) +{ + VuTestHarness h(0); + h.WriteMemU128(0, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDEADBEEFu); + h.SetVfBits(5, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::y, vf::vf5, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(5, 'x'), 0x11111111u); + EXPECT_EQ(h.GetVfBitsJit(5, 'y'), 0xBBBB2222u); + EXPECT_EQ(h.GetVfBitsJit(5, 'z'), 0x33333333u); + EXPECT_EQ(h.GetVfBitsJit(5, 'w'), 0x44444444u); +} + +TEST(Vu0AluLower, VlqWritesOnlyXLane) +{ + // X is the simplest single-lane mask; kept as a regression sentinel + // alongside the Y/Z/W cases. + VuTestHarness h(0); + h.WriteMemU128(0, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDEADBEEFu); + h.SetVfBits(3, 0u, 0xCAFEBABEu, 0xCAFEBABEu, 0xCAFEBABEu); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::x, vf::vf3, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(3, 'x'), 0xAAAA1111u); + EXPECT_EQ(h.GetVfBitsJit(3, 'y'), 0xCAFEBABEu); + EXPECT_EQ(h.GetVfBitsJit(3, 'z'), 0xCAFEBABEu); + EXPECT_EQ(h.GetVfBitsJit(3, 'w'), 0xCAFEBABEu); +} + +TEST(Vu0AluLower, VlqMultiLaneZwLeavesXyAlone) +{ + // Multi-lane partial mask: only the masked lanes (Z and W) are written + // from memory and the unmasked X/Y lanes are preserved. Covered here so a + // future refactor of the masking path doesn't regress the multi-lane case. + VuTestHarness h(0); + h.WriteMemU128(16, 0xAA01u, 0xBB02u, 0xCC03u, 0xDD04u); + h.SetVfBits(8, 0x77777777u, 0x88888888u, 0x99999999u, 0xAAAAAAAAu); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::z | mask::w, vf::vf8, vi::vi0, 1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(8, 'x'), 0x77777777u); + EXPECT_EQ(h.GetVfBitsJit(8, 'y'), 0x88888888u); + EXPECT_EQ(h.GetVfBitsJit(8, 'z'), 0xCC03u); + EXPECT_EQ(h.GetVfBitsJit(8, 'w'), 0xDD04u); +} + +TEST(Vu0AluLower, VlqWAfterIBitLiteralDoesNotLeakLiteral) +{ + // An I-bit literal sets VI[REG_I] to a recognizable sentinel, then a + // MULi.w op uses I. The subsequent `LQ.w` to a different VF must observe + // mem.w, not a value cached in the same host register lane by the + // preceding broadcast multiply. A lane-handling bug would show + // vf14.w == 0xCAFEBABE (or some other residue from the I broadcast) + // instead of the loaded value. + VuTestHarness h(0); + h.WriteMemU128(0, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDEADBEEFu); + h.SetVfBits(0, 0u, 0u, 0u, 0x3F800000u); // VF0.w = 1.0f (architectural) + h.SetVfBits(14, 0u, 0u, 0u, 0u); + h.LoadProgram({ + IBit(VuOp{VLitI(0xCAFEBABEu), VNOP_U()}), // I = sentinel + VuOp{0u, VMULi_U(mask::w, vf::vf31, vf::vf0)}, // vf31.w = vf0.w * I + LowerOnly(VLQ_L(mask::w, vf::vf14, vi::vi0, 0)), // vf14.w = mem[0].w + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(14, 'w'), 0xDEADBEEFu); + EXPECT_NE(h.GetVfBitsJit(14, 'w'), 0xCAFEBABEu); +} + +TEST(Vu0AluLower, VsqStoresOnlyWByte) +{ + // SQ partial-mask doesn't share the LQ lane-convention issue (it merges + // into a scratch register that's then Str'd directly to memory, so + // natural-lane placement is correct end-to-end). Lock down the + // correctness with explicit coverage. + VuTestHarness h(0); + h.WriteMemU128(32, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.SetVfBits(9, 0xAA01u, 0xBB02u, 0xCC03u, 0xDEADBEEFu); + h.LoadProgram({ + LowerOnly(VSQ_L(mask::w, vf::vf9, vi::vi0, 2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(32 + 0), 0x11111111u); + EXPECT_EQ(h.GetMemU32Jit(32 + 4), 0x22222222u); + EXPECT_EQ(h.GetMemU32Jit(32 + 8), 0x33333333u); + EXPECT_EQ(h.GetMemU32Jit(32 + 12), 0xDEADBEEFu); +} + +TEST(Vu0AluLower, VlqiPartialMaskWAdvancesViAndLoadsW) +{ + // Existing VLQI test only exercises the full-mask path. Partial-mask + // LQI shares the same lane-convention constraint as LQ — cover it. + VuTestHarness h(0); + h.WriteMemU128(0x40, 0xA1u, 0xA2u, 0xA3u, 0xCAFEBABEu); + h.SetVi(3, 0x40 / 16); + h.SetVfBits(2, 0x11u, 0x22u, 0x33u, 0x44u); + h.LoadProgram({ + LowerOnly(VLQI_L(mask::w, vf::vf2, vi::vi3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0x11u); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0x22u); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), 0x33u); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), 0xCAFEBABEu); + EXPECT_EQ(h.GetViJit(3), (0x40 / 16) + 1); +} + +TEST(Vu0AluLower, VlqdPartialMaskZDecrementsViAndLoadsZ) +{ + VuTestHarness h(0); + h.WriteMemU128(0x60, 0xA1u, 0xA2u, 0xFEEDFACEu, 0xA4u); + h.SetVi(2, (0x60 / 16) + 1); + h.SetVfBits(7, 0u, 0u, 0u, 0u); + h.LoadProgram({ + LowerOnly(VLQD_L(mask::z, vf::vf7, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(7, 'x'), 0u); + EXPECT_EQ(h.GetVfBitsJit(7, 'y'), 0u); + EXPECT_EQ(h.GetVfBitsJit(7, 'z'), 0xFEEDFACEu); + EXPECT_EQ(h.GetVfBitsJit(7, 'w'), 0u); + EXPECT_EQ(h.GetViJit(2), 0x60 / 16); +} + +TEST(Vu0AluLower, VsqiPartialMaskYAdvancesViAndStoresY) +{ + VuTestHarness h(0); + h.WriteMemU128(0x80, 0x11u, 0x22u, 0x33u, 0x44u); + h.SetVfBits(5, 0xAAu, 0xBADCAFEDu, 0xCCu, 0xDDu); + h.SetVi(4, 0x80 / 16); + h.LoadProgram({ + LowerOnly(VSQI_L(mask::y, vf::vf5, vi::vi4)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0x80 + 0), 0x11u); + EXPECT_EQ(h.GetMemU32Jit(0x80 + 4), 0xBADCAFEDu); + EXPECT_EQ(h.GetMemU32Jit(0x80 + 8), 0x33u); + EXPECT_EQ(h.GetMemU32Jit(0x80 + 12), 0x44u); + EXPECT_EQ(h.GetViJit(4), (0x80 / 16) + 1); +} + +TEST(Vu0AluLower, VsqdPartialMaskWDecrementsViAndStoresW) +{ + VuTestHarness h(0); + h.WriteMemU128(0xA0, 0x11u, 0x22u, 0x33u, 0x44u); + h.SetVfBits(8, 0xAAu, 0xBBu, 0xCCu, 0xC0FFEEFFu); + h.SetVi(6, (0xA0 / 16) + 1); + h.LoadProgram({ + LowerOnly(VSQD_L(mask::w, vf::vf8, vi::vi6)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0xA0 + 0), 0x11u); + EXPECT_EQ(h.GetMemU32Jit(0xA0 + 4), 0x22u); + EXPECT_EQ(h.GetMemU32Jit(0xA0 + 8), 0x33u); + EXPECT_EQ(h.GetMemU32Jit(0xA0 + 12), 0xC0FFEEFFu); + EXPECT_EQ(h.GetViJit(6), 0xA0 / 16); +} + +// -------- VU1 spot check — same lower-pipe encoding works on the larger bank -- + +TEST(Vu0AluLower, VmoveAndVmR32WorkOnVu1) +{ + VuTestHarness h(1); + h.SetVf(10, 5.0f, 6.0f, 7.0f, 8.0f); + h.SetVf(11, 0.0f, 0.0f, 0.0f, 0.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::xyzw, vf::vf11, vf::vf10)), + LowerOnly(VMR32_L(mask::xyzw, vf::vf12, vf::vf10)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(11, 'x'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(11, 'w'), 8.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(12, 'x'), 6.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(12, 'w'), 5.0f); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_alu_upper_tests.cpp b/tests/ctest/core/recompilers/vu0_alu_upper_tests.cpp new file mode 100644 index 0000000000..bf40f7b7a5 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_alu_upper_tests.cpp @@ -0,0 +1,496 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 upper-pipe FMAC DiffJitVsInterp suite. Each test runs the +// same one-instruction microprogram through microVU and the VU interpreter +// from identical pre-state and gtest-fails on any architectural divergence +// (registers, MAC/STATUS/CLIP-as-VI, memory windows). Spot-checks via +// GetVfJit confirm the test reaches the asserted post-state shape; the +// harness's diff is what actually polices correctness. + +#include "harness/VuTestHarness.h" + +#include "Config.h" +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// Pair 0 = upper op + I-bit-skipped lower (the lower word becomes a 32-bit +// float immediate into VI[REG_I] which we ignore). Pair 1 = E-bit terminator +// (the harness appends an architectural-delay-slot NOP pair automatically). +inline VuOp UpperOnly(u32 upper) +{ + return IBit(VuOp{VLitZero(), upper}); +} + +} // namespace + +// -------- VADD primary (xyzw operand) -------- + +TEST(Vu0AluUpper, VaddXyzwAcrossAllLanes) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.5f, 0.25f, 0.125f, 0.0625f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 2.25f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 3.125f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 4.0625f); +} + +TEST(Vu0AluUpper, VaddXOnlyLeavesOtherLanesPreSeed) +{ + // Mask=x; FD lanes y/z/w must retain their pre-state, only x updates. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(3, 99.0f, -77.0f, 55.5f, -33.25f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::x, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 11.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -77.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 55.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -33.25f); +} + +TEST(Vu0AluUpper, VaddYzMaskLeavesXAndW) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(3, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::y | mask::z, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 33.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -4.0f); +} + +TEST(Vu0AluUpper, VaddSelfAliasingFdEqualsFs) +{ + // FD == FS: result accumulates into the same register without trampling + // the source mid-instruction. The regalloc has to flush-or-share + // here; aliasing was a real bug class on the EE side. + VuTestHarness h(0); + h.SetVf(1, 1.5f, 2.5f, 3.5f, 4.5f); + h.SetVf(2, 0.5f, 0.5f, 0.5f, 0.5f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'y'), 3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'w'), 5.0f); +} + +TEST(Vu0AluUpper, VaddNegativeOperands) +{ + VuTestHarness h(0); + h.SetVf(1, -1.0f, -2.0f, -3.0f, -4.0f); + h.SetVf(2, 1.0f, 1.5f, 2.0f, 2.5f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -0.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -1.5f); +} + +// -------- VSUB primary -------- + +TEST(Vu0AluUpper, VsubXyzw) +{ + VuTestHarness h(0); + h.SetVf(1, 5.0f, 10.0f, 15.0f, 20.0f); + h.SetVf(2, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 8.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 16.0f); +} + +TEST(Vu0AluUpper, VsubFdEqualsFt) +{ + // FD==FT: the JIT mustn't read a stale FT after writing FD lane-by-lane. + VuTestHarness h(0); + h.SetVf(1, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(2, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf2, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 9.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 18.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 27.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 36.0f); +} + +// -------- VMUL primary -------- + +TEST(Vu0AluUpper, VmulXyzw) +{ + VuTestHarness h(0); + h.SetVf(1, 2.0f, 3.0f, 4.0f, 5.0f); + h.SetVf(2, 0.5f, 0.5f, 0.25f, -1.0f); + h.LoadProgram({ + UpperOnly(VMUL_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -5.0f); +} + +TEST(Vu0AluUpper, VmulZeroProducesZero) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.0f, 0.0f, 0.0f, 0.0f); + h.SetVf(3, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMUL_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 0.0f); +} + +// -------- VMAX / VMINI -------- + +TEST(Vu0AluUpper, VmaxPicksLargerLaneByLane) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 5.0f, -3.0f, 0.0f); + h.SetVf(2, 2.0f, 4.0f, -1.0f, -0.5f); + h.LoadProgram({ + UpperOnly(VMAX_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 0.0f); +} + +TEST(Vu0AluUpper, VminiPicksSmallerLaneByLane) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 5.0f, -3.0f, 0.0f); + h.SetVf(2, 2.0f, 4.0f, -1.0f, -0.5f); + h.LoadProgram({ + UpperOnly(VMINI_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -0.5f); +} + +// -------- VMADD / VMSUB (read-modify-write into FD via ACC) -------- +// +// VMADD: FD = ACC + FS * FT +// VMSUB: FD = ACC - FS * FT +// The interpreter and JIT have separate ACC representations; correctness +// requires both to agree at E-bit. ACC is seeded via a preceding op so the +// test doesn't have to construct ACC out-of-band. + +TEST(Vu0AluUpper, VmaddAddsAccProduct) +{ + // Seed ACC via a VMUL (FD=ACC). Trick: there's a separate VMULA op for + // "MUL into ACC" but no encoder for it yet. Instead use + // VADD to a scratch FD (which doesn't write ACC), then test VMADD with + // ACC at its zeroed default. ACC starts (0,0,0,0), so the result is + // just FS * FT — good enough to exercise the VMADD primary. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.LoadProgram({ + UpperOnly(VMADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 21.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 32.0f); +} + +TEST(Vu0AluUpper, VmsubSubtractsAccProduct) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.LoadProgram({ + UpperOnly(VMSUB_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + // ACC=0, so VMSUB writes -FS*FT to FD. + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -21.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -32.0f); +} + +// -------- Broadcast variants — VADDx/y/z/w -------- +// FD = FS + FT.bc — bc replicated across all lanes of FT before add. + +TEST(Vu0AluUpper, VaddxBroadcastsFtX) +{ + VuTestHarness h(0); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 7.0f, 99.0f, 99.0f, 99.0f); // only x lane of FT consulted + h.LoadProgram({ + UpperOnly(VADDx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu0AluUpper, VaddyBroadcastsFtY) +{ + VuTestHarness h(0); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 7.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VADDy_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu0AluUpper, VaddzBroadcastsFtZ) +{ + VuTestHarness h(0); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 99.0f, 7.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VADDz_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu0AluUpper, VaddwBroadcastsFtW) +{ + VuTestHarness h(0); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 7.0f); + h.LoadProgram({ + UpperOnly(VADDw_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +// -------- Broadcast variants — VSUBx/y/z/w -------- + +TEST(Vu0AluUpper, VsubxSubtractsBroadcastFtX) +{ + VuTestHarness h(0); + h.SetVf(1, 100.0f, 200.0f, 300.0f, 400.0f); + h.SetVf(2, 50.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VSUBx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 50.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 150.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 250.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 350.0f); +} + +TEST(Vu0AluUpper, VsubwSubtractsBroadcastFtW) +{ + VuTestHarness h(0); + h.SetVf(1, 100.0f, 200.0f, 300.0f, 400.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 50.0f); + h.LoadProgram({ + UpperOnly(VSUBw_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 50.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 150.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 250.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 350.0f); +} + +// -------- Broadcast variants — VMULx/y/z/w -------- + +TEST(Vu0AluUpper, VmulxBroadcastsFtX) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMULx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 10.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 20.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 30.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 40.0f); +} + +TEST(Vu0AluUpper, VmulyBroadcastsFtY) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 99.0f, -5.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMULy_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -10.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -15.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -20.0f); +} + +// -------- Mask + broadcast crossover -------- +// Single-lane masks combined with a broadcast op: the destination lane must +// take its FT from the broadcasted source, not from the matching FT lane. + +TEST(Vu0AluUpper, VaddwMaskZWritesOnlyZWithFtW) +{ + VuTestHarness h(0); + h.SetVf(1, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 5.0f); // bc value lives in w + h.SetVf(3, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + UpperOnly(VADDw_U(mask::z, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 35.0f); // 30 + 5 + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -4.0f); +} + +// -------- VU1 spot check: same opcode encoding works on the larger bank -------- + +TEST(Vu0AluUpper, VaddXyzwAcrossAllLanesOnVu1) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.5f, 0.25f, 0.125f, 0.0625f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 2.25f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 3.125f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 4.0625f); +} + +// -------- VuAddSubHack (tri-ace ADDi bit-exactness gamefix) -------- +// When enabled, the SS ADDi path flushes the operand whose exponent is >= 25 +// smaller to a signed zero before the scalar add (ports x86 ADD_SS_TriAceHack). +// At that separation the small operand is already below the IEEE round bit, so +// the VALUE matches a plain add and equals the larger operand — hence these +// assert the larger operand and rely on the harness JIT-vs-interp diff to catch +// an encoding bug: an inverted flush branch would discard the LARGER operand and +// diverge from the interpreter, and a clobbered lane would diverge too. + +namespace { +struct ScopedVuAddSubHack +{ + bool prev = EmuConfig.Gamefixes.VuAddSubHack; + explicit ScopedVuAddSubHack(bool on) { EmuConfig.Gamefixes.VuAddSubHack = on; } + ~ScopedVuAddSubHack() { EmuConfig.Gamefixes.VuAddSubHack = prev; } +}; +constexpr u32 kBigBits = 0x4E800000u; // 1073741824.0f == 2^30 (exp 157) +} // namespace + +TEST(Vu0AddSubHack, AddiSmallFsLargeIFlushesFs) +{ + ScopedVuAddSubHack hack(true); + VuTestHarness h(0); + h.SetVf(vf::vf2, 11.0f, 22.0f, 33.0f, 44.0f); // dest pre-seed; y/z/w must survive + h.SetVf(vf::vf1, 1.0f, 0.0f, 0.0f, 0.0f); // Fs.x is the small operand + h.LoadProgram({ + IBit(VuOp{VLitI(kBigBits), VNOP_U()}), // I = 2^30 + VuOp{0u, VADDi_U(mask::x, vf::vf2, vf::vf1)}, // vf2.x = vf1.x + I + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), kBigBits); // Fs flushed -> result is the large I + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 22.0f); // masked-out lanes preserved + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 44.0f); +} + +TEST(Vu0AddSubHack, AddiLargeFsSmallIFlushesI) +{ + ScopedVuAddSubHack hack(true); + VuTestHarness h(0); + h.SetVf(vf::vf1, 1073741824.0f, 0.0f, 0.0f, 0.0f); // Fs.x large (2^30) + h.LoadProgram({ + IBit(VuOp{VLitI(0x3F800000u /* 1.0f */), VNOP_U()}), // I = 1.0 + VuOp{0u, VADDi_U(mask::x, vf::vf2, vf::vf1)}, // vf2.x = vf1.x + I + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), kBigBits); // I flushed -> result is the large Fs +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp b/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp new file mode 100644 index 0000000000..2282621265 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp @@ -0,0 +1,438 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 control-flow + delay-slot DiffJitVsInterp suite. Covers VB +// (unconditional), VBAL (link), VJR (register), VJALR (register + link), +// and the six VIBxx conditional branches. Every test asserts: +// +// 1. Branch-taken vs not-taken: the target instruction runs (or is skipped), +// the fall-through instruction runs (or doesn't). +// 2. Delay-slot pair *always* runs — that's the architectural contract for +// all VU branches and a recurring source of recompiler bugs. +// 3. Link registers (VBAL / VJALR) hold (delay_slot_pc + 8) / 8 — the pair +// index of the instruction past the delay slot. See _vuBAL VUops.cpp:1595. +// +// Branch-displacement encoding: imm11 is signed *pairs* relative to the +// delay-slot PC, so a branch at pair N with imm11 = K targets pair (N+1+K). +// The interpreter advances REG_TPC to the delay-slot PC *before* computing +// `_branchAddr` (VU0microInterp.cpp:75-76, VUops.cpp:1456-1461). + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } + +// VI loads: each test seeds VI registers via SetVi, but a couple of tests +// also need an in-program VI write to exercise the JIT's reg-bank flush +// across branches. Use VIADDIU vi_dst, vi0, imm to materialise an in-program +// constant load. `imm` is 15-bit unsigned. +inline VuOp LoadViImm(u32 dst, u32 imm) { return LowerOnly(VIADDIU_L(dst, vi::vi0, imm)); } + +} // namespace + +// ========================================================================= +// VB — unconditional forward / backward / self-skip +// ========================================================================= + +TEST(Vu0BranchDelay, VbForwardSkipsOneInstructionDelaySlotRuns) +{ + // VB at pair 0 with imm11=+2 → target = branch_pc + (2+1)*8 = pair 3. Pair 1 + // (the delay slot) runs, pair 2 is skipped, and pair 3 (the target) runs. + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VB_L(+2)), // pair 0: jump to pair 3 + LoadViImm(vi::vi1, 0x101), // pair 1: delay slot — runs + LoadViImm(vi::vi2, 0x202), // pair 2: SKIPPED + LoadViImm(vi::vi3, 0x303), // pair 3: target — runs + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0u); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x303u); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0BranchDelay, VbBackwardLoopWithCounterTerminates) +{ + // Counter loop: vi2 starts at 3, decremented each iteration; vi1 sums. + // Two NOPs sit between the decrement and the conditional branch so the + // VI hazard backup window (VIBackupCycles=2 per VUops.cpp:430) elapses + // and the branch sees the post-decrement value. + // + // Branch displacement: imm11 = K means target = branch_pc + (K+1)*8. + // Pair 6 (branch) → pair 2 (loop top): K = (16-48)/8 - 1 = -5. + VuTestHarness h(0); + h.SetVi(vi::vi3, 1); // step constant for VISUB + h.LoadProgram({ + LoadViImm(vi::vi1, 0), // pair 0: sum = 0 + LoadViImm(vi::vi2, 3), // pair 1: counter = 3 + LowerOnly(VIADDIU_L(vi::vi1, vi::vi1, 10)), // pair 2: L1: sum += 10 + LowerOnly(VISUB_L (vi::vi2, vi::vi2, vi::vi3)), // pair 3: counter -= 1 + LowerOnly(0), // pair 4: NOP (hazard pad) + LowerOnly(0), // pair 5: NOP (hazard pad) + LowerOnly(VIBNE_L(vi::vi2, vi::vi0, -5)), // pair 6: if counter != 0, back to L1 + LowerOnly(0), // pair 7: delay slot (NOP) + EBitNopPair(), // pair 8: terminate + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), 30u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0u); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +// ========================================================================= +// VBAL — unconditional branch + link +// ========================================================================= + +TEST(Vu0BranchDelay, VbalLinksDelaySlotPlusEightDividedBy8) +{ + // VBAL at pair 0 with imm11=+1 → target = pair 2. + // Link reg = (delay_slot_pc + 8) / 8 = (8 + 8) / 8 = pair 2. + // (Per _vuBAL VUops.cpp:1595-1611: VI[_It_].US[0] = (REG_TPC + 8) / 8 + // evaluated *after* TPC has been bumped to the delay-slot PC.) + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VBAL_L(vi::vi5, +1)), // pair 0: link → vi5 + LoadViImm(vi::vi1, 0x111), // pair 1: delay slot — runs + LoadViImm(vi::vi2, 0x222), // pair 2: target — runs + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi5), 2u); + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x222u); + EXPECT_EQ(h.GetViJit(vi::vi5), h.GetViInterp(vi::vi5)); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0BranchDelay, VbalIntoVi0DoesNotWriteLink) +{ + // VBAL with _It_ == 0 must not touch VI[0] — hardwired-zero invariant. + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VBAL_L(vi::vi0, +1)), + LoadViImm(vi::vi1, 0x111), + LoadViImm(vi::vi2, 0x222), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi0), 0u); + EXPECT_EQ(h.GetViInterp(vi::vi0), 0u); +} + +// ========================================================================= +// VJR / VJALR — register-indirect jump +// ========================================================================= + +TEST(Vu0BranchDelay, VjrJumpsToRegisterPairIndex) +{ + // VJR target = VI[is].US[0] * 8 (VUops.cpp:1613-1617). Set vi5 = 3 + // → target byte address 24 → pair 3. + VuTestHarness h(0); + h.SetVi(vi::vi5, 3); + h.LoadProgram({ + LowerOnly(VJR_L(vi::vi5)), // pair 0: jump to pair 3 + LoadViImm(vi::vi1, 0x111), // pair 1: delay slot — runs + LoadViImm(vi::vi2, 0x222), // pair 2: SKIPPED + LoadViImm(vi::vi3, 0x333), // pair 3: target — runs + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0u); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x333u); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0BranchDelay, VjalrJumpsAndLinks) +{ + // VJALR at pair 0 (jump to pair 3 via vi5=3, link to vi6). + // Link reg = (delay_slot_pc + 8) / 8 = 16/8 = 2. + VuTestHarness h(0); + h.SetVi(vi::vi5, 3); + h.LoadProgram({ + LowerOnly(VJALR_L(vi::vi6, vi::vi5)), + LoadViImm(vi::vi1, 0x111), // delay slot + LoadViImm(vi::vi2, 0x222), // skipped + LoadViImm(vi::vi3, 0x333), // target + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi6), 2u); + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0u); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x333u); + EXPECT_EQ(h.GetViJit(vi::vi6), h.GetViInterp(vi::vi6)); +} + +// ========================================================================= +// VIBEQ / VIBNE — equality conditional branches +// ========================================================================= + +TEST(Vu0BranchDelay, VibeqTakenWhenEqual) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 42); + h.SetVi(vi::vi2, 42); + h.LoadProgram({ + LowerOnly(VIBEQ_L(vi::vi1, vi::vi2, +2)), // taken → pair 3 + LoadViImm(vi::vi3, 0x101), // delay slot + LoadViImm(vi::vi4, 0x202), // skipped + LoadViImm(vi::vi5, 0x303), // target + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); + EXPECT_EQ(h.GetViJit(vi::vi4), h.GetViInterp(vi::vi4)); +} + +TEST(Vu0BranchDelay, VibeqNotTakenWhenUnequal) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 1); + h.SetVi(vi::vi2, 2); + h.LoadProgram({ + LowerOnly(VIBEQ_L(vi::vi1, vi::vi2, +2)), // not taken → fall through + LoadViImm(vi::vi3, 0x101), // pair 1: runs + LoadViImm(vi::vi4, 0x202), // pair 2: runs (NOT skipped) + LoadViImm(vi::vi5, 0x303), // pair 3: runs + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); + EXPECT_EQ(h.GetViJit(vi::vi4), h.GetViInterp(vi::vi4)); + EXPECT_EQ(h.GetViJit(vi::vi5), h.GetViInterp(vi::vi5)); +} + +TEST(Vu0BranchDelay, VibneTakenWhenUnequal) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 1); + h.SetVi(vi::vi2, 2); + h.LoadProgram({ + LowerOnly(VIBNE_L(vi::vi1, vi::vi2, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); + EXPECT_EQ(h.GetViJit(vi::vi4), h.GetViInterp(vi::vi4)); +} + +TEST(Vu0BranchDelay, VibneNotTakenWhenEqual) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 7); + h.SetVi(vi::vi2, 7); + h.LoadProgram({ + LowerOnly(VIBNE_L(vi::vi1, vi::vi2, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); +} + +// ========================================================================= +// VIBLTZ / VIBGTZ / VIBLEZ / VIBGEZ — sign / zero conditional branches +// ========================================================================= + +TEST(Vu0BranchDelay, VibltzTakenWhenNegative) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, static_cast(static_cast(-1))); + h.LoadProgram({ + LowerOnly(VIBLTZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x101u); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); +} + +TEST(Vu0BranchDelay, VibltzNotTakenWhenZero) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIBLTZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); +} + +TEST(Vu0BranchDelay, VibgtzTakenWhenPositive) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 5); + h.LoadProgram({ + LowerOnly(VIBGTZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); +} + +TEST(Vu0BranchDelay, VibgtzNotTakenWhenZero) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIBGTZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); +} + +TEST(Vu0BranchDelay, VibgtzNotTakenWhenNegative) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, static_cast(static_cast(-3))); + h.LoadProgram({ + LowerOnly(VIBGTZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); +} + +TEST(Vu0BranchDelay, ViblezTakenWhenZero) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIBLEZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); +} + +TEST(Vu0BranchDelay, VibgezTakenWhenZero) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIBGEZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); + EXPECT_EQ(h.GetViJit(vi::vi5), 0x303u); +} + +TEST(Vu0BranchDelay, VibgezTakenWhenPositive) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 7); + h.LoadProgram({ + LowerOnly(VIBGEZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0u); +} + +TEST(Vu0BranchDelay, VibgezNotTakenWhenNegative) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, static_cast(static_cast(-1))); + h.LoadProgram({ + LowerOnly(VIBGEZ_L(vi::vi1, +2)), + LoadViImm(vi::vi3, 0x101), + LoadViImm(vi::vi4, 0x202), + LoadViImm(vi::vi5, 0x303), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi4), 0x202u); +} + +// ========================================================================= +// Delay-slot semantics — upper-pipe op in delay slot +// ========================================================================= + +TEST(Vu0BranchDelay, UpperFmacInDelaySlotRunsRegardless) +{ + // Delay-slot pair carries an FMAC in the upper word — must execute on + // both branch-taken and branch-not-taken paths. JIT bug shape: dead- + // code-eliminating the delay-slot upper because the lower is a NOP. + VuTestHarness h(0); + h.SetVf(vf::vf1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(vf::vf2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIBEQ_L(vi::vi1, vi::vi0, +2)), // taken → pair 3 + VuOp{0, VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)}, // pair 1: delay + LoadViImm(vi::vi5, 0x505), // pair 2: skipped + LoadViImm(vi::vi6, 0x606), // pair 3: target + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf3, 'x'), 11.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf3, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf3, 'z'), 33.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf3, 'w'), 44.0f); + EXPECT_EQ(h.GetVfBitsJit(vf::vf3, 'x'), h.GetVfBitsInterp(vf::vf3, 'x')); + EXPECT_EQ(h.GetVfBitsJit(vf::vf3, 'w'), h.GetVfBitsInterp(vf::vf3, 'w')); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_clamp_modes_tests.cpp b/tests/ctest/core/recompilers/vu0_clamp_modes_tests.cpp new file mode 100644 index 0000000000..0fb7d4d94c --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_clamp_modes_tests.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 clamping-mode DiffJitVsInterp suite. The PS2 VU has no IEEE +// inf/NaN/denormal: every numerically-special result is clamped to a finite +// IEEE float. PCSX2 makes the clamping aggressiveness configurable via four +// per-VU EmuConfig knobs: +// +// vu0Overflow — basic overflow → ±MAX_FLOAT (0x7F7FFFFF / 0xFF7FFFFF) +// vu0ExtraOverflow — clamp at MUL/MADD intermediate steps (more strict) +// vu0SignOverflow — sign-aware overflow (preserves sign of operand) +// vu0Underflow — denormal flush-to-zero +// +// JIT and interp must agree under every combination — these tests pick a +// handful of canonical edge inputs (large-positive overflow, large-negative +// overflow, denormal-producing underflow) and sweep the knob matrix. The +// assertion is cross-engine agreement (GetVfBitsJit == GetVfBitsInterp) for +// each knob setting, not a hardcoded clamped value: whatever the active +// clamping mode produces, both engines must produce bit-for-bit. Each test +// scopes its config change with a RAII saver so fixture-shared state +// (EmuConfig is a global) doesn't leak across tests. +// + +#include "harness/VuTestHarness.h" + +#include "Config.h" +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// Set the I-bit so the zero lower word is positively suppressed rather than +// decoded as a real lower instruction (a bare {0, upper} decodes lower=0 as +// the LQ family — inert today only because _vuLQ early-returns on _Ft_==0). +// Matches the canonical lower-pipe-suppression form used across the suite. +inline VuOp UpperOnly(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } + +// RAII guard: snapshots the four VU0 clamp knobs in ctor, restores in dtor. +// Tests use it as `ClampGuard cg; cg.Set(true, false, true, false);` — each +// permutation flipped explicitly so the test reads as a matrix row. +struct ClampGuard +{ + bool prev_overflow; + bool prev_extra; + bool prev_sign; + bool prev_underflow; + + ClampGuard() + { + prev_overflow = EmuConfig.Cpu.Recompiler.vu0Overflow; + prev_extra = EmuConfig.Cpu.Recompiler.vu0ExtraOverflow; + prev_sign = EmuConfig.Cpu.Recompiler.vu0SignOverflow; + prev_underflow = EmuConfig.Cpu.Recompiler.vu0Underflow; + } + ~ClampGuard() + { + EmuConfig.Cpu.Recompiler.vu0Overflow = prev_overflow; + EmuConfig.Cpu.Recompiler.vu0ExtraOverflow = prev_extra; + EmuConfig.Cpu.Recompiler.vu0SignOverflow = prev_sign; + EmuConfig.Cpu.Recompiler.vu0Underflow = prev_underflow; + } + + void Set(bool overflow, bool extra, bool sign, bool underflow) + { + EmuConfig.Cpu.Recompiler.vu0Overflow = overflow; + EmuConfig.Cpu.Recompiler.vu0ExtraOverflow = extra; + EmuConfig.Cpu.Recompiler.vu0SignOverflow = sign; + EmuConfig.Cpu.Recompiler.vu0Underflow = underflow; + } +}; + +constexpr u32 kPosBigBits = 0x7E800000u; // ~8.5e+37, multiplying by self overflows +constexpr u32 kNegBigBits = 0xFE800000u; // -8.5e+37 +constexpr u32 kPosTinyBits = 0x00800000u; // smallest positive normal float + +inline VuOp VMulxyzw(u32 fd, u32 fs, u32 ft) { return UpperOnly(VMUL_U(mask::xyzw, fd, fs, ft)); } + +void RunAndExpectAllLanesAgree(VuTestHarness& h, u32 dst_vf) +{ + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'x'), h.GetVfBitsInterp(dst_vf, 'x')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'y'), h.GetVfBitsInterp(dst_vf, 'y')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'z'), h.GetVfBitsInterp(dst_vf, 'z')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'w'), h.GetVfBitsInterp(dst_vf, 'w')); +} + +} // namespace + +// ========================================================================= +// Underflow — VMUL of two tiny normals → 0 (denormal flush) when underflow +// clamping is on. +// ========================================================================= + +TEST(Vu0ClampModes, UnderflowOnTinyProductFlushesToZero) +{ + ClampGuard cg; + cg.Set(true, false, false, /*underflow*/true); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosTinyBits, kPosTinyBits, kPosTinyBits, kPosTinyBits); + h.LoadProgram({ + VMulxyzw(vf::vf1, vf::vf2, vf::vf2), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +TEST(Vu0ClampModes, UnderflowOffTinyProductYieldsDenormal) +{ + ClampGuard cg; + cg.Set(true, false, false, /*underflow*/false); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosTinyBits, kPosTinyBits, kPosTinyBits, kPosTinyBits); + h.LoadProgram({ + VMulxyzw(vf::vf1, vf::vf2, vf::vf2), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +// ========================================================================= +// Sign-overflow knob — vu0SignOverflow drops sign-aware overflow handling. +// Use VSUB to produce a large negative result and verify both engines +// clamp identically with and without the knob. +// ========================================================================= + +TEST(Vu0ClampModes, SignOverflowOnLargeNegativeResultClampsToNegMax) +{ + ClampGuard cg; + cg.Set(true, false, /*sign*/true, false); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.SetVfBits(vf::vf3, kNegBigBits, kNegBigBits, kNegBigBits, kNegBigBits); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf1, vf::vf3, vf::vf2)), // big_neg - big_pos + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +TEST(Vu0ClampModes, SignOverflowOffLargeNegativeResultClampsViaBaseOverflow) +{ + ClampGuard cg; + cg.Set(true, false, /*sign*/false, false); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.SetVfBits(vf::vf3, kNegBigBits, kNegBigBits, kNegBigBits, kNegBigBits); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf1, vf::vf3, vf::vf2)), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +// ========================================================================= +// Extra-overflow knob — clamps inside MUL/MADD intermediate steps. Use a +// VMADD (vf1 = ACC + fs*ft) whose product overflows, so the knob clamps the +// intermediate to MAX_FLOAT before the accumulator add. +// ========================================================================= + +TEST(Vu0ClampModes, ExtraOverflowOnMaddIntermediateClampsBeforeAdd) +{ + ClampGuard cg; + cg.Set(true, /*extra*/true, false, false); + + VuTestHarness h(0); + // Pre-load ACC via VADDA, then VMADD: vf1 = ACC + (vf2 * vf3). The product + // vf2*vf3 overflows (big*big), which the extra-overflow knob clamps to + // MAX_FLOAT at the MADD intermediate, before the ACC add. + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.SetVfBits(vf::vf3, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.LoadProgram({ + UpperOnly(VADDA_U(mask::xyzw, vf::vf2, vf::vf3)), // ACC = vf2 + vf3 + UpperOnly(0u | VNOP_U()), + UpperOnly(VMADD_U(mask::xyzw, vf::vf1, vf::vf2, vf::vf3)), // vf1 = ACC + vf2*vf3 + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +TEST(Vu0ClampModes, ExtraOverflowOffSameProgramAgreesOnFinalValue) +{ + ClampGuard cg; + cg.Set(true, /*extra*/false, false, false); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.SetVfBits(vf::vf3, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.LoadProgram({ + UpperOnly(VADDA_U(mask::xyzw, vf::vf2, vf::vf3)), + UpperOnly(0u | VNOP_U()), + UpperOnly(VMADD_U(mask::xyzw, vf::vf1, vf::vf2, vf::vf3)), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +// ========================================================================= +// Overflow knob — VMUL of two large positive values produces +Inf in IEEE, +// which the interp clamps to MAX_FLOAT during the MAC flag update. The JIT +// only applies its post-multiply overflow clamp when ExtraOverflow is on — +// so this case must include extra-overflow to exercise the clamp. +// ========================================================================= + +TEST(Vu0ClampModes, OverflowOnPositiveVmulClampsToMaxFloat) +{ + ClampGuard cg; + cg.Set(/*overflow*/true, /*extra*/true, false, false); + + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.LoadProgram({ + VMulxyzw(vf::vf1, vf::vf2, vf::vf2), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +TEST(Vu0ClampModes, OverflowOnNegativeVmulClampsToNegMaxFloat) +{ + ClampGuard cg; + cg.Set(/*overflow*/true, /*extra*/true, false, false); + + VuTestHarness h(0); + // Big positive × big negative → big-negative-overflow → -MAX_FLOAT. + h.SetVfBits(vf::vf2, kPosBigBits, kPosBigBits, kPosBigBits, kPosBigBits); + h.SetVfBits(vf::vf3, kNegBigBits, kNegBigBits, kNegBigBits, kNegBigBits); + h.LoadProgram({ + VMulxyzw(vf::vf1, vf::vf2, vf::vf3), + EBitNopPair(), + }); + RunAndExpectAllLanesAgree(h, vf::vf1); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_e_d_t_m_bit_tests.cpp b/tests/ctest/core/recompilers/vu0_e_d_t_m_bit_tests.cpp new file mode 100644 index 0000000000..1ad301d90a --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_e_d_t_m_bit_tests.cpp @@ -0,0 +1,290 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 special-bit termination DiffJitVsInterp suite. Covers the +// four "specials" in the upper word's high bits: +// bits::E (0x40000000) — End-of-program. Runs one delay-slot pair after, +// then _vuFlushAll, clears VPU_STAT bit 0. +// bits::D (0x10000000) — Debug interrupt. Conditional on FBRST.D-stop +// bit (bit 2 for VU0, bit 10 for VU1). When the +// stop bit is set, raises INTC, sets VPU_STAT +// bit 1, terminates *without* a delay slot. +// bits::T (0x08000000) — Trace interrupt. Same as D but conditional on +// FBRST bit 3 (VU0) / bit 11 (VU1), latches +// VPU_STAT bit 2, calls INTC. +// bits::M (0x20000000) — VU0 only. Sets VU0.flags |= VUFLAG_MFLAGSET. +// The VU0 Execute() loop checks this and breaks. +// Doesn't touch VPU_STAT directly. +// +// Cross-engine contract: post-Run() snapshot of VPU_STAT, FBRST, MAC/STATUS/ +// CLIP must agree byte-for-byte. INTC raises are side-effects not diffed +// here; the VPU_STAT bits that latch alongside the IRQ are sufficient to +// catch the dispatch divergences. + +#include "harness/VuTestHarness.h" + +#include "VU.h" +#include "Hw.h" // INTC_STAT +#include "Dmac.h" // INTC_VU0 +#include "Memory.h" // psHu32 / eeHw + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } +inline VuOp LoadViImm(u32 dst, u32 imm) { return LowerOnly(VIADDIU_L(dst, vi::vi0, imm)); } + +} // namespace + +// ========================================================================= +// E-bit — basic termination + delay slot +// ========================================================================= + +TEST(Vu0SpecialBits, EBitClearsRunningBitAndExits) +{ + VuTestHarness h(0); + h.LoadProgram({ + LoadViImm(vi::vi1, 0x111), + EBitNopPair(), + }); + h.Run(); + EXPECT_TRUE(h.HasTerminated()); + // VPU_STAT lives in VU0's bank for both VUs; bit 0 is the running bit + // for VU0. After the E-bit drains, both engines should clear it. + EXPECT_EQ((vuRegs[0].VI[REG_VPU_STAT].UL & 0x1u), 0u); + EXPECT_EQ(h.GetViJit(REG_VPU_STAT), h.GetViInterp(REG_VPU_STAT)); +} + +TEST(Vu0SpecialBits, EBitDelaySlotRunsBeforeTermination) +{ + // Pair 0 carries the E-bit AND a useful op (VIADDIU vi1 = 0x111). Pair 1 + // is the architectural delay slot — it must execute before the program + // flushes. The harness's auto-appended NOP serves as the delay slot here. + VuTestHarness h(0); + h.LoadProgram({ + LoadViImm(vi::vi2, 0x222), // pair 0: ordinary + EBit(LoadViImm(vi::vi1, 0x111)), // pair 1: E-bit pair carrying a load + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x222u); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0SpecialBits, EBitWithFmacFlushesPipelineToArchitecturalReg) +{ + // FMAC immediately followed by E-bit. The flush should commit the + // FMAC's MAC/STATUS to REG_MAC_FLAG / REG_STATUS_FLAG. + VuTestHarness h(0); + h.LoadProgram({ + VuOp{0, VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)}, + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_MAC_FLAG), h.GetViInterp(REG_MAC_FLAG)); + EXPECT_EQ(h.GetViJit(REG_STATUS_FLAG), h.GetViInterp(REG_STATUS_FLAG)); +} + +// ========================================================================= +// D-bit — debug interrupt, conditional on FBRST +// +// The JIT termination path is intentionally unimplemented — microVU +// (microVU_Misc.h) hard-codes `static constexpr bool doDBitHandling = +// false`. The D-bit is a debug aid that shouldn't be enabled in released +// games; titles needing this style of VU pause use the T-bit instead. Only +// the silent case (FBRST D-stop clear) is testable cross-engine, since both +// engines ignore D-bit in that configuration. +// ========================================================================= + +TEST(Vu0SpecialBits, DBitWithFbrstDStopClearIsSilent) +{ + VuTestHarness h(0); + vuRegs[0].VI[REG_FBRST].UL = 0u; // no D-stop + h.LoadProgram({ + DBit(LoadViImm(vi::vi1, 0x111)), + LoadViImm(vi::vi2, 0x222), + EBitNopPair(), + }); + h.Run(); + // D-bit silent — vi2 must run normally. Both engines agree. + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x222u); + EXPECT_EQ((vuRegs[0].VI[REG_VPU_STAT].UL & 0x2u), 0u); +} + +// ========================================================================= +// T-bit — trace interrupt +// ========================================================================= + +TEST(Vu0SpecialBits, TBitWithFbrstTStopSetTriggersTermination) +{ + VuTestHarness h(0); + vuRegs[0].VI[REG_FBRST].UL = 0x8u; // T-stop for VU0 + h.LoadProgram({ + LoadViImm(vi::vi1, 0x111), + TBit(LoadViImm(vi::vi2, 0x222)), + EBitNopPair(), + }); + h.Run(); + // VPU_STAT bit 2 = T-finished. + EXPECT_EQ((vuRegs[0].VI[REG_VPU_STAT].UL & 0x4u), 0x4u); + EXPECT_EQ(h.GetViJit(REG_VPU_STAT), h.GetViInterp(REG_VPU_STAT)); +} + +// T-bit silent path: with FBRST.T-stop clear, mVUDoTBit branches over the +// IRQ-raise + terminator body. The lower op of the T-bit pair must still +// commit — it's emitted by mVUexecuteInstruction before the mVUDoTBit +// handler runs (see microVU_Compile.inl), so the silent path is just a +// no-op skip past the terminator. Both engines agree. +TEST(Vu0SpecialBits, TBitWithFbrstTStopClearIsSilent) +{ + VuTestHarness h(0); + vuRegs[0].VI[REG_FBRST].UL = 0u; // no T-stop + h.LoadProgram({ + TBit(LoadViImm(vi::vi1, 0x111)), + LoadViImm(vi::vi2, 0x222), + EBitNopPair(), + }); + h.Run(); + // T-bit silent — vi1 lower-op must commit, vi2 must run normally. + EXPECT_EQ(h.GetViJit(vi::vi1), 0x111u); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x222u); + EXPECT_EQ((vuRegs[0].VI[REG_VPU_STAT].UL & 0x4u), 0u); // T-finished bit clear + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +// ========================================================================= +// T-bit on a BRANCH instruction — exercises the branch-side T-bit handler +// in normBranch/condBranch/normJump (microVU_Branch.inl), a DIFFERENT code +// path from the mVUDoTBit compile-side handler the tests above hit. +// +// Contract: the branch-side T-bit handler must set VURegs::flags.INTC so the +// dispatcher epilogue (recMicroVU0::Execute) raises hwIntcIrq(INTC_VU0). +// +// Why this test isolates the JIT: flags.INTC is consumed by the dispatcher +// before any register snapshot, and the VPU_STAT bits are masked because the +// running bit gets cleared at termination anyway — so the only reliable +// observable is the hwIntcIrq raise itself (psHu32(INTC_STAT)). The +// interpreter raises INTC inline too, so the JIT is run ONE-SIDED +// (RunInterpOnly establishes the pre-state + block-cache reset, then +// RunJitPreserveBlockCache runs only the JIT) with INTC_STAT cleared in +// between, leaving just the JIT's raise to assert. +// ========================================================================= + +TEST(Vu0SpecialBits, TBitOnUnconditionalBranchRaisesVu0Intc) +{ + VuTestHarness h(0); + vuRegs[0].VI[REG_FBRST].UL = 0x8u; // T-stop for VU0 (FBRST bit 3) + h.LoadProgram({ + TBit(LowerOnly(VB_L(+2))), // pair 0: VB to pair 3, T-bit set on the branch + LoadViImm(vi::vi1, 0x101), // pair 1: delay slot — always runs + LoadViImm(vi::vi2, 0x202), // pair 2: skipped by the branch + LoadViImm(vi::vi3, 0x303), // pair 3: branch target + EBitNopPair(), // pair 4: terminator + }); + + // One-sided interp pass: seeds pre-state, resets the JIT block cache, and + // arms RunJitPreserveBlockCache (no cross-engine diff — the branch+T-bit + // delay-slot ordering legitimately differs between engines, which is + // orthogonal to the INTC-raise isolation being tested). + h.RunInterpOnly(); + + // Isolate the JIT's INTC raise: wipe whatever the interp left, run only + // the JIT from the same pre-state, then assert the dispatcher fired the + // VU0 interrupt. Buggy branch-side handler → flags.INTC never set → + // no raise → INTC_STAT VU0 bit stays clear → red. + psHu32(INTC_STAT) = 0; + h.RunJitPreserveBlockCache(); + EXPECT_NE(psHu32(INTC_STAT) & (1u << INTC_VU0), 0u) + << "branch-side T-bit handler must set VURegs::flags.INTC so " + "recMicroVU0::Execute raises hwIntcIrq(INTC_VU0)"; +} + +// ========================================================================= +// M-bit — VU0-only, sets VUFLAG_MFLAGSET +// ========================================================================= + +TEST(Vu0SpecialBits, MBitSetsMflagsetInFlags) +{ + // M-bit pair 0; harness's E-bit terminator drains pair 1 normally. + // VU0 Execute() clears MFLAGSET on entry then re-asserts on the M-bit + // instruction. The flag latches in VU0.flags (not in VPU_STAT). + VuTestHarness h(0); + // JIT and interp legitimately disagree on REG_TPC after an M-bit break: + // interp advances TPC to the next pair on its way out, the JIT leaves + // TPC at the breaking pair. The architectural assertion (vuRegs[0].flags + // MFLAGSET) is what the test exists to verify. + h.IgnoreViInDiff(REG_TPC); + vuRegs[0].flags = 0u; + h.LoadProgram({ + MBit(LoadViImm(vi::vi1, 0x111)), + LoadViImm(vi::vi2, 0x222), + EBitNopPair(), + }); + h.Run(); + // Both engines must set MFLAGSET. mVUcompile must emit + // `flags |= VUFLAG_MFLAGSET` for M-bit ops in the second pass. Run() + // executes interp LAST, so the global vuRegs[0].flags would mask a JIT + // bug behind interp's correct result — assert the JIT snapshot + // specifically. (VUFLAG_MFLAGSET = 0x2 per VU.h:78.) + EXPECT_EQ(h.JitSnapshot().regs.flags & VUFLAG_MFLAGSET, VUFLAG_MFLAGSET); + EXPECT_EQ(h.InterpSnapshot().regs.flags & VUFLAG_MFLAGSET, VUFLAG_MFLAGSET); +} + +TEST(Vu0SpecialBits, MBitDoesNotClearVpuStatRunningBit) +{ + // M-bit terminates the Execute() loop but does NOT clear VPU_STAT bit 0. + // The harness's E-bit terminator at pair 2 takes care of clearing + // VPU_STAT eventually. Until E-bit drains, VPU_STAT.0 stays set. + VuTestHarness h(0); + // See MBitSetsMflagsetInFlags: TPC bookkeeping diverges across the + // M-bit break boundary; the test's architectural assertion is the + // VPU_STAT bit, not TPC. + h.IgnoreViInDiff(REG_TPC); + vuRegs[0].flags = 0u; + h.LoadProgram({ + MBit(LoadViImm(vi::vi1, 0x111)), + EBitNopPair(), + }); + h.Run(); + // After M-bit the JIT/interp may exit the inner Execute loop, but the + // harness's RunInterp/RunJit calls Execute(kCycleBudget) once. With M + // breaking, Execute returns and the harness sees: (a) VPU_STAT.0 still + // set (M-bit didn't clear it), (b) but the program then also has an + // E-bit at pair 2 — Execute *should* re-enter and run pair 1's delay + // slot + pair 2's E-bit termination, clearing VPU_STAT.0. + // + // In practice both engines run to E-bit anyway. Diff must agree. + EXPECT_EQ(h.GetViJit(REG_VPU_STAT), h.GetViInterp(REG_VPU_STAT)); +} + +// ========================================================================= +// Combinations +// ========================================================================= + +TEST(Vu0SpecialBits, EBitOnFmacRunsFmacBeforeFlush) +{ + // Useful pair: FMAC + E-bit on the same instruction. The FMAC executes, + // then the delay-slot pair runs, then flush. Common terminator pattern + // in real VU programs. Architectural FMAC outputs (vf1, MAC) must match. + VuTestHarness h(0); + h.SetVf(vf::vf2, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(vf::vf3, 10.0f, 20.0f, 30.0f, 40.0f); + h.LoadProgram({ + EBit(VuOp{0, VADD_U(mask::xyzw, vf::vf1, vf::vf2, vf::vf3)}), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf1, 'x'), 11.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(vf::vf1, 'w'), 44.0f); + EXPECT_EQ(h.GetVfBitsJit(vf::vf1, 'x'), h.GetVfBitsInterp(vf::vf1, 'x')); + EXPECT_EQ(h.GetViJit(REG_MAC_FLAG), h.GetViInterp(REG_MAC_FLAG)); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_flag_pipeline_tests.cpp b/tests/ctest/core/recompilers/vu0_flag_pipeline_tests.cpp new file mode 100644 index 0000000000..3d53c9f942 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_flag_pipeline_tests.cpp @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 flag-pipeline DiffJitVsInterp suite. Covers MAC/STATUS/CLIP +// flag generation by FMAC ops + readback by VFM*/VFS*/VFC* lower-pipe ops. +// +// MAC flag layout (per VUflags.cpp:15-47): each lane gets a 4-bit nibble +// {O, U, S, Z} at bit positions {12-15, 8-11, 4-7, 0-3}. Within a nibble: +// shift = 3 (x), 2 (y), 1 (z), 0 (w). +// So bit 3 = Z_x, bit 2 = Z_y, ..., bit 7 = S_x, bit 11 = U_x, bit 15 = O_x. +// +// STATUS flag (per VU_STAT_UPDATE VUflags.cpp:89-97): bit 0x1 = any Z, 0x2 = +// any S, 0x4 = any U, 0x8 = any O. Plus sticky/D/I bits the interpreter +// preserves but FMACs don't directly touch. +// +// CLIP flag (per VCLIP VUops.cpp): 24-bit rolling history. Each VCLIP shifts +// the prior 18 bits left by 6 and ORs in 6 new bits {x>w, y>w, z>w, x<-w, +// y<-w, z<-w}. +// +// Pipeline: FMAC results commit to REG_MAC_FLAG / REG_STATUS_FLAG via a +// 4-stage rolling buffer (VU->fmac[0..3] in the interp; mVU.macFlag in the +// JIT). Reads earlier than the commit boundary read stale values. These +// tests space FMAC -> reader by 4+ pairs to clear the pipeline window. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } +inline VuOp UpperOnly(u32 upper) { return VuOp{0, upper}; } + +// Plain NOP pair without I-bit. Reads as zeros in the lower; the upper is +// the architectural NOP. Used for pipeline padding between an FMAC writer +// and a flag reader so the 4-stage commit boundary is crossed. +inline VuOp BareNopPair() { return VuOp{0, VNOP_U()}; } + +} // namespace + +// ========================================================================= +// MAC flag — Z bits via zero result +// ========================================================================= + +TEST(Vu0FlagPipeline, FmandReadsZeroFlagAfterFmacZeroResult) +{ + // VADD vf1, vf0, vf0 → vf1 = vf0 = (0, 0, 0, 1). Lanes x/y/z are zero + // and lane w is non-zero, so MAC = bits[3:1] set (Z_x, Z_y, Z_z) and + // bit 4 (S_w) clear → 0x000E. + // FMOR vi1, vi0 → vi1 = (MAC & 0xFFFF) | 0 = MAC. + VuTestHarness h(0); + h.SetVi(vi::vi1, 0xFFFFu); // pre-poison so we can see the write + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), + BareNopPair(), + BareNopPair(), + BareNopPair(), + LowerOnly(VFMOR_L(vi::vi1, vi::vi0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FmandMaskedReadsOnlyRequestedBits) +{ + // FMAND vi1, vi2 → vi1 = vi2 & MAC. Mask vi2 = 0x000F (Z bits only). + VuTestHarness h(0); + h.SetVi(vi::vi2, 0x000Fu); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFMAND_L(vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FmeqMatchesExactMacBits) +{ + // FMEQ vi1, vi2: vi1 = (MAC == vi2) ? 1 : 0. Set vi2 = 0x000E (the + // expected MAC for VADD vf1, vf0, vf0). + VuTestHarness h(0); + h.SetVi(vi::vi2, 0x000Eu); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFMEQ_L(vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +// ========================================================================= +// MAC flag — S bits via signed result +// ========================================================================= + +TEST(Vu0FlagPipeline, FmacWithNegativeResultSetsSignBits) +{ + // vf1 = (-1.5, -2.5, -3.5, -4.5); VSUB vf2, vf0, vf1 → vf2 ≈ +vf1 + // (since vf0 = 0,0,0,1, w lane is 1 - (-4.5) = 5.5 → positive). + // So x,y,z negative-input becomes positive output and S bits clear; + // instead, do VADD vf2, vf1, vf0 — keeps negative lanes for x,y,z. + VuTestHarness h(0); + h.SetVf(vf::vf1, -1.5f, -2.5f, -3.5f, -4.5f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf2, vf::vf1, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFMOR_L(vi::vi1, vi::vi0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +// ========================================================================= +// STATUS flag — VU_STAT_UPDATE rollup of MAC bits +// ========================================================================= + +TEST(Vu0FlagPipeline, FsandReadsStatusZBitAfterFmacZeroResult) +{ + // MAC has Z bits set → STATUS bit 0x1 set. FSAND vi1, 0xFFF reads + // (STATUS & 0xFFF) & 0xFFF — i.e. all 12 status bits. + VuTestHarness h(0); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFSAND_L(vi::vi1, 0xFFFu)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FseqMatchesStatusExactly) +{ + // STATUS after VADD vf1, vf0, vf0 should be 0x001 (Z bit set, all sticky + // bits and D/I bits clear at start). FSEQ vi1, 0x001 → 1. + VuTestHarness h(0); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFSEQ_L(vi::vi1, 0x001u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FsorReadsStatusOred) +{ + // FSOR vi1, imm: vi1 = (STATUS & 0xFFF) | imm. + VuTestHarness h(0); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFSOR_L(vi::vi1, 0x800u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +// ========================================================================= +// STATUS flag — FSSET writes sticky bits +// ========================================================================= + +TEST(Vu0FlagPipeline, FssetWritesStickyAndPreservesNonSticky) +{ + // FSSET imm12: VU->statusflag = (imm & 0xFC0) | (statusflag & 0x3F). + // Sets sticky bits (top 6 of low 12), preserves currents (bottom 6). + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFSSET_L(0xFC0u)), // turn on all sticky + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFSAND_L(vi::vi1, 0xFC0u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +// ========================================================================= +// CLIP flag — FCSET / FCGET / FCAND / FCOR / FCEQ +// ========================================================================= + +TEST(Vu0FlagPipeline, FcsetThenFcgetReadsBack12Bits) +{ + // FCSET writes the 24-bit imm to clipflag. FCGET reads (CLIP & 0x0FFF) + // — i.e. only the bottom 12 bits. + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0xABCDEFu)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCGET_L(vi::vi1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FcandSetsVi1OneOnAnyMatchingBit) +{ + // FCAND: VI[1] = ((CLIP & 0xFFFFFF) & imm24) != 0 ? 1 : 0. + // CLIP set to 0x0F0F0F, mask 0x000001 → matches bit 0 → vi1 = 1. + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0x0F0F0Fu)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCAND_L(0x000001u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FcandSetsVi1ZeroOnNoMatchingBit) +{ + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0x0F0F0Fu)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCAND_L(0xF0F0F0u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FcorSetsVi1OneWhenAllOnesAfterOr) +{ + // FCOR: vi1 = ((CLIP | imm24) == 0xFFFFFF) ? 1 : 0. + // CLIP = 0xF0F0F0; imm = 0x0F0F0F → OR = 0xFFFFFF → vi1 = 1. + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0xF0F0F0u)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCOR_L(0x0F0F0Fu)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FceqSetsVi1OneOnExactMatch) +{ + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0x123456u)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCEQ_L(0x123456u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +TEST(Vu0FlagPipeline, FceqSetsVi1ZeroOnMismatch) +{ + VuTestHarness h(0); + h.LoadProgram({ + LowerOnly(VFCSET_L(0x123456u)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VFCEQ_L(0x123455u)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); +} + +// ========================================================================= +// Pipeline timing — back-to-back FMAC then read (no padding) is the +// fragile case that catches commit-stage bugs in the JIT. +// ========================================================================= + +TEST(Vu0FlagPipeline, FsandImmediatelyAfterFmacReadsStatusPipelineCorrectly) +{ + VuTestHarness h(0); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf0, vf::vf0)), + LowerOnly(VFSAND_L(vi::vi1, 0xFFFu)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetViJit(REG_STATUS_FLAG), h.GetViInterp(REG_STATUS_FLAG)); +} + +// ========================================================================= +// Architectural-state invariants — REG_MAC_FLAG / REG_STATUS_FLAG / +// REG_CLIP_FLAG always agree post-program even if intermediate reads +// diverge. This is the bare minimum the JIT should preserve. +// ========================================================================= + +TEST(Vu0FlagPipeline, RegMacFlagMatchesAfterChainOfFmacs) +{ + // Three back-to-back FMACs producing distinct flag patterns. After + // E-bit drains the pipeline, REG_MAC_FLAG must hold the *last* FMAC's + // flag — the prior two are gone. Both engines should land here. + VuTestHarness h(0); + h.SetVf(vf::vf2, 1.0f, 0.0f, -3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf0, vf::vf0)), // all-Z input + BareNopPair(), + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf2, vf::vf0)), // mixed + BareNopPair(), + UpperOnly(VSUB_U(mask::xyzw, vf::vf3, vf::vf0, vf::vf2)), // negate vf2 + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_MAC_FLAG), h.GetViInterp(REG_MAC_FLAG)); + EXPECT_EQ(h.GetViJit(REG_STATUS_FLAG), h.GetViInterp(REG_STATUS_FLAG)); + EXPECT_EQ(h.GetViJit(REG_CLIP_FLAG), h.GetViInterp(REG_CLIP_FLAG)); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_harness_validation_tests.cpp b/tests/ctest/core/recompilers/vu0_harness_validation_tests.cpp new file mode 100644 index 0000000000..a414f89469 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_harness_validation_tests.cpp @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU harness self-validation. These smoke tests confirm that +// VuSnapshot / VuTestHarness / VuEncode round-trip cleanly through +// the interpreter — the foundation the DiffJitVsInterp suites +// will rest on. Each test runs the same program twice through the +// interpreter from identical pre-state and expects zero divergence; +// any diff means the harness itself is wrong. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +TEST(VuHarness, EBitNopProgramTerminatesOnVu0) +{ + VuTestHarness h(0); + h.LoadProgram({EBitNopPair()}); + h.Run(); + // REG_TPC after Execute is in pair-index form (Execute reverses the + // internal <<= 3 with a >>= 3 on exit). Two pairs executed (the user- + // supplied E-bit pair + the harness-appended delay-slot NOP) means + // TPC advances from 0 to 2. + EXPECT_EQ(h.GetViInterp(REG_TPC), 2u); + EXPECT_TRUE(h.HasTerminated()); +} + +TEST(VuHarness, EBitNopProgramTerminatesOnVu1) +{ + VuTestHarness h(1); + h.LoadProgram({EBitNopPair()}); + h.Run(); + EXPECT_EQ(h.GetViInterp(REG_TPC), 2u); + EXPECT_TRUE(h.HasTerminated()); +} + +TEST(VuHarness, VfPreStateSurvivesNopProgramVu0) +{ + VuTestHarness h(0); + h.SetVf(5, 1.5f, -2.25f, 3.75f, 0.5f); + h.LoadProgram({EBitNopPair()}); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfInterp(5, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfInterp(5, 'y'), -2.25f); + EXPECT_FLOAT_EQ(h.GetVfInterp(5, 'z'), 3.75f); + EXPECT_FLOAT_EQ(h.GetVfInterp(5, 'w'), 0.5f); +} + +TEST(VuHarness, ViPreStateSurvivesNopProgramVu0) +{ + VuTestHarness h(0); + h.SetVi(7, 0xABCD); + h.LoadProgram({EBitNopPair()}); + h.Run(); + EXPECT_EQ(h.GetViInterp(7), 0xABCDu); +} + +TEST(VuHarness, MemoryWindowRoundTripsVu0) +{ + VuTestHarness h(0); + h.WriteMemU32(0x100, 0xDEADBEEFu); + h.TrackMemWindow(0x100, 4); + h.LoadProgram({EBitNopPair()}); + h.Run(); + EXPECT_EQ(h.GetMemU32Interp(0x100), 0xDEADBEEFu); +} + +TEST(VuHarness, MemoryWindowRoundTripsVu1) +{ + VuTestHarness h(1); + h.WriteMemU128(0x200, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.TrackMemWindow(0x200, 16); + h.LoadProgram({EBitNopPair()}); + h.Run(); + EXPECT_EQ(h.GetMemU32Interp(0x200 + 0), 0x11111111u); + EXPECT_EQ(h.GetMemU32Interp(0x200 + 4), 0x22222222u); + EXPECT_EQ(h.GetMemU32Interp(0x200 + 8), 0x33333333u); + EXPECT_EQ(h.GetMemU32Interp(0x200 + 12), 0x44444444u); +} + +TEST(VuHarness, VaddProducesCorrectSumOnVu0) +{ + // VF[3] ← VF[1] + VF[2] across all four lanes — primitive test that + // VuEncode's upper-pipe FMAC encoder, the interpreter dispatch, and + // the snapshot/diff machinery all line up. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.5f, 0.25f, 0.125f, 0.0625f); + h.LoadProgram({ + // Pair 0: VADD.xyzw vf3, vf1, vf2 (upper) | I-bit-skip lower + IBit(VuOp{VLitZero(), VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)}), + // Pair 1: E-bit NOP — the architectural delay slot. + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfInterp(3, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfInterp(3, 'y'), 2.25f); + EXPECT_FLOAT_EQ(h.GetVfInterp(3, 'z'), 3.125f); + EXPECT_FLOAT_EQ(h.GetVfInterp(3, 'w'), 4.0625f); +} + +TEST(VuHarness, IaddiuMutatesViOnVu0) +{ + // VI[2] ← VI[1] + 0x100. Validates the lower-pipe IADDIU encoder + // (split immediate field) end-to-end through the interpreter. + VuTestHarness h(0); + h.SetVi(1, 0x10); + h.LoadProgram({ + VuOp{VIADDIU_L(vi::vi2, vi::vi1, 0x100), VNOP_U()}, + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViInterp(2), 0x110u); +} + +TEST(VuHarness, IaddiuMutatesViOnVu1) +{ + VuTestHarness h(1); + h.SetVi(3, 0x80); + h.LoadProgram({ + VuOp{VIADDIU_L(vi::vi4, vi::vi3, 0x4), VNOP_U()}, + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViInterp(4), 0x84u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_integer_alu_tests.cpp b/tests/ctest/core/recompilers/vu0_integer_alu_tests.cpp new file mode 100644 index 0000000000..28a114e5b9 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_integer_alu_tests.cpp @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 lower-pipe integer ALU DiffJitVsInterp suite. Covers the +// VI-bank arithmetic ops: VIADD/VISUB/VIAND/VIOR (LowerOP sub-table) and +// the immediate forms VIADDI (5-bit signed), VIADDIU/VISUBIU (15-bit +// unsigned). All VI registers are 16-bit on the architecture; the snapshot +// diffs the low 16 bits and ignores the hardwired-zero upper half. +// +// Edge cases under test: +// - Writes to VI[0] are silently dropped (hardwired-zero invariant). +// - Add/sub wrap behaviour at the 16-bit boundary. +// - VIADDI sign extension from a 5-bit immediate (range -16..15). +// - VIADDIU/VISUBIU 15-bit immediate split between bits[14:11] and +// bits[10:0] (the encoder hides the split — encoder regression bait). +// - VIAND/VIOR operate on US[0] (zero-extended low 16) per VUops.cpp:1046. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } + +} // namespace + +// -------- VIADD -------- + +TEST(Vu0IntegerAlu, ViaddSimpleSignedSum) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.SetVi(vi::vi2, 250); + h.LoadProgram({ + LowerOnly(VIADD_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 350u); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, ViaddSignedNegativePlusPositive) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, static_cast(static_cast(-200))); + h.SetVi(vi::vi2, 50); + h.LoadProgram({ + LowerOnly(VIADD_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), static_cast(static_cast(-150)) & 0xFFFFu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, ViaddOverflowWrapsLow16) +{ + // 0x7FFF + 0x0010 = 0x800F, bit-15 overflow into negative. + VuTestHarness h(0); + h.SetVi(vi::vi1, 0x7FFFu); + h.SetVi(vi::vi2, 0x0010u); + h.LoadProgram({ + LowerOnly(VIADD_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x800Fu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, ViaddIntoVi0IsNoop) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.SetVi(vi::vi2, 50); + h.LoadProgram({ + LowerOnly(VIADD_L(vi::vi0, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi0), 0u); + EXPECT_EQ(h.GetViInterp(vi::vi0), 0u); +} + +// -------- VISUB -------- + +TEST(Vu0IntegerAlu, VisubBasicDifference) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 1000); + h.SetVi(vi::vi2, 250); + h.LoadProgram({ + LowerOnly(VISUB_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 750u); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, VisubUnderflowWrapsLow16) +{ + // 0 - 1 = -1 -> 0xFFFF (16-bit two's complement). + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.SetVi(vi::vi2, 1); + h.LoadProgram({ + LowerOnly(VISUB_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0xFFFFu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, VisubChainsCorrectly) +{ + // vi3 = vi1 - vi2; vi4 = vi3 - vi1 + VuTestHarness h(0); + h.SetVi(vi::vi1, 500); + h.SetVi(vi::vi2, 200); + h.LoadProgram({ + LowerOnly(VISUB_L(vi::vi3, vi::vi1, vi::vi2)), + LowerOnly(VISUB_L(vi::vi4, vi::vi3, vi::vi1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 300u); + EXPECT_EQ(h.GetViJit(vi::vi4), static_cast(static_cast(-200)) & 0xFFFFu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); + EXPECT_EQ(h.GetViJit(vi::vi4), h.GetViInterp(vi::vi4)); +} + +// -------- VIAND -------- + +TEST(Vu0IntegerAlu, ViandBitwiseAnd) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0xF0F0u); + h.SetVi(vi::vi2, 0x0FFFu); + h.LoadProgram({ + LowerOnly(VIAND_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x00F0u); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, ViandPreservesAllOnes) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0xABCDu); + h.SetVi(vi::vi2, 0xFFFFu); + h.LoadProgram({ + LowerOnly(VIAND_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0xABCDu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +// -------- VIOR -------- + +TEST(Vu0IntegerAlu, ViorBitwiseOr) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0x00F0u); + h.SetVi(vi::vi2, 0x0F00u); + h.LoadProgram({ + LowerOnly(VIOR_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0x0FF0u); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +TEST(Vu0IntegerAlu, ViorWithZeroIsIdentity) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0xBEEFu); + h.SetVi(vi::vi2, 0x0000u); + h.LoadProgram({ + LowerOnly(VIOR_L(vi::vi3, vi::vi1, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi3), 0xBEEFu); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); +} + +// -------- VIADDI (5-bit signed immediate) -------- + +TEST(Vu0IntegerAlu, ViaddiPositiveSmallImm) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.LoadProgram({ + LowerOnly(VIADDI_L(vi::vi2, vi::vi1, 7)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 107u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiNegativeImmSignExtends) +{ + // imm5 = -1 (0x1F) → sign-extends to -1, so vi2 = vi1 + (-1). + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.LoadProgram({ + LowerOnly(VIADDI_L(vi::vi2, vi::vi1, -1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 99u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiImmMinusSixteenBoundary) +{ + // imm5 = -16 = 0x10. _vuIADDI sign-extends bit-4 → 0xFFF0 → -16. + VuTestHarness h(0); + h.SetVi(vi::vi1, 1000); + h.LoadProgram({ + LowerOnly(VIADDI_L(vi::vi2, vi::vi1, -16)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 984u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiImmPlusFifteenBoundary) +{ + // imm5 = 0x0F = 15 (bit-4 clear → no sign extension). + VuTestHarness h(0); + h.SetVi(vi::vi1, 1000); + h.LoadProgram({ + LowerOnly(VIADDI_L(vi::vi2, vi::vi1, 15)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 1015u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiIntoVi0IsNoop) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.LoadProgram({ + LowerOnly(VIADDI_L(vi::vi0, vi::vi1, 5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi0), 0u); + EXPECT_EQ(h.GetViInterp(vi::vi0), 0u); +} + +// -------- VIADDIU (15-bit unsigned immediate) -------- + +TEST(Vu0IntegerAlu, ViaddiuLowImmInLow11Bits) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 100); + h.LoadProgram({ + LowerOnly(VIADDIU_L(vi::vi2, vi::vi1, 0x3FF)), // 1023, low-11 + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 100u + 0x3FFu); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiuHighImmInHi4Bits) +{ + // 0x7800 = 0b0111100000000000 — exercises only the bits[14:11] half. + VuTestHarness h(0); + h.SetVi(vi::vi1, 0); + h.LoadProgram({ + LowerOnly(VIADDIU_L(vi::vi2, vi::vi1, 0x7800)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x7800u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiuMixedHighAndLowImmBits) +{ + // 0x5555 — spans hi4 + low11 of the encoder split. Encoder regression bait. + VuTestHarness h(0); + h.SetVi(vi::vi1, 0x0001u); + h.LoadProgram({ + LowerOnly(VIADDIU_L(vi::vi2, vi::vi1, 0x5555)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x5556u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, ViaddiuMaxImmIsZeroX7FFF) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0x0001u); + h.LoadProgram({ + LowerOnly(VIADDIU_L(vi::vi2, vi::vi1, 0x7FFF)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x8000u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +// -------- VISUBIU -------- + +TEST(Vu0IntegerAlu, VisubiuBasic) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 1000); + h.LoadProgram({ + LowerOnly(VISUBIU_L(vi::vi2, vi::vi1, 250)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 750u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, VisubiuLargeImm) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0x7FFFu); + h.LoadProgram({ + LowerOnly(VISUBIU_L(vi::vi2, vi::vi1, 0x4321)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 0x7FFFu - 0x4321u); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +TEST(Vu0IntegerAlu, VisubiuUnderflowWraps) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 5); + h.LoadProgram({ + LowerOnly(VISUBIU_L(vi::vi2, vi::vi1, 100)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), static_cast(static_cast(5 - 100)) & 0xFFFFu); + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); +} + +// -------- Mixed sequence smoke test -------- + +TEST(Vu0IntegerAlu, MixedSequenceBuildsExpectedValue) +{ + // vi2 = vi1 + 100 ; VIADDIU + // vi3 = vi2 + (-7) ; VIADDI + // vi4 = vi3 & 0x00FF ; VIAND via vi5=0x00FF + // vi6 = vi4 | 0x8000 ; VIOR via vi7=0x8000 + // vi8 = vi6 - vi5 ; VISUB + VuTestHarness h(0); + h.SetVi(vi::vi1, 1000); + h.SetVi(vi::vi5, 0x00FFu); + h.SetVi(vi::vi7, 0x8000u); + h.LoadProgram({ + LowerOnly(VIADDIU_L(vi::vi2, vi::vi1, 100)), + LowerOnly(VIADDI_L (vi::vi3, vi::vi2, -7)), + LowerOnly(VIAND_L (vi::vi4, vi::vi3, vi::vi5)), + LowerOnly(VIOR_L (vi::vi6, vi::vi4, vi::vi7)), + LowerOnly(VISUB_L (vi::vi8, vi::vi6, vi::vi5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(vi::vi2), 1100u); + EXPECT_EQ(h.GetViJit(vi::vi3), 1093u); + EXPECT_EQ(h.GetViJit(vi::vi4), 1093u & 0x00FFu); + EXPECT_EQ(h.GetViJit(vi::vi6), (1093u & 0x00FFu) | 0x8000u); + EXPECT_EQ(h.GetViJit(vi::vi8), (((1093u & 0x00FFu) | 0x8000u) - 0x00FFu) & 0xFFFFu); + + EXPECT_EQ(h.GetViJit(vi::vi2), h.GetViInterp(vi::vi2)); + EXPECT_EQ(h.GetViJit(vi::vi3), h.GetViInterp(vi::vi3)); + EXPECT_EQ(h.GetViJit(vi::vi4), h.GetViInterp(vi::vi4)); + EXPECT_EQ(h.GetViJit(vi::vi6), h.GetViInterp(vi::vi6)); + EXPECT_EQ(h.GetViJit(vi::vi8), h.GetViInterp(vi::vi8)); +} + +// ========================================================================= +// ISW must store the wrapped 16-bit VI value after in-place arithmetic. +// +// VI registers are 16-bit architecturally. After an in-place RMW that wraps +// the 16-bit boundary (e.g. VIADDI of 0xFFFF + 1 = 0x10000), a subsequent +// ISW must store the wrapped 16-bit value (0x0000), not the wider 32-bit +// intermediate. Storing the wider value corrupts the destination word — in +// real microcode that lands in GIFtag PRIM/NREG fields, which the GIF then +// rejects, silently dropping entire packets. +// +// Trigger: +// 1. VIAND vi3, vi2, vi1 ; bring vi1 into the VI register state +// 2. VIADDI vi1, vi1, +1 ; in-place RMW; 0xFFFF + 1 = 0x10000 +// 3. ISW.x vi1, 0(vi0) ; must store 0x00000000, not 0x00010000 +// ========================================================================= + +TEST(Vu0IntegerAlu, IswReadsZeroExtendedAfterInPlaceArithmetic) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 0xFFFFu); + h.SetVi(vi::vi2, 0x00FFu); + h.WriteMemU32(0, 0xDEADBEEFu); // sentinel — both engines should overwrite + h.LoadProgram({ + LowerOnly(VIAND_L (vi::vi3, vi::vi2, vi::vi1)), // bring vi1 into play + LowerOnly(VIADDI_L(vi::vi1, vi::vi1, 1)), // in-place RMW; wraps 16 bits + LowerOnly(VISW_L (mask::x, vi::vi1, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + // Architectural vi1 = (0xFFFF + 1) & 0xFFFF = 0x0000 → ISW writes 0. + EXPECT_EQ(h.GetViJit(vi::vi1), 0u); + EXPECT_EQ(h.GetViJit(vi::vi1), h.GetViInterp(vi::vi1)); + EXPECT_EQ(h.GetMemU32Jit(0), 0u); + EXPECT_EQ(h.GetMemU32Jit(0), h.GetMemU32Interp(0)); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp b/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp new file mode 100644 index 0000000000..f5cb0bd5f1 --- /dev/null +++ b/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU0 Q-pipeline DiffJitVsInterp suite. Covers VDIV / VSQRT / +// VRSQRT (the three Q-producers) plus VWAITQ at varying cycle distances, +// plus the broadcast-from-Q upper ops VADDq / VMULq / VMSUBq. End-state +// is captured at E-bit, so the JIT's mVUendProgram() spill of pending_q +// → q is what we diff against the interpreter's per-instruction `VU.q` +// updates. STATUS-flag bits 0x10 (invalid op) and 0x20 (div-by-zero) are +// exercised — both the architectural REG_STATUS_FLAG and the magic q +// payload values 0x7F7FFFFF / 0xFF7FFFFF the VU emits on /0 with sign. +// +// Known JIT divergence: on short standalone programs the divFlag → STATUS +// propagation in the FDIV unit's pipeline isn't drained at end-of-program, +// so STATUS bits 0x10/0x20 land in interp but not in JIT. Tests that probe +// this opt out of REG_STATUS_FLAG via IgnoreViInDiff and route their +// architectural status asserts through the interp side. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } +inline VuOp UpperOnly(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } +inline VuOp Pair(u32 lower, u32 upper) { return VuOp{lower, upper}; } + +// VWAITQ + upper-NOP — the canonical "drain Q-pipe" pair. +inline VuOp WaitQPair() { return VuOp{VWAITQ_L(), VNOP_U()}; } + +constexpr u32 kQPlusInfMagic = 0x7F7FFFFFu; // VU's "infinity" sentinel +constexpr u32 kQMinusInfMagic = 0xFF7FFFFFu; + +} // namespace + +// -------- VDIV — basic positive -------- + +TEST(Vu0Qpipe, VdivBasicPositiveProducesQuotient) +{ + VuTestHarness h(0); + h.SetVf(1, 6.0f, 99.0f, 99.0f, 99.0f); // fs.x = numerator + h.SetVf(2, 99.0f, 99.0f, 2.0f, 99.0f); // ft.z = denominator + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, /*fsf=*/0, vf::vf2, /*ftf=*/2)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + const u32 q_jit = h.GetViJit(REG_Q); + const u32 q_int = h.GetViInterp(REG_Q); + EXPECT_EQ(q_jit, q_int); + EXPECT_FLOAT_EQ(std::bit_cast(q_jit), 3.0f); +} + +TEST(Vu0Qpipe, VdivNegativeOverPositiveSignsResult) +{ + VuTestHarness h(0); + h.SetVf(1, -10.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 4.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), -2.5f); +} + +// -------- VDIV — div-by-zero status flags + magic Q values -------- + +TEST(Vu0Qpipe, VdivByZeroSetsBit20AndMaxFloatQ) +{ + // Per _vuDIV: ft==0 && fs!=0 → statusflag |= 0x20, q = 0x7F7FFFFF + // (or 0xFF7FFFFF if signs differ). + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); // see file header — divFlag→STATUS lost on short programs + h.SetVf(1, 1.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_Q), kQPlusInfMagic); + // Status bit 0x20 = D-flag (div-by-zero). Asserted via interp (the JIT + // loses the divFlag on short programs — see file header). + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x20, 0u) << "div-by-zero D bit missing in interp status"; +} + +TEST(Vu0Qpipe, VdivByZeroNegativeNumeratorSignsMagicQ) +{ + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, -1.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_Q), kQMinusInfMagic); +} + +TEST(Vu0Qpipe, VdivZeroOverZeroSetsBit10InvalidOp) +{ + // Per _vuDIV: ft==0 && fs==0 → statusflag |= 0x10 (invalid-op I-flag), + // q gets the +max-float magic since signs match (both +0). + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 0.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_Q), kQPlusInfMagic); + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10, 0u); +} + +// -------- VSQRT -------- + +TEST(Vu0Qpipe, VsqrtPositive) +{ + VuTestHarness h(0); + h.SetVf(1, 0, 16.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VSQRT_L(vf::vf1, /*ftf=*/1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 4.0f); +} + +TEST(Vu0Qpipe, VsqrtNegativeSetsInvalidAndUsesAbs) +{ + // Per _vuSQRT: ft<0 → statusflag |= 0x10 (I); q = sqrt(|ft|). + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 0, 0, -25.0f, 0); + h.LoadProgram({ + LowerOnly(VSQRT_L(vf::vf1, 2)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 5.0f); + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10, 0u); +} + +// -------- VRSQRT -------- + +TEST(Vu0Qpipe, VrsqrtBasic) +{ + // q = fs / sqrt(|ft|). 8 / sqrt(4) = 4. + VuTestHarness h(0); + h.SetVf(1, 8.0f, 0, 0, 0); + h.SetVf(2, 0, 4.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VRSQRT_L(vf::vf1, 0, vf::vf2, 1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 4.0f); +} + +TEST(Vu0Qpipe, VrsqrtNegativeFtSetsInvalidAndUsesAbs) +{ + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 8.0f, 0, 0, 0); + h.SetVf(2, 0, -4.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VRSQRT_L(vf::vf1, 0, vf::vf2, 1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 4.0f); + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10, 0u); +} + +TEST(Vu0Qpipe, VrsqrtByZeroNonZeroNumeratorMagicQ) +{ + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 1.0f, 0, 0, 0); + h.SetVf(2, 0, 0.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VRSQRT_L(vf::vf1, 0, vf::vf2, 1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_Q), kQPlusInfMagic); +} + +TEST(Vu0Qpipe, VrsqrtByZeroZeroNumeratorSpecialQ) +{ + // _vuRSQRT (interp): ft==0, fs==0, signs match → q = 0 (positive), bits I+D set. + // The mVU recompiler emits sign(Fs)|maxvals for the ENTIRE divide-by-zero + // path, including 0/0, while only the interpreter special-cases 0/0 → 0. This + // is a known JIT-vs-interp divergence where matching the recompiler is the + // correct behavior, so REG_Q (and STATUS) are opted out of the JIT-vs-interp + // diff and the architectural intent is asserted via the interp. + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.IgnoreViInDiff(REG_Q); + h.SetVf(1, 0.0f, 0, 0, 0); + h.SetVf(2, 0, 0.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VRSQRT_L(vf::vf1, 0, vf::vf2, 1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViInterp(REG_Q), 0u); + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x30, 0u); +} + +// -------- VDIV → VWAITQ → VADDq broadcast (Q observable after wait) -------- + +TEST(Vu0Qpipe, AddqPicksUpVdivResultAfterWaitq) +{ + // 12.0 / 4.0 = 3.0 → ADDq broadcasts 3.0 → fd.{xyzw} = fs.{xyzw} + 3. + VuTestHarness h(0); + h.SetVf(1, 12.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 4.0f); + h.SetVf(3, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + UpperOnly(VADDq_U(mask::xyzw, vf::vf4, vf::vf3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'x'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'y'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'z'), 6.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'w'), 7.0f); +} + +TEST(Vu0Qpipe, MulqBroadcastsQ) +{ + // 1.0 / 0.5 = 2.0; broadcast multiply. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.5f); + h.SetVf(3, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + WaitQPair(), + UpperOnly(VMULq_U(mask::xyzw, vf::vf4, vf::vf3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(4, 'w'), 8.0f); +} + +// -------- VWAITQ at varying cycle distances -------- +// +// The Q-pipe latency model is fixed in microVU (7 cycles for VDIV, 7 for +// VRSQRT, 4 for VSQRT — see the per-op schedules). The interpreter doesn't +// model latency, so the *value* visible at E-bit is identical regardless +// of how many filler instructions sit between VDIV and VWAITQ. The diff +// catches any off-by-one in the JIT's drain bookkeeping. + +TEST(Vu0Qpipe, VdivThenLongPipelineThenWaitq) +{ + VuTestHarness h(0); + h.SetVf(1, 21.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 7.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + // Fill 8 cycles of NOPs to safely span Q-pipe latency. + NopPair(), NopPair(), NopPair(), NopPair(), + NopPair(), NopPair(), NopPair(), NopPair(), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 3.0f); +} + +TEST(Vu0Qpipe, BackToBackVdivOverwritesPendingQ) +{ + // Two VDIVs without intervening VWAITQ: the second's pending_q overwrites + // the first before either lands in Q. Final Q at E-bit must be the second + // quotient — both JIT and interp must agree. + VuTestHarness h(0); + h.SetVf(1, 10.0f, 0, 0, 0); + h.SetVf(2, 0, 2.0f, 0, 0); // first = 5 + h.SetVf(3, 100.0f, 0, 0, 0); + h.SetVf(4, 0, 0, 0, 4.0f); // second = 25 + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 1)), + LowerOnly(VDIV_L(vf::vf3, 0, vf::vf4, 3)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 25.0f); +} + +// -------- VU1 spot check -------- + +TEST(Vu0Qpipe, VdivWorksOnVu1) +{ + VuTestHarness h(1); + h.SetVf(1, 9.0f, 0, 0, 0); + h.SetVf(2, 0, 3.0f, 0, 0); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 1)), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(std::bit_cast(h.GetViJit(REG_Q)), 3.0f); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp b/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp new file mode 100644 index 0000000000..52943ec59d --- /dev/null +++ b/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp @@ -0,0 +1,475 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU1 lower-pipe non-Q ALU DiffJitVsInterp suite — parity counterpart to +// vu0_alu_lower_tests.cpp. Same opcode encodings driving mVUexecuteVU1 +// instead of VU0. Covers VMOVE, VMR32, VMTIR, VMFIR; the eight VFTOIx / +// VITOFx fixed-point conversions; and VLQI / VSQI / VLQD / VSQD +// pre/post-increment loads/stores. Q-pipe ops live in their dedicated +// VU0 suite — adding a VU1 Q-pipe parity suite is a separate task. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// Pair the supplied lower-word op with an upper-word NOP (FD_11 sub 0x0B). +// No I-bit needed since the lower already carries a real opcode. +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } + +} // namespace + +// -------- VMOVE (lower-pipe register copy) -------- + +TEST(Vu1AluLower, VmoveXyzwCopiesAllLanes) +{ + VuTestHarness h(1); + h.SetVf(1, 1.5f, 2.5f, 3.5f, 4.5f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 3.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 4.5f); +} + +TEST(Vu1AluLower, VmoveMaskedLeavesUnmaskedLanes) +{ + VuTestHarness h(1); + h.SetVf(1, 11.0f, 22.0f, 33.0f, 44.0f); + h.SetVf(2, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::y | mask::w, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), -3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 44.0f); +} + +TEST(Vu1AluLower, VmoveSelfIsIdentity) +{ + VuTestHarness h(1); + h.SetVf(5, 7.0f, -8.0f, 9.0f, -10.0f); + h.LoadProgram({ + LowerOnly(VMOVE_L(mask::xyzw, vf::vf5, vf::vf5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'y'), -8.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'z'), 9.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(5, 'w'), -10.0f); +} + +// -------- VMR32 (rotate xyzw → yzwx) -------- + +TEST(Vu1AluLower, VmR32RotatesByOneLane) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + // Per _vuMR32: ft.x = fs.y, ft.y = fs.z, ft.z = fs.w, ft.w = fs.x. + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 1.0f); +} + +TEST(Vu1AluLower, VmR32MaskedZWritesOnlyZ) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 100.0f, 200.0f, 300.0f, 400.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::z, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 100.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 200.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); // pre-rotation src lane = w + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 400.0f); +} + +TEST(Vu1AluLower, VmR32SelfAliasingRotatesIntoSelf) +{ + // fs == ft: the rotate must fully complete from the saved input — + // regalloc has to either spill or use a temp, otherwise lane writes + // stomp later lane reads. Classic aliasing-bug shape. + VuTestHarness h(1); + h.SetVf(3, 10.0f, 20.0f, 30.0f, 40.0f); + h.LoadProgram({ + LowerOnly(VMR32_L(mask::xyzw, vf::vf3, vf::vf3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 20.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 30.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 40.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 10.0f); +} + +// -------- VMTIR / VMFIR (VF ↔ VI scalar transfer) -------- + +TEST(Vu1AluLower, VmtirCopiesLaneToVi) +{ + // Per _vuMTIR: VI[it].US[0] = u16(fs.f[fsf]) — the *low 16 bits of the + // IEEE-754 representation* of the chosen lane. NOT a float→int cast. + VuTestHarness h(1); + h.SetVfBits(1, 0xCAFEBABE, 0xDEADBEEF, 0x12345678, 0xAAAA5555); + // Pick fsf=2 (z lane) → VI[1] should get low 16 bits of 0x12345678 = 0x5678. + h.LoadProgram({ + LowerOnly(VMTIR_L(vi::vi1, vf::vf1, /*fsf=*/2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(1), 0x5678u); +} + +TEST(Vu1AluLower, VmfirSignExtendsViIntoAllLanes) +{ + // Per _vuMFIR: ft.SL[lane] = (s32)VI[is].SS[0] for each masked lane. + // VI is 16-bit signed; result populates the s32 slot of each VF lane. + VuTestHarness h(1); + h.SetVi(2, 0xFFFFu); // -1 as s16 + h.SetVfBits(3, 0u, 0u, 0u, 0u); + h.LoadProgram({ + LowerOnly(VMFIR_L(mask::xyzw, vf::vf3, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + // VI[2] = -1 (s16) → (s32)-1 = 0xFFFFFFFF stored as 32-bit pattern in VF. + EXPECT_EQ(h.GetVfBitsJit(3, 'x'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'y'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'z'), 0xFFFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(3, 'w'), 0xFFFFFFFFu); +} + +TEST(Vu1AluLower, VmfirMaskedOnlyTouchesSelectedLanes) +{ + VuTestHarness h(1); + h.SetVi(4, 0x1234u); + h.SetVfBits(5, 0xAAAA1111u, 0xBBBB2222u, 0xCCCC3333u, 0xDDDD4444u); + h.LoadProgram({ + LowerOnly(VMFIR_L(mask::y, vf::vf5, vi::vi4)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(5, 'x'), 0xAAAA1111u); + EXPECT_EQ(h.GetVfBitsJit(5, 'y'), 0x00001234u); + EXPECT_EQ(h.GetVfBitsJit(5, 'z'), 0xCCCC3333u); + EXPECT_EQ(h.GetVfBitsJit(5, 'w'), 0xDDDD4444u); +} + +// -------- VITOFx / VFTOIx (fixed-point conversions; UPPER pipe) -------- + +namespace { +inline VuOp UpperOnlyPair(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } +} // namespace + +TEST(Vu1AluLower, VitofZeroMatchesIntegerCast) +{ + // VITOF0: just (float)(s32)bits — no scaling. + VuTestHarness h(1); + h.SetVfBits(1, 0u, 1u, static_cast(-1), 0x80000000u); + h.LoadProgram({ + UpperOnlyPair(VITOF0_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + // Diff already polices the architectural answer; sanity-check the JIT side. + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), h.GetVfBitsInterp(2, 'x')); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), h.GetVfBitsInterp(2, 'y')); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), h.GetVfBitsInterp(2, 'z')); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), h.GetVfBitsInterp(2, 'w')); +} + +TEST(Vu1AluLower, VitofFourScalesByExp4) +{ + // VITOF4: (float)bits / 2^4 — used for 28.4 fixed-point reads. + VuTestHarness h(1); + h.SetVfBits(1, 16u, 32u, 64u, 256u); + h.LoadProgram({ + UpperOnlyPair(VITOF4_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 16.0f); +} + +TEST(Vu1AluLower, Vitof12ScalesByExp12) +{ + VuTestHarness h(1); + h.SetVfBits(1, 4096u, 8192u, 16384u, 32768u); + h.LoadProgram({ + UpperOnlyPair(VITOF12_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 8.0f); +} + +TEST(Vu1AluLower, Vitof15ScalesByExp15) +{ + VuTestHarness h(1); + h.SetVfBits(1, 32768u, 65536u, 131072u, 262144u); + h.LoadProgram({ + UpperOnlyPair(VITOF15_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 8.0f); +} + +TEST(Vu1AluLower, Vftoi0RoundsTowardZero) +{ + // VFTOI0: (s32)f — truncate toward zero. + VuTestHarness h(1); + h.SetVf(1, 1.7f, -1.7f, 2.999f, -0.5f); + h.LoadProgram({ + UpperOnlyPair(VFTOI0_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), -1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 2); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), 0); +} + +TEST(Vu1AluLower, Vftoi4ScalesUp16XThenTruncates) +{ + // VFTOI4: (s32)(f * 16). Used for 28.4 fixed-point writes. + VuTestHarness h(1); + h.SetVf(1, 1.0f, 0.5f, 0.0625f, -2.5f); + h.LoadProgram({ + UpperOnlyPair(VFTOI4_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 16); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 8); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 1); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -40); +} + +TEST(Vu1AluLower, Vftoi12ScalesUp4096X) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 0.5f, 0.0f, -1.0f); + h.LoadProgram({ + UpperOnlyPair(VFTOI12_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 4096); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 2048); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 0); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -4096); +} + +TEST(Vu1AluLower, Vftoi15ScalesUp32768X) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 0.5f, 0.25f, -1.0f); + h.LoadProgram({ + UpperOnlyPair(VFTOI15_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'x')), 32768); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'y')), 16384); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'z')), 8192); + EXPECT_EQ(static_cast(h.GetVfBitsJit(2, 'w')), -32768); +} + +TEST(Vu1AluLower, Vftoi0SaturatesAtIntMaxOnHugeFloat) +{ + // Per floatToInt<>: any float whose exponent is ≥ 0x4F (≈ 2^31) saturates + // to ±INT_MAX/INT_MIN. A bug here would be the runaway int-cast undefined + // behaviour seen on the EE side (MTSA float-mul cast). + VuTestHarness h(1); + h.SetVfBits(1, 0x7F7FFFFFu, 0xFF7FFFFFu, 0u, 0u); // ±FLT_MAX, 0 + h.LoadProgram({ + UpperOnlyPair(VFTOI0_U(mask::x | mask::y, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0x7FFFFFFFu); // INT_MAX + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0x80000000u); // INT_MIN +} + +// -------- VABS (UPPER, FD_01 sub 0x07) — included here since it's a unary +// ALU op alongside VITOF/VFTOI. Bit-strip-the-sign-bit semantics. -- + +TEST(Vu1AluLower, VabsClearsSignBitPerLane) +{ + VuTestHarness h(1); + h.SetVf(1, -1.0f, 2.0f, -3.5f, 0.0f); + h.LoadProgram({ + UpperOnlyPair(VABS_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 3.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 0.0f); +} + +TEST(Vu1AluLower, VabsOnNegativeZeroProducesPositiveZero) +{ + VuTestHarness h(1); + h.SetVfBits(1, 0x80000000u, 0u, 0x80000000u, 0u); + h.LoadProgram({ + UpperOnlyPair(VABS_U(mask::xyzw, vf::vf2, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), 0u); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), 0u); +} + +// -------- VLQI / VSQI / VLQD / VSQD (auto-increment loads/stores) -------- + +TEST(Vu1AluLower, VlqiLoadsThenIncrementsViPointer) +{ + VuTestHarness h(1); + h.WriteMemU128(0x40, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.SetVi(3, 0x40 / 16); // VI in units of 16-byte quads + h.SetVfBits(2, 0u, 0u, 0u, 0u); + h.TrackMemWindow(0x40, 16); + h.LoadProgram({ + LowerOnly(VLQI_L(mask::xyzw, vf::vf2, vi::vi3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(2, 'x'), 0x11111111u); + EXPECT_EQ(h.GetVfBitsJit(2, 'y'), 0x22222222u); + EXPECT_EQ(h.GetVfBitsJit(2, 'z'), 0x33333333u); + EXPECT_EQ(h.GetVfBitsJit(2, 'w'), 0x44444444u); + EXPECT_EQ(h.GetViJit(3), (0x40 / 16) + 1); +} + +TEST(Vu1AluLower, VsqiStoresThenIncrementsViPointer) +{ + VuTestHarness h(1); + h.SetVfBits(5, 0xDEADBEEFu, 0xCAFEBABEu, 0xFEEDFACEu, 0x12345678u); + h.SetVi(4, 0x80 / 16); + h.TrackMemWindow(0x80, 16); + h.LoadProgram({ + LowerOnly(VSQI_L(mask::xyzw, vf::vf5, vi::vi4)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0x80), 0xDEADBEEFu); + EXPECT_EQ(h.GetMemU32Jit(0x84), 0xCAFEBABEu); + EXPECT_EQ(h.GetMemU32Jit(0x88), 0xFEEDFACEu); + EXPECT_EQ(h.GetMemU32Jit(0x8C), 0x12345678u); + EXPECT_EQ(h.GetViJit(4), (0x80 / 16) + 1); +} + +TEST(Vu1AluLower, VlqdPredecrementsThenLoads) +{ + VuTestHarness h(1); + h.WriteMemU128(0x60, 0xAAAA0000u, 0xBBBB0000u, 0xCCCC0000u, 0xDDDD0000u); + h.SetVi(2, (0x60 / 16) + 1); // pre-decrement → load from 0x60 + h.SetVfBits(7, 0u, 0u, 0u, 0u); + h.TrackMemWindow(0x60, 16); + h.LoadProgram({ + LowerOnly(VLQD_L(mask::xyzw, vf::vf7, vi::vi2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(7, 'x'), 0xAAAA0000u); + EXPECT_EQ(h.GetViJit(2), (0x60 / 16)); +} + +TEST(Vu1AluLower, VsqdPredecrementsThenStores) +{ + VuTestHarness h(1); + h.SetVfBits(8, 0xEEEE1111u, 0xEEEE2222u, 0xEEEE3333u, 0xEEEE4444u); + h.SetVi(6, (0xA0 / 16) + 1); // pre-decrement → store at 0xA0 + h.TrackMemWindow(0xA0, 16); + h.LoadProgram({ + LowerOnly(VSQD_L(mask::xyzw, vf::vf8, vi::vi6)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(0xA0), 0xEEEE1111u); + EXPECT_EQ(h.GetMemU32Jit(0xAC), 0xEEEE4444u); + EXPECT_EQ(h.GetViJit(6), 0xA0 / 16); +} + +// -------- VLQ base+imm single-lane (.w) mask coverage on VU1 -------- +// +// The microVU LQ codegen is shared between VU0 and VU1. The full partial-mask +// matrix lives in the VU0 suite; this section covers consecutive `LQ.w` loads +// from mem[0..1] into vf14/vf15 on VU1. Encoded as the plain VLQ_L (base+imm +// form), the common microcode shape, not the auto-incrementing variants. + +TEST(Vu1AluLower, VlqWMaskBaseImmSingleLane) +{ + VuTestHarness h(1); + // mem[0].w = 1.0f, mem[1].w = 20000.0f. + h.WriteMemU128(0, 0x44000000u, 0x44000000u, 0u, 0x3F800000u); + h.WriteMemU128(16, 0x45600000u, 0x45600000u, 0u, 0x469C4000u); + h.SetVfBits(14, 0u, 0u, 0u, 0u); + h.SetVfBits(15, 0u, 0u, 0u, 0u); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::w, vf::vf14, vi::vi0, 0)), + LowerOnly(VLQ_L(mask::w, vf::vf15, vi::vi0, 1)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(14, 'w'), 0x3F800000u) << "vf14.w should be mem[0].w (1.0f)"; + EXPECT_EQ(h.GetVfBitsJit(15, 'w'), 0x469C4000u) << "vf15.w should be mem[1].w (20000.0f)"; +} + +TEST(Vu1AluLower, VlqYLoadsOnlyYLane) +{ + VuTestHarness h(1); + h.WriteMemU128(0, 0xAAAAu, 0xBADCAFEDu, 0xCCCCu, 0xDDDDu); + h.SetVfBits(20, 0x11u, 0x22u, 0x33u, 0x44u); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::y, vf::vf20, vi::vi0, 0)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(20, 'x'), 0x11u); + EXPECT_EQ(h.GetVfBitsJit(20, 'y'), 0xBADCAFEDu); + EXPECT_EQ(h.GetVfBitsJit(20, 'z'), 0x33u); + EXPECT_EQ(h.GetVfBitsJit(20, 'w'), 0x44u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu1_alu_upper_tests.cpp b/tests/ctest/core/recompilers/vu1_alu_upper_tests.cpp new file mode 100644 index 0000000000..fe5e0e14e3 --- /dev/null +++ b/tests/ctest/core/recompilers/vu1_alu_upper_tests.cpp @@ -0,0 +1,442 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU1 upper-pipe FMAC DiffJitVsInterp suite — parity counterpart to +// vu0_alu_upper_tests.cpp. Same opcode encodings, same microprograms; +// drives the VU1 instance instead of VU0. Exercises the VU1 codegen path +// specifically: VU1 register allocator state, the VU1 dispatcher, and any +// VU1-conditional code in shared microVU helpers. +// +// Architectural caveat: on short standalone programs the JIT does not spill +// the FMAC pipeline's MAC/STATUS into REG_*_FLAG the way a real running +// program would, so those two flag registers can read 0 where the +// interpreter has the architectural flag bits. Tests that only measure VF +// results opt out of the auto-diff on those registers via IgnoreViInDiff. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// Pair 0 = upper op + I-bit-skipped lower (the lower word becomes a 32-bit +// float immediate into VI[REG_I] which is ignored). Pair 1 = E-bit terminator +// (the harness appends an architectural-delay-slot NOP pair automatically). +inline VuOp UpperOnly(u32 upper) +{ + return IBit(VuOp{VLitZero(), upper}); +} + +} // namespace + +// -------- VADD primary (xyzw operand) -------- + +TEST(Vu1AluUpper, VaddXyzwAcrossAllLanes) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.5f, 0.25f, 0.125f, 0.0625f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 2.25f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 3.125f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 4.0625f); +} + +TEST(Vu1AluUpper, VaddXOnlyLeavesOtherLanesPreSeed) +{ + // Mask=x; FD lanes y/z/w must retain their pre-state, only x updates. + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(3, 99.0f, -77.0f, 55.5f, -33.25f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::x, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 11.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -77.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 55.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -33.25f); +} + +TEST(Vu1AluUpper, VaddYzMaskLeavesXAndW) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(3, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::y | mask::z, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 22.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 33.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -4.0f); +} + +TEST(Vu1AluUpper, VaddSelfAliasingFdEqualsFs) +{ + // FD == FS: result accumulates into the same register without trampling + // the source mid-instruction. The regalloc has to flush-or-share + // here; aliasing is a real bug class on the EE side. + VuTestHarness h(1); + h.SetVf(1, 1.5f, 2.5f, 3.5f, 4.5f); + h.SetVf(2, 0.5f, 0.5f, 0.5f, 0.5f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf1, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'y'), 3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(1, 'w'), 5.0f); +} + +TEST(Vu1AluUpper, VaddNegativeOperands) +{ + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); // see file header — short-program flag spill + h.IgnoreViInDiff(REG_MAC_FLAG); + h.SetVf(1, -1.0f, -2.0f, -3.0f, -4.0f); + h.SetVf(2, 1.0f, 1.5f, 2.0f, 2.5f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -0.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -1.5f); +} + +// -------- VSUB primary -------- + +TEST(Vu1AluUpper, VsubXyzw) +{ + VuTestHarness h(1); + h.SetVf(1, 5.0f, 10.0f, 15.0f, 20.0f); + h.SetVf(2, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 8.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 16.0f); +} + +TEST(Vu1AluUpper, VsubFdEqualsFt) +{ + // FD==FT: the JIT mustn't read a stale FT after writing FD lane-by-lane. + VuTestHarness h(1); + h.SetVf(1, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(2, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VSUB_U(mask::xyzw, vf::vf2, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'x'), 9.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'y'), 18.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'z'), 27.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(2, 'w'), 36.0f); +} + +// -------- VMUL primary -------- + +TEST(Vu1AluUpper, VmulXyzw) +{ + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.IgnoreViInDiff(REG_MAC_FLAG); + h.SetVf(1, 2.0f, 3.0f, 4.0f, 5.0f); + h.SetVf(2, 0.5f, 0.5f, 0.25f, -1.0f); + h.LoadProgram({ + UpperOnly(VMUL_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 1.5f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -5.0f); +} + +TEST(Vu1AluUpper, VmulZeroProducesZero) +{ + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.IgnoreViInDiff(REG_MAC_FLAG); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.0f, 0.0f, 0.0f, 0.0f); + h.SetVf(3, 99.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMUL_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 0.0f); +} + +// -------- VMAX / VMINI -------- + +TEST(Vu1AluUpper, VmaxPicksLargerLaneByLane) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 5.0f, -3.0f, 0.0f); + h.SetVf(2, 2.0f, 4.0f, -1.0f, -0.5f); + h.LoadProgram({ + UpperOnly(VMAX_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 0.0f); +} + +TEST(Vu1AluUpper, VminiPicksSmallerLaneByLane) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 5.0f, -3.0f, 0.0f); + h.SetVf(2, 2.0f, 4.0f, -1.0f, -0.5f); + h.LoadProgram({ + UpperOnly(VMINI_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -0.5f); +} + +// -------- VMADD / VMSUB (read-modify-write into FD via ACC) -------- +// +// VMADD: FD = ACC + FS * FT +// VMSUB: FD = ACC - FS * FT +// The interpreter and JIT have separate ACC representations; correctness +// requires both to agree at E-bit. ACC is seeded via a preceding op so the +// test doesn't have to construct ACC out-of-band. + +TEST(Vu1AluUpper, VmaddAddsAccProduct) +{ + // Seed ACC via a VMUL (FD=ACC). Trick: there's a separate VMULA op for + // "MUL into ACC" but no encoder for it is available here. Instead use + // VADD to a scratch FD (which doesn't write ACC), then test VMADD with + // ACC at its zeroed default. ACC starts (0,0,0,0), so the result is + // just FS * FT — good enough to exercise the VMADD primary. + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.LoadProgram({ + UpperOnly(VMADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 21.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 32.0f); +} + +TEST(Vu1AluUpper, VmsubSubtractsAccProduct) +{ + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.IgnoreViInDiff(REG_MAC_FLAG); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 5.0f, 6.0f, 7.0f, 8.0f); + h.LoadProgram({ + UpperOnly(VMSUB_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + // ACC=0, so VMSUB writes -FS*FT to FD. + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -12.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -21.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -32.0f); +} + +// -------- Broadcast variants — VADDx/y/z/w -------- +// FD = FS + FT.bc — bc replicated across all lanes of FT before add. + +TEST(Vu1AluUpper, VaddxBroadcastsFtX) +{ + VuTestHarness h(1); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 7.0f, 99.0f, 99.0f, 99.0f); // only x lane of FT consulted + h.LoadProgram({ + UpperOnly(VADDx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu1AluUpper, VaddyBroadcastsFtY) +{ + VuTestHarness h(1); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 7.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VADDy_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu1AluUpper, VaddzBroadcastsFtZ) +{ + VuTestHarness h(1); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 99.0f, 7.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VADDz_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +TEST(Vu1AluUpper, VaddwBroadcastsFtW) +{ + VuTestHarness h(1); + h.SetVf(1, 0.0f, 100.0f, 200.0f, 300.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 7.0f); + h.LoadProgram({ + UpperOnly(VADDw_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 7.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 107.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 207.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 307.0f); +} + +// -------- Broadcast variants — VSUBx/y/z/w -------- + +TEST(Vu1AluUpper, VsubxSubtractsBroadcastFtX) +{ + VuTestHarness h(1); + h.SetVf(1, 100.0f, 200.0f, 300.0f, 400.0f); + h.SetVf(2, 50.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VSUBx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 50.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 150.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 250.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 350.0f); +} + +TEST(Vu1AluUpper, VsubwSubtractsBroadcastFtW) +{ + VuTestHarness h(1); + h.SetVf(1, 100.0f, 200.0f, 300.0f, 400.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 50.0f); + h.LoadProgram({ + UpperOnly(VSUBw_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 50.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 150.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 250.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 350.0f); +} + +// -------- Broadcast variants — VMULx/y/z/w -------- + +TEST(Vu1AluUpper, VmulxBroadcastsFtX) +{ + VuTestHarness h(1); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 10.0f, 99.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMULx_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 10.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), 20.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 30.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 40.0f); +} + +TEST(Vu1AluUpper, VmulyBroadcastsFtY) +{ + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.IgnoreViInDiff(REG_MAC_FLAG); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 99.0f, -5.0f, 99.0f, 99.0f); + h.LoadProgram({ + UpperOnly(VMULy_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -5.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -10.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), -15.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -20.0f); +} + +// -------- Mask + broadcast crossover -------- +// Single-lane masks combined with a broadcast op: the destination lane must +// take its FT from the broadcasted source, not from the matching FT lane. + +TEST(Vu1AluUpper, VaddwMaskZWritesOnlyZWithFtW) +{ + VuTestHarness h(1); + h.SetVf(1, 10.0f, 20.0f, 30.0f, 40.0f); + h.SetVf(2, 99.0f, 99.0f, 99.0f, 5.0f); // bc value lives in w + h.SetVf(3, -1.0f, -2.0f, -3.0f, -4.0f); + h.LoadProgram({ + UpperOnly(VADDw_U(mask::z, vf::vf3, vf::vf1, vf::vf2)), + EBitNopPair(), + }); + h.Run(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), -1.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'y'), -2.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'z'), 35.0f); // 30 + 5 + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), -4.0f); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu1_efu_p_pipeline_tests.cpp b/tests/ctest/core/recompilers/vu1_efu_p_pipeline_tests.cpp new file mode 100644 index 0000000000..0fdead6a44 --- /dev/null +++ b/tests/ctest/core/recompilers/vu1_efu_p_pipeline_tests.cpp @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU1 EFU (Elementary Function Unit) P-pipeline DiffJitVsInterp. +// +// EFU ops live in the lower-pipe LowerOP_T3 sub-tables (see VUops.cpp:3607- +// 3650). Every EFU op writes its result into VU->p (the P-pipeline staging +// scalar); microVU's mVUendProgram commits VU.p to VI[REG_P] at E-bit. +// VWAITP stalls until the P-pipeline drains. +// +// EFU is VU1-only (VU0 has no EFU dispatch — opcode hits `unknown`). All +// tests target vu_index = 1. +// +// Pipeline timing: EFU ops have variable latency depending on the op (12 +// cycles for VEEXP/VESIN/VEATAN family; 18 for VERSQRT). Tests pad the +// reader by 18 NOP pairs to clear the longest possible window — the JIT +// and interp may legitimately differ on intermediate VI[REG_P] values, but +// the architectural value at E-bit must agree. +// +// EFU divergences from VUops.cpp's _vuE* are inherent JIT-vs-interp +// differences shared with x86 microVU: mVU_ESQRT masks Fs &= 0x7FFFFFFF +// then Fsqrt, dropping the interp's negative-input passthrough and +// zero-input early-return. Real PS2 games are tolerant of these +// niche-input behaviors, and fixing them would diverge from upstream — +// so this file covers the architectural happy paths only. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } +// I-bit set so the zero lower word is suppressed (becomes the VI[REG_I] +// immediate) instead of decoding as LQ vf0 — the canonical NOP-pair idiom. +inline VuOp BareNopPair() { return IBit(VuOp{VLitZero(), VNOP_U()}); } + +} // namespace + +// ========================================================================= +// VESADD — sum of squares of the xyz lanes +// ========================================================================= + +TEST(Vu1EfuPpipe, VesaddSumsSquaresOfXyz) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 3.0f, 4.0f, 0.0f, 99.0f); // expected: 9 + 16 + 0 = 25 + h.LoadProgram({ + LowerOnly(VESADD_L(vf::vf1)), + // 18-pair latency pad + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + // Architectural REG_P committed at E-bit — diff must agree. + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +TEST(Vu1EfuPpipe, VesaddZeroVectorYieldsZero) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 0.0f, 0.0f, 0.0f, 1.0f); + h.LoadProgram({ + LowerOnly(VESADD_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +// ========================================================================= +// VERSADD — reciprocal of sum-of-squares (1 / VESADD) +// ========================================================================= + +TEST(Vu1EfuPpipe, VersaddProducesReciprocal) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 3.0f, 4.0f, 0.0f, 1.0f); // ESADD = 25, ERSADD = 1/25 + h.LoadProgram({ + LowerOnly(VERSADD_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +// ========================================================================= +// VELENG — sqrt of sum-of-squares (length of xyz) +// ========================================================================= + +TEST(Vu1EfuPpipe, VelengComputesEuclideanLength) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 3.0f, 4.0f, 0.0f, 1.0f); // length = 5 + h.LoadProgram({ + LowerOnly(VELENG_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +// ========================================================================= +// VESQRT — scalar sqrt of fs.fsf. (VERSQRT positive case + negative-input +// passthrough are coverage gaps — see file-header note.) +// ========================================================================= + +TEST(Vu1EfuPpipe, VesqrtScalarPositive) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 9.0f, 16.0f, 25.0f, 1.0f); + h.LoadProgram({ + LowerOnly(VESQRT_L(vf::vf1, /*fsf*/0)), // sqrt(9) = 3 + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +// ========================================================================= +// VESUM / VERLENG / VERCPR — remaining EFU ops +// ========================================================================= + +TEST(Vu1EfuPpipe, VesumSumsAllFourLanes) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 1.0f, 2.0f, 3.0f, 4.0f); // sum = 10 + h.LoadProgram({ + LowerOnly(VESUM_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +TEST(Vu1EfuPpipe, VercprReciprocalScalar) +{ + VuTestHarness h(1); + h.SetVf(vf::vf1, 4.0f, 0.0f, 0.0f, 1.0f); // 1/4 = 0.25 + h.LoadProgram({ + LowerOnly(VERCPR_L(vf::vf1, 0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +// ========================================================================= +// pInst rotation — VI[REG_P] / pending_p storage at program termination +// +// mVUendProgram (and mVUDTendProgram) on VU1 stash the P-pipeline state +// out of qmmPQ at E-bit. qmmPQ layout: [0]=Q, [1]=pending_q, [2]=P, +// [3]=pending_p when mVU.p == 0; lanes 2/3 swap when mVU.p == 1 +// (each EFU op flips mVU.p via incP). The emit performs the pInst-conditioned +// shuffle by selecting the destination lane index instead of emitting a +// physical lane swap. +// +// All existing tests above issue exactly ONE EFU op + VWAITP — that's +// the pInst=1 path. The tests below add coverage for: +// - pInst=0 trivially (no EFU op) +// - pInst=0 after an even number of EFU ops +// - pInst-active without VWAITP (P-pipe still in-flight at E-bit) +// Each diffs JIT against interp, so a wrong lane index on either of the +// two emitted St1 lanes — VI[REG_P] or pending_p — surfaces here. +// ========================================================================= + +TEST(Vu1EfuPpipe, NoEfuOpPInstZeroPath) +{ + // Empty P-pipeline activity: mVU.p stays 0 across the whole block, + // so pInst=0 at termination → P stored from lane 2, pending_p from + // lane 3. Pre-seed VI[REG_P] to a sentinel and verify JIT and + // interp commit the same architectural value (both should drain + // the freshly-zeroed qmmPQ lanes). + VuTestHarness h(1); + h.SetVi(REG_P, 0xDEADBEEF); + h.LoadProgram({ + BareNopPair(), BareNopPair(), BareNopPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +TEST(Vu1EfuPpipe, TwoEfuOpsPInstZeroPath) +{ + // Two EFU ops with intervening VWAITP. mVU.p toggles twice across + // the block (incP fires once per EFU op), + // so pInst=0 at the final E-bit. Distinct from the one-EFU pattern + // covered by every other test in this file. + VuTestHarness h(1); + h.SetVf(vf::vf1, 3.0f, 4.0f, 0.0f, 1.0f); // ESADD = 25 + h.SetVf(vf::vf2, 9.0f, 0.0f, 0.0f, 1.0f); // ESQRT(9) = 3 + h.LoadProgram({ + LowerOnly(VESADD_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + LowerOnly(VESQRT_L(vf::vf2, 0)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + LowerOnly(VWAITP_L()), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +TEST(Vu1EfuPpipe, EfuWithoutWaitPInFlightAtEbit) +{ + // EFU op without a trailing VWAITP, but with enough latency pad that + // the P-pipeline result has been computed by E-bit. Single EFU → + // pInst=1 at termination, so VI[REG_P] gets the pInst?3:2 lane. + // All the existing tests above gate on VWAITP; this exercises the + // drain path that fires when E-bit alone terminates the program. + VuTestHarness h(1); + h.SetVf(vf::vf1, 3.0f, 4.0f, 0.0f, 1.0f); // ESADD = 25 + h.LoadProgram({ + LowerOnly(VESADD_L(vf::vf1)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(REG_P), h.GetViInterp(REG_P)); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu1_xgkick_tests.cpp b/tests/ctest/core/recompilers/vu1_xgkick_tests.cpp new file mode 100644 index 0000000000..b86d522c49 --- /dev/null +++ b/tests/ctest/core/recompilers/vu1_xgkick_tests.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU1 XGKICK DiffJitVsInterp suite. +// +// XGKICK initiates a GIF Path 1 transfer from VU1 memory. The production +// path runs through `gifUnit.TransferGSPacketData(GIF_TRANS_XGKICK, ...)` +// → MTGS, which asserts when no GS thread is running. Under +// `PCSX2_RECOMPILER_TESTS`, Gif_Unit's TransferGSPacketData early-returns +// when `gif_test_hooks::g_path1_sink` is non-null, appending packet bytes +// to the sink. VuTestHarness installs the sink in its constructor and +// captures separate per-pass byte streams via `Path1PacketBytesJit/Interp`. +// +// State-machine note: the JIT and interp track XGKICK *internal* scratch +// state differently. The interp's `_vuXGKICK` writes +// `VU1.xgkickaddr/diff/cyclecount/enable` directly. The JIT non-XGKICKHACK +// path (`mVU_XGKICK_`) computes addr/size in locals and fires the GIF +// transfer without touching VU1.xgkick* — so +// post-state on those scratch fields legitimately diverges. The +// architectural truth is the GIF Path 1 byte stream that reaches GS, so +// these tests use VuDiffMode::XgkickPacketEquivalent (silences the +// xgkick scratch-field diff) and assert byte-for-byte equivalence on the +// captured packet streams. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp LowerOnly(u32 lower) { return VuOp{lower, VNOP_U()}; } +inline VuOp BareNopPair() { return VuOp{0, VNOP_U()}; } + +constexpr u32 kEopOnlyTagLower = 0x00008000u; + +void WriteEopOnlyTagToVu1(VuTestHarness& h, u32 addr) +{ + h.WriteMemU128(addr, kEopOnlyTagLower, 0u, 0u, 0u); +} + +} // namespace + +TEST(Vu1Xgkick, EopOnlyTagEmitsMatchingPath1Stream) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0); + h.SetVi(vi::vi5, 0); + h.LoadProgram({ + LowerOnly(VXGKICK_L(vi::vi5)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()); + // One EOP-only tag = 16 bytes per kick — exactly one kick here. + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u); +} + +TEST(Vu1Xgkick, EBitFlushesInflightXgkick) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0); + h.SetVi(vi::vi5, 0); + h.LoadProgram({ + LowerOnly(VXGKICK_L(vi::vi5)), + EBitNopPair(), + }); + h.Run(); + // Even with E-bit one pair away, both engines must drain the kick. + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()); + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u); +} + +TEST(Vu1Xgkick, BackToBackXgkicksEmitMatchingPath1Stream) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0x000); + WriteEopOnlyTagToVu1(h, 0x100); + h.SetVi(vi::vi5, 0x000 / 16); + h.SetVi(vi::vi6, 0x100 / 16); + h.LoadProgram({ + LowerOnly(VXGKICK_L(vi::vi5)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + LowerOnly(VXGKICK_L(vi::vi6)), + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()); + // Two EOP-only kicks = 32 bytes total. + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 32u); +} + +// IBNE that does not branch, with XGKICK in its (always-executed) delay slot, +// immediately followed by E-bit. The simple-XGKICK tests above don't place +// XGKICK in a branch delay slot; the branch-taken control-flow analysis at +// compile time can cause the JIT to emit different code for a delay-slot +// XGKICK adjacent to an E-bit, so this pins the not-branching case. +TEST(Vu1Xgkick, IbneDelaySlotXgkickThenEBit_NotBranching) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0); + h.SetVi(vi::vi5, 0); + // IBNE compares two registers that are always equal (vi0 == vi0 == 0), + // so the branch never fires. The XGKICK in the delay slot must still + // execute, then E-bit terminates. + h.LoadProgram({ + VuOp{VIBNE_L(vi::vi0, vi::vi0, -1), VNOP_U()}, // pc=0x00 IBNE not taken + LowerOnly(VXGKICK_L(vi::vi5)), // pc=0x08 delay slot + EBitNopPair(), // pc=0x10 E-bit + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()); + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u); +} + +// Same pattern as above but with a backward IBNE target (branch into an +// earlier loop body). The branch still does not fire, so control falls +// through to the XGKICK in the delay slot. A backward branch may flip the +// JIT's block-chaining heuristic compared to the forward variant above. +TEST(Vu1Xgkick, IbneBackwardDelaySlotXgkickThenEBit_NotBranching) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0); + h.SetVi(vi::vi5, 0); + // Pad the front of the program with NOPs so the IBNE has somewhere to + // branch backward to. Branch target = (pc_of_delay_slot + 1*8) + imm*8. + // With IBNE at pc=0x18, delay slot at 0x20, target = 0x28 + (-5)*8 = 0x00. + h.LoadProgram({ + BareNopPair(), BareNopPair(), BareNopPair(), // pc=0x00..0x10 + VuOp{VIBNE_L(vi::vi0, vi::vi0, -5), VNOP_U()}, // pc=0x18 IBNE not taken + LowerOnly(VXGKICK_L(vi::vi5)), // pc=0x20 delay slot + EBitNopPair(), // pc=0x28 E-bit + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()); + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u); +} + +// Reproducer: MTIR loads vi from a vf lane whose full 32-bit bits have dirty +// top 16 bits. XGKICK then reads vi. The arm64 microRegAlloc allocGPR +// caches the full 32-bit Umov result from MTIR; when XGKICK calls allocGPR +// without zext_if_dirty=true, it stores the dirty 32-bit value to mVU.VIxgkick +// and passes it as `addr` to mVU_XGKICK_. Because mVU_XGKICK_ does +// `(addr & 0x3ff) * 16`, the low-10 bits survive — so the masked vumem offset +// is the same as if we'd truncated to u16 first. Both engines should hit the +// same vumem byte offset and emit identical PATH1. +// +// Setup: vf7.x = 0x4b000208 (low-16 = 0x0208, & 0x3ff = 0x208). MTIR vi5 +// from vf7.x. XGKICK vi5. Stage an EOP tag at vumem[0x208 * 16 = 0x2080] +// so both engines emit one 16-byte tag. +TEST(Vu1Xgkick, MtirDirtyHighBits_XgkickLandsOnSameVumemQword) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + WriteEopOnlyTagToVu1(h, 0x2080); + h.SetVfBits(vf::vf7, 0x4b000208u, 0u, 0u, 0u); + h.LoadProgram({ + // MTIR.x vi5 = low-16(vf7.x) per VU spec. JIT cache may hold the + // full 32-bit Umov result; interp truncates to u16. + VuOp{VMTIR_L(vi::vi5, vf::vf7, /*fsf=x*/0), VNOP_U()}, + BareNopPair(), + LowerOnly(VXGKICK_L(vi::vi5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()) + << "JIT and interp computed different vumem offsets for XGKICK after " + "MTIR from a vf lane with dirty top 16 bits. " + "JIT: " << h.Path1PacketBytesJit().size() << " B, " + "Interp: " << h.Path1PacketBytesInterp().size() << " B."; + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u) + << "Expected one 16-byte EOP tag emitted from vumem[0x2080]"; +} + +// Probe: do JIT and interp diverge by 1 ULP on (1.0 / 3.0) → MULq → MTIR? +// If yes, the JIT's vi5 lands at a different qword than interp's vi5. +// To detect that, stage a *distinct* EOP-terminated GIF tag at every qword +// of vumem — each tag carries its own qword index in the data lane. The +// PATH1 byte stream then directly encodes which qword the engine kicked +// from, and a divergent vi5 produces a divergent PATH1 byte. +// +// Each tag is 16 bytes (NLOOP=0, EOP=1, qword index in the upper 64 bits) +// so size = 16, no wrap, no harness sink loss. +TEST(Vu1Xgkick, DivOneOverThreeFmacChainPreservesXgkickAddr) +{ + VuTestHarness h(1); + h.SetDiffMode(VuDiffMode::XgkickPacketEquivalent); + // Stage one distinct EOP-only tag per qword of vumem (1024 qwords = + // 16384 bytes = whole VU1 mem). Tag's lower bits are the EOP marker + // (bit 15 = 1), upper 64 bits encode the qword index. + for (u32 q = 0; q < 1024; ++q) + h.WriteMemU128(q * 16, kEopOnlyTagLower, 0u, q, 0u); + + h.SetVfBits(vf::vf1, 0x3F800000u, 0u, 0u, 0u); // 1.0 + h.SetVfBits(vf::vf2, 0x40400000u, 0u, 0u, 0u); // 3.0 + h.LoadProgram({ + // Q = 1.0 / 3.0 (irrational; FP rounding in DIV may diverge) + VuOp{VDIV_L(vf::vf1, /*fsf=x*/0, vf::vf2, /*ftf=x*/0), VNOP_U()}, + BareNopPair(), BareNopPair(), BareNopPair(), BareNopPair(), + BareNopPair(), BareNopPair(), BareNopPair(), + // vf3.x = vf1.x * Q = 1.0 * 0.333… + VuOp{VNOP_U(), VMULq_U(mask::x, vf::vf3, vf::vf1)}, + BareNopPair(), + // MTIR.x vi5 = low-16(vf3.x bits) + VuOp{VMTIR_L(vi::vi5, vf::vf3, /*fsf=x*/0), VNOP_U()}, + BareNopPair(), + LowerOnly(VXGKICK_L(vi::vi5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.Path1PacketBytesJit(), h.Path1PacketBytesInterp()) + << "JIT and interp emit different PATH1 after DIV(1/3) → MULq → MTIR. " + "JIT vi5 = different qword than interp vi5 — likely FP precision " + "divergence in the DIV or MULq emit path."; + // Each engine's emitted byte stream should be exactly one 16-byte tag. + EXPECT_EQ(h.Path1PacketBytesInterp().size(), 16u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu_capture_format_tests.cpp b/tests/ctest/core/recompilers/vu_capture_format_tests.cpp new file mode 100644 index 0000000000..2889afb24c --- /dev/null +++ b/tests/ctest/core/recompilers/vu_capture_format_tests.cpp @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Round-trip coverage for the on-disk VU capture format (vu_capture.h). +// The format is consumed by pcsx2-vurunner and VuTestHarness::LoadFromFile; +// breaking the layout would silently make every captured replay misbehave. +// These tests pin the magic / version / sizes / field round-trip so a +// drift surfaces here before it reaches the replay side. + +#include "vu_capture.h" + +#include "VUmicro.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +std::string MakeTempPath(const char* tag) +{ + std::filesystem::path p = std::filesystem::temp_directory_path() / + (std::string("pcsx2-vucap-test-") + tag + "-" + std::to_string(::getpid()) + ".vucap"); + return p.string(); +} + +vu_capture::CaptureRecord MakeRandomRecord(u8 vu_index, u32 seed) +{ + const u32 size = vu_index ? VU1_PROGSIZE : VU0_PROGSIZE; + vu_capture::CaptureRecord rec; + rec.vu_index = vu_index; + rec.start_pc = 0xDEAD0000u | seed; + rec.cycle_budget = 4096 + seed; + rec.microcode.resize(size); + rec.vumem.resize(size); + + std::mt19937 rng(seed); + for (u32 i = 0; i < size; ++i) + { + rec.microcode[i] = static_cast(rng()); + rec.vumem[i] = static_cast(rng()); + } + for (auto& vf : rec.state.VF) + for (auto& lane : vf) + lane = rng(); + for (auto& vi : rec.state.VI) + vi = rng(); + for (auto& a : rec.state.ACC) + a = rng(); + rec.state.q = rng(); + rec.state.p = rng(); + rec.state.pending_q = rng(); + rec.state.pending_p = rng(); + for (auto& f : rec.state.micro_macflags) + f = rng(); + for (auto& f : rec.state.micro_clipflags) + f = rng(); + for (auto& f : rec.state.micro_statusflags) + f = rng(); + rec.state.xgkickaddr = rng(); + rec.state.xgkickdiff = rng(); + rec.state.xgkicksizeremaining = rng(); + rec.state.xgkicklastcycle = (static_cast(rng()) << 32) | rng(); + rec.state.xgkickcyclecount = rng(); + rec.state.xgkickenable = rng(); + rec.state.xgkickendpacket = rng(); + return rec; +} + +void ExpectEqual(const vu_capture::CaptureRecord& a, const vu_capture::CaptureRecord& b) +{ + EXPECT_EQ(a.vu_index, b.vu_index); + EXPECT_EQ(a.start_pc, b.start_pc); + EXPECT_EQ(a.cycle_budget, b.cycle_budget); + ASSERT_EQ(a.microcode.size(), b.microcode.size()); + ASSERT_EQ(a.vumem.size(), b.vumem.size()); + EXPECT_EQ(0, std::memcmp(a.microcode.data(), b.microcode.data(), a.microcode.size())); + EXPECT_EQ(0, std::memcmp(a.vumem.data(), b.vumem.data(), a.vumem.size())); + EXPECT_EQ(0, std::memcmp(&a.state, &b.state, sizeof(a.state))); +} + +} // namespace + +TEST(VuCaptureFormat, FileHeaderIsExactly32Bytes) +{ + EXPECT_EQ(32u, sizeof(vu_capture::FileHeader)); +} + +TEST(VuCaptureFormat, MagicIsPCSX2VUC) +{ + EXPECT_EQ(0, std::memcmp(vu_capture::kMagic, "PCSX2VUC", 8)); +} + +TEST(VuCaptureFormat, RoundTripVu0) +{ + const auto path = MakeTempPath("vu0"); + const auto written = MakeRandomRecord(0, 0xC0FFEE); + ASSERT_TRUE(vu_capture::WriteToFile(path, written)); + + vu_capture::CaptureRecord read_back; + ASSERT_TRUE(vu_capture::ReadFromFile(path, read_back)); + ExpectEqual(written, read_back); + + std::filesystem::remove(path); +} + +TEST(VuCaptureFormat, RoundTripVu1) +{ + const auto path = MakeTempPath("vu1"); + const auto written = MakeRandomRecord(1, 0xBEEF); + ASSERT_TRUE(vu_capture::WriteToFile(path, written)); + + vu_capture::CaptureRecord read_back; + ASSERT_TRUE(vu_capture::ReadFromFile(path, read_back)); + ExpectEqual(written, read_back); + + std::filesystem::remove(path); +} + +TEST(VuCaptureFormat, ReadRejectsBadMagic) +{ + const auto path = MakeTempPath("badmagic"); + const auto rec = MakeRandomRecord(0, 1); + ASSERT_TRUE(vu_capture::WriteToFile(path, rec)); + + // Corrupt magic. + std::FILE* f = std::fopen(path.c_str(), "r+b"); + ASSERT_TRUE(f != nullptr); + std::fputc('X', f); + std::fclose(f); + + vu_capture::CaptureRecord out; + EXPECT_FALSE(vu_capture::ReadFromFile(path, out)); + + std::filesystem::remove(path); +} + +TEST(VuCaptureFormat, ReadRejectsBadVersion) +{ + const auto path = MakeTempPath("badver"); + const auto rec = MakeRandomRecord(0, 2); + ASSERT_TRUE(vu_capture::WriteToFile(path, rec)); + + // Bump the version byte at offset 8. + std::FILE* f = std::fopen(path.c_str(), "r+b"); + ASSERT_TRUE(f != nullptr); + std::fseek(f, 8, SEEK_SET); + const u32 wrong_version = vu_capture::kVersion + 99; + std::fwrite(&wrong_version, sizeof(wrong_version), 1, f); + std::fclose(f); + + vu_capture::CaptureRecord out; + EXPECT_FALSE(vu_capture::ReadFromFile(path, out)); + + std::filesystem::remove(path); +} + +TEST(VuCaptureFormat, WriteRejectsWrongSizedBuffers) +{ + vu_capture::CaptureRecord bad; + bad.vu_index = 0; + bad.microcode.resize(VU0_PROGSIZE - 1); + bad.vumem.resize(VU0_MEMSIZE); + EXPECT_FALSE(vu_capture::WriteToFile(MakeTempPath("badsize"), bad)); +} + +TEST(VuCaptureFormat, OnDiskByteSize) +{ + // Fixed expected size: 32 (header) + 2 * PROGSIZE (microcode + vumem) + // + sizeof(CapturedState). Pinning this catches accidental padding. + const auto path = MakeTempPath("size"); + const auto rec = MakeRandomRecord(1, 7); + ASSERT_TRUE(vu_capture::WriteToFile(path, rec)); + const auto sz = std::filesystem::file_size(path); + EXPECT_EQ(sz, 32u + 2u * VU1_PROGSIZE + sizeof(vu_capture::CapturedState)); + std::filesystem::remove(path); +} diff --git a/tests/ctest/core/recompilers/vu_ftoi_saturation_tests.cpp b/tests/ctest/core/recompilers/vu_ftoi_saturation_tests.cpp new file mode 100644 index 0000000000..4472c83a93 --- /dev/null +++ b/tests/ctest/core/recompilers/vu_ftoi_saturation_tests.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// FTOI{0,4,12,15} out-of-range / NaN saturation — DiffJitVsInterp. +// +// The PS2 VU float→int conversion saturates: the interp's floatToInt +// (VUops.cpp) returns 0x7FFFFFFF (INT_MAX) for a positive value whose exponent +// is >= that of 2^31 (which includes +Inf and any positive NaN), and +// 0x80000000 (INT_MIN) for the negative side. x86 PCSX2 mVU reproduces this via +// CVTTPS2DQ + a PCMP/PXOR correction. +// +// A bare FCVTZS saturates *finite* overflow and ±Inf correctly but converts +// NaN→0 — diverging from both the interp and x86. Real VU programs do feed +// positive NaN into FTOI0 (producing JIT=0 vs interp=0x7FFFFFFF). These tests +// pin the saturation contract so the arm64 emit matches the interp. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// I-bit set so the zero lower word is suppressed (becomes the VI[REG_I] +// immediate) rather than decoding as LQ vf0 — the canonical suppression idiom. +inline VuOp UpperOnly(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } + +// FTOI0 dst <- trunc(src). VFTOI0_U(mask, ft=dst, fs=src). +inline VuOp VFtoi0(u32 dst, u32 src) { return UpperOnly(VFTOI0_U(mask::xyzw, dst, src)); } + +void RunAndExpectAllLanesAgree(VuTestHarness& h, u32 dst_vf) +{ + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'x'), h.GetVfBitsInterp(dst_vf, 'x')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'y'), h.GetVfBitsInterp(dst_vf, 'y')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'z'), h.GetVfBitsInterp(dst_vf, 'z')); + EXPECT_EQ(h.GetVfBitsJit(dst_vf, 'w'), h.GetVfBitsInterp(dst_vf, 'w')); +} + +constexpr u32 kPosNan = 0x7FFFFFFFu; // positive quiet NaN (sign 0, exp 0xFF) +constexpr u32 kNegNan = 0xFFFFFFFFu; // negative NaN (sign 1) +constexpr u32 kPosInf = 0x7F800000u; +constexpr u32 kNegInf = 0xFF800000u; +constexpr u32 kPosBig = 0x4F800000u; // 2^32, overflows s32 positive +constexpr u32 kNegBig = 0xCF800000u; // -2^32 +constexpr u32 kOne = 0x3F800000u; // 1.0 → 1 + +} // namespace + +// ========================================================================= +// NaN — interp/x86 give sign-based INT_MAX/INT_MIN; a bare FCVTZS gives 0. +// JIT and interp must agree. +// ========================================================================= + +TEST(VuFtoiSaturation, PositiveNanSaturatesToIntMax) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosNan, kPosNan, kPosNan, kPosNan); + h.LoadProgram({VFtoi0(vf::vf1, vf::vf2), EBitNopPair()}); + RunAndExpectAllLanesAgree(h, vf::vf1); + // Pin the architectural value too (interp oracle): positive NaN → INT_MAX. + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'x'), 0x7FFFFFFFu); + EXPECT_EQ(h.GetVfBitsJit(vf::vf1, 'x'), 0x7FFFFFFFu); +} + +TEST(VuFtoiSaturation, NegativeNanSaturatesToIntMin) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kNegNan, kNegNan, kNegNan, kNegNan); + h.LoadProgram({VFtoi0(vf::vf1, vf::vf2), EBitNopPair()}); + RunAndExpectAllLanesAgree(h, vf::vf1); + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'x'), 0x80000000u); +} + +// ========================================================================= +// Finite overflow + Inf — FCVTZS handles these correctly; guard against +// any NaN-path fix perturbing the finite-overflow behavior. +// ========================================================================= + +TEST(VuFtoiSaturation, PositiveOverflowAndInf) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosBig, kPosInf, kPosBig, kPosInf); + h.LoadProgram({VFtoi0(vf::vf1, vf::vf2), EBitNopPair()}); + RunAndExpectAllLanesAgree(h, vf::vf1); + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'x'), 0x7FFFFFFFu); +} + +TEST(VuFtoiSaturation, NegativeOverflowAndInf) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kNegBig, kNegInf, kNegBig, kNegInf); + h.LoadProgram({VFtoi0(vf::vf1, vf::vf2), EBitNopPair()}); + RunAndExpectAllLanesAgree(h, vf::vf1); + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'x'), 0x80000000u); +} + +// Mixed lanes — a NaN lane next to in-range lanes must not perturb the others. +TEST(VuFtoiSaturation, MixedNanAndInRange) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf2, kPosNan, kOne, kNegNan, kOne); + h.LoadProgram({VFtoi0(vf::vf1, vf::vf2), EBitNopPair()}); + RunAndExpectAllLanesAgree(h, vf::vf1); + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'y'), 1u); + EXPECT_EQ(h.GetVfBitsInterp(vf::vf1, 'w'), 1u); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu_madda_acc_lane_tests.cpp b/tests/ctest/core/recompilers/vu_madda_acc_lane_tests.cpp new file mode 100644 index 0000000000..b6cdfc31df --- /dev/null +++ b/tests/ctest/core/recompilers/vu_madda_acc_lane_tests.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Single-dest-lane MADDA/MSUBA must preserve ACC's non-written lanes — +// DiffJitVsInterp. +// +// The PS2 ACC-accumulate FMACs (MADDA/MSUBA) write only the lanes named by the +// dest mask; the other ACC lanes are untouched. A single-lane MADDA/MSUBA (or +// a broadcast form that takes the same single-lane path) must leave the three +// unwritten ACC lanes at their prior values, not zero them. +// +// These tests pin the lane-preservation contract. They use the non-broadcast +// VMADDA_U/VMSUBA_U at single-lane masks, which route through the identical +// accumulate path as the broadcast forms. + +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +// I-bit set so the zero lower word is suppressed (becomes the VI[REG_I] +// immediate) rather than decoding as LQ vf0 — the canonical suppression idiom. +inline VuOp UpperOnly(u32 upper) { return IBit(VuOp{VLitZero(), upper}); } + +// ACC ← Fs * Ft, full xyzw (FMACa PS path — known-good, used to seed ACC). +inline VuOp Mula(u32 fs, u32 ft) { return UpperOnly(VMULA_U(mask::xyzw, fs, ft)); } +// ACC. ← ACC. + Fs. * Ft.. +inline VuOp Madda(u32 m, u32 fs, u32 ft) { return UpperOnly(VMADDA_U(m, fs, ft)); } +// ACC. ← ACC. - Fs. * Ft.. +inline VuOp Msuba(u32 m, u32 fs, u32 ft) { return UpperOnly(VMSUBA_U(m, fs, ft)); } + +constexpr u32 k2 = 0x40000000u; // 2.0 +constexpr u32 k3 = 0x40400000u; // 3.0 +constexpr u32 k4 = 0x40800000u; // 4.0 +constexpr u32 k5 = 0x40A00000u; // 5.0 +constexpr u32 k1 = 0x3F800000u; // 1.0 + +void ExpectAccLanesAgree(VuTestHarness& h) +{ + h.Run(); + const VURegs& j = h.JitSnapshot().regs; + const VURegs& i = h.InterpSnapshot().regs; + EXPECT_EQ(j.ACC.UL[0], i.ACC.UL[0]) << "ACC.x"; + EXPECT_EQ(j.ACC.UL[1], i.ACC.UL[1]) << "ACC.y"; + EXPECT_EQ(j.ACC.UL[2], i.ACC.UL[2]) << "ACC.z"; + EXPECT_EQ(j.ACC.UL[3], i.ACC.UL[3]) << "ACC.w"; +} + +} // namespace + +// ========================================================================= +// The regression: a single-lane MADDA must leave the other 3 ACC lanes +// exactly as the seeding MULA left them. +// ========================================================================= + +TEST(VuMaddaAccLane, MaddaWPreservesXyz) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf1, k2, k3, k4, k5); // ACC seed operand + h.SetVfBits(vf::vf2, k1, k1, k1, k1); // *1.0 → ACC = [2,3,4,5] + h.SetVfBits(vf::vf3, 0, 0, 0, k2); // Fs.w = 2.0 + h.SetVfBits(vf::vf4, 0, 0, 0, k3); // Ft.w = 3.0 + // ACC.w ← 5 + 2*3 = 11; ACC.x/y/z must stay [2,3,4]. + h.LoadProgram({Mula(vf::vf1, vf::vf2), NopPair(), NopPair(), NopPair(), + Madda(mask::w, vf::vf3, vf::vf4), EBitNopPair()}); + ExpectAccLanesAgree(h); + const VURegs& i = h.InterpSnapshot().regs; + EXPECT_EQ(i.ACC.UL[0], k2); // interp oracle: x preserved + EXPECT_EQ(i.ACC.UL[1], k3); + EXPECT_EQ(i.ACC.UL[2], k4); +} + +TEST(VuMaddaAccLane, MaddaXPreservesYzw) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf1, k2, k3, k4, k5); + h.SetVfBits(vf::vf2, k1, k1, k1, k1); + h.SetVfBits(vf::vf3, k2, 0, 0, 0); + h.SetVfBits(vf::vf4, k3, 0, 0, 0); + h.LoadProgram({Mula(vf::vf1, vf::vf2), NopPair(), NopPair(), NopPair(), + Madda(mask::x, vf::vf3, vf::vf4), EBitNopPair()}); + ExpectAccLanesAgree(h); + const VURegs& i = h.InterpSnapshot().regs; + EXPECT_EQ(i.ACC.UL[1], k3); // y/z/w preserved + EXPECT_EQ(i.ACC.UL[2], k4); + EXPECT_EQ(i.ACC.UL[3], k5); +} + +TEST(VuMaddaAccLane, MsubaWPreservesXyz) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf1, k2, k3, k4, k5); + h.SetVfBits(vf::vf2, k1, k1, k1, k1); + h.SetVfBits(vf::vf3, 0, 0, 0, k2); + h.SetVfBits(vf::vf4, 0, 0, 0, k3); + // MSUBA.w: ACC.w ← 5 - 2*3 = -1; ACC.x/y/z stay [2,3,4]. + h.LoadProgram({Mula(vf::vf1, vf::vf2), NopPair(), NopPair(), NopPair(), + Msuba(mask::w, vf::vf3, vf::vf4), EBitNopPair()}); + ExpectAccLanesAgree(h); + const VURegs& i = h.InterpSnapshot().regs; + EXPECT_EQ(i.ACC.UL[0], k2); + EXPECT_EQ(i.ACC.UL[1], k3); + EXPECT_EQ(i.ACC.UL[2], k4); +} + +// ========================================================================= +// Regression guards for the paths the fix must NOT disturb. +// ========================================================================= + +// Full-mask MADDA writes all four lanes (NEON_PS path) — must stay correct. +TEST(VuMaddaAccLane, MaddaXyzwWritesAllLanes) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf1, k2, k3, k4, k5); + h.SetVfBits(vf::vf2, k1, k1, k1, k1); + h.SetVfBits(vf::vf3, k2, k2, k2, k2); + h.SetVfBits(vf::vf4, k3, k3, k3, k3); + h.LoadProgram({Mula(vf::vf1, vf::vf2), NopPair(), NopPair(), NopPair(), + Madda(mask::xyzw, vf::vf3, vf::vf4), EBitNopPair()}); + ExpectAccLanesAgree(h); +} + +// Partial multi-lane mask (.xy) uses the tempACC+mergeRegs path — must preserve +// the unmasked lanes too. +TEST(VuMaddaAccLane, MaddaXyPreservesZw) +{ + VuTestHarness h(0); + h.SetVfBits(vf::vf1, k2, k3, k4, k5); + h.SetVfBits(vf::vf2, k1, k1, k1, k1); + h.SetVfBits(vf::vf3, k2, k2, 0, 0); + h.SetVfBits(vf::vf4, k3, k3, 0, 0); + h.LoadProgram({Mula(vf::vf1, vf::vf2), NopPair(), NopPair(), NopPair(), + Madda(mask::x | mask::y, vf::vf3, vf::vf4), EBitNopPair()}); + ExpectAccLanesAgree(h); + const VURegs& i = h.InterpSnapshot().regs; + EXPECT_EQ(i.ACC.UL[2], k4); // z/w preserved + EXPECT_EQ(i.ACC.UL[3], k5); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/vu_replay_tests.cpp b/tests/ctest/core/recompilers/vu_replay_tests.cpp new file mode 100644 index 0000000000..3eb422a8b0 --- /dev/null +++ b/tests/ctest/core/recompilers/vu_replay_tests.cpp @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// End-to-end coverage for the VU capture-replay path: synthesize a tiny +// CaptureRecord (VADD pre-state in VF1/VF2, E-bit terminator) and replay +// it through ReplayCapture. Asserts the JIT produces the architecturally +// expected result *and* converges with the interpreter — i.e. the replay +// driver primes both engines from the captured state correctly. + +#include "harness/VuReplay.h" +#include "harness/VuEncode.h" + +#include "vu_capture.h" +#include "VU.h" +#include "VUmicro.h" + +#include +#include +#include +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace +{ + +// Pad a one-pair-plus-NOP-delay-slot program into a full ProgSize buffer so +// the CaptureRecord's microcode field passes the format size check. +std::vector BuildProgramBytes(int vu_index, std::initializer_list pairs) +{ + const u32 prog_size = vu_index ? VU1_PROGSIZE : VU0_PROGSIZE; + std::vector bytes(prog_size, 0); + size_t off = 0; + for (const auto& p : pairs) + { + std::memcpy(bytes.data() + off + 0, &p.lower, 4); + std::memcpy(bytes.data() + off + 4, &p.upper, 4); + off += 8; + } + return bytes; +} + +vu_capture::CaptureRecord MakeVaddProgram(int vu_index) +{ + vu_capture::CaptureRecord rec; + rec.vu_index = static_cast(vu_index); + rec.start_pc = 0; + rec.cycle_budget = 4096; + + // Pair 0: VADD.xyzw vf3, vf1, vf2 (upper) + I-bit-skipped lower. + const VuOp p0 = IBit(VuOp{VLitZero(), VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)}); + // Pair 1: E-bit NOP. + const VuOp p1 = EBitNopPair(); + // Pair 2: NOP (architectural delay slot after E-bit). + const VuOp p2 = NopPair(); + rec.microcode = BuildProgramBytes(vu_index, {p0, p1, p2}); + rec.vumem.assign(vu_index ? VU1_MEMSIZE : VU0_MEMSIZE, 0); + + // Seed pre-state: VF1 = (1, 2, 3, 4), VF2 = (10, 20, 30, 40); expect + // VF3 = (11, 22, 33, 44) post-execution. + const float vf1[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + const float vf2[4] = {10.0f, 20.0f, 30.0f, 40.0f}; + for (int i = 0; i < 4; ++i) + { + std::memcpy(&rec.state.VF[1][i], &vf1[i], 4); + std::memcpy(&rec.state.VF[2][i], &vf2[i], 4); + } + return rec; +} + +float AsFloat(u32 bits) { float f; std::memcpy(&f, &bits, 4); return f; } + +// Portable, collision-free temp path (mirrors vu_capture_format_tests.cpp). +// std::filesystem::temp_directory_path() honors TMPDIR on macOS — /tmp is not +// the conventional temp dir there — and the getpid() suffix keeps parallel +// ctest -j workers from racing on a shared filename. +std::string MakeTempPath(const char* tag) +{ + std::filesystem::path p = std::filesystem::temp_directory_path() / + (std::string("pcsx2-vureplay-test-") + tag + "-" + std::to_string(::getpid()) + ".vucap"); + return p.string(); +} + +} // namespace + +TEST(VuReplay, ReplayVu1VaddProducesExpectedAndDoesNotDiverge) +{ + const auto rec = MakeVaddProgram(1); + const auto result = ReplayCapture(rec); + + ASSERT_TRUE(result.ok); + EXPECT_FALSE(result.diverged) << [&] { + std::string s; + for (const auto& l : result.diff_lines) { s += " "; s += l; s += "\n"; } + return s; + }(); + + const auto& jit_vf3 = result.jit_snapshot.regs.VF[3]; + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.x), 11.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.y), 22.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.z), 33.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.w), 44.0f); + + const auto& interp_vf3 = result.interp_snapshot.regs.VF[3]; + EXPECT_FLOAT_EQ(AsFloat(interp_vf3.i.x), 11.0f); + EXPECT_FLOAT_EQ(AsFloat(interp_vf3.i.y), 22.0f); + EXPECT_FLOAT_EQ(AsFloat(interp_vf3.i.z), 33.0f); + EXPECT_FLOAT_EQ(AsFloat(interp_vf3.i.w), 44.0f); +} + +TEST(VuReplay, ReplayVu0VaddProducesExpectedAndDoesNotDiverge) +{ + const auto rec = MakeVaddProgram(0); + const auto result = ReplayCapture(rec); + + ASSERT_TRUE(result.ok); + EXPECT_FALSE(result.diverged); + + const auto& jit_vf3 = result.jit_snapshot.regs.VF[3]; + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.x), 11.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.y), 22.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.z), 33.0f); + EXPECT_FLOAT_EQ(AsFloat(jit_vf3.i.w), 44.0f); +} + +TEST(VuReplay, LoadAndReplayRoundTripsThroughDisk) +{ + const auto rec = MakeVaddProgram(1); + const std::string path = MakeTempPath("roundtrip"); + ASSERT_TRUE(vu_capture::WriteToFile(path, rec)); + + const auto result = LoadAndReplay(path); + std::remove(path.c_str()); + + ASSERT_TRUE(result.ok); + EXPECT_FALSE(result.diverged); + const auto& vf3 = result.jit_snapshot.regs.VF[3]; + EXPECT_FLOAT_EQ(AsFloat(vf3.i.x), 11.0f); +} + +TEST(VuReplay, LoadAndReplayReturnsNotOkOnMissingFile) +{ + const auto result = LoadAndReplay(MakeTempPath("missing-never-created")); + EXPECT_FALSE(result.ok); + EXPECT_FALSE(result.diverged); +} + +} // namespace recompiler_tests From cd9d9b4d1169639dd297514860c1011186d0ad83 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:56 -0700 Subject: [PATCH 011/292] arm64: persisted-JIT VU program cache (fork-only) On-disk VU program cache ($Cache/vu_jit/): VERSION handshake, content-addressed .vuprog block-graph payloads, cross-process hydration, background MRU preload, and the EnableVUProgramCache INI gate (default off). Includes the disk/versioning/ roundtrip/abi-digest tests and the non-PIE pcsx2-qt link option for cross-boot validation. Kept on the fork branch pending RK3562 field data. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2-qt/CMakeLists.txt | 16 + pcsx2/CMakeLists.txt | 4 + pcsx2/Config.h | 6 + pcsx2/Pcsx2Config.cpp | 2 + pcsx2/VMManager.cpp | 5 + pcsx2/arm64/MvuObservedEntries.h | 33 + pcsx2/arm64/microVU_Persist-arm64.h | 155 +++ pcsx2/arm64/microVU_Persist-arm64.inl | 1227 +++++++++++++++++ pcsx2/arm64/microVU_ProgCache-arm64.h | 177 +++ pcsx2/arm64/microVU_ProgCache-arm64.inl | 958 +++++++++++++ tests/ctest/core/recompilers/CMakeLists.txt | 26 + .../core/recompilers/mvu_abi_digest_tests.cpp | 169 +++ .../mvu_observed_entries_tests.cpp | 134 ++ .../mvu_persist_roundtrip_tests.cpp | 338 +++++ .../recompilers/mvu_progcache_disk_tests.cpp | 426 ++++++ .../mvu_progcache_versioning_tests.cpp | 720 ++++++++++ 16 files changed, 4396 insertions(+) create mode 100644 pcsx2/arm64/MvuObservedEntries.h create mode 100644 pcsx2/arm64/microVU_Persist-arm64.h create mode 100644 pcsx2/arm64/microVU_Persist-arm64.inl create mode 100644 pcsx2/arm64/microVU_ProgCache-arm64.h create mode 100644 pcsx2/arm64/microVU_ProgCache-arm64.inl create mode 100644 tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp create mode 100644 tests/ctest/core/recompilers/mvu_observed_entries_tests.cpp create mode 100644 tests/ctest/core/recompilers/mvu_persist_roundtrip_tests.cpp create mode 100644 tests/ctest/core/recompilers/mvu_progcache_disk_tests.cpp create mode 100644 tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 0cb16ae0c1..32af247c82 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -366,3 +366,19 @@ endif() fixup_file_properties(pcsx2-qt) setup_main_executable(pcsx2-qt) + +# Opt-in non-PIE link for cross-run VU program cache validation. +# A PIE build is loaded at an ASLR-randomized image base, so a cached +# program's recorded image base mismatches on every cold boot and +# hydration degrades to recompile — correct, but no cross-boot reuse. +# PCSX2_QT_NO_PIE=ON links it ET_EXEC (fixed link address even with +# ASLR), making every libpcsx2 symbol run-invariant. Off by default so +# the normal build is untouched — configure a separate build dir with +# -DPCSX2_QT_NO_PIE=ON for the cache double-boot test. Linux-only; macOS +# has no non-PIE executables. libpcsx2's -fPIC objects link into an +# ET_EXEC fine. +option(PCSX2_QT_NO_PIE "Link pcsx2-qt non-PIE (VU program cache cross-boot validation)" OFF) +if(PCSX2_QT_NO_PIE AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_target_properties(pcsx2-qt PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_link_options(pcsx2-qt PRIVATE -no-pie) +endif() diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index d89df09c34..1d0e010043 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1098,6 +1098,10 @@ set(pcsx2arm64Headers arm64/iR5900-arm64.h arm64/iR5900Analysis.h arm64/microVU-arm64.h + arm64/microVU_Persist-arm64.h + arm64/microVU_Persist-arm64.inl + arm64/microVU_ProgCache-arm64.h + arm64/microVU_ProgCache-arm64.inl arm64/microVU_Misc-arm64.h ) diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 1274ba06af..5958aa9317 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -657,6 +657,12 @@ struct Pcsx2Config EnableFastmem : 1; bool PauseOnTLBMiss : 1; + + // Cache compiled VU micro-programs to disk and reload them across + // sessions to cut recompilation stutter on later runs. arm64-only; + // no-op on x86. + bool + EnableVUProgramCache : 1; BITFIELD_END RecompilerOptions(); diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index d8b77316f9..8fa9ad8608 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -458,6 +458,7 @@ Pcsx2Config::RecompilerOptions::RecompilerOptions() EnableVU1 = true; EnableFastmem = true; PauseOnTLBMiss = false; + EnableVUProgramCache = false; // default off; opt-in until the on-disk cache is validated on the target hardware // vu and fpu clamping default to standard overflow. vu0Overflow = true; @@ -536,6 +537,7 @@ void Pcsx2Config::RecompilerOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBool(EnableVU1); SettingsWrapBitBool(EnableFastmem); SettingsWrapBitBool(PauseOnTLBMiss); + SettingsWrapBitBool(EnableVUProgramCache); SettingsWrapBitBool(vu0Overflow); SettingsWrapBitBool(vu0ExtraOverflow); diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index d416ebc085..c0dc49c3d3 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -2959,6 +2959,11 @@ void VMManager::CheckForCPUConfigChanges(const Pcsx2Config& old_config) Console.WriteLn("Updating CPU configuration..."); FPControlRegister::SetCurrent(EmuConfig.Cpu.FPUFPCR); + + // The VU program cache toggle (EnableVUProgramCache) is picked up by the + // mVUreset that ClearCPUExecutionCaches triggers below — recording and the + // disk cache are re-synced there from the live config, so no explicit sync + // is needed here. Internal::ClearCPUExecutionCaches(); memBindConditionalHandlers(); diff --git a/pcsx2/arm64/MvuObservedEntries.h b/pcsx2/arm64/MvuObservedEntries.h new file mode 100644 index 0000000000..d6b183d3ee --- /dev/null +++ b/pcsx2/arm64/MvuObservedEntries.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +// Fixed-cap set of microMem byte offsets at which the dispatcher has +// handed off into a microProgram. Single-threaded per VU. `version` +// bumps on every new-PC insert so consumers can detect that the entry +// set has grown. Overflow beyond `kMax` silently drops; those entries +// retain JIT coverage but are not tracked here. +// +// Lives in a standalone header so tests can exercise the helper +// without pulling the full microVU-arm64.h surface. +struct MvuObservedEntries +{ + static constexpr u32 kMax = 32; + u32 pcs[kMax]; + u8 count; + u8 pad[3]; + u32 version; + + // Records `startPC_bytes` (microMem byte offset) as a new entry + // point. Returns true if a fresh slot was filled (and `version` + // bumped); false if `startPC_bytes` was already present or the + // cap was hit. + bool record(u32 startPC_bytes); + + // Reset to the empty state. Equivalent to memset-zero, exposed + // so callers don't need to know the layout. + void clear(); +}; diff --git a/pcsx2/arm64/microVU_Persist-arm64.h b/pcsx2/arm64/microVU_Persist-arm64.h new file mode 100644 index 0000000000..6a602e2931 --- /dev/null +++ b/pcsx2/arm64/microVU_Persist-arm64.h @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +#include +#include + +struct microVU; +struct microProgram; +struct microBlock; +struct MvuPersistLog; + +// Persisted-JIT VU program cache — relocation recorder + block-graph +// serializer. +// +// While recording is enabled, every mVU code-cache emission episode (the +// contiguous region emitted between mVUopenCodeCache and mVUcloseCodeCache) +// is captured as a "chunk" on the owning microProgram's persist log: the raw +// code bytes, the blocks whose entry points live inside it, and a fixup table +// for the only address classes that are NOT run-invariant under the +// deterministic process layout: +// +// SelfBlockAbs movz+movk×3 materializing a pointer into one of the +// program's own heap-allocated microBlock objects +// (&pState / &pStateEnd / the block itself). +// BlockEntryRel26 direct B/BL to another block of the same program whose +// code lives in a different chunk. +// StubRel26 direct B/BL to a per-VU dispatcher stub (exitFunct, +// copyPLState, ...) — re-resolved by name on hydration. +// AdrpPage21 ADRP whose target is run-invariant (image/arena) but +// whose page displacement is PC-relative and must be +// re-paged when the chunk moves. +// +// Everything else a block bakes (absolute movz/movk or out-of-range BLR to +// image globals / C helpers, intra-chunk PC-relative branches and literal +// pools) is byte-valid at any 16-aligned slab position in any run with +// matching image/arena bases, and is verified-not-recorded. +// +// Fail-safe invariant: an address the recorder cannot classify marks the +// episode non-persistable and drops it from the log. Hydration of the +// surviving chunk prefix stays sound because chunks only ever reference +// blocks already present in the log (a chunk referencing a dropped block is +// itself dropped). +namespace mVUPersist +{ + // Master gate for emit-time recording. Default off. In production, + // VMManager syncs it from the EmuCore/CPU/Recompiler EnableVUProgramCache + // config bool (at CPU-provider init and on settings changes, before the + // recompiler clear); tests and pcsx2-vurunner set it directly. Must be + // set before the programs to persist are compiled — already-compiled + // episodes are not retroactively recorded. + // + // Changing it changes emitted code forms (canonical movs / forced-long + // cond branches), so it is mixed into the mVU options sentinel: a state + // flip invalidates both VUs' sentinels, re-keying program identity and + // the ProgCache VERSION handshake. SetProcessDisable(true) (the + // offline-tooling determinism gate) overrides any enable request. + void SetRecordingEnabled(bool enabled); + bool IsRecordingEnabled(); + + // Process-wide determinism kill switch for the offline tools. When set, + // both the on-disk program cache and emit-time recording are forced off + // for the whole process, making a JIT-vs-interp diff run byte-reproducible. + // Driven by pcsx2-vurunner --no-progcache. Replaces the former + // PCSX2_VU_PROGCACHE_DISABLE env var — no env gate in production code. + void SetProcessDisable(bool disable); + bool IsProcessDisabled(); + + // Production recording sync: set recording to match the + // EnableVUProgramCache config bool, unless the test-manual override is + // engaged. Called from mVUinit/mVUreset before the options sentinel is + // rebuilt, so the recording byte the sentinel bakes — and every program + // compiled after the reset — reflects the live config. This is the + // authoritative sync point (InitializeCPUProviders runs before settings + // load the bool, so a one-shot enable there would latch the default). + void SyncRecordingFromConfig(bool config_enabled); + + // Test-only: disable SyncRecordingFromConfig so the recompiler-test + // harness can drive recording manually. Called once by + // RecompilerTestEnvironment. No effect in production. + void SetTestManualRecording(bool manual); + + // --- microVU integration points (called from the mVU core) --- + + // mVUopenCodeCache bound armAsm: begin a chunk. No-op when disabled. + void BeginEpisode(microVU& mVU, u8* chunkBase); + // mVUcloseCodeCache is about to unbind: finalize the chunk onto the + // owning program's persist log (or drop it if the episode failed). + // `chunkEnd` is the cursor after FinalizeCode (literal pool included). + void EndEpisode(microVU& mVU, u8* chunkEnd); + // mVUinitFirstPass registered a block with the block manager. Also + // counts block compiles for the round-trip "no recompile" gate. + void OnBlockCompiled(microVU& mVU, microBlock* block, u8* entry, u32 startPC_bytes); + // mVUdeleteProg: free the program's persist log, if any. + void OnProgramDeleted(microProgram& prog); + + // --- Serialization / hydration --- + + // Serialize a program's recorded block graph into `out`. Returns false + // if the program has no log, the log is non-persistable, or recording + // missed part of the program. + bool SerializeProgram(microVU& mVU, const microProgram& prog, std::vector& out); + + // Rebuild a program from a serialized image: verifies the layout bases + // and the content hash against live microMem, copies the chunks to the + // current code-cache cursor, patches fixups, and registers every block + // with a fresh microProgram (created via mVUcreateProg, so the normal + // dispatch path resolves it through the content map). Returns the new + // program, or nullptr on any mismatch (caller falls back to recompile). + // Must be called with the code cache CLOSED (it opens its own episode). + microProgram* HydrateProgram(microVU& mVU, const u8* data, size_t size); + + // --- Test hooks (vu_index-keyed so test TUs don't need microVU types) --- + + // Serialize the most-recently-created program of the given VU. + bool TestSerializeNewestProgram(u32 vu_index, std::vector& out); + // Hydrate into the given VU from a serialized image. Returns true on + // success. The guest program bytes must already be in VU Micro memory + // (the content hash is verified against them). + bool TestHydrate(u32 vu_index, const u8* data, size_t size); + // Compare the live code bytes of the most recent TestHydrate against the + // chunk bytes in `image`, skipping the spans covered by fixup records. + // Proves hydrated code is bit-identical modulo relocation operands. + // Test-only (reads s_lastHydratedChunkBase, gated out of Release). +#ifdef PCSX2_RECOMPILER_TESTS + bool TestVerifyHydratedCode(u32 vu_index, const u8* image, size_t size); +#endif + // Total mVU block compiles since process start (bumped in + // mVUinitFirstPass regardless of recording state). + u64 GetBlockCompileCount(u32 vu_index); + // Structural digest of the newest program's serialized image with every + // address-bearing operand masked (sf=1 movz/movn/movk imm16, B/BL + // imm26, ADRP imm, fixup target fields). What survives is the emitter's + // SHAPE — opcode selection, register allocation, instruction order, + // block/chunk/fixup structure — which is deterministic across runs, + // machines, and PIE/ASLR. The ABI-digest guard test pins this per + // kMvuCompilerAbiVersion: emitted-form drift without an ABI bump (= + // stale on-disk programs would run wrong-shaped code) goes red there. + bool TestComputeEmitDigest(u32 vu_index, u64& out_digest); + + struct Stats + { + u64 chunksRecorded = 0; + u64 chunksDropped = 0; + u64 blocksRecorded = 0; + u64 fixupsRecorded = 0; + u64 programsHydrated = 0; + u64 blocksHydrated = 0; + u64 hydrationRejects = 0; + }; + Stats GetStats(u32 vu_index); +} diff --git a/pcsx2/arm64/microVU_Persist-arm64.inl b/pcsx2/arm64/microVU_Persist-arm64.inl new file mode 100644 index 0000000000..61a012fb51 --- /dev/null +++ b/pcsx2/arm64/microVU_Persist-arm64.inl @@ -0,0 +1,1227 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Implementation of microVU_Persist-arm64.h. Included at the bottom of +// microVU-arm64.cpp (same pattern as microVU_ProgCache-arm64.inl) so it sees +// the full microVU/microProgram/microBlock types plus the TU-local helpers +// (mVUopenCodeCache / mVUcloseCodeCache / mVUcreateProg / mVUcomputeProgramHash). + +#include "arm64/microVU_Persist-arm64.h" +#include "Memory.h" + +#ifdef __linux__ +#include +#endif + +#include +#include +#include +#include + +namespace mVUPersist +{ + enum : u8 + { + kFixSelfBlockAbs = 0, // movz+movk×3 → &log.blocks[blockIndex] + fieldOffset + kFixBlockEntryRel26, // B/BL → entry of log.blocks[blockIndex] + kFixStubRel26, // B/BL → per-VU dispatcher stub `stubId` + kFixAdrpPage21, // ADRP → run-invariant absolute `target` + }; + + enum : u8 + { + kStubStartFunct = 0, + kStubExitFunct, + kStubStartFunctXG, + kStubExitFunctXG, + kStubWaitMTVU, + kStubCopyPLState, + kStubEndFlagsA, + kStubEndFlagsB, + kStubResumeXG, + kStubCount, + }; + + struct PersistFixup + { + u32 codeOffset; // chunk-relative offset of the first patched insn + u8 kind; + u8 stubId; + u16 fieldOffset; // kFixSelfBlockAbs: byte offset within microBlock + u32 blockIndex; // kFixSelfBlockAbs / kFixBlockEntryRel26 + u32 _pad; + u64 target; // kFixAdrpPage21: absolute target; debug aid otherwise + }; + static_assert(sizeof(PersistFixup) == 24); + + struct PersistChunk + { + std::vector code; + std::vector fixups; + }; + + struct PersistBlockRec + { + u32 chunkIndex; + u32 entryOffset; // within chunk + u32 startPC; // microMem byte offset + u32 hasJumpCache; + microRegInfo pState; + microRegInfo pStateEnd; + microBlock* live; // manager's copy; not serialized + }; +} // namespace mVUPersist + +// Global-scope definition (fwd-declared in the header so microProgram can +// hold a pointer without pulling the implementation types). +struct MvuPersistLog +{ + std::vector chunks; + std::vector blocks; + // hostEntry → blocks[] index, for cross-chunk branch resolution. Keyed on + // the manager's hostEntry (what armEmitJmp call sites actually target). + std::unordered_map blockByEntry; +}; + +namespace mVUPersist +{ + static bool s_recordingEnabled = false; + // When true, mVUinit/mVUreset's SyncRecordingFromConfig is a no-op so the + // unit-test harness keeps manual control of recording (ABI-digest pins, + // in-process round-trips, disk round-trips). Default false = production, + // where recording follows EnableVUProgramCache at every reset. + static bool s_manualRecording = false; + static u64 s_blockCompiles[2] = {}; + static Stats s_stats[2] = {}; + + // Process-wide determinism kill switch for the offline tools (pcsx2-vurunner + // --no-progcache). When set, both the on-disk program cache and emit-time + // recording are forced off for the whole process, so a JIT-vs-interp diff run + // is byte-reproducible. Replaces the former PCSX2_VU_PROGCACHE_DISABLE env var + // — no env gate in production code. Default false = no effect. + static bool s_processDisabled = false; + + //------------------------------------------------------------------ + // Address classification + //------------------------------------------------------------------ + + // Run-invariant static ranges under the deterministic layout: + // the executable image (incl. bss) and the fixed-base data arena. The + // code arena is deliberately NOT here — slab addresses are exactly the + // relocatable class and must resolve to stubs/blocks or fail. + struct InvariantRanges + { + uptr imgBegin = 0, imgEnd = 0; + uptr dataBegin = 0, dataEnd = 0; + bool valid = false; + }; + + static bool ResolveImageRange(uptr& out_begin, uptr& out_end) + { +#ifdef __linux__ + char exe_path[512]; + const ssize_t exe_len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (exe_len <= 0) + return false; + exe_path[exe_len] = 0; + + std::FILE* maps = std::fopen("/proc/self/maps", "r"); + if (!maps) + return false; + + uptr begin = 0, end = 0; + bool extended = false; + char line[1024]; + while (std::fgets(line, sizeof(line), maps)) + { + unsigned long long m_begin = 0, m_end = 0; + char perms[8] = {}; + unsigned long long offset = 0; + unsigned dev_major = 0, dev_minor = 0; + unsigned long long inode = 0; + int path_pos = -1; + if (std::sscanf(line, "%llx-%llx %7s %llx %x:%x %llu %n", + &m_begin, &m_end, perms, &offset, &dev_major, &dev_minor, &inode, &path_pos) < 7) + continue; + const char* path = (path_pos >= 0) ? line + path_pos : ""; + // Trim trailing newline for the comparison. + std::string_view pv(path); + while (!pv.empty() && (pv.back() == '\n' || pv.back() == ' ')) + pv.remove_suffix(1); + + if (pv == exe_path) + { + if (!begin) + begin = static_cast(m_begin); + end = static_cast(m_end); + // A fresh file segment: the bss following the *final* file + // segment is the only anonymous region we want to absorb, so + // re-arm the one-shot extension on every file mapping. + extended = false; + } + else if (!extended && end && static_cast(m_begin) == end && pv.empty()) + { + // Anonymous mapping contiguous with the image's last file + // mapping: the bss. Extend exactly once — a run of contiguous + // unnamed mappings (glibc arena, ld scratch) must NOT chain the + // range past true bss. ([heap]/named regions also stop here.) + end = static_cast(m_end); + extended = true; + } + } + std::fclose(maps); + + if (!begin || end <= begin) + return false; + out_begin = begin; + out_end = end; + return true; +#else + return false; +#endif + } + + static const InvariantRanges& GetInvariantRanges() + { + static InvariantRanges r = []() { + InvariantRanges out; + if (!ResolveImageRange(out.imgBegin, out.imgEnd)) + { + DevCon.Warning("mVUPersist: could not resolve executable image range — recording will mark all programs non-persistable"); + return out; + } + // Sanity: a known bss global must land inside the resolved image + // range; if it doesn't, the maps parse is wrong and trusting it + // would misclassify heap pointers as invariant (silent corruption). + const uptr probe = reinterpret_cast(µVU0); + if (probe < out.imgBegin || probe >= out.imgEnd) + { + DevCon.Warning("mVUPersist: image-range sanity probe failed (µVU0=%p not in [%p,%p)) — recording disabled", + µVU0, (void*)out.imgBegin, (void*)out.imgEnd); + return out; + } + out.dataBegin = reinterpret_cast(SysMemory::GetDataPtr(0)); + out.dataEnd = out.dataBegin + HostMemoryMap::MainSize; + out.valid = true; + return out; + }(); + return r; + } + + static bool IsInvariantAddress(const void* addr) + { + const InvariantRanges& r = GetInvariantRanges(); + if (!r.valid) + return false; + const uptr a = reinterpret_cast(addr); + return (a >= r.imgBegin && a < r.imgEnd) || (a >= r.dataBegin && a < r.dataEnd); + } + + //------------------------------------------------------------------ + // Dispatcher stub table + //------------------------------------------------------------------ + + static const void* StubAddress(microVU& mVU, u8 id) + { + switch (id) + { + case kStubStartFunct: return mVU.startFunct; + case kStubExitFunct: return mVU.exitFunct; + case kStubStartFunctXG: return mVU.startFunctXG; + case kStubExitFunctXG: return mVU.exitFunctXG; + case kStubWaitMTVU: return mVU.waitMTVU; + case kStubCopyPLState: return mVU.copyPLState; + case kStubEndFlagsA: return mVU.endProgramFlagsA; + case kStubEndFlagsB: return mVU.endProgramFlagsB; + case kStubResumeXG: return mVU.resumePtrXG; + default: return nullptr; + } + } + + static bool ResolveStub(microVU& mVU, const void* target, u8& out_id) + { + for (u8 i = 0; i < kStubCount; i++) + { + if (StubAddress(mVU, i) == target && target) + { + out_id = i; + return true; + } + } + return false; + } + + //------------------------------------------------------------------ + // Emit-time recorder + //------------------------------------------------------------------ + + class Recorder final : public ArmAddressRecorder + { + public: + struct EpisodeBlock + { + microBlock* live; + u32 entryOffset; + u32 startPC; + }; + + microVU* mvu = nullptr; + u8* chunkBase = nullptr; + microProgram* prog = nullptr; + MvuPersistLog* log = nullptr; + std::vector fixups; + std::vector episodeBlocks; + bool active = false; + bool failed = false; + const char* failReason = nullptr; + + void Begin(microVU& m, u8* base) + { + pxAssert(!active); + mvu = &m; + chunkBase = base; + prog = nullptr; + log = nullptr; + fixups.clear(); + episodeBlocks.clear(); + active = true; + failed = false; + failReason = nullptr; + } + + void Fail(const char* reason) + { + if (!failed) + { + failed = true; + failReason = reason; + } + } + + bool InSlab(const void* addr) const + { + const u8* a = static_cast(addr); + return a >= mvu->cache && a < mvu->prog.x86end; + } + + bool InChunk(const void* addr) const + { + const u8* a = static_cast(addr); + return a >= chunkBase && a < armGetCurrentCodePointer(); + } + + u32 OffsetOf(u8* at) const + { + pxAssert(at >= chunkBase); + return static_cast(at - chunkBase); + } + + // Index a block of this episode will get once committed. + u32 ProvisionalIndex(size_t episode_pos) const + { + return static_cast((log ? log->blocks.size() : 0) + episode_pos); + } + + // --- ArmAddressRecorder --- + + MoveForm ClassifyMove(const void* addr) override + { + const u8* a = static_cast(addr); + for (const EpisodeBlock& eb : episodeBlocks) + { + const u8* blk = reinterpret_cast(eb.live); + if (a >= blk && a < blk + sizeof(microBlock)) + return MoveForm::CanonicalAbs; + } + if (IsInvariantAddress(addr)) + return MoveForm::Default; + if (InSlab(addr)) + Fail("address-of-code materialized outside branch helpers"); + else + Fail("unclassifiable pointer materialized (heap?)"); + return MoveForm::Default; + } + + void OnCanonicalAbsMove(u8* at, const void* addr) override + { + const u8* a = static_cast(addr); + for (size_t i = 0; i < episodeBlocks.size(); i++) + { + const u8* blk = reinterpret_cast(episodeBlocks[i].live); + if (a >= blk && a < blk + sizeof(microBlock)) + { + PersistFixup f = {}; + f.codeOffset = OffsetOf(at); + f.kind = kFixSelfBlockAbs; + f.fieldOffset = static_cast(a - blk); + f.blockIndex = ProvisionalIndex(i); + f.target = reinterpret_cast(addr); + fixups.push_back(f); + return; + } + } + Fail("canonical move target lost between classify and emit"); + } + + void OnAdrp(u8* at, const void* addr) override + { + if (IsInvariantAddress(addr)) + { + PersistFixup f = {}; + f.codeOffset = OffsetOf(at); + f.kind = kFixAdrpPage21; + f.target = reinterpret_cast(addr); + fixups.push_back(f); + return; + } + Fail("ADRP to non-invariant target"); + } + + void OnDirectBranch(u8* at, const void* target, bool is_call) override + { + if (InChunk(target)) + return; // relative distance preserved on relocation + + u8 stub_id = 0; + if (ResolveStub(*mvu, target, stub_id)) + { + PersistFixup f = {}; + f.codeOffset = OffsetOf(at); + f.kind = kFixStubRel26; + f.stubId = stub_id; + f.target = reinterpret_cast(target); + fixups.push_back(f); + return; + } + + if (log) + { + const auto it = log->blockByEntry.find(target); + if (it != log->blockByEntry.end()) + { + PersistFixup f = {}; + f.codeOffset = OffsetOf(at); + f.kind = kFixBlockEntryRel26; + f.blockIndex = it->second; + f.target = reinterpret_cast(target); + fixups.push_back(f); + return; + } + } + + Fail(InSlab(target) ? "direct branch to unrecorded slab target" + : "direct branch to non-slab target"); + } + + bool WantsLongCondBranch(const void* target) override + { + // Intra-chunk: short form relocates fine. Cross-chunk: needs the + // imm26 reach of a plain B so the patcher can retarget it. + return !InChunk(target) && InSlab(target); + } + + void OnAbsoluteTarget(const void* target) override + { + if (IsInvariantAddress(target)) + return; + Fail(InSlab(target) ? "absolute slab address baked" + : "absolute unclassifiable address baked"); + } + + // --- episode lifecycle --- + + void RegisterBlock(microBlock* block, u8* entry, u32 startPC_bytes) + { + if (failed) + return; + + microProgram* cur = mvu->prog.cur; + if (!cur) + return; + if (!prog) + { + prog = cur; + if (!prog->persist) + prog->persist = new MvuPersistLog(); + log = prog->persist; + } + else if (prog != cur) + { + // mVUsearchProg switched programs mid-episode (JR/JALR slow + // path). Splitting the chunk at the switch is possible but + // not worth it — drop the episode. + Fail("program switch mid-episode"); + return; + } + + episodeBlocks.push_back({block, OffsetOf(entry), startPC_bytes}); + } + + void End(u8* chunkEnd) + { + pxAssert(active); + active = false; + + const u32 vu = mvu->index & 1; + if (episodeBlocks.empty() && fixups.empty()) + return; // nothing emitted (pure lookup episode / hydration) + + if (failed || episodeBlocks.empty() || !log) + { + s_stats[vu].chunksDropped++; + if (failed) + DevCon.WriteLn("mVUPersist: VU%u chunk dropped (%s)", vu, failReason); + return; + } + + // add() may have deduped a block against an existing variant (the + // manager then keeps the OLD entry and this episode's copy of the + // code is unreachable). Skipping just that record would shift the + // provisional indices already baked into SelfBlockAbs fixups — + // drop the whole episode instead. Rare corner, conservative. + for (const EpisodeBlock& eb : episodeBlocks) + { + if (eb.live->hostEntry != chunkBase + eb.entryOffset) + { + s_stats[vu].chunksDropped++; + DevCon.WriteLn("mVUPersist: VU%u chunk dropped (block manager deduped a variant)", vu); + return; + } + } + + const u32 chunkIndex = static_cast(log->chunks.size()); + for (const EpisodeBlock& eb : episodeBlocks) + { + PersistBlockRec rec = {}; + rec.chunkIndex = chunkIndex; + rec.entryOffset = eb.entryOffset; + rec.startPC = eb.startPC; + rec.hasJumpCache = (eb.live->jumpCache != nullptr) ? 1 : 0; + std::memcpy(&rec.pState, &eb.live->pState, sizeof(microRegInfo)); + std::memcpy(&rec.pStateEnd, &eb.live->pStateEnd, sizeof(microRegInfo)); + rec.live = eb.live; + log->blockByEntry.emplace(eb.live->hostEntry, static_cast(log->blocks.size())); + log->blocks.push_back(rec); + s_stats[vu].blocksRecorded++; + } + + PersistChunk chunk; + chunk.code.assign(chunkBase, chunkEnd); + chunk.fixups = std::move(fixups); + s_stats[vu].fixupsRecorded += chunk.fixups.size(); + log->chunks.push_back(std::move(chunk)); + s_stats[vu].chunksRecorded++; + } + }; + + // VU0 episodes run on the EE thread, VU1 on the MTVU thread — same + // affinity as armAsm itself, so per-VU recorder objects are safe. + static Recorder s_recorder[2]; + + void SetRecordingEnabled(bool enabled) + { + // The offline-tooling determinism gate trumps every enable request: + // recording changes emitted code forms, and SetProcessDisable(true) + // (pcsx2-vurunner --no-progcache) promises a byte-reproducible JIT. + if (enabled && s_processDisabled) + enabled = false; + if (enabled == s_recordingEnabled) + return; + s_recordingEnabled = enabled; + + // Recording state is part of program identity (canonical movs / + // forced-long cond branches change the bytes a program compiles to), + // so flipping it invalidates both VUs' options sentinels. The next + // consumer — mVUcomputeProgramHash at dispatch, mVUbuildOptionsSentinel + // at init/reset, the ProgCache VERSION handshake — rebuilds with the + // new state mixed in. Benign race: an MTVU compile between this store + // and the recompiler reset that follows a config toggle hashes against + // the new sentinel inside a cache dir keyed to the old one; the entry + // is unreachable and the dir is evicted on the next handshake. + microVU0.optionsSentinelValid = false; + microVU1.optionsSentinelValid = false; + } + bool IsRecordingEnabled() { return s_recordingEnabled; } + + void SetProcessDisable(bool disable) { s_processDisabled = disable; } + bool IsProcessDisabled() { return s_processDisabled; } + + void SyncRecordingFromConfig(bool config_enabled) + { + // Production: recording follows EnableVUProgramCache, called from + // mVUinit/mVUreset before the options sentinel is rebuilt. The + // test-manual override lets the recompiler-test harness drive + // recording itself without these reset-time syncs clobbering it. + if (s_manualRecording) + return; + const bool was = s_recordingEnabled; + SetRecordingEnabled(config_enabled); + if (s_recordingEnabled != was) + { + // One line per state change — the authoritative signal that the + // persisted-JIT cache will (or won't) write .vuprog payloads this + // session. Cheap to grep in emulog when diagnosing a "no payloads" + // report. DevCon so it stays out of production logs but remains + // greppable in Devel / raised-loglevel during bring-up. + DevCon.WriteLn(Color_StrongBlue, + "mVUProgCache: persisted-JIT recording %s (VuJitProgramCache=%d)", + s_recordingEnabled ? "ENABLED" : "disabled", + config_enabled ? 1 : 0); + } + } + + void SetTestManualRecording(bool manual) { s_manualRecording = manual; } + + void BeginEpisode(microVU& mVU, u8* chunkBase) + { + if (!s_recordingEnabled) + return; + Recorder& r = s_recorder[mVU.index & 1]; + r.Begin(mVU, chunkBase); + armAddressRecorder = &r; + } + + void EndEpisode(microVU& mVU, u8* chunkEnd) + { + Recorder& r = s_recorder[mVU.index & 1]; + if (!r.active) + return; + pxAssert(armAddressRecorder == &r); + armAddressRecorder = nullptr; + r.End(chunkEnd); + } + + void OnBlockCompiled(microVU& mVU, microBlock* block, u8* entry, u32 startPC_bytes) + { + const u32 vu = mVU.index & 1; + s_blockCompiles[vu]++; + Recorder& r = s_recorder[vu]; + if (r.active && armAddressRecorder == &r) + r.RegisterBlock(block, entry, startPC_bytes); + } + + void OnProgramDeleted(microProgram& prog) + { + delete prog.persist; + prog.persist = nullptr; + } + + //------------------------------------------------------------------ + // Serialization + //------------------------------------------------------------------ + + static constexpr u32 kImageMagic = 0x5055564Du; // 'MVUP' + static constexpr u32 kImageVersion = 1; + + struct ImageHeader + { + u32 magic; + u32 version; + u32 vuIndex; + u32 flags; + u64 hashLo; + u64 hashHi; + u64 imageAnchor; // µVU0 — any bss global; fixed under non-PIE + u64 dataArena; // SysMemory::GetDataPtr(0) + u64 codeArena; // SysMemory::GetCodePtr(0) + u32 progStartPC; // microMem byte offset of the creating entry + u32 rangeCount; + u32 blockCount; + u32 chunkCount; + u64 payloadHash; // XXH3-64 of every byte after this header — guards + // the on-disk .vuprog against bit rot / partial + // writes the structural checks can't see (a flipped + // CODE byte is valid structure but wrong code) + u32 reserved[2]; + }; + static_assert(sizeof(ImageHeader) == 88); + + struct DiskRange + { + s32 start; + s32 end; + }; + + struct DiskBlock + { + u32 chunkIndex; + u32 entryOffset; + u32 startPC; + u32 hasJumpCache; + u8 pState[sizeof(microRegInfo)]; + u8 pStateEnd[sizeof(microRegInfo)]; + }; + + struct DiskChunkHeader + { + u32 codeSize; + u32 fixupCount; + }; + + template + static void AppendPod(std::vector& out, const T& v) + { + const u8* p = reinterpret_cast(&v); + out.insert(out.end(), p, p + sizeof(T)); + } + + bool SerializeProgram(microVU& mVU, const microProgram& prog, std::vector& out) + { + const MvuPersistLog* log = prog.persist; + if (!log || log->blocks.empty() || !prog.contentHashValid) + return false; + + out.clear(); + ImageHeader hdr = {}; + hdr.magic = kImageMagic; + hdr.version = kImageVersion; + hdr.vuIndex = mVU.index; + hdr.hashLo = prog.contentHash.low64; + hdr.hashHi = prog.contentHash.high64; + hdr.imageAnchor = reinterpret_cast(µVU0); + hdr.dataArena = reinterpret_cast(SysMemory::GetDataPtr(0)); + hdr.codeArena = reinterpret_cast(SysMemory::GetCodePtr(0)); + hdr.progStartPC = static_cast(prog.startPC) * 8u; + hdr.rangeCount = prog.ranges ? static_cast(prog.ranges->size()) : 0; + hdr.blockCount = static_cast(log->blocks.size()); + hdr.chunkCount = static_cast(log->chunks.size()); + AppendPod(out, hdr); + + if (prog.ranges) + { + for (const microRange& mr : *prog.ranges) + AppendPod(out, DiskRange{mr.start, mr.end}); + } + + for (const PersistBlockRec& rec : log->blocks) + { + DiskBlock db = {}; + db.chunkIndex = rec.chunkIndex; + db.entryOffset = rec.entryOffset; + db.startPC = rec.startPC; + db.hasJumpCache = rec.hasJumpCache; + std::memcpy(db.pState, &rec.pState, sizeof(microRegInfo)); + std::memcpy(db.pStateEnd, &rec.pStateEnd, sizeof(microRegInfo)); + AppendPod(out, db); + } + + for (const PersistChunk& chunk : log->chunks) + { + AppendPod(out, DiskChunkHeader{static_cast(chunk.code.size()), + static_cast(chunk.fixups.size())}); + for (const PersistFixup& f : chunk.fixups) + AppendPod(out, f); + out.insert(out.end(), chunk.code.begin(), chunk.code.end()); + } + + // Stamp the payload checksum into the already-appended header. + const u64 payload_hash = XXH3_64bits( + out.data() + sizeof(ImageHeader), out.size() - sizeof(ImageHeader)); + std::memcpy(out.data() + offsetof(ImageHeader, payloadHash), + &payload_hash, sizeof(payload_hash)); + return true; + } + + //------------------------------------------------------------------ + // Instruction patchers + //------------------------------------------------------------------ + + static bool PatchAbsMov(u32* insn, u64 value) + { + // movz xN, #imm16 (hw=0) — 0xD2800000 + // movk xN, #imm16, lsl #s (hw=s/16) — 0xF2800000 + static constexpr u32 kOpMask = 0xFF800000u; + static constexpr u32 kMovz = 0xD2800000u; + static constexpr u32 kMovk = 0xF2800000u; + for (int i = 0; i < 4; i++) + { + const u32 expect_op = (i == 0) ? kMovz : kMovk; + const u32 expect_hw = static_cast(i) << 21; + if ((insn[i] & kOpMask) != expect_op || (insn[i] & 0x00600000u) != expect_hw) + return false; + const u32 imm16 = static_cast((value >> (16 * i)) & 0xFFFFu); + insn[i] = (insn[i] & ~(0xFFFFu << 5)) | (imm16 << 5); + } + return true; + } + + static bool PatchRel26(u32* insn, const u8* at, const void* target) + { + // B = 0x14000000, BL = 0x94000000 — bit 31 selects, imm26 in [25:0]. + if ((*insn & 0x7C000000u) != 0x14000000u) + return false; + const s64 disp = GetPCDisplacement(at, target); + if (!vixl::IsInt26(disp)) + return false; + *insn = (*insn & 0xFC000000u) | (static_cast(disp) & 0x03FFFFFFu); + return true; + } + + static bool PatchAdrpPage21(u32* insn, const u8* at, u64 target) + { + // ADRP = 0x90000000 | immlo[30:29] | immhi[23:5]. + if ((*insn & 0x9F000000u) != 0x90000000u) + return false; + const s64 page_disp = static_cast(target >> 12) - + static_cast(reinterpret_cast(at) >> 12); + if (!vixl::IsInt21(page_disp)) + return false; + const u32 immlo = static_cast(page_disp) & 3u; + const u32 immhi = (static_cast(page_disp) >> 2) & 0x7FFFFu; + *insn = (*insn & 0x9F00001Fu) | (immlo << 29) | (immhi << 5); + return true; + } + + //------------------------------------------------------------------ + // Hydration + //------------------------------------------------------------------ + +#ifdef PCSX2_RECOMPILER_TESTS + // Placement of the most recent hydration, for TestVerifyHydratedCode. + // Test-only state — never written in a hooks-off Release build. + static std::vector s_lastHydratedChunkBase[2]; +#endif + + template + static bool ReadPod(const u8*& p, const u8* end, T& out) + { + if (static_cast(end - p) < sizeof(T)) + return false; + std::memcpy(&out, p, sizeof(T)); + p += sizeof(T); + return true; + } + + microProgram* HydrateProgram(microVU& mVU, const u8* data, size_t size) + { + const u32 vu = mVU.index & 1; + const u8* p = data; + const u8* const end = data + size; + + ImageHeader hdr; + if (!ReadPod(p, end, hdr) || hdr.magic != kImageMagic || hdr.version != kImageVersion || + hdr.vuIndex != mVU.index) + { + s_stats[vu].hydrationRejects++; + return nullptr; + } + + // Payload integrity. The structural checks below validate shape, not + // content — a flipped code byte parses fine and runs wrong. + if (XXH3_64bits(p, static_cast(end - p)) != hdr.payloadHash) + { + DevCon.Warning("mVUPersist: VU%u hydration rejected — payload checksum mismatch", vu); + s_stats[vu].hydrationRejects++; + return nullptr; + } + + // Layout-base verification: every unrecorded address class in the + // chunks assumes these. A PIE dev build / layout drift fails here and + // falls back to recompile (degrade, never corrupt). + if (hdr.imageAnchor != reinterpret_cast(µVU0) || + hdr.dataArena != reinterpret_cast(SysMemory::GetDataPtr(0)) || + hdr.codeArena != reinterpret_cast(SysMemory::GetCodePtr(0))) + { + DevCon.Warning("mVUPersist: VU%u hydration rejected — layout bases differ", vu); + s_stats[vu].hydrationRejects++; + return nullptr; + } + + // Identity: the image must describe the program bytes currently in + // micro memory. + const XXH128_hash_t live_hash = mVUcomputeProgramHash(mVU); + if (hdr.hashLo != live_hash.low64 || hdr.hashHi != live_hash.high64) + { + s_stats[vu].hydrationRejects++; + return nullptr; + } + + std::vector ranges(hdr.rangeCount); + for (DiskRange& r : ranges) + if (!ReadPod(p, end, r)) + return nullptr; + + std::vector blocks(hdr.blockCount); + for (DiskBlock& b : blocks) + if (!ReadPod(p, end, b)) + return nullptr; + + struct ParsedChunk + { + const u8* code; + u32 codeSize; + std::vector fixups; + }; + std::vector chunks(hdr.chunkCount); + size_t total_code = 0; + for (ParsedChunk& c : chunks) + { + DiskChunkHeader ch; + if (!ReadPod(p, end, ch)) + return nullptr; + c.fixups.resize(ch.fixupCount); + for (PersistFixup& f : c.fixups) + if (!ReadPod(p, end, f)) + return nullptr; + if (static_cast(end - p) < ch.codeSize) + return nullptr; + c.code = p; + c.codeSize = ch.codeSize; + p += ch.codeSize; + total_code += ch.codeSize + 16; + } + + // Structural validation before any side effects. + for (const DiskBlock& b : blocks) + { + if (b.chunkIndex >= chunks.size() || b.startPC >= mVU.microMemSize || + (b.startPC & 7) || b.entryOffset >= chunks[b.chunkIndex].codeSize) + return nullptr; + } + for (const ParsedChunk& c : chunks) + { + for (const PersistFixup& f : c.fixups) + { + const u32 span = (f.kind == kFixSelfBlockAbs) ? 16 : 4; + if (f.codeOffset + span > c.codeSize) + return nullptr; + if ((f.kind == kFixSelfBlockAbs || f.kind == kFixBlockEntryRel26) && + f.blockIndex >= blocks.size()) + return nullptr; + if (f.kind == kFixStubRel26 && f.stubId >= kStubCount) + return nullptr; + if (f.kind > kFixAdrpPage21) + return nullptr; + } + } + + // Capacity: refuse rather than trip the mid-hydration cache-full reset. + const size_t cache_free = static_cast(mVU.prog.x86end - mVU.prog.x86ptr); + if (total_code + (mVUcacheSafeZone * _1mb) > cache_free) + { + s_stats[vu].hydrationRejects++; + return nullptr; + } + + pxAssert(!armAsm); // caller must not have an open emission episode + mVUopenCodeCache(mVU); + + microProgram* prog = mVUcreateProg(mVU, static_cast(hdr.progStartPC / 8)); + + // Restore compiled ranges + the range-compare image (mVUcmpProg under + // !doWholeProgCompare diffs prog.data inside these windows; the live + // microMem is identity-verified above, so copying from it is exact). + prog->ranges->clear(); + for (const DiskRange& r : ranges) + { + prog->ranges->push_back(microRange{r.start, r.end}); + if (!doWholeProgCompare && r.start >= 0 && r.end > r.start && + static_cast(r.end) <= mVU.microMemSize) + { + std::memcpy(reinterpret_cast(prog->data) + r.start, + mVU.regs().Micro + r.start, static_cast(r.end - r.start)); + } + } + + // Copy each chunk to the current cursor (16-aligned, as recorded). + std::vector chunk_base(chunks.size()); + for (size_t i = 0; i < chunks.size(); i++) + { + while (armAsm->GetCursorOffset() & 15) + armAsm->Nop(); + chunk_base[i] = mVU.prog.x86start + armAsm->GetCursorOffset(); + { + vixl::CodeBufferCheckScope scope(armAsm, chunks[i].codeSize, + vixl::CodeBufferCheckScope::kReserveBufferSpace, + vixl::CodeBufferCheckScope::kNoAssert); + armAsm->GetBuffer()->EmitData(chunks[i].code, chunks[i].codeSize); + } + } + + // Register every block with the program's managers. add() dedupes by + // pState, mirroring compile-time behavior. + std::vector final_block(blocks.size()); + for (size_t i = 0; i < blocks.size(); i++) + { + const DiskBlock& b = blocks[i]; + const u32 slot = b.startPC / 8; + if (!prog->block[slot]) + prog->block[slot] = new microBlockManager(); + + microBlock tmp; + std::memcpy(&tmp.pState, b.pState, sizeof(microRegInfo)); + std::memcpy(&tmp.pStateEnd, b.pStateEnd, sizeof(microRegInfo)); + tmp.x86ptrStart = chunk_base[b.chunkIndex] + b.entryOffset; + tmp.hostEntry = tmp.x86ptrStart; + tmp.jumpCache = nullptr; + + microBlock* installed = prog->block[slot]->add(mVU, &tmp); + // mVUcompileJIT dereferences jumpCache unguarded (it was allocated + // at compile time by normJumpCompile) — restore it eagerly. + if (b.hasJumpCache && !installed->jumpCache) + installed->jumpCache = new microJumpCache[mProgSize / 2]; + final_block[i] = installed; + } + + // Patch fixups directly in the slab (still inside the BeginCodeWrite + // window; mVUcloseCodeCache flushes the icache over the whole span). + for (size_t i = 0; i < chunks.size(); i++) + { + for (const PersistFixup& f : chunks[i].fixups) + { + u8* at = chunk_base[i] + f.codeOffset; + u32* insn = reinterpret_cast(at); + bool ok = false; + switch (f.kind) + { + case kFixSelfBlockAbs: + ok = PatchAbsMov(insn, + reinterpret_cast(final_block[f.blockIndex]) + f.fieldOffset); + break; + case kFixBlockEntryRel26: + ok = PatchRel26(insn, at, final_block[f.blockIndex]->hostEntry); + break; + case kFixStubRel26: + ok = PatchRel26(insn, at, StubAddress(mVU, f.stubId)); + break; + case kFixAdrpPage21: + ok = PatchAdrpPage21(insn, at, f.target); + break; + } + if (!ok) + { + // Encoding mismatch — the image doesn't describe this + // build's emitter output. Abandon: unregister nothing + // (blocks point at fully-written code; the only unsound + // state would be a half-patched chunk), so fail hard + // before any block can run. + pxFailRel("mVUPersist: fixup patch failed — serialized image inconsistent with emitter"); + mVUcloseCodeCache(mVU); + return nullptr; + } + } + } + + mVUcloseCodeCache(mVU); + + // Rebuild the persist log so the hydrated program can grow new chunks + // and be re-serialized. + delete prog->persist; + prog->persist = new MvuPersistLog(); + MvuPersistLog* log = prog->persist; + for (size_t i = 0; i < chunks.size(); i++) + { + PersistChunk pc; + pc.code.assign(chunks[i].code, chunks[i].code + chunks[i].codeSize); + pc.fixups = chunks[i].fixups; + log->chunks.push_back(std::move(pc)); + } + for (size_t i = 0; i < blocks.size(); i++) + { + PersistBlockRec rec = {}; + rec.chunkIndex = blocks[i].chunkIndex; + rec.entryOffset = blocks[i].entryOffset; + rec.startPC = blocks[i].startPC; + rec.hasJumpCache = blocks[i].hasJumpCache; + std::memcpy(&rec.pState, blocks[i].pState, sizeof(microRegInfo)); + std::memcpy(&rec.pStateEnd, blocks[i].pStateEnd, sizeof(microRegInfo)); + rec.live = final_block[i]; + log->blockByEntry.emplace(final_block[i]->hostEntry, static_cast(i)); + log->blocks.push_back(rec); + } + +#ifdef PCSX2_RECOMPILER_TESTS + s_lastHydratedChunkBase[vu] = chunk_base; +#endif + s_stats[vu].programsHydrated++; + s_stats[vu].blocksHydrated += blocks.size(); + return prog; + } + + //------------------------------------------------------------------ + // Test hooks + //------------------------------------------------------------------ + + bool TestSerializeNewestProgram(u32 vu_index, std::vector& out) + { + microVU& mVU = (vu_index & 1) ? microVU1 : microVU0; + microProgram* newest = nullptr; + for (const auto& entry : mVU.mvuContentMap) + { + if (!newest || entry.second->idx > newest->idx) + newest = entry.second; + } + if (!newest) + return false; + return SerializeProgram(mVU, *newest, out); + } + + bool TestHydrate(u32 vu_index, const u8* data, size_t size) + { + microVU& mVU = (vu_index & 1) ? microVU1 : microVU0; + return HydrateProgram(mVU, data, size) != nullptr; + } + +#ifdef PCSX2_RECOMPILER_TESTS + bool TestVerifyHydratedCode(u32 vu_index, const u8* image, size_t size) + { + const u32 vu = vu_index & 1; + const std::vector& placed = s_lastHydratedChunkBase[vu]; + if (placed.empty()) + return false; + + // Re-parse the image (cheap, and avoids holding parse state). + const u8* p = image; + const u8* const end = image + size; + ImageHeader hdr; + if (!ReadPod(p, end, hdr) || hdr.chunkCount != placed.size()) + return false; + // Bounds-check each section advance before adding (mirror ReadPod): + // an unchecked p += count*size can step past end, after which the + // per-chunk `end - p` guard below underflows to a huge size_t. + if (static_cast(end - p) < static_cast(hdr.rangeCount) * sizeof(DiskRange)) + return false; + p += static_cast(hdr.rangeCount) * sizeof(DiskRange); + if (static_cast(end - p) < static_cast(hdr.blockCount) * sizeof(DiskBlock)) + return false; + p += static_cast(hdr.blockCount) * sizeof(DiskBlock); + + for (u32 i = 0; i < hdr.chunkCount; i++) + { + DiskChunkHeader ch; + if (!ReadPod(p, end, ch)) + return false; + std::vector fixups(ch.fixupCount); + for (PersistFixup& f : fixups) + if (!ReadPod(p, end, f)) + return false; + if (static_cast(end - p) < ch.codeSize) + return false; + const u8* expect = p; + p += ch.codeSize; + + std::vector mask(ch.codeSize, 0); + for (const PersistFixup& f : fixups) + { + const u32 span = (f.kind == kFixSelfBlockAbs) ? 16 : 4; + for (u32 b = 0; b < span && f.codeOffset + b < ch.codeSize; b++) + mask[f.codeOffset + b] = 1; + } + + const u8* live = placed[i]; + for (u32 b = 0; b < ch.codeSize; b++) + { + if (!mask[b] && live[b] != expect[b]) + { + DevCon.Error("mVUPersist: hydrated code mismatch at chunk %u offset 0x%x (live %02x != image %02x)", + i, b, live[b], expect[b]); + return false; + } + } + } + return true; + } +#endif // PCSX2_RECOMPILER_TESTS + + u64 GetBlockCompileCount(u32 vu_index) { return s_blockCompiles[vu_index & 1]; } + Stats GetStats(u32 vu_index) { return s_stats[vu_index & 1]; } + + //------------------------------------------------------------------ + // ABI-digest guard support + //------------------------------------------------------------------ + + // Zero every operand field that may legitimately differ between two + // correct emissions of the same program (addresses + PC-relative + // displacements to relocatable or layout-dependent targets). Everything + // left — opcode bits, register fields, data immediates, instruction + // order — is the emitter's shape. + static u32 CanonicalizeInsn(u32 insn) + { + // 64-bit MOVZ/MOVN/MOVK (sf=1): address materialization. 32-bit + // forms carry data constants (cycle counts etc.) and stay intact. + const u32 mov_op = insn & 0xFF800000u; + if (mov_op == 0xD2800000u || mov_op == 0xF2800000u || mov_op == 0x92800000u) + return insn & ~(0xFFFFu << 5); // clear imm16, keep hw + Rd + // B / BL: stub + cross-chunk displacements depend on placement. + if ((insn & 0x7C000000u) == 0x14000000u) + return insn & 0xFC000000u; + // ADRP: page displacement is placement-dependent by construction. + if ((insn & 0x9F000000u) == 0x90000000u) + return insn & ~((3u << 29) | (0x7FFFFu << 5)); + return insn; + } + + bool TestComputeEmitDigest(u32 vu_index, u64& out_digest) + { + out_digest = 0; + std::vector image; + if (!TestSerializeNewestProgram(vu_index, image)) + return false; + + const u8* p = image.data(); + const u8* const end = p + image.size(); + ImageHeader hdr; + if (!ReadPod(p, end, hdr)) + return false; + + // Canonical buffer: structure counts + ranges + block geometry + + // fixup records (sans address payloads) + canonicalized code. + // Header hashes/bases and microRegInfo bytes are excluded — the + // former are per-run, the latter may carry padding. + std::vector canon; + AppendPod(canon, hdr.vuIndex); + AppendPod(canon, hdr.progStartPC); + AppendPod(canon, hdr.rangeCount); + AppendPod(canon, hdr.blockCount); + AppendPod(canon, hdr.chunkCount); + + for (u32 i = 0; i < hdr.rangeCount; i++) + { + DiskRange r; + if (!ReadPod(p, end, r)) + return false; + AppendPod(canon, r); + } + for (u32 i = 0; i < hdr.blockCount; i++) + { + DiskBlock b; + if (!ReadPod(p, end, b)) + return false; + AppendPod(canon, b.chunkIndex); + AppendPod(canon, b.entryOffset); + AppendPod(canon, b.startPC); + AppendPod(canon, b.hasJumpCache); + } + for (u32 i = 0; i < hdr.chunkCount; i++) + { + DiskChunkHeader ch; + if (!ReadPod(p, end, ch)) + return false; + AppendPod(canon, ch); + for (u32 f = 0; f < ch.fixupCount; f++) + { + PersistFixup fx; + if (!ReadPod(p, end, fx)) + return false; + AppendPod(canon, fx.codeOffset); + AppendPod(canon, fx.kind); + AppendPod(canon, fx.stubId); + AppendPod(canon, fx.fieldOffset); + AppendPod(canon, fx.blockIndex); + } + if (static_cast(end - p) < ch.codeSize || (ch.codeSize & 3)) + return false; + for (u32 off = 0; off < ch.codeSize; off += 4) + { + u32 insn; + std::memcpy(&insn, p + off, 4); + const u32 c = CanonicalizeInsn(insn); + AppendPod(canon, c); + } + p += ch.codeSize; + } + + out_digest = XXH3_64bits(canon.data(), canon.size()); + return true; + } +} // namespace mVUPersist diff --git a/pcsx2/arm64/microVU_ProgCache-arm64.h b/pcsx2/arm64/microVU_ProgCache-arm64.h new file mode 100644 index 0000000000..00a5d098b9 --- /dev/null +++ b/pcsx2/arm64/microVU_ProgCache-arm64.h @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +#ifndef XXH_versionNumber +#define XXH_STATIC_LINKING_ONLY 1 +#define XXH_INLINE_ALL 1 +#include "xxhash.h" +#endif + +struct microVU; +struct microProgram; + +// Disk-side scaffolding for the persisted-JIT VU program cache. Owns: +// 1. The per-VU vu_jit/vu{0,1}/{VERSION,INDEX,/...} filesystem layout. +// 2. Version handshake: a build-time ABI version + options sentinel + arch tag +// anchors the cache to a specific PCSX2 binary + config. Mismatch on startup +// atomically renames vu_jit...// → vu_jit.../.stale./ and +// starts fresh. +// 3. In-memory INDEX populated from disk at Init. Subsequent Save calls append +// one variable-length entry per program to disk + in-memory map. +// 4. TryLoadProgram hydrates the block graph via mVUPersist::HydrateProgram +// on an INDEX+payload hit. Constant-VA arena layout + a placement-relative +// fixup table make the persisted vixl output relocatable (absolute +// armEmitCall / armMoveAddressToReg targets are patched on load); any +// miss or payload rejection falls back to a normal recompile. +namespace mVUProgCache +{ + // Bumped together with kMvuCompilerAbiVersion when the on-disk layout + // changes. Mismatch nukes the cache directory at startup. + // + // IndexEntry is variable-length: a fixed 64-byte header carrying + // `entryCount`, followed by `entryCount` × `u32` entry-PC values + // (microMem byte offsets), so multi-entry programs carry every + // dispatched PC. Older fixed-layout entries are structurally + // incompatible with this format — a version mismatch evicts rather + // than silently loading wrong data. + static constexpr u32 kProgCacheFormatVersion = 3; + + // Initialize the per-VU on-disk cache. Gated on the + // EmuCore/CPU/Recompiler EnableVUProgramCache config bool (off = no disk + // side effects at all). Reads VERSION; if it diverges from the running + // build's (compiler ABI, options sentinel, arch tag), evicts the stale + // dir and starts fresh. Loads INDEX into memory. Idempotent — called + // from mVUinit AND every mVUreset (the reset call is what activates a + // runtime config toggle); only does real work on first effective call. + void Init(microVU& mVU); + + // Same Init logic but takes (vu_index, options_sentinel) directly so + // test TUs don't have to pull microVU-arm64.h (which includes the .inl + // bodies and would link-collide with libpcsx2.a). Not for production + // use; mVU.optionsSentinel is built by mVUbuildOptionsSentinel during + // mVUreset and that path must run before any Init. + void InitWithSentinel(u32 vu_index, const XXH128_hash_t& options_sentinel); + + // Persist pending writes, close handles. Called from mVUclose. + void Close(microVU& mVU); + + // Walk mVU.mvuContentMap and save every program whose contentHash is not + // already represented in the on-disk INDEX (or whose observed entry-PC + // set widened). Called from mVUreset (pre-program-free) and mVUclose so + // the next process boot has artifacts. + void SaveAllPrograms(microVU& mVU); + + // Save a single program: append an INDEX entry, and — when the program + // carries a persist log (mVUPersist recording was enabled while it + // compiled) — write the serialized block graph as a content-addressed + // .vuprog payload (//.vuprog, tmp+rename). The + // payload is rewritten when the recorded block graph grew past what is + // on disk; the INDEX append still short-circuits when neither the + // entry-PC set nor the payload changed. + void SaveProgram(microVU& mVU, const microProgram& prog); + + // Promote an on-disk program to a live one. On an INDEX hit with a + // readable .vuprog payload, hydrates the block graph through + // mVUPersist::HydrateProgram and returns the new program (registered in + // mVU.mvuContentMap, dispatchable immediately). Returns nullptr on + // INDEX miss, missing/corrupt payload, or any hydration rejection + // (layout-base / content-hash / structural mismatch) — the caller falls + // back to recompiling. Called from mVUsearchProg with the code cache + // OPEN; the (empty) emission episode is closed around the hydration and + // reopened before returning. + microProgram* TryLoadProgram(microVU& mVU, const XXH128_hash_t& contentHash); + + // Dispatch-time probe used by mVUsearchProg on the in-process-miss + // path. Tracks whether the INDEX already knows the (program, startPC) + // tuple the dispatcher is about to recompile — the hit-rate equals the + // upper bound on what a real hydration path would have served. Cheap + // (one unordered_map lookup), called once per program-switch — kept + // off the per-dispatch fast path. + void ObserveDispatchHash(microVU& mVU, const XXH128_hash_t& contentHash, + u32 startPC_bytes); + + // DevCon summary of save / hit / miss / evict counters. + void DumpStats(const microVU& mVU, const char* tag); + + // Test-only: drop per-VU in-memory state (entries, stats, initialized + // flag) so the next Init() re-runs the disk-handshake path. Does NOT + // touch the on-disk cache directory; the caller is responsible for + // that. + void ResetForTest(u32 vu_index); + + // The running build's kMvuCompilerAbiVersion (microVU-arm64.h can't be + // included from test TUs — it re-emits the .inl bodies). Used by the + // ABI-digest guard test to key its pinned-digest table. + u32 GetCompilerAbiVersion(); + + // Test-only: re-run Init for the given VU using the LIVE microVU's + // options sentinel (production Init is config-gated and only fires from + // mVUinit/mVUreset — this lets a test point EmuFolders::Cache at a temp + // dir and bring the disk cache up mid-process without rebuilding the + // whole VU, regardless of EmuConfig). Rebuilds the sentinel first if a + // recording-state change invalidated it. Call ResetForTest first. + void TestReinitFromLiveSentinel(u32 vu_index); + + // Test-only: the given VU's live options sentinel, rebuilt first if + // stale. Lets tests pin sentinel-identity properties (e.g. that the + // mVUPersist recording state is mixed in — a cache of recording-enabled + // code forms must never be served to a recording-disabled run and vice + // versa). + bool TestGetLiveSentinel(u32 vu_index, XXH128_hash_t* out_sentinel); + + // Test-only: block until the background payload preload spawned by Init + // (if any) has finished. Lets tests deterministically assert the RAM- + // served hydration path (e.g. by deleting the on-disk payload after the + // preload and proving hydration still succeeds). + void TestWaitForPreload(u32 vu_index); + + // Test-only: append an INDEX entry for `hash` carrying the given + // entry-PC list, applying the same persistence path SaveProgram uses + // (single atomic append + in-memory registration; dedupe on existing + // hash). Returns true iff the entry was written. Lets format tests + // drive the variable-length serializer without constructing a + // microVU/microProgram. + bool TestAppendIndexEntry(u32 vu_index, const XXH128_hash_t& hash, + const u32* entry_pcs, size_t entry_pc_count); + + // Test-only: read back the entry-PC list the cache holds for a given + // content hash. Returns true if the hash is in the in-memory entries + // map and writes the entry_pcs (microMem byte offsets) into `out_pcs`; + // false otherwise. Used by the IndexFormat tests to pin the + // variable-length serializer's round-trip behavior. + bool TestGetEntryPcs(u32 vu_index, const XXH128_hash_t& hash, + u32* out_pcs, size_t out_pcs_cap, + size_t* out_count); + + // Test-only: read back the per-VU counters DumpStats would log. Used by + // the versioning tests to assert that eviction fired (staleEvictions++) + // or that the cache came up cleanly. + struct Stats + { + bool initialized = false; + bool enabled = false; + u64 entries = 0; + u64 savesAttempted = 0; + u64 savesWritten = 0; + u64 wouldBeHits = 0; + u64 misses = 0; + u64 staleEvictions = 0; + u64 dispatchProbes = 0; + u64 dispatchIndexAvail = 0; + u64 payloadWrites = 0; // .vuprog files written (tmp+rename) + u64 payloadHits = 0; // TryLoadProgram hydrations served + u64 payloadMissing = 0; // INDEX hit but no .vuprog on disk + u64 payloadRejects = 0; // payload read but hydration refused + u64 preloadedPayloads = 0; // .vuprog files read by the Init-time + // background preload thread + u64 preloadedBytes = 0; // bytes currently held by the preload + // map (consumed buffers are released) + u64 preloadHits = 0; // hydrations served from RAM instead of + // a dispatch-thread disk read + }; + Stats GetStats(u32 vu_index); +} diff --git a/pcsx2/arm64/microVU_ProgCache-arm64.inl b/pcsx2/arm64/microVU_ProgCache-arm64.inl new file mode 100644 index 0000000000..0ac47fa61d --- /dev/null +++ b/pcsx2/arm64/microVU_ProgCache-arm64.inl @@ -0,0 +1,958 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// Implementation of microVU_ProgCache-arm64.h. Included exactly once from +// microVU-arm64.cpp — the mVU module is a single TU (the microVU_*-arm64.inl +// files emit non-inline function bodies), so this implementation lives next to +// them rather than as a stand-alone .cpp. + +#include "Config.h" +#include "common/Console.h" +#include "common/Error.h" +#include "common/FileSystem.h" +#include "common/Path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mVUProgCache +{ +namespace +{ +//------------------------------------------------------------------ +// Disk layout +// $EmuFolders::Cache/vu_jit/ +// vu0/ +// VERSION (64-byte handshake block) +// INDEX (append-only stream of entries) +// /... (payload artifacts) +// vu1/ (same shape) +// +// VERSION + INDEX are per-VU so each VU initializes independently from the +// sequential mVUinit(microVU0) / mVUinit(microVU1) calls in cpuMicroVU::Init. +//------------------------------------------------------------------ +static constexpr u32 kVersionMagic = 0x4A56554Du; // 'MVUJ' +static constexpr char kRootDirName[] = "vu_jit"; +static constexpr char kVersionFileName[] = "VERSION"; +static constexpr char kIndexFileName[] = "INDEX"; + +static_assert(sizeof(XXH128_hash_t) == 16, "XXH128_hash_t expected 16 bytes"); + +#pragma pack(push, 1) +struct VersionHeader +{ + u32 magic; // kVersionMagic + u32 formatVersion; // kProgCacheFormatVersion + u32 compilerAbiVersion; // kMvuCompilerAbiVersion + u32 vuIndex; // 0 or 1 — sentinel mixes VU index + XXH128_hash_t optionsSentinel; // this VU's mVU.optionsSentinel + char archTag[32]; // "arm64-jit" +}; +static_assert(sizeof(VersionHeader) == 64, "VersionHeader layout drift"); + +// On-disk header for an IndexEntry. Each record is the 64-byte header +// followed by `entryCount` × u32 entry-PC values (microMem byte +// offsets). entryCount must be >= 1 for a valid entry; entryCount == 0 +// indicates a tombstone alongside the flags-bit-0 check. +struct IndexEntryHeader +{ + XXH128_hash_t contentHash; // 16 — the program's identity + u32 vuIndex; // 4 + u32 codeSize; // 4 — size of the last-written .vuprog + // payload (0 = no payload on disk) + u32 blockCount; // 4 + u32 flags; // 4 — bit 0 = valid; tombstones leave it clear + u64 lastUsedNs; // 8 + u64 execCount; // 8 — running dispatch count, reserved + u32 entryCount; // 4 — number of trailing u32 entry-PC values + u8 pad[12]; // 12 — round to 64 for trivial mmap +}; +static_assert(sizeof(IndexEntryHeader) == 64, + "IndexEntryHeader layout drift"); +#pragma pack(pop) + +// In-memory companion. Holds the on-disk header plus the trailing +// entry-PC list. Tests + dispatcher consume IndexEntry; serialization +// in/out of disk uses IndexEntryHeader + the appended u32 array. +struct IndexEntry +{ + IndexEntryHeader hdr; + std::vector entry_pcs; +}; + +struct State +{ + bool initialized = false; + bool enabled = true; + std::string root; // $EmuFolders::Cache/vu_jit/vu{0,1} + std::string indexPath; // $root/INDEX + std::string versionPath; // $root/VERSION + + std::unordered_map entries; + + u64 wouldBeHits = 0; + u64 misses = 0; + u64 savesAttempted = 0; + u64 savesWritten = 0; + u64 staleEvictions = 0; + u64 dispatchProbes = 0; // mVUsearchProg dispatch-time probes + u64 dispatchIndexAvail = 0; // subset where the hash is in the INDEX + u64 payloadWrites = 0; + u64 payloadHits = 0; + u64 payloadMissing = 0; + u64 payloadRejects = 0; + + // Background payload preload. InitImpl spawns the thread when the INDEX + // carries payload-bearing entries; it fills `preloaded` (most-recently- + // used first, kPreloadBudgetBytes bound) and exits, so first-dispatch + // hydration on the EE/MTVU threads is a map lookup instead of an eMMC + // read. Buffers are consumed (moved out + erased) on hydration, so the + // resident cost drains as the game warms up. preloadMutex guards + // `preloaded` + the preload* counters; everything else in State keeps + // its existing single-thread/reset-quiesced discipline. + std::thread preloadThread; + std::mutex preloadMutex; + std::atomic preloadStop{false}; + std::unordered_map, + MvuContentHashHash, MvuContentHashEq> preloaded; + u64 preloadedPayloads = 0; + u64 preloadedBytes = 0; + u64 preloadHits = 0; + + void StopPreload() + { + preloadStop.store(true, std::memory_order_relaxed); + if (preloadThread.joinable()) + preloadThread.join(); + } + + // Joinable-thread backstop: a std::thread destroyed while joinable + // terminates the process. Close/ResetForTest join explicitly; this + // covers exit paths that skip mVUclose. + ~State() { StopPreload(); } +}; + +static State g_state[2]; + +static u64 NowNs() +{ + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + +static void BuildVersionHeader(u32 vu_index, + const XXH128_hash_t& options_sentinel, + VersionHeader& out) +{ + std::memset(&out, 0, sizeof(out)); + out.magic = kVersionMagic; + out.formatVersion = kProgCacheFormatVersion; + out.compilerAbiVersion = kMvuCompilerAbiVersion; + out.vuIndex = vu_index & 1u; + out.optionsSentinel = options_sentinel; + std::strncpy(out.archTag, "arm64-jit", sizeof(out.archTag) - 1); +} + +static bool VersionMatches(const VersionHeader& a, const VersionHeader& b) +{ + return a.magic == b.magic + && a.formatVersion == b.formatVersion + && a.compilerAbiVersion == b.compilerAbiVersion + && a.vuIndex == b.vuIndex + && a.optionsSentinel.low64 == b.optionsSentinel.low64 + && a.optionsSentinel.high64 == b.optionsSentinel.high64 + && std::memcmp(a.archTag, b.archTag, sizeof(a.archTag)) == 0; +} + +static bool WriteVersion(const std::string& path, const VersionHeader& hdr) +{ + // Atomic-replace via tmp+rename — keeps a partial write from poisoning + // the cache on power-loss. + const std::string tmp = path + ".tmp"; + if (!FileSystem::WriteBinaryFile(tmp.c_str(), &hdr, sizeof(hdr))) + return false; + return std::rename(tmp.c_str(), path.c_str()) == 0; +} + +static void EvictStaleCache(State& s) +{ + if (!FileSystem::DirectoryExists(s.root.c_str())) + return; + const std::string stale = s.root + ".stale." + std::to_string(NowNs()); + if (std::rename(s.root.c_str(), stale.c_str()) != 0) + { + Console.Warning("mVUProgCache: rename %s -> %s failed", + s.root.c_str(), stale.c_str()); + } + else + { + Console.WriteLn("mVUProgCache: evicted stale cache %s -> %s", + s.root.c_str(), stale.c_str()); + ++s.staleEvictions; + } +} + +// Parse a variable-length INDEX. Each record is a fixed 64-byte +// IndexEntryHeader followed by `entryCount` × u32 entry-PC values. +// Truncated tails (partial header, partial trailing array) are silently +// dropped — the next SaveProgram will rewrite a clean copy, and the +// partial bytes don't poison any in-memory state. Last-writer-wins on +// duplicate contentHash so re-saves can extend a program's entry list +// in place. +static void LoadIndex(State& s) +{ + auto bytes = FileSystem::ReadBinaryFile(s.indexPath.c_str()); + if (!bytes.has_value()) + return; + const u8* base = bytes->data(); + const size_t total = bytes->size(); + size_t off = 0; + while (off + sizeof(IndexEntryHeader) <= total) + { + IndexEntryHeader hdr; + std::memcpy(&hdr, base + off, sizeof(hdr)); + off += sizeof(hdr); + + const size_t trail_bytes = static_cast(hdr.entryCount) * sizeof(u32); + if (off + trail_bytes > total) + { + Console.Warning("mVUProgCache: INDEX %s truncated at off=%zu " + "(need %zu trailing bytes, have %zu) — ignoring tail", + s.indexPath.c_str(), off, trail_bytes, + total - off); + return; + } + + if ((hdr.flags & 1u) == 0u || hdr.entryCount == 0u) + { + off += trail_bytes; + continue; + } + + IndexEntry e; + e.hdr = hdr; + e.entry_pcs.resize(hdr.entryCount); + std::memcpy(e.entry_pcs.data(), base + off, trail_bytes); + off += trail_bytes; + + s.entries[hdr.contentHash] = std::move(e); // last-writer-wins + } + if (off != total) + { + Console.Warning("mVUProgCache: INDEX %s has %zu trailing bytes past last " + "header — ignoring", s.indexPath.c_str(), total - off); + } +} + +// Serialize one in-memory IndexEntry as a single contiguous +// header+trailing block. Single fwrite keeps the append atomic against +// process death between header and trailing writes. +static bool AppendIndexEntry(const State& s, const IndexEntry& e) +{ + std::FILE* fp = FileSystem::OpenCFile(s.indexPath.c_str(), "ab"); + if (!fp) + return false; + + std::vector buf(sizeof(IndexEntryHeader) + + e.entry_pcs.size() * sizeof(u32)); + std::memcpy(buf.data(), &e.hdr, sizeof(IndexEntryHeader)); + if (!e.entry_pcs.empty()) + { + std::memcpy(buf.data() + sizeof(IndexEntryHeader), + e.entry_pcs.data(), + e.entry_pcs.size() * sizeof(u32)); + } + const bool ok = std::fwrite(buf.data(), buf.size(), 1, fp) == 1; + std::fclose(fp); + return ok; +} + +// Persist an entry: append to disk + register in the in-memory map. +// Keeps header.entryCount and entry_pcs.size() in lock-step — callers +// populate entry_pcs; this is the single point that updates the +// on-disk count field. +static bool PersistIndexEntry(State& s, const IndexEntry& e_in) +{ + IndexEntry e = e_in; + e.hdr.entryCount = static_cast(e.entry_pcs.size()); + + if (!AppendIndexEntry(s, e)) + { + Console.Warning("mVUProgCache[%u]: INDEX append failed", e.hdr.vuIndex); + return false; + } + s.entries[e.hdr.contentHash] = std::move(e); + ++s.savesWritten; + return true; +} + +//------------------------------------------------------------------ +// .vuprog payload files — the serialized block graph (mVUPersist image), +// content-addressed under a 256-way shard: +// $root//.vuprog +// where is the first hex byte of the 128-bit content hash. +//------------------------------------------------------------------ + +static std::string PayloadPath(const State& s, const XXH128_hash_t& hash, + std::string* out_shard_dir = nullptr) +{ + char name[48]; + std::snprintf(name, sizeof(name), "%016" PRIx64 "%016" PRIx64 ".vuprog", + hash.high64, hash.low64); + const char shard[3] = {name[0], name[1], 0}; + std::string shard_dir = Path::Combine(s.root, shard); + std::string path = Path::Combine(shard_dir, name); + if (out_shard_dir) + *out_shard_dir = std::move(shard_dir); + return path; +} + +// Atomic-replace write (tmp+rename), creating the shard dir on demand. +static bool WritePayload(State& s, const XXH128_hash_t& hash, + const std::vector& image) +{ + std::string shard_dir; + const std::string path = PayloadPath(s, hash, &shard_dir); + Error error; + if (!FileSystem::EnsureDirectoryExists(shard_dir.c_str(), false, &error)) + { + Console.Warning("mVUProgCache: cannot create payload shard for %s: %s", + path.c_str(), error.GetDescription().c_str()); + return false; + } + const std::string tmp = path + ".tmp"; + if (!FileSystem::WriteBinaryFile(tmp.c_str(), image.data(), image.size())) + return false; + if (std::rename(tmp.c_str(), path.c_str()) != 0) + return false; + + // A rewritten payload supersedes any not-yet-consumed preloaded copy + // (which would otherwise serve the pre-growth image — sound under the + // consistent-prefix rule, but pointlessly stale). + { + std::lock_guard lock(s.preloadMutex); + auto pit = s.preloaded.find(hash); + if (pit != s.preloaded.end()) + { + s.preloadedBytes -= pit->second.size(); + s.preloaded.erase(pit); + } + } + return true; +} + +// Read the payload-bearing INDEX entries into RAM on a background thread, +// most-recently-used first, bounded by kPreloadBudgetBytes. The bound is a +// cap, not a target — resident cost matters on low-memory devices, but +// buffers drain as hydrations consume them. +static constexpr size_t kPreloadBudgetBytes = 32 * 1024 * 1024; + +static void StartPreload(State& s) +{ + struct Item + { + XXH128_hash_t hash; + u64 lastUsedNs; + u32 size; + }; + std::vector items; + for (const auto& kv : s.entries) + { + if (kv.second.hdr.codeSize > 0) + items.push_back({kv.first, kv.second.hdr.lastUsedNs, kv.second.hdr.codeSize}); + } + if (items.empty()) + return; + std::sort(items.begin(), items.end(), + [](const Item& a, const Item& b) { return a.lastUsedNs > b.lastUsedNs; }); + + // Strict MRU prefix (first over-budget item stops the walk, no + // skip-and-refill) so what got preloaded is a deterministic function of + // the INDEX, not of file-size interleaving. + std::vector> files; + size_t budget = 0; + for (const Item& it : items) + { + if (budget + it.size > kPreloadBudgetBytes) + break; + budget += it.size; + files.emplace_back(it.hash, PayloadPath(s, it.hash)); + } + if (files.size() < items.size()) + { + Console.WriteLn("mVUProgCache: preload budget (%zu MiB) reached — " + "%zu of %zu payloads stay on disk", + kPreloadBudgetBytes / (1024 * 1024), + items.size() - files.size(), items.size()); + } + + s.preloadStop.store(false, std::memory_order_relaxed); + // The &s capture is intentional and safe: s aliases a process-lifetime + // static (g_state[]), and StopPreload() joins this thread before Close/ + // ResetForTest ever mutate that state. Don't "fix" this into a by-value or + // shared_ptr capture — there is no lifetime bug to solve. + s.preloadThread = std::thread([&s, files = std::move(files)]() { + for (const auto& [hash, path] : files) + { + if (s.preloadStop.load(std::memory_order_relaxed)) + return; + auto bytes = FileSystem::ReadBinaryFile(path.c_str()); + if (!bytes.has_value() || bytes->empty()) + continue; // dispatch-time read will count payloadMissing + std::lock_guard lock(s.preloadMutex); + s.preloadedBytes += bytes->size(); + ++s.preloadedPayloads; + s.preloaded[hash] = std::move(*bytes); + } + }); +} +} // anonymous namespace + +namespace +{ +// microVU-free initialization core. Production Init wraps this with +// (mVU.index, mVU.optionsSentinel); tests reach the same code path +// through InitWithSentinel without dragging the microVU struct through +// the test TU (which would re-emit the .inl bodies). +static void InitImpl(u32 vu_index, const XXH128_hash_t& options_sentinel) +{ + State& s = g_state[vu_index & 1]; + if (s.initialized) + return; + + // Determinism gate for offline tooling (pcsx2-vurunner --no-progcache). The + // program cache is a process-persistent, on-disk side effect, so two + // "identical" runs are not byte-identical. SetProcessDisable(true) forces the + // cache off for the whole process so a JIT-vs-interp divergence run is + // reproducible. No effect in production (never set there). + if (mVUPersist::IsProcessDisabled()) + { + s.enabled = false; + s.initialized = true; + return; + } + + // EmuFolders::Cache is set by the Qt frontend after parsing user paths; the + // unit-test harness leaves it empty (RecompilerTestEnvironment doesn't run + // the full VMManager bring-up). Skip the on-disk cache cleanly in that + // mode — tests run faster and don't leave a ./vu_jit/ in their cwd. + if (EmuFolders::Cache.empty()) + { + s.enabled = false; + s.initialized = true; + return; + } + + const std::string root = Path::Combine(EmuFolders::Cache, kRootDirName); + s.root = Path::Combine(root, (vu_index & 1) ? "vu1" : "vu0"); + s.indexPath = Path::Combine(s.root, kIndexFileName); + s.versionPath = Path::Combine(s.root, kVersionFileName); + + if (!s.enabled) + { + s.initialized = true; + return; + } + + // Read VERSION BEFORE creating $root so a true fresh-init (no $root, + // no VERSION) doesn't trip the EvictStaleCache path (which would + // rename the just-created empty $root aside and inflate + // staleEvictions). EvictStaleCache itself is a no-op if $root doesn't + // exist, so the rebuild branch below covers both "directory was + // missing entirely" and "directory existed with mismatched VERSION." + VersionHeader live; + BuildVersionHeader(vu_index, options_sentinel, live); + + bool versionOk = false; + auto vbytes = FileSystem::ReadBinaryFile(s.versionPath.c_str()); + if (vbytes.has_value() && vbytes->size() == sizeof(VersionHeader)) + { + VersionHeader disk; + std::memcpy(&disk, vbytes->data(), sizeof(disk)); + versionOk = VersionMatches(disk, live); + } + + Error error; + if (!versionOk) + { + // If the directory exists, treat it as stale (rename it aside). + // If it doesn't, this is a no-op and execution falls through to mkdir + + // WriteVersion below. + EvictStaleCache(s); + if (!FileSystem::EnsureDirectoryExists(s.root.c_str(), true, &error)) + { + Console.Warning("mVUProgCache[%u]: cannot create %s: %s — disabling", + vu_index, s.root.c_str(), + error.GetDescription().c_str()); + s.enabled = false; + s.initialized = true; + return; + } + if (!WriteVersion(s.versionPath, live)) + { + Console.Warning("mVUProgCache[%u]: write VERSION failed — disabling", + vu_index); + s.enabled = false; + s.initialized = true; + return; + } + } + + s.entries.clear(); + LoadIndex(s); + + Console.WriteLn(Color_StrongBlue, + "mVUProgCache[%u]: init root=%s entries=%zu", + vu_index, s.root.c_str(), s.entries.size()); + + StartPreload(s); + + s.initialized = true; +} +} // anonymous namespace + +void Init(microVU& mVU) +{ + // Production gate: the whole on-disk cache — init, INDEX + // telemetry, .vuprog payloads, dispatch-time hydration — sits behind the + // EmuCore/CPU/Recompiler EnableVUProgramCache INI bool. Off (the default) + // means no disk side effects at all. Toggling it on at runtime works + // because mVUreset re-enters here after every settings-driven recompiler + // clear; toggling it OFF leaves the already-initialized state up for the + // rest of the session (entries written after the toggle are keyed to the + // post-toggle sentinel and are evicted wholesale on the next handshake — + // bounded garbage, never corruption). Tests bypass this gate through + // InitWithSentinel / TestReinitFromLiveSentinel. + if (!EmuConfig.Cpu.Recompiler.EnableVUProgramCache) + return; + InitImpl(mVU.index & 1u, mVU.optionsSentinel); +} + +void InitWithSentinel(u32 vu_index, const XXH128_hash_t& options_sentinel) +{ + InitImpl(vu_index & 1u, options_sentinel); +} + +void Close(microVU& mVU) +{ + State& s = g_state[mVU.index & 1]; + if (!s.initialized) + return; + DumpStats(mVU, "close"); + // No file handles kept open (per-call open/append/close). Clear the + // in-memory index so the next mVUreset starts fresh. + s.StopPreload(); + { + std::lock_guard lock(s.preloadMutex); + s.preloaded.clear(); + s.preloadedBytes = 0; + // Zero the cumulative preload counters too (mirror ResetForTest) so a + // Close+Init cycle starts from a consistent baseline — otherwise + // GetStats would report preloadedBytes==0 next to stale nonzero + // preloadedPayloads/preloadHits. DumpStats("close") above already + // emitted this lifecycle's lifetime totals. + s.preloadedPayloads = 0; + s.preloadHits = 0; + } + s.initialized = false; + s.entries.clear(); +} + +void SaveProgram(microVU& mVU, const microProgram& prog) +{ + State& s = g_state[mVU.index & 1]; + if (!s.initialized || !s.enabled || !prog.contentHashValid) + return; + + ++s.savesAttempted; + + u32 blockCount = 0; + for (u32 i = 0; i < (mVU.progSize / 2); ++i) + { + microBlockManager* bm = prog.block[i]; + if (!bm) + continue; + blockCount += static_cast(bm->getFullListCount()); + } + + // Serialize the recorded block graph. Empty when mVUPersist recording + // was off while this program compiled, or the program was marked + // non-persistable — the INDEX entry is then telemetry-only (no + // payload). + std::vector image; + const bool have_image = mVUPersist::SerializeProgram( + mVU, prog, image); + + // Write/refresh the .vuprog payload when the serialized graph grew (a + // hydrated program compiled new blocks since the last save) or the + // file is missing. hdr.codeSize tracks the last-written payload size — + // the cheap growth signal (same program + same emitters can't change + // bytes without changing size). + auto write_payload_if_grown = [&](IndexEntry& e) -> bool { + if (!have_image) + return false; + if (e.hdr.codeSize == static_cast(image.size()) && + FileSystem::FileExists(PayloadPath(s, prog.contentHash).c_str())) + return false; + if (!WritePayload(s, prog.contentHash, image)) + { + Console.Warning("mVUProgCache[%u]: payload write failed", mVU.index); + return false; + } + e.hdr.codeSize = static_cast(image.size()); + ++s.payloadWrites; + return true; + }; + + // When this hash has been seen before, the on-disk entry_pcs is the + // cumulative truth. Union the existing set with what the current + // program instance observed; only re-persist if the union widens past + // what's already on disk. This handles the cross-program-instance + // case: the dispatcher hits N PCs across several mVUreset cycles, but + // each instance starts fresh with observed={its createProg PC}. + // Without the union, the FIRST instance's SaveProgram wins and + // subsequent calls short-circuit via s.entries dedupe. Reading the + // existing set and unioning lets the on-disk entry list converge to + // all observed PCs over enough boots. + const u32 primary_pc = static_cast(prog.startPC) * 8u; + + auto it_existing = s.entries.find(prog.contentHash); + if (it_existing != s.entries.end()) + { + // Existing entry — widening check. Probe for genuinely-new PCs + // against the on-disk set without copying it first: once a + // program's observed-PC set has converged (the steady state + // after a few boots) this branch runs on every SaveProgram and + // the copy would be pure waste. Only materialize the wider list + // when a new PC actually appears. + IndexEntry& existing = it_existing->second; + std::unordered_map seen; + for (u32 pc : existing.entry_pcs) seen.emplace(pc, true); + + std::vector new_pcs; + auto note = [&](u32 pc) { + if (seen.emplace(pc, true).second) new_pcs.push_back(pc); + }; + // Ensure primary_pc is present (it would be, but seed + // defensively in case a prior save was malformed). + note(primary_pc); + for (size_t i = 0; i < prog.observed.count; ++i) + note(prog.observed.pcs[i]); + + const bool widened = !new_pcs.empty(); + const bool payload_written = write_payload_if_grown(existing); + if (!widened && !payload_written) + return; // No new PCs, payload current — short-circuit. + + // Re-persist with the wider set / refreshed payload size + // (last-writer-wins on the next LoadIndex). + IndexEntry e = existing; + if (widened) + { + e.entry_pcs.reserve(e.entry_pcs.size() + new_pcs.size()); + e.entry_pcs.insert(e.entry_pcs.end(), new_pcs.begin(), new_pcs.end()); + } + e.hdr.lastUsedNs = NowNs(); + e.hdr.blockCount = blockCount; + PersistIndexEntry(s, e); + return; + } + + // First-time save for this hash. + IndexEntry e; + e.hdr.contentHash = prog.contentHash; + e.hdr.vuIndex = mVU.index & 1u; + e.hdr.codeSize = 0; // set by write_payload_if_grown on success + e.hdr.blockCount = blockCount; + e.hdr.flags = 1u; + e.hdr.lastUsedNs = NowNs(); + e.hdr.execCount = 0; + // Primary entry is always the creator's startPC at index 0; observed + // PCs follow. + e.entry_pcs.push_back(primary_pc); + const auto& observed = prog.observed; + for (u32 i = 0; i < observed.count; ++i) + { + const u32 pc = observed.pcs[i]; + if (pc == primary_pc) + continue; + e.entry_pcs.push_back(pc); + } + + write_payload_if_grown(e); + PersistIndexEntry(s, e); +} + +bool TestGetEntryPcs(u32 vu_index, const XXH128_hash_t& hash, + u32* out_pcs, size_t out_pcs_cap, + size_t* out_count) +{ + if (out_count) + *out_count = 0; + State& s = g_state[vu_index & 1]; + if (!s.initialized) + return false; + auto it = s.entries.find(hash); + if (it == s.entries.end()) + return false; + const auto& pcs = it->second.entry_pcs; + const size_t n = pcs.size(); + if (out_count) + *out_count = n; + if (out_pcs && out_pcs_cap > 0) + { + const size_t copy = (n < out_pcs_cap) ? n : out_pcs_cap; + for (size_t i = 0; i < copy; ++i) + out_pcs[i] = pcs[i]; + } + return true; +} + +bool TestAppendIndexEntry(u32 vu_index, const XXH128_hash_t& hash, + const u32* entry_pcs, size_t entry_pc_count) +{ + State& s = g_state[vu_index & 1]; + if (!s.initialized || !s.enabled) + return false; + + ++s.savesAttempted; + + if (s.entries.find(hash) != s.entries.end()) + return false; + + IndexEntry e; + e.hdr.contentHash = hash; + e.hdr.vuIndex = vu_index & 1u; + e.hdr.codeSize = 0; + e.hdr.blockCount = 0; + e.hdr.flags = 1u; + e.hdr.lastUsedNs = NowNs(); + e.hdr.execCount = 0; + e.entry_pcs.assign(entry_pcs, entry_pcs + entry_pc_count); + + return PersistIndexEntry(s, e); +} + +void SaveAllPrograms(microVU& mVU) +{ + State& s = g_state[mVU.index & 1]; + if (!s.initialized || !s.enabled) + return; + for (const auto& kv : mVU.mvuContentMap) + { + if (kv.second) + SaveProgram(mVU, *kv.second); + } +} + +microProgram* TryLoadProgram(microVU& mVU, const XXH128_hash_t& contentHash) +{ + State& s = g_state[mVU.index & 1]; + if (!s.initialized || !s.enabled) + { + // Cache disabled/not initialized: the INDEX was never consulted, so + // this is not an INDEX miss. Leave `misses` alone (it counts genuine + // hash-not-in-INDEX lookups below) — otherwise the production default + // (EnableVUProgramCache OFF) inflates the miss count on every dispatch. + return nullptr; + } + auto it = s.entries.find(contentHash); + if (it == s.entries.end()) + { + ++s.misses; + return nullptr; + } + ++s.wouldBeHits; + + // RAM first: consume the preloaded buffer if the background preload got + // to this payload (moved out + erased, so the resident footprint drains + // as the game warms up). Disk is the fallback for entries past the + // preload budget or dispatched before the preload thread reached them. + std::optional> payload; + { + std::lock_guard lock(s.preloadMutex); + auto pit = s.preloaded.find(contentHash); + if (pit != s.preloaded.end()) + { + payload = std::move(pit->second); + s.preloadedBytes -= payload->size(); + s.preloaded.erase(pit); + ++s.preloadHits; + } + } + if (!payload.has_value()) + payload = FileSystem::ReadBinaryFile(PayloadPath(s, contentHash).c_str()); + if (!payload.has_value() || payload->empty()) + { + // Telemetry-only entry (saved with recording off) or the payload + // was evicted independently of the INDEX. Recompile. + ++s.payloadMissing; + return nullptr; + } + + // mVUsearchProg calls this inside a freshly-opened — still empty — + // emission episode, but HydrateProgram needs the code cache closed so + // it can run its own open/close pair. Close around the hydration and + // restore on the way out; closing an empty episode is a recorded + // no-op, and new blocks compiled after the reopen attach to the + // hydrated program's rebuilt persist log as growth chunks. + const bool was_open = (armAsm != nullptr); + if (was_open) + mVUcloseCodeCache(mVU); + microProgram* prog = mVUPersist::HydrateProgram(mVU, payload->data(), payload->size()); + if (was_open) + mVUopenCodeCache(mVU); + + if (!prog) + { + // Layout-base / content-hash / structural rejection — degrade to + // recompile, never corrupt. HydrateProgram counted the reason. + ++s.payloadRejects; + return nullptr; + } + ++s.payloadHits; + it->second.hdr.lastUsedNs = NowNs(); + return prog; +} + +void ObserveDispatchHash(microVU& mVU, const XXH128_hash_t& contentHash, + u32 startPC_bytes) +{ + State& s = g_state[mVU.index & 1]; + if (!s.initialized || !s.enabled) + return; + ++s.dispatchProbes; + (void)startPC_bytes; + if (s.entries.find(contentHash) != s.entries.end()) + ++s.dispatchIndexAvail; +} + +Stats GetStats(u32 vu_index) +{ + State& s = g_state[vu_index & 1]; + Stats out; + out.initialized = s.initialized; + out.enabled = s.enabled; + out.entries = s.entries.size(); + out.savesAttempted = s.savesAttempted; + out.savesWritten = s.savesWritten; + out.wouldBeHits = s.wouldBeHits; + out.misses = s.misses; + out.staleEvictions = s.staleEvictions; + out.dispatchProbes = s.dispatchProbes; + out.dispatchIndexAvail = s.dispatchIndexAvail; + out.payloadWrites = s.payloadWrites; + out.payloadHits = s.payloadHits; + out.payloadMissing = s.payloadMissing; + out.payloadRejects = s.payloadRejects; + { + std::lock_guard lock(s.preloadMutex); + out.preloadedPayloads = s.preloadedPayloads; + out.preloadedBytes = s.preloadedBytes; + out.preloadHits = s.preloadHits; + } + return out; +} + +u32 GetCompilerAbiVersion() +{ + return kMvuCompilerAbiVersion; +} + +void TestReinitFromLiveSentinel(u32 vu_index) +{ + microVU& mVU = (vu_index & 1) ? microVU1 : microVU0; + if (!mVU.optionsSentinelValid) + mVUbuildOptionsSentinel(mVU); + InitImpl(vu_index & 1, mVU.optionsSentinel); +} + +bool TestGetLiveSentinel(u32 vu_index, XXH128_hash_t* out_sentinel) +{ + if (!out_sentinel) + return false; + microVU& mVU = (vu_index & 1) ? microVU1 : microVU0; + if (!mVU.optionsSentinelValid) + mVUbuildOptionsSentinel(mVU); + *out_sentinel = mVU.optionsSentinel; + return true; +} + +void ResetForTest(u32 vu_index) +{ + State& s = g_state[vu_index & 1]; + s.StopPreload(); + s.initialized = false; + s.enabled = true; + s.root.clear(); + s.indexPath.clear(); + s.versionPath.clear(); + s.entries.clear(); + s.wouldBeHits = 0; + s.misses = 0; + s.savesAttempted = 0; + s.savesWritten = 0; + s.staleEvictions = 0; + s.dispatchProbes = 0; + s.dispatchIndexAvail = 0; + s.payloadWrites = 0; + s.payloadHits = 0; + s.payloadMissing = 0; + s.payloadRejects = 0; + { + std::lock_guard lock(s.preloadMutex); + s.preloaded.clear(); + s.preloadedPayloads = 0; + s.preloadedBytes = 0; + s.preloadHits = 0; + } +} + +void TestWaitForPreload(u32 vu_index) +{ + State& s = g_state[vu_index & 1]; + if (s.preloadThread.joinable()) + s.preloadThread.join(); +} + +void DumpStats(const microVU& mVU, const char* tag) +{ + const Stats s = GetStats(mVU.index & 1); + const u64 entries = s.entries; + Console.WriteLn(Color_StrongGreen, + "mVUProgCache[%u] %s: entries=%llu saves(att/written)=%llu/%llu " + "loads(indexHit/miss)=%llu/%llu staleEvicts=%llu " + "dispatchProbes=%llu dispatchIndexAvail=%llu " + "payload(writes/hits/missing/rejects)=%llu/%llu/%llu/%llu " + "preload(loaded/residentB/hits)=%llu/%llu/%llu", + mVU.index, tag, + (unsigned long long)entries, + (unsigned long long)s.savesAttempted, + (unsigned long long)s.savesWritten, + (unsigned long long)s.wouldBeHits, + (unsigned long long)s.misses, + (unsigned long long)s.staleEvictions, + (unsigned long long)s.dispatchProbes, + (unsigned long long)s.dispatchIndexAvail, + (unsigned long long)s.payloadWrites, + (unsigned long long)s.payloadHits, + (unsigned long long)s.payloadMissing, + (unsigned long long)s.payloadRejects, + (unsigned long long)s.preloadedPayloads, + (unsigned long long)s.preloadedBytes, + (unsigned long long)s.preloadHits); +} +} diff --git a/tests/ctest/core/recompilers/CMakeLists.txt b/tests/ctest/core/recompilers/CMakeLists.txt index 6f8ebe2188..a267f9a889 100644 --- a/tests/ctest/core/recompilers/CMakeLists.txt +++ b/tests/ctest/core/recompilers/CMakeLists.txt @@ -75,6 +75,10 @@ add_pcsx2_test(recompiler_tests ee_vu1_vif_dispatch_tests.cpp vu_capture_format_tests.cpp vu_replay_tests.cpp + mvu_observed_entries_tests.cpp + mvu_abi_digest_tests.cpp + mvu_persist_roundtrip_tests.cpp + mvu_progcache_disk_tests.cpp ) target_include_directories(recompiler_tests PRIVATE @@ -88,3 +92,25 @@ target_link_libraries(recompiler_tests PUBLIC common ) +# mVUProgCache VERSION-handshake / stale-cache eviction / INDEX-format +# tests. Drives mVUProgCache::Init through a temp EmuFolders::Cache and +# asserts that poisoned on-disk VERSION fields (magic / formatVersion / +# compilerAbiVersion / optionsSentinel) all rename the cache aside +# before the new run accepts INDEX entries, and that the variable-length +# INDEX serializer round-trips / drops truncated tails / skips +# tombstones. The disk scaffolding is the foundation of the +# persisted-JIT program cache. +add_pcsx2_test(mvu_progcache_versioning_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../StubHost.cpp + mvu_progcache_versioning_tests.cpp +) +target_include_directories(mvu_progcache_versioning_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../pcsx2 +) +target_link_libraries(mvu_progcache_versioning_tests PUBLIC + PCSX2_FLAGS + PCSX2 + common +) diff --git a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp new file mode 100644 index 0000000000..cf45f5468e --- /dev/null +++ b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// ABI-digest guard — the #1 corruption backstop for the persisted-JIT VU +// program cache. +// +// The on-disk cache trusts kMvuCompilerAbiVersion (mixed into the VERSION +// handshake) to mean "the emitters that produced a .vuprog are the emitters +// running now." An emitter change WITHOUT an ABI bump silently runs stale +// code shapes hydrated from disk — the payload checksum can't see it (the +// bytes match what was saved; it's the saver that changed). This test pins +// the emitted SHAPE per ABI version so that drift fails loudly at commit +// time instead. +// +// The digest (mVUPersist::TestComputeEmitDigest) masks every operand that +// may legitimately differ between correct emissions — 64-bit mov-chain +// immediates, B/BL displacements, ADRP pages, fixup address payloads — and +// hashes what remains: opcode selection, register allocation, instruction +// order, block/chunk/fixup structure. That is deterministic across runs, +// machines, and PIE/ASLR. +// +// WHEN THIS TEST GOES RED: +// 1. You changed mVU codegen (any microVU_*-arm64 emit path, AsmHelpers +// canonical forms, the serializer layout): bump kMvuCompilerAbiVersion +// in microVU-arm64.h (+ the mirror in mvu_progcache_versioning_tests), +// then add the new {abi, digests} row below. The bump evicts every +// stale on-disk cache — that's the point. +// 2. You changed a default config value that alters emitted forms (clamp +// mode etc.): the options sentinel already evicts those caches; just +// re-pin the digests here (no ABI bump needed). +// Never "fix" this test by re-pinning without deciding which case you +// are in. +// +// Recording is enabled during compilation because the cache only ever +// stores recording-enabled forms (canonical movs, forced-long cond +// branches) — those are the shapes worth pinning. + +#include "harness/VuTestHarness.h" +#include "harness/RecompilerTestEnvironment.h" + +#include "VU.h" +#include "VUmicro.h" +#include "arm64/microVU_Persist-arm64.h" +#include "arm64/microVU_ProgCache-arm64.h" + +#include + +#include +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp UpperOnly(u32 upper) +{ + return IBit(VuOp{VLitZero(), upper}); +} + +inline VuOp LowerOnly(u32 lower) +{ + return VuOp{lower, VNOP_U()}; +} + +// One digest per probe program. The set mirrors the round-trip suite's +// coverage axes: FMAC straight-line (upper pipeline + clamp emitters), +// conditional branch both-arms (block linking, SelfBlockAbs fixups), and +// indirect jump (two emission episodes, jump-cache path, stub calls). +struct DigestSet +{ + u64 straightLine; + u64 branchBothArms; + u64 indirectJump; +}; + +struct AbiPin +{ + u32 abi; + DigestSet digests; +}; + +// === THE PIN TABLE === (see header comment for the update protocol) +constexpr AbiPin kPins[] = { + {3, {0x4c3b6e1330199619, 0xd6f530cc13f0d0aa, 0xfcead342cc0b7df8}}, +}; + +u64 CompileAndDigest(std::initializer_list pairs) +{ + VuTestHarness h(0); + h.SetVf(1, 1.5f, -2.25f, 3.0f, 0.0625f); + h.SetVf(2, 4.0f, 0.5f, -1.0f, 8.0f); + h.SetVi(1, 1); + h.LoadProgram(pairs); + h.Run(); + h.RunJitPreserveBlockCache(); + u64 digest = 0; + EXPECT_TRUE(mVUPersist::TestComputeEmitDigest(0, digest)); + RecompilerTestEnvironment::ResetVuBlockCache(0); + return digest; +} + +} // namespace + +TEST(MvuAbiDigest, EmittedShapePinnedPerAbiVersion) +{ + ASSERT_TRUE(RecompilerTestEnvironment::IsReady()); + mVUPersist::SetRecordingEnabled(true); + + DigestSet actual = {}; + actual.straightLine = CompileAndDigest({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + UpperOnly(VMUL_U(mask::xyzw, vf::vf4, vf::vf3, vf::vf2)), + UpperOnly(bits::E | VSUB_U(mask::xyzw, vf::vf5, vf::vf4, vf::vf1)), + }); + actual.branchBothArms = CompileAndDigest({ + LowerOnly(VIBNE_L(vi::vi1, vi::vi0, 3)), + UpperOnly(VADD_U(mask::xyzw, vf::vf4, vf::vf1, vf::vf2)), + UpperOnly(bits::E | VSUB_U(mask::xyzw, vf::vf5, vf::vf1, vf::vf2)), + NopPair(), + UpperOnly(bits::E | VMUL_U(mask::xyzw, vf::vf6, vf::vf1, vf::vf2)), + }); + actual.indirectJump = CompileAndDigest({ + LowerOnly(VIADDIU_L(vi::vi1, vi::vi0, 4)), + LowerOnly(VJR_L(vi::vi1)), + NopPair(), + NopPair(), + UpperOnly(bits::E | VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + }); + + mVUPersist::SetRecordingEnabled(false); + + ASSERT_NE(actual.straightLine, 0u); + ASSERT_NE(actual.branchBothArms, 0u); + ASSERT_NE(actual.indirectJump, 0u); + + const u32 abi = mVUProgCache::GetCompilerAbiVersion(); + const AbiPin* pin = nullptr; + for (const AbiPin& p : kPins) + { + if (p.abi == abi) + pin = &p; + } + ASSERT_NE(pin, nullptr) + << "kMvuCompilerAbiVersion=" << abi << " has no digest pin — add a " + << "row to kPins with the values printed below.\n" + << " actual: {0x" << std::hex << actual.straightLine + << ", 0x" << actual.branchBothArms + << ", 0x" << actual.indirectJump << "}"; + + const auto explain = [&](const char* which, u64 got, u64 want) { + char buf[256]; + std::snprintf(buf, sizeof(buf), + "%s digest drifted for ABI v%u: got 0x%016" PRIx64 ", pinned 0x%016" PRIx64 ".\n" + "Emitted code shape changed — bump kMvuCompilerAbiVersion (emitter " + "change) or re-pin (config-default change). See file header.", + which, abi, got, want); + return std::string(buf); + }; + EXPECT_EQ(actual.straightLine, pin->digests.straightLine) + << explain("straightLine", actual.straightLine, pin->digests.straightLine); + EXPECT_EQ(actual.branchBothArms, pin->digests.branchBothArms) + << explain("branchBothArms", actual.branchBothArms, pin->digests.branchBothArms); + EXPECT_EQ(actual.indirectJump, pin->digests.indirectJump) + << explain("indirectJump", actual.indirectJump, pin->digests.indirectJump); +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/mvu_observed_entries_tests.cpp b/tests/ctest/core/recompilers/mvu_observed_entries_tests.cpp new file mode 100644 index 0000000000..c2c092634b --- /dev/null +++ b/tests/ctest/core/recompilers/mvu_observed_entries_tests.cpp @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// Unit tests for MvuObservedEntries, the fixed-cap set of microMem +// byte offsets the dispatcher records for each microProgram. The +// persisted-JIT program cache uses the observed entry-PC set for INDEX +// serialization and preload prioritization, so its contract (idempotent +// insert, version bumps only on new PC, cap silently drops) stays +// pinned here. + +#include "arm64/MvuObservedEntries.h" + +#include + +namespace +{ + +TEST(MvuObservedEntries, default_constructed_is_empty) +{ + MvuObservedEntries e{}; + EXPECT_EQ(0u, e.count); + EXPECT_EQ(0u, e.version); +} + +TEST(MvuObservedEntries, record_first_inserts_and_bumps_version) +{ + MvuObservedEntries e{}; + EXPECT_TRUE(e.record(0x100u)); + EXPECT_EQ(1u, e.count); + EXPECT_EQ(0x100u, e.pcs[0]); + EXPECT_EQ(1u, e.version); +} + +TEST(MvuObservedEntries, record_duplicate_is_no_op) +{ + MvuObservedEntries e{}; + ASSERT_TRUE(e.record(0x100u)); + EXPECT_FALSE(e.record(0x100u)); + EXPECT_EQ(1u, e.count); + EXPECT_EQ(1u, e.version); +} + +TEST(MvuObservedEntries, record_distinct_pcs_accumulate_in_order) +{ + MvuObservedEntries e{}; + ASSERT_TRUE(e.record(0x000u)); + ASSERT_TRUE(e.record(0x058u)); + ASSERT_TRUE(e.record(0x0A8u)); + ASSERT_TRUE(e.record(0x148u)); + EXPECT_EQ(4u, e.count); + EXPECT_EQ(0x000u, e.pcs[0]); + EXPECT_EQ(0x058u, e.pcs[1]); + EXPECT_EQ(0x0A8u, e.pcs[2]); + EXPECT_EQ(0x148u, e.pcs[3]); + EXPECT_EQ(4u, e.version); +} + +TEST(MvuObservedEntries, record_interleaved_duplicates_keep_version_stable) +{ + MvuObservedEntries e{}; + ASSERT_TRUE(e.record(0x000u)); + ASSERT_TRUE(e.record(0x058u)); + ASSERT_FALSE(e.record(0x000u)); + ASSERT_FALSE(e.record(0x058u)); + ASSERT_TRUE(e.record(0x0A8u)); + ASSERT_FALSE(e.record(0x058u)); + EXPECT_EQ(3u, e.count); + EXPECT_EQ(3u, e.version); +} + +TEST(MvuObservedEntries, record_at_cap_drops_overflow_silently) +{ + MvuObservedEntries e{}; + for (u32 i = 0; i < MvuObservedEntries::kMax; ++i) + ASSERT_TRUE(e.record(i * 8u)) << "filling slot " << i; + EXPECT_EQ(MvuObservedEntries::kMax, e.count); + EXPECT_EQ(MvuObservedEntries::kMax, e.version); + + // One past the cap: silently dropped, count + version unchanged. + EXPECT_FALSE(e.record(0xDEADu)); + EXPECT_EQ(MvuObservedEntries::kMax, e.count); + EXPECT_EQ(MvuObservedEntries::kMax, e.version); + + // An already-observed PC at the cap is still detected as a + // duplicate (the cap check fires only when the linear scan misses). + EXPECT_FALSE(e.record(0u)); + EXPECT_EQ(MvuObservedEntries::kMax, e.count); + EXPECT_EQ(MvuObservedEntries::kMax, e.version); +} + +TEST(MvuObservedEntries, clear_resets_to_empty_state) +{ + MvuObservedEntries e{}; + ASSERT_TRUE(e.record(0x100u)); + ASSERT_TRUE(e.record(0x200u)); + ASSERT_TRUE(e.record(0x300u)); + e.clear(); + EXPECT_EQ(0u, e.count); + EXPECT_EQ(0u, e.version); + // Re-record after clear works exactly like a fresh struct. + EXPECT_TRUE(e.record(0x100u)); + EXPECT_EQ(1u, e.count); + EXPECT_EQ(1u, e.version); +} + +TEST(MvuObservedEntries, version_bump_count_matches_distinct_inserts) +{ + // The "re-compile when version moves" heuristic depends on the version + // reading exactly the number of distinct PCs the dispatcher has + // observed. Pin that invariant here so a future refactor (e.g. "bump + // on every record() call") doesn't break the re-compile threshold. + MvuObservedEntries e{}; + // Mirrors the 9 distinct entry PCs observed on Katamari's vu0 + // program `7009298a…`, with deliberate duplicates sprinkled in to + // exercise the idempotent path. + const u32 inserts[] = { + 0x000, 0x058, 0x000, 0x0A8, 0x0C8, 0x058, + 0x0D8, 0x100, 0x118, 0x128, 0x148, 0x148, 0x148, + }; + u32 expected_distinct = 0; + for (u32 pc : inserts) + { + bool inserted = e.record(pc); + if (inserted) + ++expected_distinct; + EXPECT_EQ(expected_distinct, e.version); + EXPECT_EQ(expected_distinct, e.count); + } + // 9 distinct PCs in the Katamari sequence. + EXPECT_EQ(9u, e.count); + EXPECT_EQ(9u, e.version); +} + +} // namespace diff --git a/tests/ctest/core/recompilers/mvu_persist_roundtrip_tests.cpp b/tests/ctest/core/recompilers/mvu_persist_roundtrip_tests.cpp new file mode 100644 index 0000000000..81cb2e6afc --- /dev/null +++ b/tests/ctest/core/recompilers/mvu_persist_roundtrip_tests.cpp @@ -0,0 +1,338 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Persisted-JIT VU program cache — in-process round-trip gate. +// +// The contract under test (see microVU_Persist-arm64.h): +// +// compile (recording on) → serialize → mVUreset → hydrate → re-run +// +// (a) the hydrated run produces a bit-identical post-VU-state to the +// freshly-compiled run from the same pre-state; +// (b) the hydrated code bytes equal the recorded chunk bytes everywhere +// outside the fixup spans (relocation operands are the ONLY thing +// the patcher may touch); +// (c) the hydrated run performs ZERO block compiles; +// (d) a corrupted image (hash, layout bases, fixup table, truncation) is +// rejected before any side effect — fail-safe means recompile, never +// run wrong code. +// +// All tests drive VU0: its compile + execution stay on the test thread +// (VU1 routes through the MTVU thread, which TestHydrate must not cross). + +#include "harness/VuTestHarness.h" +#include "harness/RecompilerTestEnvironment.h" + +#include "VU.h" +#include "VUmicro.h" +#include "arm64/microVU_Persist-arm64.h" + +#include + +#include +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp UpperOnly(u32 upper) +{ + return IBit(VuOp{VLitZero(), upper}); +} + +inline VuOp LowerOnly(u32 lower) +{ + return VuOp{lower, VNOP_U()}; +} + +// Serialized-image header offsets (pinned by mVUPersist::ImageHeader v1; +// the corruption tests poke these fields directly). +constexpr size_t kOffHashLo = 16; +constexpr size_t kOffImageAnchor = 32; +constexpr size_t kOffChunkCount = 68; +constexpr size_t kHeaderSize = 88; + +class MvuPersistRoundTrip : public ::testing::Test +{ +protected: + void SetUp() override + { + ASSERT_TRUE(RecompilerTestEnvironment::IsReady()); + mVUPersist::SetRecordingEnabled(true); + } + + void TearDown() override + { + mVUPersist::SetRecordingEnabled(false); + // Drop any recorded program so recording state can't leak into + // later (non-persist) tests through a surviving block cache. + RecompilerTestEnvironment::ResetVuBlockCache(0); + } + + // Compile `h`'s loaded program fresh with recording enabled and leave it + // LIVE in the VU0 program cache (VuTestHarness::Run's interp pass resets + // the block cache, deleting the JIT-pass program — so after the + // correctness diff we re-run the JIT side alone, which recompiles into a + // surviving program). + void RunAndKeepProgram(VuTestHarness& h) + { + h.Run(); // JIT-vs-interp correctness diff (program gets reset away) + h.RunJitPreserveBlockCache(); // fresh compile, program survives + } + + // Full round-trip with gates (a)-(c); leaves the hydrated program live. + // `pairs` is the same program handed to h.LoadProgram by the caller — + // the padding step below clobbers VU Micro, so it must be re-written + // before hydration (the content hash is computed over live Micro). + // `out_image` (optional) receives the serialized image so callers can + // pin format facts (e.g. chunk counts). (void return: ASSERT_* needs it.) + void RoundTrip(VuTestHarness& h, std::initializer_list pairs, + std::vector* out_image = nullptr) + { + RunAndKeepProgram(h); + const VuSnapshot fresh_jit = h.JitSnapshot(); + + std::vector image; + ASSERT_TRUE(mVUPersist::TestSerializeNewestProgram(0, image)) + << "program was not recorded — recorder dropped the episode?"; + ASSERT_GT(image.size(), kHeaderSize); + + // Wipe every compiled block + program, then occupy the front of the + // code cache with an unrelated program so the hydrated chunks land at + // a DIFFERENT slab offset than they were recorded at. Without this + // shift the deterministic cursor would replay every chunk at its + // original address and an unpatched (broken) fixup would still pass. + { + VuTestHarness pad(0); + pad.SetVf(7, 9.0f, 9.0f, 9.0f, 9.0f); + pad.LoadProgram({ + UpperOnly(VMAX_U(mask::xyzw, vf::vf8, vf::vf7, vf::vf7)), + UpperOnly(bits::E | VMINI_U(mask::xyzw, vf::vf9, vf::vf7, vf::vf8)), + }); + pad.Run(); + pad.RunJitPreserveBlockCache(); // pad program now holds the cursor base + } + + // Restore the main program's bytes in VU Micro (the pad harness + // zeroed and rewrote it). LoadProgram memcpys into Micro directly, + // bypassing the vtlb write path that calls mVUclear in production — + // without the explicit Clear, the stale quick slot would dispatch + // the PAD program for startPC 0 instead of re-resolving by hash. + h.LoadProgram(pairs); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + ASSERT_TRUE(mVUPersist::TestHydrate(0, image.data(), image.size())); + EXPECT_EQ(mVUPersist::GetBlockCompileCount(0), compiles_before) + << "hydration itself must not compile"; + + // Gate (b): live code == recorded code modulo fixup operands. + EXPECT_TRUE(mVUPersist::TestVerifyHydratedCode(0, image.data(), image.size())); + + // Gate (c): the hydrated run must not compile anything. + h.RunJitPreserveBlockCache(); + EXPECT_EQ(mVUPersist::GetBlockCompileCount(0), compiles_before) + << "hydrated program recompiled at dispatch — block graph not resolved"; + + // Gate (a): bit-identical post-state vs the fresh compile. + const auto diffs = DiffVu(fresh_jit, h.JitSnapshot(), VuDiffMode::Strict); + EXPECT_TRUE(diffs.empty()) << [&] { + std::string s = "hydrated JIT diverged from fresh JIT:\n"; + for (const auto& d : diffs) + s += " " + d + "\n"; + return s; + }(); + + if (out_image) + *out_image = std::move(image); + } +}; + +} // namespace + +//------------------------------------------------------------------ +// Round trips +//------------------------------------------------------------------ + +TEST_F(MvuPersistRoundTrip, StraightLineProgram) +{ + VuTestHarness h(0); + h.SetVf(1, 1.5f, -2.25f, 3.0f, 0.0625f); + h.SetVf(2, 4.0f, 0.5f, -1.0f, 8.0f); + const std::initializer_list pairs = { + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + UpperOnly(VMUL_U(mask::xyzw, vf::vf4, vf::vf3, vf::vf2)), + UpperOnly(bits::E | VSUB_U(mask::xyzw, vf::vf5, vf::vf4, vf::vf1)), + }; + h.LoadProgram(pairs); + RoundTrip(h, pairs); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 5.5f); + + const auto stats = mVUPersist::GetStats(0); + EXPECT_GE(stats.chunksRecorded, 1u); + EXPECT_GE(stats.blocksRecorded, 1u); + EXPECT_GE(stats.programsHydrated, 1u); +} + +TEST_F(MvuPersistRoundTrip, BranchBothArms) +{ + // IBNE with both arms compiled eagerly (condBranch compiles the + // not-taken block inline and resolves the taken target via + // mVUblockFetch) — multi-block, single-chunk program. + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.SetVf(2, 0.5f, 0.5f, 0.5f, 0.5f); + h.SetVi(1, 1); // branch taken + const std::initializer_list pairs = { + LowerOnly(VIBNE_L(vi::vi1, vi::vi0, 3)), // pair 0 → taken target pair 4 + UpperOnly(VADD_U(mask::xyzw, vf::vf4, vf::vf1, vf::vf2)), // pair 1: delay slot + UpperOnly(bits::E | VSUB_U(mask::xyzw, vf::vf5, vf::vf1, vf::vf2)), // pair 2: not-taken exit + NopPair(), // pair 3: not-taken E-bit delay slot + UpperOnly(bits::E | VMUL_U(mask::xyzw, vf::vf6, vf::vf1, vf::vf2)), // pair 4: taken exit + }; + h.LoadProgram(pairs); + RoundTrip(h, pairs); + EXPECT_FLOAT_EQ(h.GetVfJit(6, 'x'), 0.5f); + + const auto stats = mVUPersist::GetStats(0); + EXPECT_GE(stats.chunksRecorded, 1u); + EXPECT_GE(stats.blocksRecorded, 2u); + EXPECT_GE(stats.fixupsRecorded, 1u); + EXPECT_GE(stats.programsHydrated, 1u); +} + +TEST_F(MvuPersistRoundTrip, IndirectJumpTwoChunks) +{ + // JR compiles the jump target in a SECOND emission episode (the runtime + // mVUcompileJIT path) — exercises multi-chunk programs and the + // jumpCache restore (mVUcompileJIT dereferences it unguarded). + VuTestHarness h(0); + h.SetVf(1, 2.0f, 4.0f, 8.0f, 16.0f); + h.SetVf(2, 1.0f, 1.0f, 1.0f, 1.0f); + const std::initializer_list pairs = { + LowerOnly(VIADDIU_L(vi::vi1, vi::vi0, 4)), // pair 0: vi1 = 4 (target pair) + LowerOnly(VJR_L(vi::vi1)), // pair 1: JR vi1 → pair 4 + NopPair(), // pair 2: delay slot + NopPair(), // pair 3: skipped + UpperOnly(bits::E | VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), // pair 4 + }; + h.LoadProgram(pairs); + std::vector image; + RoundTrip(h, pairs, &image); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 3.0f); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'w'), 17.0f); + + // The JR target compiles in a SECOND emission episode (runtime + // mVUcompileJIT) — pin that the image really carries two chunks, i.e. + // the multi-chunk path (cross-chunk placement + per-chunk fixups) was + // exercised and not silently collapsed into one episode. + ASSERT_GE(image.size(), kHeaderSize); + u32 chunk_count = 0; + std::memcpy(&chunk_count, image.data() + kOffChunkCount, sizeof(chunk_count)); + EXPECT_GE(chunk_count, 2u); +} + +//------------------------------------------------------------------ +// Gate (d): fail-safe rejection +//------------------------------------------------------------------ + +namespace { + +// Compile + serialize a minimal program and hand back the image. +std::vector MakeImage(VuTestHarness& h) +{ + h.Run(); + h.RunJitPreserveBlockCache(); + std::vector image; + EXPECT_TRUE(mVUPersist::TestSerializeNewestProgram(0, image)); + RecompilerTestEnvironment::ResetVuBlockCache(0); + return image; +} + +} // namespace + +TEST_F(MvuPersistRoundTrip, RejectsContentHashMismatch) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf1)), + EBitNopPair(), + }); + std::vector image = MakeImage(h); + ASSERT_GT(image.size(), kHeaderSize); + + image[kOffHashLo] ^= 0xFF; + EXPECT_FALSE(mVUPersist::TestHydrate(0, image.data(), image.size())); +} + +TEST_F(MvuPersistRoundTrip, RejectsLayoutBaseMismatch) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf1)), + EBitNopPair(), + }); + std::vector image = MakeImage(h); + + // Pretend the image was produced by a process with a different + // executable base (PIE dev box / layout drift). + image[kOffImageAnchor + 5] ^= 0x01; + EXPECT_FALSE(mVUPersist::TestHydrate(0, image.data(), image.size())); +} + +TEST_F(MvuPersistRoundTrip, RejectsTruncatedImage) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf1)), + EBitNopPair(), + }); + std::vector image = MakeImage(h); + + EXPECT_FALSE(mVUPersist::TestHydrate(0, image.data(), image.size() - 8)); + EXPECT_FALSE(mVUPersist::TestHydrate(0, image.data(), kHeaderSize / 2)); +} + +TEST_F(MvuPersistRoundTrip, RejectsStructuralCorruption) +{ + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf1)), + EBitNopPair(), + }); + std::vector image = MakeImage(h); + + // Inflate chunkCount so the parser walks past the payload. + u32 chunk_count = 0; + std::memcpy(&chunk_count, image.data() + kOffChunkCount, sizeof(chunk_count)); + chunk_count += 7; + std::memcpy(image.data() + kOffChunkCount, &chunk_count, sizeof(chunk_count)); + EXPECT_FALSE(mVUPersist::TestHydrate(0, image.data(), image.size())); +} + +TEST_F(MvuPersistRoundTrip, NoLogWhenRecordingDisabled) +{ + mVUPersist::SetRecordingEnabled(false); + + VuTestHarness h(0); + h.SetVf(1, 1.0f, 2.0f, 3.0f, 4.0f); + h.LoadProgram({ + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf1)), + EBitNopPair(), + }); + h.Run(); + h.RunJitPreserveBlockCache(); + + std::vector image; + EXPECT_FALSE(mVUPersist::TestSerializeNewestProgram(0, image)) + << "programs compiled with recording off must have no persist log"; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/mvu_progcache_disk_tests.cpp b/tests/ctest/core/recompilers/mvu_progcache_disk_tests.cpp new file mode 100644 index 0000000000..e3982c46bb --- /dev/null +++ b/tests/ctest/core/recompilers/mvu_progcache_disk_tests.cpp @@ -0,0 +1,426 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// Persisted-JIT VU program cache — disk round-trip gate. +// +// The in-process serialize → hydrate path is covered via test hooks elsewhere. +// This file proves the PRODUCTION path end to end, through the real disk +// cache and the real dispatcher seam: +// +// compile (recording on) → mVUreset → SaveAllPrograms writes INDEX + +// .vuprog payload → next dispatch misses contentMap + per-PC deque → +// mVUsearchProg calls mVUProgCache::TryLoadProgram → payload read + +// HydrateProgram (episode closed/reopened around it) → program runs +// with ZERO block compiles and a bit-identical post-state. +// +// Only process death separates this from the cross-process flow — the +// in-memory INDEX is rebuilt from disk by Init, and the payload bytes +// travel exclusively through the filesystem (the vurunner --cache-dir +// gate covers the true two-process case on corpus captures). +// +// Fail-safe gates: a corrupt payload (checksum), a missing payload file, +// and a telemetry-only INDEX entry (recording off when saved) must all +// degrade to a clean recompile. +// +// All tests drive VU0 — same thread-affinity reasoning as +// mvu_persist_roundtrip_tests.cpp. + +#include "harness/VuTestHarness.h" +#include "harness/RecompilerTestEnvironment.h" + +#include "Config.h" +#include "VU.h" +#include "VUmicro.h" +#include "arm64/microVU_Persist-arm64.h" +#include "arm64/microVU_ProgCache-arm64.h" + +#include "common/FileSystem.h" +#include "common/Path.h" + +#include + +#include +#include +#include +#include + +#include + +namespace recompiler_tests { + +using namespace vu; + +namespace { + +inline VuOp UpperOnly(u32 upper) +{ + return IBit(VuOp{VLitZero(), upper}); +} + +void RecursiveRm(const std::string& dir) +{ + if (!FileSystem::DirectoryExists(dir.c_str())) + { + ::unlink(dir.c_str()); + return; + } + FileSystem::FindResultsArray ents; + FileSystem::FindFiles(dir.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &ents); + for (const auto& e : ents) + { + if (e.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + RecursiveRm(e.FileName); + else + ::unlink(e.FileName.c_str()); + } + ::rmdir(dir.c_str()); +} + +// Collect every *.vuprog under `dir` (payloads live one shard level down). +void FindPayloads(const std::string& dir, std::vector& out) +{ + if (!FileSystem::DirectoryExists(dir.c_str())) + return; + FileSystem::FindResultsArray ents; + FileSystem::FindFiles(dir.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &ents); + for (const auto& e : ents) + { + if (e.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + FindPayloads(e.FileName, out); + else if (e.FileName.size() > 7 && + e.FileName.compare(e.FileName.size() - 7, 7, ".vuprog") == 0) + out.push_back(e.FileName); + } +} + +class MvuProgCacheDisk : public ::testing::Test +{ +protected: + void SetUp() override + { + ASSERT_TRUE(RecompilerTestEnvironment::IsReady()); + // Recording is under manual test control (RecompilerTestEnvironment + // engaged SetTestManualRecording), so mVUreset's SyncRecordingFromConfig + // won't fight these tests — drive it directly. + mVUPersist::SetRecordingEnabled(true); + + char tmpl[] = "/tmp/progcache_disk_XXXXXX"; + char* p = ::mkdtemp(tmpl); + ASSERT_NE(p, nullptr); + root_ = p; + prev_cache_ = EmuFolders::Cache; + EmuFolders::Cache = root_; + } + + void TearDown() override + { + // Free live programs while the cache is still up (the reset-side + // SaveAllPrograms writing into the doomed temp dir is harmless), + // then disable the disk cache for everything that runs after us. + RecompilerTestEnvironment::ResetVuBlockCache(0); + mVUProgCache::ResetForTest(0); + mVUPersist::SetRecordingEnabled(false); + EmuConfig.Cpu.Recompiler.EnableVUProgramCache = false; + EmuFolders::Cache = prev_cache_; + RecursiveRm(root_); + } + + // Bring the on-disk cache up at the temp root. Deferred out of SetUp so + // tests can compile a genuine no-cache baseline first. + void InitDiskCache() + { + mVUProgCache::ResetForTest(0); + mVUProgCache::TestReinitFromLiveSentinel(0); + ASSERT_TRUE(mVUProgCache::GetStats(0).enabled) + << "disk cache failed to init at " << root_; + } + + std::string vu0_root() const + { + return Path::Combine(Path::Combine(root_, "vu_jit"), "vu0"); + } + + std::string root_; + std::string prev_cache_; +}; + +// The shared three-op straight-line program. Kept identical across tests so +// each test's expectations stay easy to eyeball. +const std::initializer_list kProgram = { + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + UpperOnly(VMUL_U(mask::xyzw, vf::vf4, vf::vf3, vf::vf2)), + UpperOnly(bits::E | VSUB_U(mask::xyzw, vf::vf5, vf::vf4, vf::vf1)), +}; + +void SeedAndLoad(VuTestHarness& h) +{ + h.SetVf(1, 1.5f, -2.25f, 3.0f, 0.0625f); + h.SetVf(2, 4.0f, 0.5f, -1.0f, 8.0f); + h.LoadProgram(kProgram); +} + +} // namespace + +TEST_F(MvuProgCacheDisk, SaveThenHydrateAcrossReset) +{ + // 1. Genuine fresh compile, no disk cache in the loop yet. + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + const VuSnapshot fresh_jit = h.JitSnapshot(); + + // 2. Cache up; reset writes INDEX + payload for the live program. + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + + auto cstats = mVUProgCache::GetStats(0); + ASSERT_GE(cstats.payloadWrites, 1u) << "reset-side SaveAllPrograms wrote no payload"; + std::vector payloads; + FindPayloads(vu0_root(), payloads); + ASSERT_EQ(payloads.size(), 1u); + + // 3. Next dispatch must hydrate from disk, not compile. + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + h.RunJitPreserveBlockCache(); + EXPECT_EQ(mVUPersist::GetBlockCompileCount(0), compiles_before) + << "dispatch recompiled instead of hydrating from disk"; + + cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.payloadHits, 1u); + EXPECT_EQ(cstats.payloadRejects, 0u); + EXPECT_GE(mVUPersist::GetStats(0).programsHydrated, 1u); + + const auto diffs = DiffVu(fresh_jit, h.JitSnapshot(), VuDiffMode::Strict); + EXPECT_TRUE(diffs.empty()) << [&] { + std::string s = "disk-hydrated JIT diverged from fresh JIT:\n"; + for (const auto& d : diffs) + s += " " + d + "\n"; + return s; + }(); + EXPECT_FLOAT_EQ(h.GetVfJit(3, 'x'), 5.5f); +} + +TEST_F(MvuProgCacheDisk, CorruptPayloadFallsBackToRecompile) +{ + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + const VuSnapshot fresh_jit = h.JitSnapshot(); + + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + + std::vector payloads; + FindPayloads(vu0_root(), payloads); + ASSERT_EQ(payloads.size(), 1u); + + // Flip one byte past the image header — valid structure, wrong code. + // Only the payload checksum can catch this. + auto bytes = FileSystem::ReadBinaryFile(payloads[0].c_str()); + ASSERT_TRUE(bytes.has_value()); + ASSERT_GT(bytes->size(), 96u); + (*bytes)[bytes->size() - 4] ^= 0xFF; + ASSERT_TRUE(FileSystem::WriteBinaryFile(payloads[0].c_str(), bytes->data(), bytes->size())); + + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + h.RunJitPreserveBlockCache(); + + EXPECT_GT(mVUPersist::GetBlockCompileCount(0), compiles_before) + << "corrupt payload should force a recompile"; + const auto cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.payloadRejects, 1u); + EXPECT_EQ(cstats.payloadHits, 0u); + + // Degrade, never corrupt: the recompiled run is still correct. + const auto diffs = DiffVu(fresh_jit, h.JitSnapshot(), VuDiffMode::Strict); + EXPECT_TRUE(diffs.empty()); +} + +TEST_F(MvuProgCacheDisk, MissingPayloadFallsBackToRecompile) +{ + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + + std::vector payloads; + FindPayloads(vu0_root(), payloads); + ASSERT_EQ(payloads.size(), 1u); + ASSERT_EQ(::unlink(payloads[0].c_str()), 0); + + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + h.RunJitPreserveBlockCache(); + + EXPECT_GT(mVUPersist::GetBlockCompileCount(0), compiles_before); + const auto cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.payloadMissing, 1u); + EXPECT_EQ(cstats.payloadHits, 0u); +} + +TEST_F(MvuProgCacheDisk, RecordingDisabledSavesIndexOnly) +{ + // With recording off the INDEX keeps its telemetry role and no payload + // appears on disk. + mVUPersist::SetRecordingEnabled(false); + + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + + auto cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.entries, 1u); + EXPECT_EQ(cstats.payloadWrites, 0u); + std::vector payloads; + FindPayloads(vu0_root(), payloads); + EXPECT_TRUE(payloads.empty()); + + // Dispatch hits the INDEX, finds no payload, recompiles. + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + h.RunJitPreserveBlockCache(); + EXPECT_GT(mVUPersist::GetBlockCompileCount(0), compiles_before); + cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.payloadMissing, 1u); +} + +TEST_F(MvuProgCacheDisk, PreloadServesHydrationWithoutDisk) +{ + // Init's background preload reads payload-bearing INDEX entries into RAM + // so first-dispatch hydration never does file I/O on the EE/MTVU + // threads. Proven by force: preload, then DELETE the .vuprog from disk — + // hydration must still succeed, served from the preload map. + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + const VuSnapshot fresh_jit = h.JitSnapshot(); + + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + ASSERT_GE(mVUProgCache::GetStats(0).payloadWrites, 1u); + + // Re-init from disk: the INDEX now carries a payload-bearing entry, so + // this Init spawns the preload. + mVUProgCache::ResetForTest(0); + mVUProgCache::TestReinitFromLiveSentinel(0); + mVUProgCache::TestWaitForPreload(0); + auto cstats = mVUProgCache::GetStats(0); + ASSERT_GE(cstats.preloadedPayloads, 1u) + << "Init did not preload the payload-bearing INDEX entry"; + ASSERT_GT(cstats.preloadedBytes, 0u); + + std::vector payloads; + FindPayloads(vu0_root(), payloads); + ASSERT_EQ(payloads.size(), 1u); + ASSERT_EQ(::unlink(payloads[0].c_str()), 0); + + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + const u64 compiles_before = mVUPersist::GetBlockCompileCount(0); + h.RunJitPreserveBlockCache(); + EXPECT_EQ(mVUPersist::GetBlockCompileCount(0), compiles_before) + << "dispatch recompiled — preloaded payload was not served"; + + cstats = mVUProgCache::GetStats(0); + EXPECT_GE(cstats.preloadHits, 1u); + EXPECT_GE(cstats.payloadHits, 1u); + EXPECT_EQ(cstats.payloadMissing, 0u); + EXPECT_EQ(cstats.preloadedBytes, 0u) << "consumed buffer was not released"; + + const auto diffs = DiffVu(fresh_jit, h.JitSnapshot(), VuDiffMode::Strict); + EXPECT_TRUE(diffs.empty()); +} + +TEST_F(MvuProgCacheDisk, RecordingStateJoinsOptionsSentinel) +{ + // Safety property: recording changes emitted code forms + // (canonical movs, forced-long cond branches), so the recording state + // MUST be part of program identity. A cache built with recording on + // must never be served to a recording-off run (or vice versa) — the + // sentinel divergence below is what makes the VERSION handshake evict + // it and what keys contentHash lookups apart. + XXH128_hash_t on{}, off{}, on_again{}; + ASSERT_TRUE(mVUProgCache::TestGetLiveSentinel(0, &on)); // SetUp: recording on + + mVUPersist::SetRecordingEnabled(false); + ASSERT_TRUE(mVUProgCache::TestGetLiveSentinel(0, &off)); + + mVUPersist::SetRecordingEnabled(true); + ASSERT_TRUE(mVUProgCache::TestGetLiveSentinel(0, &on_again)); + + EXPECT_TRUE(on.low64 != off.low64 || on.high64 != off.high64) + << "recording on/off produced the SAME options sentinel — the " + "recording bit is not mixed into program identity"; + EXPECT_EQ(on.low64, on_again.low64); + EXPECT_EQ(on.high64, on_again.high64); +} + +TEST_F(MvuProgCacheDisk, ConfigBoolGatesProductionInit) +{ + // The production seam: mVUreset must bring the disk cache up when (and + // only when) EmuCore/CPU/Recompiler EnableVUProgramCache is set — even + // though EmuFolders::Cache points at a perfectly writable directory. + mVUProgCache::ResetForTest(0); + + ASSERT_FALSE(EmuConfig.Cpu.Recompiler.EnableVUProgramCache); + RecompilerTestEnvironment::ResetVuBlockCache(0); + EXPECT_FALSE(mVUProgCache::GetStats(0).initialized) + << "disk cache initialized with the config bool OFF"; + + // As VMManager would after a settings toggle: flip the bool; the next + // recompiler reset activates the cache. + EmuConfig.Cpu.Recompiler.EnableVUProgramCache = true; + RecompilerTestEnvironment::ResetVuBlockCache(0); + const auto stats = mVUProgCache::GetStats(0); + EXPECT_TRUE(stats.initialized) + << "reset with the config bool ON did not initialize the disk cache"; + EXPECT_TRUE(stats.enabled); +} + +TEST_F(MvuProgCacheDisk, HydratedProgramResavesWithoutGrowth) +{ + // A hydrated program that runs and resets again must NOT rewrite its + // payload (the rebuilt persist log serializes to the same bytes) — + // pins the size-based growth signal that keeps reset-side saves cheap. + VuTestHarness h(0); + SeedAndLoad(h); + h.Run(); + h.RunJitPreserveBlockCache(); + + InitDiskCache(); + RecompilerTestEnvironment::ResetVuBlockCache(0); + auto cstats = mVUProgCache::GetStats(0); + ASSERT_EQ(cstats.payloadWrites, 1u); + + h.LoadProgram(kProgram); + CpuMicroVU0.Clear(0, VU0_PROGSIZE); + h.RunJitPreserveBlockCache(); // hydrates + ASSERT_GE(mVUProgCache::GetStats(0).payloadHits, 1u); + + RecompilerTestEnvironment::ResetVuBlockCache(0); // saves again + cstats = mVUProgCache::GetStats(0); + EXPECT_EQ(cstats.payloadWrites, 1u) + << "unchanged hydrated program rewrote its payload on re-save"; +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp b/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp new file mode 100644 index 0000000000..1a3e5c6866 --- /dev/null +++ b/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp @@ -0,0 +1,720 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// mVUProgCache VERSION handshake + stale-cache eviction + INDEX format. +// +// mVUProgCache::Init writes a VersionHeader to $root/VERSION on first +// boot. On every subsequent boot it reads back the on-disk header and +// compares against the live build's (kProgCacheFormatVersion, +// kMvuCompilerAbiVersion, optionsSentinel, vuIndex, archTag). Any +// mismatch atomically renames $root → $root.stale. and starts +// fresh. +// +// What this proves: +// +// 1. First Init on an empty cache dir writes a VERSION + creates +// $root. +// 2. Re-init with the same options sentinel keeps the cache alive +// (staleEvictions stays at 0). +// 3. Re-init with a different optionsSentinel triggers an eviction +// (staleEvictions ticks; $root.stale.* exists; INDEX entries +// from the prior run aren't visible). +// 4. A manually-poisoned VERSION (bad magic / bad formatVersion / +// bad compilerAbiVersion / bad archTag) triggers the same +// eviction path — defends the format-bump invariant. +// +// References: +// pcsx2/arm64/microVU_ProgCache-arm64.inl VersionHeader, VersionMatches, +// EvictStaleCache, Init +// pcsx2/arm64/microVU-arm64.h kMvuCompilerAbiVersion +// pcsx2/arm64/microVU_ProgCache-arm64.h kProgCacheFormatVersion, ResetForTest + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Config.h" +#include "common/FileSystem.h" +#include "common/Path.h" + +#include "arm64/microVU_ProgCache-arm64.h" + +// kMvuCompilerAbiVersion lives in microVU-arm64.h, but that header +// also #includes the .inl files at the bottom — including it from +// a test TU collides with libpcsx2.a's emitted bodies. Mirror the +// constant here; a future bump must keep the two in sync. Drift is +// caught directly by `mirror_matches_production_abi_version` below +// (which compares this against the linkable accessor +// mVUProgCache::GetCompilerAbiVersion()), with a clear message — +// rather than only surfacing as a confusing eviction in the +// round-trip tests. +namespace pcsx2_test +{ + static constexpr u32 kMvuCompilerAbiVersionMirror = 3; +} + +namespace +{ + // Mirrors the anonymous-namespace layout in microVU_ProgCache-arm64.inl. + // Test-side copy so we can poison the on-disk VERSION file without + // adding a backdoor to production. If the production layout drifts, + // the static_assert below will fail-loudly at compile time. +#pragma pack(push, 1) + struct VersionHeaderMirror + { + u32 magic; + u32 formatVersion; + u32 compilerAbiVersion; + u32 vuIndex; + XXH128_hash_t optionsSentinel; + char archTag[32]; + }; +#pragma pack(pop) + static_assert(sizeof(VersionHeaderMirror) == 64, + "VersionHeader test mirror drifted from production layout"); + + constexpr u32 kVersionMagicMirror = 0x4A56554Du; // 'MVUJ' + + // RAII: make a fresh tmpdir at EmuFolders::Cache and clean it up + // on destruction. Most tests do not actually exercise file ops, so + // using mkdtemp + nftw isn't worth pulling in — the tests below + // only create a small fixed set of files which we list explicitly. + class TempCacheRoot + { + public: + TempCacheRoot() + { + char tmpl[] = "/tmp/progcache_ver_XXXXXX"; + char* p = ::mkdtemp(tmpl); + EXPECT_NE(p, nullptr) << "mkdtemp failed: " << std::strerror(errno); + m_root = p ? p : std::string(); + m_prev_cache = EmuFolders::Cache; + EmuFolders::Cache = m_root; + } + ~TempCacheRoot() + { + EmuFolders::Cache = m_prev_cache; + // If mkdtemp failed the ctor left m_root empty (EXPECT_NE already + // flagged it). Don't proceed: Path::Combine("", "vu_jit/...") + // yields absolute /vu_jit paths, so cleanup would walk and + // RecursiveRm system-root locations. Bail out instead. + if (m_root.empty()) + return; + // Wipe everything we know we might have created under m_root. + // Order matters: deepest first. + std::vector dirs_to_try; + dirs_to_try.push_back(Path::Combine(m_root, "vu_jit/vu0")); + dirs_to_try.push_back(Path::Combine(m_root, "vu_jit/vu1")); + dirs_to_try.push_back(Path::Combine(m_root, "vu_jit")); + dirs_to_try.push_back(m_root); + // Also remove any *.stale.* dirs the eviction path created. + FileSystem::FindResultsArray staleDirs; + FileSystem::FindFiles( + Path::Combine(m_root, "vu_jit").c_str(), + "vu0.stale.*", + FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &staleDirs); + for (const auto& sd : staleDirs) + RecursiveRm(sd.FileName); + staleDirs.clear(); + FileSystem::FindFiles( + Path::Combine(m_root, "vu_jit").c_str(), + "vu1.stale.*", + FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &staleDirs); + for (const auto& sd : staleDirs) + RecursiveRm(sd.FileName); + for (const auto& d : dirs_to_try) + RecursiveRm(d); + } + const std::string& root() const { return m_root; } + std::string vu_subdir(u32 vu_index) const + { + return Path::Combine(Path::Combine(m_root, "vu_jit"), + (vu_index & 1) ? "vu1" : "vu0"); + } + private: + static void RecursiveRm(const std::string& dir) + { + if (!FileSystem::DirectoryExists(dir.c_str())) + { + // Maybe a regular file. + ::unlink(dir.c_str()); + return; + } + FileSystem::FindResultsArray ents; + FileSystem::FindFiles(dir.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS + | FILESYSTEM_FIND_HIDDEN_FILES, &ents); + for (const auto& e : ents) + { + if (e.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + RecursiveRm(e.FileName); + else + ::unlink(e.FileName.c_str()); + } + ::rmdir(dir.c_str()); + } + std::string m_root; + std::string m_prev_cache; + }; + + // Write the given header verbatim to `version_path`. Callers supply + // either a current-format header or a deliberately-poisoned one. + void WriteVersionFile(const std::string& version_path, + VersionHeaderMirror& hdr) + { + // Atomic-replace via tmp + rename, same pattern Init uses. + const std::string tmp = version_path + ".tmp"; + std::FILE* fp = std::fopen(tmp.c_str(), "wb"); + ASSERT_NE(fp, nullptr) << "fopen: " << std::strerror(errno); + ASSERT_EQ(std::fwrite(&hdr, sizeof(hdr), 1, fp), 1u); + std::fclose(fp); + ASSERT_EQ(std::rename(tmp.c_str(), version_path.c_str()), 0); + } + + // Read the on-disk VERSION back, asserting the file exists + the + // magic + format/abi/vuIndex/archTag agree with the running build. + void ReadAndVerifyCurrentVersion(const std::string& version_path, + u32 vu_index) + { + auto bytes = FileSystem::ReadBinaryFile(version_path.c_str()); + ASSERT_TRUE(bytes.has_value()) + << "VERSION not on disk at " << version_path; + ASSERT_EQ(bytes->size(), sizeof(VersionHeaderMirror)); + VersionHeaderMirror hdr; + std::memcpy(&hdr, bytes->data(), sizeof(hdr)); + EXPECT_EQ(hdr.magic, kVersionMagicMirror); + EXPECT_EQ(hdr.formatVersion, + mVUProgCache::kProgCacheFormatVersion); + EXPECT_EQ(hdr.compilerAbiVersion, + pcsx2_test::kMvuCompilerAbiVersionMirror); + EXPECT_EQ(hdr.vuIndex, vu_index & 1u); + EXPECT_STREQ(hdr.archTag, "arm64-jit"); + } +} + +// 1. Init on an empty cache dir creates $root + writes a VERSION file +// matching the running build. +TEST(ProgCacheVersioning, fresh_init_writes_version_file) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xAAAAAAAAAAAAAAAAull; + sentinel.high64 = 0xBBBBBBBBBBBBBBBBull; + mVUProgCache::InitWithSentinel(1, sentinel); + + const auto stats = mVUProgCache::GetStats(1); + EXPECT_TRUE(stats.initialized); + EXPECT_TRUE(stats.enabled); + EXPECT_EQ(stats.staleEvictions, 0u); + EXPECT_EQ(stats.entries, 0u); + + const std::string version_path = Path::Combine(tmp.vu_subdir(1), "VERSION"); + ReadAndVerifyCurrentVersion(version_path, /*vu_index=*/1); + + mVUProgCache::ResetForTest(1); +} + +// 2. Re-init with the same sentinel keeps the cache alive (no eviction). +TEST(ProgCacheVersioning, reinit_same_sentinel_keeps_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0x1111111111111111ull; + sentinel.high64 = 0x2222222222222222ull; + mVUProgCache::InitWithSentinel(1, sentinel); + mVUProgCache::ResetForTest(1); + + // Second Init — same sentinel. + mVUProgCache::InitWithSentinel(1, sentinel); + const auto stats = mVUProgCache::GetStats(1); + EXPECT_TRUE(stats.initialized); + EXPECT_EQ(stats.staleEvictions, 0u); + + mVUProgCache::ResetForTest(1); +} + +// 3. Re-init with a different optionsSentinel triggers an eviction. +TEST(ProgCacheVersioning, optionsSentinel_mismatch_evicts_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t s_a{}; + s_a.low64 = 0xDEAD0000ull; s_a.high64 = 0xBEEF0000ull; + mVUProgCache::InitWithSentinel(1, s_a); + mVUProgCache::ResetForTest(1); + + XXH128_hash_t s_b{}; + s_b.low64 = 0x1234ull; s_b.high64 = 0x5678ull; // differs from s_a + mVUProgCache::InitWithSentinel(1, s_b); + + const auto stats = mVUProgCache::GetStats(1); + EXPECT_TRUE(stats.initialized); + EXPECT_EQ(stats.staleEvictions, 1u) + << "optionsSentinel mismatch must rename the cache aside"; + EXPECT_EQ(stats.entries, 0u); + + // $root.stale.* should exist somewhere under $root_parent. + FileSystem::FindResultsArray stales; + FileSystem::FindFiles( + Path::Combine(tmp.root(), "vu_jit").c_str(), + "vu1.stale.*", + FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &stales); + EXPECT_FALSE(stales.empty()) + << "expected at least one vu1.stale. directory after eviction"; + + // Fresh VERSION on the new $root reflects s_b. + const std::string version_path = Path::Combine(tmp.vu_subdir(1), "VERSION"); + auto bytes = FileSystem::ReadBinaryFile(version_path.c_str()); + ASSERT_TRUE(bytes.has_value()); + VersionHeaderMirror hdr; + std::memcpy(&hdr, bytes->data(), sizeof(hdr)); + EXPECT_EQ(hdr.optionsSentinel.low64, s_b.low64); + EXPECT_EQ(hdr.optionsSentinel.high64, s_b.high64); + + mVUProgCache::ResetForTest(1); +} + +// 4. Poisoned VERSION on disk (wrong magic) triggers eviction. Same +// code path as (3), exercising the VersionMatches.magic check. +TEST(ProgCacheVersioning, bad_magic_evicts_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(0); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0x7777ull; sentinel.high64 = 0x8888ull; + mVUProgCache::InitWithSentinel(0, sentinel); + mVUProgCache::ResetForTest(0); + + // Poison: overwrite VERSION with bogus magic but otherwise-current + // fields. Init should reject and evict. + const std::string version_path = Path::Combine(tmp.vu_subdir(0), "VERSION"); + VersionHeaderMirror hdr{}; + hdr.magic = 0xDEADBEEFu; // wrong + hdr.formatVersion = mVUProgCache::kProgCacheFormatVersion; + hdr.compilerAbiVersion = pcsx2_test::kMvuCompilerAbiVersionMirror; + hdr.vuIndex = 0; + hdr.optionsSentinel = sentinel; + std::strncpy(hdr.archTag, "arm64-jit", sizeof(hdr.archTag) - 1); + WriteVersionFile(version_path, hdr); + + mVUProgCache::InitWithSentinel(0, sentinel); + const auto stats = mVUProgCache::GetStats(0); + EXPECT_EQ(stats.staleEvictions, 1u); + ReadAndVerifyCurrentVersion(version_path, /*vu_index=*/0); + + mVUProgCache::ResetForTest(0); +} + +// 5. Poisoned formatVersion triggers eviction. Defends the format-version +// bump invariant: a future bump must invalidate any older cache on disk. +TEST(ProgCacheVersioning, bad_formatVersion_evicts_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xAAAAull; sentinel.high64 = 0xBBBBull; + mVUProgCache::InitWithSentinel(1, sentinel); + mVUProgCache::ResetForTest(1); + + const std::string version_path = Path::Combine(tmp.vu_subdir(1), "VERSION"); + VersionHeaderMirror hdr{}; + hdr.magic = kVersionMagicMirror; + hdr.formatVersion = mVUProgCache::kProgCacheFormatVersion + 100u; // wrong + hdr.compilerAbiVersion = pcsx2_test::kMvuCompilerAbiVersionMirror; + hdr.vuIndex = 1; + hdr.optionsSentinel = sentinel; + std::strncpy(hdr.archTag, "arm64-jit", sizeof(hdr.archTag) - 1); + WriteVersionFile(version_path, hdr); + + mVUProgCache::InitWithSentinel(1, sentinel); + const auto stats = mVUProgCache::GetStats(1); + EXPECT_EQ(stats.staleEvictions, 1u); + ReadAndVerifyCurrentVersion(version_path, /*vu_index=*/1); + + mVUProgCache::ResetForTest(1); +} + +// 6. Poisoned compilerAbiVersion triggers eviction. The user-visible +// incantation in the docs: "to nuke the cache, bump +// kMvuCompilerAbiVersion". +TEST(ProgCacheVersioning, bad_compilerAbiVersion_evicts_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(0); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xCCCCull; sentinel.high64 = 0xDDDDull; + mVUProgCache::InitWithSentinel(0, sentinel); + mVUProgCache::ResetForTest(0); + + const std::string version_path = Path::Combine(tmp.vu_subdir(0), "VERSION"); + VersionHeaderMirror hdr{}; + hdr.magic = kVersionMagicMirror; + hdr.formatVersion = mVUProgCache::kProgCacheFormatVersion; + hdr.compilerAbiVersion = + pcsx2_test::kMvuCompilerAbiVersionMirror + 100u; // wrong + hdr.vuIndex = 0; + hdr.optionsSentinel = sentinel; + std::strncpy(hdr.archTag, "arm64-jit", sizeof(hdr.archTag) - 1); + WriteVersionFile(version_path, hdr); + + mVUProgCache::InitWithSentinel(0, sentinel); + const auto stats = mVUProgCache::GetStats(0); + EXPECT_EQ(stats.staleEvictions, 1u); + ReadAndVerifyCurrentVersion(version_path, /*vu_index=*/0); + + mVUProgCache::ResetForTest(0); +} + +// 0. The hand-mirrored ABI constant must equal production. This pins the +// mirror against the linkable accessor so drift fails here with a clear +// message, not as a downstream eviction in the round-trip tests. +TEST(ProgCacheVersioning, mirror_matches_production_abi_version) +{ + EXPECT_EQ(pcsx2_test::kMvuCompilerAbiVersionMirror, + mVUProgCache::GetCompilerAbiVersion()) + << "kMvuCompilerAbiVersionMirror drifted from production " + "kMvuCompilerAbiVersion — bump the mirror in lockstep."; +} + +// 6b. Poisoned archTag triggers eviction. Pins the production +// VersionMatches archTag memcmp (microVU_ProgCache-arm64.inl): a cache +// written by a different-arch build must not be hydrated. +TEST(ProgCacheVersioning, bad_archTag_evicts_cache) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xEEEEull; sentinel.high64 = 0xFFFFull; + mVUProgCache::InitWithSentinel(1, sentinel); + mVUProgCache::ResetForTest(1); + + const std::string version_path = Path::Combine(tmp.vu_subdir(1), "VERSION"); + VersionHeaderMirror hdr{}; + hdr.magic = kVersionMagicMirror; + hdr.formatVersion = mVUProgCache::kProgCacheFormatVersion; + hdr.compilerAbiVersion = pcsx2_test::kMvuCompilerAbiVersionMirror; + hdr.vuIndex = 1; + hdr.optionsSentinel = sentinel; + std::strncpy(hdr.archTag, "x86-64-jit", sizeof(hdr.archTag) - 1); // wrong arch + WriteVersionFile(version_path, hdr); + + mVUProgCache::InitWithSentinel(1, sentinel); + const auto stats = mVUProgCache::GetStats(1); + EXPECT_EQ(stats.staleEvictions, 1u); + ReadAndVerifyCurrentVersion(version_path, /*vu_index=*/1); + + mVUProgCache::ResetForTest(1); +} + +// 7. Empty EmuFolders::Cache (e.g. test harness with no VMManager +// init) disables the ProgCache silently. Init must NOT touch /tmp +// or anywhere unsolicited. +TEST(ProgCacheVersioning, empty_emu_folders_disables_cache) +{ + const std::string prev = EmuFolders::Cache; + EmuFolders::Cache.clear(); + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + mVUProgCache::InitWithSentinel(1, sentinel); + + const auto stats = mVUProgCache::GetStats(1); + EXPECT_TRUE(stats.initialized); + EXPECT_FALSE(stats.enabled) + << "empty EmuFolders::Cache must result in enabled=false"; + EXPECT_EQ(stats.staleEvictions, 0u); + + mVUProgCache::ResetForTest(1); + EmuFolders::Cache = prev; +} + +// --- Variable-length INDEX format --------------------------------- +// +// IndexEntry on disk is a fixed 64-byte header followed by +// `entryCount` × u32 entry-PC values (microMem byte offsets). The +// tests below mirror that header layout test-side so they can write +// synthetic records and assert the loader parses them. A +// static_assert on the mirror size catches any drift from the +// production layout at compile time. + +namespace +{ +#pragma pack(push, 1) + struct IndexEntryHeaderMirror + { + XXH128_hash_t contentHash; + u32 vuIndex; + u32 codeSize; + u32 blockCount; + u32 flags; + u64 lastUsedNs; + u64 execCount; + u32 entryCount; + u8 pad[12]; + }; +#pragma pack(pop) + static_assert(sizeof(IndexEntryHeaderMirror) == 64, + "IndexEntryHeader mirror drifted from production layout"); + + // Build a current-format VERSION file in `vu_subdir/VERSION` so Init + // doesn't evict the synthetic INDEX we're about to write. + void WriteCurrentVersion(const std::string& version_path, u32 vu_index, + const XXH128_hash_t& sentinel) + { + VersionHeaderMirror hdr{}; + hdr.magic = kVersionMagicMirror; + hdr.formatVersion = mVUProgCache::kProgCacheFormatVersion; + hdr.compilerAbiVersion = pcsx2_test::kMvuCompilerAbiVersionMirror; + hdr.vuIndex = vu_index & 1u; + hdr.optionsSentinel = sentinel; + std::strncpy(hdr.archTag, "arm64-jit", sizeof(hdr.archTag) - 1); + WriteVersionFile(version_path, hdr); + } + + void WriteRawIndex(const std::string& index_path, + const std::vector& bytes) + { + std::FILE* fp = std::fopen(index_path.c_str(), "wb"); + ASSERT_NE(fp, nullptr) << "fopen: " << std::strerror(errno); + if (!bytes.empty()) + { + ASSERT_EQ(std::fwrite(bytes.data(), bytes.size(), 1, fp), 1u); + } + std::fclose(fp); + } + + std::vector SerializeOne(const IndexEntryHeaderMirror& hdr, + const std::vector& entry_pcs) + { + IndexEntryHeaderMirror h = hdr; + h.entryCount = static_cast(entry_pcs.size()); + std::vector out(sizeof(h) + entry_pcs.size() * sizeof(u32)); + std::memcpy(out.data(), &h, sizeof(h)); + if (!entry_pcs.empty()) + { + std::memcpy(out.data() + sizeof(h), + entry_pcs.data(), + entry_pcs.size() * sizeof(u32)); + } + return out; + } +} + +TEST(ProgCacheIndexFormatV3, single_entry_round_trip_via_test_helper) +{ + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xC0FFEE77C0FFEE77ull; + sentinel.high64 = 0xC0FFEE77C0FFEE77ull; + mVUProgCache::InitWithSentinel(1, sentinel); + + XXH128_hash_t hash{}; + hash.low64 = 0x1111222233334444ull; + hash.high64 = 0x5555666677778888ull; + + // Append a single-entry record through the production persistence + // path (atomic append + in-memory registration), then assert the + // entry-PC round-trips in-process and across a simulated process + // boundary (ResetForTest + re-Init re-reads the on-disk INDEX). + const u32 seeded_pc = 0x58u; + ASSERT_TRUE(mVUProgCache::TestAppendIndexEntry(1, hash, &seeded_pc, 1)); + + // Duplicate hash short-circuits via the in-memory dedupe. + EXPECT_FALSE(mVUProgCache::TestAppendIndexEntry(1, hash, &seeded_pc, 1)); + + // On-disk record is the 64-byte header + 1 trailing u32 = 68 bytes. + const std::string idx = Path::Combine(tmp.vu_subdir(1), "INDEX"); + ASSERT_TRUE(FileSystem::FileExists(idx.c_str())); + const auto bytes = FileSystem::ReadBinaryFile(idx.c_str()); + ASSERT_TRUE(bytes.has_value()); + EXPECT_EQ(bytes->size(), 68u); + + // In-process: in-memory entry has the seeded PC. + u32 pcs[8] = {0}; + size_t cnt = 0; + ASSERT_TRUE(mVUProgCache::TestGetEntryPcs(1, hash, pcs, 8, &cnt)); + ASSERT_EQ(cnt, 1u); + EXPECT_EQ(pcs[0], seeded_pc); + + // Reset + Init: cross-process reload reads it back from disk. + mVUProgCache::ResetForTest(1); + mVUProgCache::InitWithSentinel(1, sentinel); + + std::memset(pcs, 0, sizeof(pcs)); + cnt = 0; + ASSERT_TRUE(mVUProgCache::TestGetEntryPcs(1, hash, pcs, 8, &cnt)); + EXPECT_EQ(cnt, 1u); + EXPECT_EQ(pcs[0], seeded_pc); + + mVUProgCache::ResetForTest(1); +} + +TEST(ProgCacheIndexFormatV3, multi_entry_synthetic_load) +{ + // Synthesize a 3-entry record. Init must round-trip every entry + // PC through LoadIndex. + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xC0FFEE88C0FFEE88ull; + sentinel.high64 = 0xC0FFEE88C0FFEE88ull; + + // Lay down a valid VERSION + INDEX before Init runs. + const std::string sub = tmp.vu_subdir(1); + ASSERT_TRUE(FileSystem::EnsureDirectoryExists(sub.c_str(), true)); + WriteCurrentVersion(Path::Combine(sub, "VERSION"), 1, sentinel); + + XXH128_hash_t hash{}; + hash.low64 = 0xAAAA111122223333ull; + hash.high64 = 0xBBBB444455556666ull; + + IndexEntryHeaderMirror hdr{}; + hdr.contentHash = hash; + hdr.vuIndex = 1u; + hdr.flags = 1u; // valid + const std::vector entry_pcs = {0x000u, 0x058u, 0x148u}; + const auto bytes = SerializeOne(hdr, entry_pcs); + WriteRawIndex(Path::Combine(sub, "INDEX"), bytes); + + mVUProgCache::InitWithSentinel(1, sentinel); + + const auto stats = mVUProgCache::GetStats(1); + ASSERT_EQ(stats.entries, 1u); + + u32 pcs[8] = {0}; + size_t cnt = 0; + ASSERT_TRUE(mVUProgCache::TestGetEntryPcs(1, hash, pcs, 8, &cnt)); + ASSERT_EQ(cnt, 3u); + EXPECT_EQ(pcs[0], 0x000u); + EXPECT_EQ(pcs[1], 0x058u); + EXPECT_EQ(pcs[2], 0x148u); + + mVUProgCache::ResetForTest(1); +} + +TEST(ProgCacheIndexFormatV3, truncated_trailing_drops_record_cleanly) +{ + // Header claims entryCount=4 but only 2 trailing u32s follow. + // LoadIndex must drop the (partial) record and not poison + // in-memory state. + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xC0FFEE99C0FFEE99ull; + sentinel.high64 = 0xC0FFEE99C0FFEE99ull; + + const std::string sub = tmp.vu_subdir(1); + ASSERT_TRUE(FileSystem::EnsureDirectoryExists(sub.c_str(), true)); + WriteCurrentVersion(Path::Combine(sub, "VERSION"), 1, sentinel); + + XXH128_hash_t hash{}; + hash.low64 = 0xC0DEFADEC0DEFADEull; + hash.high64 = 0xC0DEC0DEC0DEC0DEull; + + IndexEntryHeaderMirror hdr{}; + hdr.contentHash = hash; + hdr.vuIndex = 1u; + hdr.flags = 1u; + hdr.entryCount = 4u; // claims 4 + std::vector bytes(sizeof(hdr) + 2 * sizeof(u32)); + std::memcpy(bytes.data(), &hdr, sizeof(hdr)); + const u32 trail[2] = {0x10u, 0x20u}; + std::memcpy(bytes.data() + sizeof(hdr), trail, sizeof(trail)); + WriteRawIndex(Path::Combine(sub, "INDEX"), bytes); + + mVUProgCache::InitWithSentinel(1, sentinel); + + const auto stats = mVUProgCache::GetStats(1); + EXPECT_EQ(stats.entries, 0u) + << "truncated tail must not be loaded as a valid entry"; + + u32 pcs[8] = {0}; + size_t cnt = 0; + EXPECT_FALSE(mVUProgCache::TestGetEntryPcs(1, hash, pcs, 8, &cnt)); + + mVUProgCache::ResetForTest(1); +} + +TEST(ProgCacheIndexFormatV3, tombstone_skipped_on_load) +{ + // flags-bit-0 cleared OR entryCount==0 → tombstone; LoadIndex + // must skip the record (advancing past the trailing bytes if + // any) and continue parsing the rest of the file. + TempCacheRoot tmp; + mVUProgCache::ResetForTest(1); + + XXH128_hash_t sentinel{}; + sentinel.low64 = 0xC0FFEEAAC0FFEEAAull; + sentinel.high64 = 0xC0FFEEAAC0FFEEAAull; + + const std::string sub = tmp.vu_subdir(1); + ASSERT_TRUE(FileSystem::EnsureDirectoryExists(sub.c_str(), true)); + WriteCurrentVersion(Path::Combine(sub, "VERSION"), 1, sentinel); + + XXH128_hash_t deadHash{}; + deadHash.low64 = 0xDEADAAAA00000001ull; + deadHash.high64 = 0xDEADAAAA00000002ull; + XXH128_hash_t liveHash{}; + liveHash.low64 = 0xA11EAAAA00000001ull; + liveHash.high64 = 0xA11EAAAA00000002ull; + + IndexEntryHeaderMirror dead{}; + dead.contentHash = deadHash; + dead.vuIndex = 1u; + dead.flags = 0u; // tombstone via cleared valid-bit + const std::vector dead_pcs = {0x000u, 0x004u}; + + IndexEntryHeaderMirror live{}; + live.contentHash = liveHash; + live.vuIndex = 1u; + live.flags = 1u; + const std::vector live_pcs = {0x100u}; + + std::vector bytes; + { + const auto a = SerializeOne(dead, dead_pcs); + const auto b = SerializeOne(live, live_pcs); + bytes.insert(bytes.end(), a.begin(), a.end()); + bytes.insert(bytes.end(), b.begin(), b.end()); + } + WriteRawIndex(Path::Combine(sub, "INDEX"), bytes); + + mVUProgCache::InitWithSentinel(1, sentinel); + + const auto stats = mVUProgCache::GetStats(1); + EXPECT_EQ(stats.entries, 1u) << "tombstone must not count"; + + u32 pcs[8] = {0}; + size_t cnt = 0; + EXPECT_FALSE(mVUProgCache::TestGetEntryPcs(1, deadHash, pcs, 8, &cnt)); + ASSERT_TRUE(mVUProgCache::TestGetEntryPcs(1, liveHash, pcs, 8, &cnt)); + EXPECT_EQ(cnt, 1u); + EXPECT_EQ(pcs[0], 0x100u); + + mVUProgCache::ResetForTest(1); +} From d884fbc08e8a9388fcff96784b29fa0daa58a37a Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:56 -0700 Subject: [PATCH 012/292] arm64/libmali GS backend: device gates, depth quantization, display rotation (fork-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handheld/libmali GS device adaptations — VK_KHR_display direct-to-monitor WSI, 3-image swapchain, optional VK_KHR_push_descriptor with per-frame pool fallback, ColorClip HDR fallback, present-time DisplayRotation, PS2 depth-quantization gate (no_ps2_z_quantization), and present-timing diagnostics. libmali-specific; kept on the fork branch. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- common/WindowInfo.h | 7 +- pcsx2/Config.h | 16 + pcsx2/GS/Renderers/Common/GSDevice.cpp | 9 + pcsx2/GS/Renderers/Common/GSDevice.h | 22 ++ pcsx2/GS/Renderers/Common/GSRenderer.cpp | 6 +- pcsx2/GS/Renderers/HW/GSRendererHW.cpp | 6 +- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp | 452 +++++++++++++++++++--- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h | 13 + pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp | 289 +++++++++++++- pcsx2/GS/Renderers/Vulkan/VKSwapChain.h | 24 ++ pcsx2/ImGui/ImGuiManager.cpp | 16 +- pcsx2/Pcsx2Config.cpp | 12 + 12 files changed, 810 insertions(+), 62 deletions(-) diff --git a/common/WindowInfo.h b/common/WindowInfo.h index 4ccad5a4de..18c454ff88 100644 --- a/common/WindowInfo.h +++ b/common/WindowInfo.h @@ -15,7 +15,12 @@ struct WindowInfo Win32, X11, Wayland, - MacOS + MacOS, + // Vulkan VK_KHR_display direct-to-monitor (no compositor / no GBM + // intermediate). Frontend supplies no native window handle; the + // renderer enumerates displays itself. surface_width/surface_height + // carry the requested mode (0 = pick the display's preferred mode). + VulkanDirect }; /// The type of the surface. Surfaceless indicates it will not be displayed on screen at all. diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 5958aa9317..6c6d3ddd6f 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -241,6 +241,19 @@ enum class FMVAspectRatioSwitchType : u8 MaxCount }; +// Display rotation applied at present time. Useful for handhelds whose panel +// is mounted in one orientation but the user wants the game in another. +// Rotation is applied to the *final* swapchain blit only; internal GS +// coordinates and aspect-ratio math run in the unrotated frame. +enum class DisplayRotation : u8 +{ + Rot0, + Rot90, + Rot180, + Rot270, + MaxCount +}; + enum class MemoryCardType { Empty, @@ -709,6 +722,7 @@ struct Pcsx2Config { static const char* AspectRatioNames[]; static const char* FMVAspectRatioSwitchNames[]; + static const char* DisplayRotationNames[]; static const char* BlendingLevelNames[]; static const char* CaptureContainers[]; @@ -771,6 +785,7 @@ struct Pcsx2Config UseBlitSwapChain : 1, DisableShaderCache : 1, DisableFramebufferFetch : 1, + DisablePS2DepthQuantization : 1, DisableVertexShaderExpand : 1, SkipDuplicateFrames : 1, OsdShowSpeed : 1, @@ -855,6 +870,7 @@ struct Pcsx2Config AspectRatioType AspectRatio = DEFAULT_ASPECT_RATIO; FMVAspectRatioSwitchType FMVAspectRatioSwitch = DEFAULT_FMV_ASPECT_RATIO; + DisplayRotation Rotation = DisplayRotation::Rot0; GSInterlaceMode InterlaceMode = DEFAULT_INTERLACE_MODE; GSPostBilinearMode LinearPresent = DEFAULT_BILINEAR_FILTERING_MODE; diff --git a/pcsx2/GS/Renderers/Common/GSDevice.cpp b/pcsx2/GS/Renderers/Common/GSDevice.cpp index 9653b99301..e71e22c961 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.cpp +++ b/pcsx2/GS/Renderers/Common/GSDevice.cpp @@ -246,6 +246,15 @@ GSDevice::~GSDevice() pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas); } +GSVector2i GSDevice::GetPresentationSize() const +{ + const s32 w = GetWindowWidth(); + const s32 h = GetWindowHeight(); + return (GSConfig.Rotation == DisplayRotation::Rot90 || GSConfig.Rotation == DisplayRotation::Rot270) + ? GSVector2i(h, w) + : GSVector2i(w, h); +} + const char* GSDevice::RenderAPIToString(RenderAPI api) { switch (api) diff --git a/pcsx2/GS/Renderers/Common/GSDevice.h b/pcsx2/GS/Renderers/Common/GSDevice.h index f53f9f58cc..701209383c 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.h +++ b/pcsx2/GS/Renderers/Common/GSDevice.h @@ -14,6 +14,8 @@ #include "GS/GSExtra.h" #include #include +#include +#include enum class Filter { @@ -1387,6 +1389,7 @@ public: bool stencil_buffer : 1; ///< Supports stencil buffer, and can use for DATE. bool cas_sharpening : 1; ///< Supports sufficient functionality for contrast adaptive sharpening. bool test_and_sample_depth: 1; ///< Supports concurrently binding the depth-stencil buffer for sampling and depth testing. + bool no_ps2_z_quantization: 1; ///< Skip PS2 32-bit-fixed Z floor (saves SPIR-V DepthReplacing → re-enables early-ZS on tilers). bool depth_feedback : 1; ///< Depth feedback loops can be done with DS directly (otherwise need to copy to separate RT). Implies `feedback_loops`. bool aa1 : 1; ///< Supports the GS AA1 feature. bool rov : 1; ///< Supports rasterizer ordered views for both depth and color. @@ -1551,6 +1554,16 @@ public: __fi s32 GetWindowWidth() const { return static_cast(m_window_info.surface_width); } __fi s32 GetWindowHeight() const { return static_cast(m_window_info.surface_height); } __fi GSVector2i GetWindowSize() const { return GSVector2i(static_cast(m_window_info.surface_width), static_cast(m_window_info.surface_height)); } + // Logical window dimensions for layout: same as GetWindowSize for + // Rot0/Rot180, swapped for Rot90/Rot270 so callers compute the present + // rect against a portrait box that the rotation transform then maps onto + // the landscape swapchain. Use this in callers that produce coordinates + // later consumed by the rotation-aware Vulkan present path (game draw_rect, + // ImGui DisplaySize). Other callers (GS Resize, viewport setup) want the + // raw physical dims and should keep using GetWindowWidth/Height. + GSVector2i GetPresentationSize() const; + __fi s32 GetPresentationWidth() const { return GetPresentationSize().x; } + __fi s32 GetPresentationHeight() const { return GetPresentationSize().y; } __fi float GetWindowScale() const { return m_window_info.surface_scale; } __fi GSVSyncMode GetVSyncMode() const { return m_vsync_mode; } __fi bool IsPresentThrottleAllowed() const { return m_allow_present_throttle; } @@ -1606,6 +1619,15 @@ public: /// Returns the amount of GPU time utilized since the last time this method was called. virtual float GetAndResetAccumulatedGPUTime() = 0; + /// Enables backend-specific diagnostic counters (e.g. Vulkan acquire/present timing). + /// Off by default to surface WSI-layer timing in diagnostic tools without paying + /// the cost on the normal present hot path. + virtual void EnableExtendedStats(bool enabled) {} + + /// Returns backend-specific diagnostic lines (swapchain config, present/acquire timing, etc). + /// Each line is a fully-formatted string, ready to print as-is. Default: empty. + virtual std::vector GetExtendedStats() const { return {}; } + /// Returns true if not enough time has passed for present to not block. bool ShouldSkipPresentingFrame(); diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp index 71708ade6e..15414b7bf0 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp +++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp @@ -661,7 +661,8 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame) { src_rect = CalculateDrawSrcRect(current, m_real_size); src_uv = GSVector4(src_rect) / GSVector4(current->GetSize()).xyxy(); - draw_rect = CalculateDrawDstRect(g_gs_device->GetWindowWidth(), g_gs_device->GetWindowHeight(), + const GSVector2i pres_size = g_gs_device->GetPresentationSize(); + draw_rect = CalculateDrawDstRect(pres_size.x, pres_size.y, src_rect, current->GetSize(), s_display_alignment, g_gs_device->UsesLowerLeftOrigin(), GetVideoMode() == GSVideoMode::SDTV_480P); s_last_draw_rect = draw_rect; @@ -962,7 +963,8 @@ void GSRenderer::PresentCurrentFrame() { const GSVector4i src_rect(CalculateDrawSrcRect(current, m_real_size)); const GSVector4 src_uv(GSVector4(src_rect) / GSVector4(current->GetSize()).xyxy()); - const GSVector4 draw_rect(CalculateDrawDstRect(g_gs_device->GetWindowWidth(), g_gs_device->GetWindowHeight(), + const GSVector2i pres_size = g_gs_device->GetPresentationSize(); + const GSVector4 draw_rect(CalculateDrawDstRect(pres_size.x, pres_size.y, src_rect, current->GetSize(), s_display_alignment, g_gs_device->UsesLowerLeftOrigin(), GetVideoMode() == GSVideoMode::SDTV_480P)); s_last_draw_rect = draw_rect; diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index c70d17d58b..965499f635 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -5589,7 +5589,9 @@ void GSRendererHW::EmulateZbuffer(const GSTextureCache::Target* ds) // Even when Z is read-only, Z floor must be enabled with ZTST_GREATER since otherwise there // can be false passing if the incoming Z is not floored when the buffer value is floored. - m_conf.ps.zfloor = !flat_z && + // On tilers (Mali), the device can opt out: declaring gl_FragDepth disables early-ZS for + // the entire pipeline. zclamp (large_z) is independent and stays correct. + m_conf.ps.zfloor = !flat_z && !g_gs_device->Features().no_ps2_z_quantization && (m_cached_ctx.DepthWrite() || (m_cached_ctx.DepthRead() && m_cached_ctx.TEST.ZTST == ZTST_GREATER)); if (m_cached_ctx.DepthWrite() && large_z) @@ -6527,7 +6529,7 @@ __ri u32 GSRendererHW::EmulateChannelShuffle(GSTextureCache::Target* src, bool t const GSLocalMemory::psm_t& t_psm = GSLocalMemory::m_psm[m_cached_ctx.TEX0.PSM]; const GSLocalMemory::psm_t& f_psm = GSLocalMemory::m_psm[m_cached_ctx.FRAME.PSM]; GSVector4i block_offset = GSVector4i(min_uv.x / t_psm.bs.x, min_uv.y / t_psm.bs.y).xyxy(); - GSVector4i m_r_block_offset = GSVector4i((m_r.x & (f_psm.pgs.x - 1)) / f_psm.bs.x, (m_r.y & (f_psm.pgs.y - 1)) / f_psm.bs.y); + [[maybe_unused]] GSVector4i m_r_block_offset = GSVector4i((m_r.x & (f_psm.pgs.x - 1)) / f_psm.bs.x, (m_r.y & (f_psm.pgs.y - 1)) / f_psm.bs.y); // Adjust it back to the page boundary min_uv.x -= block_offset.x * t_psm.bs.x; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index fed43abe73..3a3c61cbfa 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -21,6 +21,7 @@ #include "common/HostSys.h" #include "common/Path.h" #include "common/ScopedGuard.h" +#include "common/Timer.h" #include "imgui.h" @@ -36,7 +37,12 @@ enum : u32 MAX_COMBINED_IMAGE_SAMPLER_DESCRIPTORS_PER_FRAME = 2 * MAX_DRAW_CALLS_PER_FRAME, MAX_SAMPLED_IMAGE_DESCRIPTORS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME, // assume at least half our draws aren't going to be shuffle/blending - MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME = 4, // Currently used by CAS only + // CAS uses one storage image per frame, but the TFX texture set also carries the + // two ROV storage-image bindings (TFX_TEXTURE_RT_ROV / _DEPTH_ROV), and every + // vkAllocateDescriptorSets of that layout reserves both whether written or not. + // On the ROV-without-push-descriptor path that is two per TFX draw, so + // size to match the draw budget rather than the old CAS-only value of 4. + MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME = 2 * MAX_DRAW_CALLS_PER_FRAME, MAX_INPUT_ATTACHMENT_IMAGE_DESCRIPTORS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME, MAX_DESCRIPTOR_SETS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME * 2, @@ -80,7 +86,6 @@ 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, }; GSDeviceVK::GSDeviceVK() @@ -199,6 +204,16 @@ bool GSDeviceVK::SelectInstanceExtensions(ExtensionList* extension_list, const W return false; #endif + // VK_KHR_display direct-to-monitor surface (kmsdrm handhelds). + // VK_KHR_get_display_properties2 is optional but lets us read HDR / extended + // display info on ICDs that support it. + if (wi.type == WindowInfo::Type::VulkanDirect) + { + if (!SupportsExtension(VK_KHR_DISPLAY_EXTENSION_NAME, true)) + return false; + SupportsExtension(VK_KHR_GET_DISPLAY_PROPERTIES_2_EXTENSION_NAME, false); + } + // VK_EXT_debug_utils if (enable_debug_utils && !SupportsExtension(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, false)) Console.Warning("VK: Debug report requested, but extension is not available."); @@ -407,6 +422,7 @@ bool GSDeviceVK::SelectDeviceExtensions(ExtensionList* extension_list, bool enab return false; } + 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 = @@ -771,18 +787,24 @@ 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) + if (m_optional_extensions.vk_khr_push_descriptor) { - Console.Error("VK: maxPushDescriptors (%u) is below required (%u)", push_descriptor_properties.maxPushDescriptors, - NUM_TFX_TEXTURES); - return false; + if (push_descriptor_properties.maxPushDescriptors < NUM_TFX_TEXTURES) + { + Console.Warning("VK: maxPushDescriptors (%u) is below required (%u), disabling push descriptors", + push_descriptor_properties.maxPushDescriptors, NUM_TFX_TEXTURES); + m_optional_extensions.vk_khr_push_descriptor = false; + } } + if (!m_optional_extensions.vk_khr_push_descriptor) + Console.Warning("VK: VK_KHR_push_descriptor is not available, using per-frame descriptor pools instead."); if (m_optional_extensions.vk_ext_line_rasterization && !line_rasterization_feature.bresenhamLines) { @@ -957,6 +979,30 @@ bool GSDeviceVK::CreateCommandBuffers() return false; } Vulkan::SetObjectName(m_device, resources.fence, "Frame Fence %u", frame_index); + + // Create per-frame descriptor pool when push descriptors are not available. + if (!m_optional_extensions.vk_khr_push_descriptor) + { + // Pool sizes cover TFX textures, utility, and CAS descriptor sets per frame. + static constexpr const VkDescriptorPoolSize frame_pool_sizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_COMBINED_IMAGE_SAMPLER_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, MAX_SAMPLED_IMAGE_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, MAX_INPUT_ATTACHMENT_IMAGE_DESCRIPTORS_PER_FRAME}, + }; + + VkDescriptorPoolCreateInfo dp_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, nullptr, 0, + MAX_DESCRIPTOR_SETS_PER_FRAME, static_cast(std::size(frame_pool_sizes)), frame_pool_sizes}; + + res = vkCreateDescriptorPool(m_device, &dp_info, nullptr, &resources.descriptor_pool); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkCreateDescriptorPool (per-frame) failed: "); + return false; + } + Vulkan::SetObjectName(m_device, resources.descriptor_pool, "Frame Descriptor Pool %u", frame_index); + } + ++frame_index; } @@ -1085,6 +1131,28 @@ void GSDeviceVK::FreePersistentDescriptorSet(VkDescriptorSet set) vkFreeDescriptorSets(m_device, m_global_descriptor_pool, 1, &set); } +VkDescriptorSet GSDeviceVK::AllocateDescriptorSetFromFramePool(VkDescriptorSetLayout set_layout) +{ + VkDescriptorPool pool = m_frame_resources[m_current_frame].descriptor_pool; + pxAssert(pool != VK_NULL_HANDLE); + + 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) + return descriptor_set; + + // Pool exhausted. Recovery (flush the command buffer to reset the frame pool, + // then restart the render pass and re-apply state) must be driven by the caller: + // callers capture their command buffer and emit binding state before calling us, + // so flushing here would leave them writing to a submitted command buffer with no + // active render pass. Signal exhaustion with a null set and let the caller flush, + // restart, and re-enter (mirroring the uniform-buffer overflow paths). + return VK_NULL_HANDLE; +} + void GSDeviceVK::WaitForFenceCounter(u64 fence_counter) { if (m_completed_fence_counter >= fence_counter) @@ -1122,6 +1190,55 @@ bool GSDeviceVK::SetGPUTimingEnabled(bool enabled) return (enabled == m_gpu_timing_enabled); } +void GSDeviceVK::EnableExtendedStats(bool enabled) +{ + VKSwapChain::SetPresentStatsEnabled(enabled); +} + +std::vector GSDeviceVK::GetExtendedStats() const +{ + std::vector lines; + if (m_swap_chain) + { + const WindowInfo& wi = m_swap_chain->GetWindowInfo(); + const char* wsi_name = "?"; + switch (wi.type) + { + case WindowInfo::Type::Surfaceless: wsi_name = "Surfaceless"; break; + case WindowInfo::Type::Win32: wsi_name = "Win32"; break; + case WindowInfo::Type::X11: wsi_name = "X11"; break; + case WindowInfo::Type::Wayland: wsi_name = "Wayland"; break; + case WindowInfo::Type::MacOS: wsi_name = "MacOS"; break; + case WindowInfo::Type::VulkanDirect: wsi_name = "VulkanDirect"; break; + } + const char* present_name = "?"; + switch (m_swap_chain->GetPresentMode()) + { + case VK_PRESENT_MODE_IMMEDIATE_KHR: present_name = "IMMEDIATE"; break; + case VK_PRESENT_MODE_MAILBOX_KHR: present_name = "MAILBOX"; break; + case VK_PRESENT_MODE_FIFO_KHR: present_name = "FIFO"; break; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: present_name = "FIFO_RELAXED"; break; + default: break; + } + lines.push_back(fmt::format( + "Swapchain: {}x{} (scale {:.2f}) fmt={} present={} images={} wsi={}", + m_swap_chain->GetWidth(), m_swap_chain->GetHeight(), wi.surface_scale, + static_cast(m_swap_chain->GetTextureFormat()), + present_name, m_swap_chain->GetImageCount(), wsi_name)); + } + + const VKSwapChain::PresentStats ps = VKSwapChain::GetPresentStats(); + const double acquire_avg_ms = ps.acquire_count ? (ps.acquire_total_ms / ps.acquire_count) : 0.0; + const double present_avg_ms = ps.present_count ? (ps.present_total_ms / ps.present_count) : 0.0; + lines.push_back(fmt::format( + "vkAcquireNextImage: avg {:.3f} ms, max {:.3f} ms, n={}", acquire_avg_ms, ps.acquire_max_ms, ps.acquire_count)); + lines.push_back(fmt::format( + "vkQueuePresent: avg {:.3f} ms, max {:.3f} ms, n={}", present_avg_ms, ps.present_max_ms, ps.present_count)); + lines.push_back(fmt::format( + "Suboptimal: {}, OutOfDate: {}", ps.suboptimal_count, ps.out_of_date_count)); + return lines; +} + void GSDeviceVK::ScanForCommandBufferCompletion() { for (u32 check_index = (m_current_frame + 1) % NUM_COMMAND_BUFFERS; check_index != m_current_frame; @@ -1297,7 +1414,15 @@ void GSDeviceVK::SubmitCommandBuffer(VKSwapChain* present_swap_chain) present_swap_chain->ResetImageAcquireResult(); + const bool stats = VKSwapChain::IsPresentStatsEnabled(); + const Common::Timer::Value t_present_start = stats ? Common::Timer::GetCurrentValue() : 0; res = vkQueuePresentKHR(m_present_queue, &present_info); + if (stats) + { + const double present_elapsed_ms = + Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - t_present_start); + VKSwapChain::NotePresent(present_elapsed_ms, res); + } if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) { // VK_ERROR_OUT_OF_DATE_KHR is not fatal, just means we need to recreate our swap chain. @@ -1389,6 +1514,14 @@ void GSDeviceVK::ActivateCommandBuffer(u32 index) if (res != VK_SUCCESS) LOG_VULKAN_ERROR(res, "vkResetCommandPool failed: "); + // Reset per-frame descriptor pool when push descriptors are not available. + 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}; @@ -2717,6 +2850,11 @@ bool GSDeviceVK::CheckFeatures() // Use D32F depth instead of D32S8 when we have framebuffer fetch. m_features.stencil_buffer &= !m_features.framebuffer_fetch; + // On tiler GPUs, declaring gl_FragDepth (for PS2 32-bit Z quantization) emits + // SPIR-V ExecutionMode DepthReplacing, which disables early-ZS for the entire + // pipeline. Default-on for ARM GPUs; opt-out via INI for Z-precision-sensitive titles. + m_features.no_ps2_z_quantization = GSConfig.DisablePS2DepthQuantization || IsDeviceARM(); + // whether we can do point/line expand depends on the range of the device const float f_upscale = static_cast(GSConfig.UpscaleMultiplier); m_features.point_expand = (m_device_features.largePoints && limits.pointSizeRange[0] <= f_upscale && @@ -2727,9 +2865,10 @@ bool GSDeviceVK::CheckFeatures() m_features.depth_feedback = m_features.feedback_loops(); 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" : "", + DevCon.WriteLn("Optional features:%s%s%s%s%s%s", m_features.primitive_id ? " primitive_id" : "", m_features.texture_barrier ? " texture_barrier" : "", m_features.framebuffer_fetch ? " framebuffer_fetch" : "", - m_features.provoking_vertex_last ? " provoking_vertex_last" : "", m_features.vs_expand ? " vs_expand" : ""); + m_features.provoking_vertex_last ? " provoking_vertex_last" : "", m_features.vs_expand ? " vs_expand" : "", + m_features.no_ps2_z_quantization ? " no_ps2_z_quantization" : ""); DevCon.WriteLn("Using %s for point expansion and %s for line expansion.", m_features.point_expand ? "hardware" : "vertex expanding", @@ -2748,6 +2887,16 @@ bool GSDeviceVK::CheckFeatures() vkGetPhysicalDeviceFormatProperties(m_physical_device, vkfmt, &props); if ((props.optimalTilingFeatures & bits) != bits) { + // ColorClip (R16G16B16A16_UNORM) may not be supported as a render target on some GPUs + // (e.g. Broadcom V3D). Fall back to ColorHDR (R16G16B16A16_SFLOAT) which provides + // equivalent precision for color clamping emulation. + if (static_cast(fmt) == GSTexture::Format::ColorClip) + { + Console.Warning("VK: ColorClip format (R16G16B16A16_UNORM) not supported as render target, falling back to ColorHDR (R16G16B16A16_SFLOAT)."); + m_colorclip_fallback_to_hdr = true; + continue; + } + Host::ReportFormattedErrorAsync("VK: Renderer Unavailable", "Required format %u is missing bits, you may need to update your driver. (vk:%u, has:0x%x, needs:0x%x)", fmt, static_cast(vkfmt), props.optimalTilingFeatures, bits); @@ -2848,6 +2997,9 @@ VkFormat GSDeviceVK::LookupNativeFormat(GSTexture::Format format) const VK_FORMAT_BC7_UNORM_BLOCK, // BC7 }}; + if (format == GSTexture::Format::ColorClip && m_colorclip_fallback_to_hdr) + return VK_FORMAT_R16G16B16A16_SFLOAT; + return (format != GSTexture::Format::DepthStencil || m_features.stencil_buffer) ? s_format_mapping[static_cast(format)] : VK_FORMAT_D32_SFLOAT; @@ -2971,7 +3123,7 @@ void GSDeviceVK::PresentRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* { DisplayConstantBuffer cb; cb.SetSource(sRect, sTex->GetSize()); - cb.SetTarget(dRect, dTex ? dTex->GetSize() : GSVector2i(GetWindowWidth(), GetWindowHeight())); + cb.SetTarget(dRect, dTex ? dTex->GetSize() : GetPresentationSize()); cb.SetTime(shaderTime); SetUtilityPushConstants(&cb, sizeof(cb)); @@ -3155,7 +3307,7 @@ void GSDeviceVK::DoStretchRect(GSTextureVK* sTex, const GSVector4& sRect, GSText const bool is_present = (!dTex); const bool depth = (dTex && dTex->GetType() == GSTexture::Type::DepthStencil); - const GSVector2i size(is_present ? GSVector2i(GetWindowWidth(), GetWindowHeight()) : dTex->GetSize()); + const GSVector2i size(is_present ? GetPresentationSize() : dTex->GetSize()); const GSVector4i dtex_rc(0, 0, size.x, size.y); const GSVector4i dst_rc(GSVector4i(dRect).rintersect(dtex_rc)); @@ -3178,6 +3330,43 @@ void GSDeviceVK::DoStretchRect(GSTextureVK* sTex, const GSVector4& sRect, GSText DrawStretchRect(sRect, dRect, size); } +// Rotate a logical-NDC point (x, y) into physical-NDC by GSConfig.Rotation. +// Used by the present pass to map a quad laid out for a logically-rotated +// window onto the unrotated swapchain viewport. +// Rot0: (x, y) +// Rot90: (y, -x) (image rotates 90° CW on the panel) +// Rot180: (-x,-y) +// Rot270: (-y, x) (image rotates 90° CCW on the panel) +static void RotateNDCForPresent(float& x, float& y) +{ + switch (GSConfig.Rotation) + { + case DisplayRotation::Rot90: + { + const float nx = y; + const float ny = -x; + x = nx; + y = ny; + break; + } + case DisplayRotation::Rot180: + x = -x; + y = -y; + break; + case DisplayRotation::Rot270: + { + const float nx = -y; + const float ny = x; + x = nx; + y = ny; + break; + } + case DisplayRotation::Rot0: + default: + break; + } +} + void GSDeviceVK::DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const GSVector2i& ds) { g_perfmon.Put(GSPerfMon::TextureCopies, 1); @@ -3186,18 +3375,43 @@ void GSDeviceVK::DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const float inv_x = 2.0f / ds.x; const float inv_y = 2.0f / ds.y; - const float left = dRect.x * inv_x - 1.0f; - const float right = dRect.z * inv_x - 1.0f; - const float top = 1.0f - dRect.y * inv_y; - const float bottom = 1.0f - dRect.w * inv_y; + float left = dRect.x * inv_x - 1.0f; + float right = dRect.z * inv_x - 1.0f; + float top = 1.0f - dRect.y * inv_y; + float bottom = 1.0f - dRect.w * inv_y; - const GSVertexPT1 vertices[] = { - {GSVector4(left, top, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, - {GSVector4(right, top, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, - {GSVector4(left, bottom, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, - {GSVector4(right, bottom, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, - }; - IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + // Present pass: map logical-NDC (computed against the rotated window) onto + // the unrotated physical swapchain viewport. Non-present passes pass the + // real dst-texture size in `ds` and must not rotate. + if (m_is_presenting && GSConfig.Rotation != DisplayRotation::Rot0) + { + float tlx = left, tly = top; + float trx = right, try_ = top; + float blx = left, bly = bottom; + float brx = right, bry = bottom; + RotateNDCForPresent(tlx, tly); + RotateNDCForPresent(trx, try_); + RotateNDCForPresent(blx, bly); + RotateNDCForPresent(brx, bry); + + const GSVertexPT1 vertices[] = { + {GSVector4(tlx, tly, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, + {GSVector4(trx, try_, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, + {GSVector4(blx, bly, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, + {GSVector4(brx, bry, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, + }; + IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + } + else + { + const GSVertexPT1 vertices[] = { + {GSVector4(left, top, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, + {GSVector4(right, top, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, + {GSVector4(left, bottom, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, + {GSVector4(right, bottom, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, + }; + IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + } if (ApplyUtilityState()) DrawPrimitive(); @@ -3902,7 +4116,8 @@ bool GSDeviceVK::CreatePipelineLayouts() // Convert Pipeline Layout ////////////////////////////////////////////////////////////////////////// - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + 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; @@ -3931,7 +4146,8 @@ bool GSDeviceVK::CreatePipelineLayouts() return false; Vulkan::SetObjectName(dev, m_tfx_ubo_ds_layout, "TFX UBO descriptor layout"); - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + 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, @@ -4460,7 +4676,8 @@ bool GSDeviceVK::CompileCASPipelines() Vulkan::DescriptorSetLayoutBuilder dslb; Vulkan::PipelineLayoutBuilder plb; - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + 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) @@ -4562,11 +4779,15 @@ void GSDeviceVK::RenderImGui() UpdateImGuiTextures(); - const GSVector4 uniforms( - 2.0f / static_cast(m_window_info.surface_width), - 2.0f / static_cast(m_window_info.surface_height), - -1.0f, - -1.0f); + // ImGui's vertex Position and ClipRect both come in *logical* pixel coords + // (against io.DisplaySize, which we set to the rotated presentation size). + // uScale/uTranslate map pixel coords directly to NDC. Rotation transforms + // logical pixels into the physical pixel space of the swapchain viewport, + // so uScale must always be in physical units (not logical) for both the + // rotated and unrotated paths. + const float phys_w = static_cast(m_window_info.surface_width); + const float phys_h = static_cast(m_window_info.surface_height); + const GSVector4 uniforms(2.0f / phys_w, 2.0f / phys_h, -1.0f, -1.0f); SetUtilityPushConstants(&uniforms, sizeof(uniforms)); SetPipeline(m_imgui_pipeline); @@ -4580,6 +4801,40 @@ void GSDeviceVK::RenderImGui() // this is for presenting, we don't want to screw with the viewport/scissor set by display m_dirty_flags &= ~(DIRTY_FLAG_VIEWPORT | DIRTY_FLAG_SCISSOR); + // Logical/physical coords differ for Rot90/Rot270; the rotation transform + // maps a logical pixel (lx, ly) on a (lw, lh) logical surface to a + // physical pixel on the (phys_w, phys_h) swapchain. Rotation is around + // the geometric centre of each surface. + const bool rotate = (GSConfig.Rotation != DisplayRotation::Rot0); + const GSVector2i pres = GetPresentationSize(); + const float lw = static_cast(pres.x); + const float lh = static_cast(pres.y); + const auto rotate_pixel = [&](float lx, float ly, float& px, float& py) { + const float lcx = lx - lw * 0.5f; + const float lcy = ly - lh * 0.5f; + float pcx = lcx; + float pcy = lcy; + switch (GSConfig.Rotation) + { + case DisplayRotation::Rot90: + pcx = lcy; + pcy = -lcx; + break; + case DisplayRotation::Rot180: + pcx = -lcx; + pcy = -lcy; + break; + case DisplayRotation::Rot270: + pcx = -lcy; + pcy = lcx; + break; + default: + break; + } + px = pcx + phys_w * 0.5f; + py = pcy + phys_h * 0.5f; + }; + for (int n = 0; n < draw_data->CmdListsCount; n++) { const ImDrawList* cmd_list = draw_data->CmdLists[n]; @@ -4594,7 +4849,22 @@ void GSDeviceVK::RenderImGui() } vertex_offset = m_vertex_stream_buffer.GetCurrentOffset() / sizeof(ImDrawVert); - std::memcpy(m_vertex_stream_buffer.GetCurrentHostPointer(), cmd_list->VtxBuffer.Data, size); + if (!rotate) + { + std::memcpy(m_vertex_stream_buffer.GetCurrentHostPointer(), + cmd_list->VtxBuffer.Data, size); + } + else + { + ImDrawVert* dst = reinterpret_cast( + m_vertex_stream_buffer.GetCurrentHostPointer()); + const ImDrawVert* src = cmd_list->VtxBuffer.Data; + for (int i = 0; i < cmd_list->VtxBuffer.Size; i++) + { + dst[i] = src[i]; + rotate_pixel(src[i].pos.x, src[i].pos.y, dst[i].pos.x, dst[i].pos.y); + } + } m_vertex_stream_buffer.CommitMemory(size); } @@ -4606,10 +4876,28 @@ void GSDeviceVK::RenderImGui() const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; pxAssert(!pcmd->UserCallback); - const GSVector4 clip = GSVector4::load(&pcmd->ClipRect); + GSVector4 clip = GSVector4::load(&pcmd->ClipRect); if ((clip.zwzw() <= clip.xyxy()).mask() != 0) continue; + if (rotate) + { + // Rotate the four corners of the logical clip rect into + // physical space, then take their axis-aligned bounding box. + // (90/270 rotations preserve axis-alignment.) + float x0 = clip.x, y0 = clip.y, x1 = clip.z, y1 = clip.w; + float c0x, c0y, c1x, c1y, c2x, c2y, c3x, c3y; + rotate_pixel(x0, y0, c0x, c0y); + rotate_pixel(x1, y0, c1x, c1y); + rotate_pixel(x0, y1, c2x, c2y); + rotate_pixel(x1, y1, c3x, c3y); + const float xmin = std::min(std::min(c0x, c1x), std::min(c2x, c3x)); + const float xmax = std::max(std::max(c0x, c1x), std::max(c2x, c3x)); + const float ymin = std::min(std::min(c0y, c1y), std::min(c2y, c3y)); + const float ymax = std::max(std::max(c0y, c1y), std::max(c2y, c3y)); + clip = GSVector4(xmin, ymin, xmax, ymax); + } + SetScissor(GSVector4i(clip).max_i32(GSVector4i::zero())); // Since we don't have the GSTexture... @@ -4666,9 +4954,22 @@ bool GSDeviceVK::DoCAS( // only happening once a frame, so the update isn't a huge deal. 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_optional_extensions.vk_khr_push_descriptor) + { + 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); + } + else + { + VkDescriptorSet ds = AllocateDescriptorSetFromFramePool(m_cas_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + return false; // single alloc per frame after EndRenderPass — exhaustion implausible; skip the sharpen pass + dsub.AddImageDescriptorWrite(ds, 0, sTexVK->GetView(), sTexVK->GetVkLayout()); + dsub.AddStorageImageDescriptorWrite(ds, 1, dTexVK->GetView(), dTexVK->GetVkLayout()); + dsub.Update(m_device); + 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; @@ -4803,6 +5104,8 @@ void GSDeviceVK::DestroyResources() vkFreeCommandBuffers(m_device, resources.command_pool, static_cast(resources.command_buffers.size()), resources.command_buffers.data()); } + if (resources.descriptor_pool != VK_NULL_HANDLE) + vkDestroyDescriptorPool(m_device, resources.descriptor_pool, nullptr); if (resources.command_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, resources.command_pool, nullptr); } @@ -5664,15 +5967,35 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (flags & DIRTY_FLAG_TFX_TEXTURES) { + VkDescriptorSet ds = VK_NULL_HANDLE; + // Without push descriptors, we must write all bindings to a fresh descriptor set. + if (!m_optional_extensions.vk_khr_push_descriptor) + { + ds = AllocateDescriptorSetFromFramePool(m_tfx_texture_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + { + if (already_execed) + { + Console.Error("VK: Failed to allocate TFX texture descriptor set"); + return false; + } + + // Frame descriptor pool exhausted — flush to reset it, then restart + // the render pass and re-apply all state on the fresh command buffer. + ExecuteCommandBufferAndRestartRenderPass(false, "Out of TFX texture descriptors"); + return ApplyTFXState(true); + } + } + if (flags & DIRTY_FLAG_TFX_TEXTURE_TEX) { - dsub.AddCombinedImageSamplerDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_TEXTURE, + dsub.AddCombinedImageSamplerDescriptorWrite(ds, TFX_TEXTURE_TEXTURE, m_tfx_textures[TFX_TEXTURE_TEXTURE]->GetView(), m_tfx_sampler, m_tfx_textures[TFX_TEXTURE_TEXTURE]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_PALETTE) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_PALETTE, + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_PALETTE, m_tfx_textures[TFX_TEXTURE_PALETTE]->GetView(), m_tfx_textures[TFX_TEXTURE_PALETTE]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_RT) @@ -5680,17 +6003,17 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (m_features.texture_barrier && !UseFeedbackLoopLayout()) { dsub.AddInputAttachmentDescriptorWrite( - VK_NULL_HANDLE, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), VK_IMAGE_LAYOUT_GENERAL); + ds, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), VK_IMAGE_LAYOUT_GENERAL); } else { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), m_tfx_textures[TFX_TEXTURE_RT]->GetVkLayout()); } } if (flags & DIRTY_FLAG_TFX_TEXTURE_PRIMID) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_PRIMID, + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_PRIMID, m_tfx_textures[TFX_TEXTURE_PRIMID]->GetView(), m_tfx_textures[TFX_TEXTURE_PRIMID]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_DEPTH) @@ -5698,26 +6021,35 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (m_features.texture_barrier && !UseFeedbackLoopLayout()) { dsub.AddInputAttachmentDescriptorWrite( - VK_NULL_HANDLE, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), VK_IMAGE_LAYOUT_GENERAL); + ds, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), VK_IMAGE_LAYOUT_GENERAL); } else { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), m_tfx_textures[TFX_TEXTURE_DEPTH]->GetVkLayout()); } } if (flags & DIRTY_FLAG_TFX_TEXTURE_RT_ROV) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_RT_ROV, m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_RT_ROV, m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetView(), m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetVkLayout(), true); } if (flags & DIRTY_FLAG_TFX_TEXTURE_DEPTH_ROV) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_DEPTH_ROV, m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_DEPTH_ROV, m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetView(), 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_optional_extensions.vk_khr_push_descriptor) + { + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, TFX_DESCRIPTOR_SET_TEXTURES); + } + else + { + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, + TFX_DESCRIPTOR_SET_TEXTURES, 1, &ds, 0, nullptr); + } } ApplyBaseState(flags, cmdbuf); @@ -5738,9 +6070,33 @@ bool GSDeviceVK::ApplyUtilityState(bool already_execed) m_current_pipeline_layout = PipelineLayout::Utility; 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_optional_extensions.vk_khr_push_descriptor) + { + 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); + } + else + { + VkDescriptorSet ds = AllocateDescriptorSetFromFramePool(m_utility_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + { + if (already_execed) + { + Console.Error("VK: Failed to allocate utility descriptor set"); + return false; + } + + // Frame descriptor pool exhausted — flush to reset it, then restart + // the render pass and re-apply all state on the fresh command buffer. + ExecuteCommandBufferAndRestartRenderPass(false, "Out of utility descriptors"); + return ApplyUtilityState(true); + } + dsub.AddCombinedImageSamplerDescriptorWrite( + ds, 0, m_utility_texture->GetView(), m_utility_sampler, m_utility_texture->GetVkLayout()); + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, 1, &ds, 0, nullptr); + } } diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h index 647527f2e0..8d42264d8c 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h @@ -43,6 +43,7 @@ public: bool vk_ext_line_rasterization : 1; bool vk_swapchain_maintenance1 : 1; bool vk_swapchain_maintenance1_is_khr : 1; + bool vk_khr_push_descriptor : 1; bool vk_khr_driver_properties : 1; bool vk_khr_shader_non_semantic_info : 1; bool vk_ext_attachment_feedback_loop_layout : 1; @@ -82,6 +83,9 @@ 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 GPU (Mali). + __fi bool IsDeviceARM() const { return (m_device_properties.vendorID == 0x13B5); } + // Creates a simple render pass. VkRenderPass GetRenderPass(VkFormat color_format, VkFormat depth_format, VkAttachmentLoadOp color_load_op = VK_ATTACHMENT_LOAD_OP_LOAD, @@ -104,6 +108,10 @@ public: /// Allocates a descriptor set from the pool reserved for the current frame. VkDescriptorSet AllocatePersistentDescriptorSet(VkDescriptorSetLayout set_layout); + /// Allocates a descriptor set from the current frame's per-frame pool (push descriptor fallback). + /// Returns VK_NULL_HANDLE on pool exhaustion after flushing the command buffer. + VkDescriptorSet AllocateDescriptorSetFromFramePool(VkDescriptorSetLayout set_layout); + /// Frees a descriptor set allocated from the global pool. void FreePersistentDescriptorSet(VkDescriptorSet set); @@ -214,6 +222,7 @@ 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}; + VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; // Per-frame pool, used when push descriptors are unavailable VkFence fence = VK_NULL_HANDLE; u64 fence_counter = 0; s32 spin_id = -1; @@ -292,6 +301,7 @@ private: VkPhysicalDeviceProperties m_device_properties = {}; VkPhysicalDeviceDriverPropertiesKHR m_device_driver_properties = {}; OptionalExtensions m_optional_extensions = {}; + bool m_colorclip_fallback_to_hdr = false; public: enum FeedbackLoopFlag : u8 @@ -562,6 +572,9 @@ public: bool SetGPUTimingEnabled(bool enabled) override; float GetAndResetAccumulatedGPUTime() override; + void EnableExtendedStats(bool enabled) override; + std::vector GetExtendedStats() const override; + void PushDebugGroup(const char* fmt, ...) override; void PopDebugGroup() override; void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override; diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp index f93b1673fc..83230b85bd 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp @@ -9,9 +9,11 @@ #include "common/Assertions.h" #include "common/CocoaTools.h" #include "common/Console.h" +#include "common/Timer.h" #include #include +#include #include #if defined(VK_USE_PLATFORM_XLIB_KHR) @@ -20,6 +22,34 @@ static_assert(VKSwapChain::NUM_SEMAPHORES == (GSDeviceVK::NUM_COMMAND_BUFFERS + 1)); +namespace +{ + // Diagnostic counters for present/acquire stalls (esp. on tiler-class drivers). + // Atomics so MTGS-thread acquire/present writes are race-free under reads from any thread. + std::atomic s_stats_enabled{false}; + std::atomic s_acquire_count{0}; + std::atomic s_acquire_total_ns{0}; + std::atomic s_acquire_max_ns{0}; + std::atomic s_present_count{0}; + std::atomic s_present_total_ns{0}; + std::atomic s_present_max_ns{0}; + // Aggregate counts of SUBOPTIMAL / OUT_OF_DATE results across BOTH the + // acquire and the present path (NoteAcquire + NotePresent both tick these). + // A persistently-stale swapchain can therefore tick up to twice per frame — + // these are "results observed", not "frames affected". Intentional: it keeps + // both event sources visible in the overlay without a 4-counter schema. + std::atomic s_suboptimal_count{0}; + std::atomic s_out_of_date_count{0}; + + void UpdateMax(std::atomic& dst, u64 sample) + { + u64 prev = dst.load(std::memory_order_relaxed); + while (sample > prev && !dst.compare_exchange_weak(prev, sample, std::memory_order_relaxed)) + { + } + } +} // namespace + VKSwapChain::VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode, std::optional exclusive_fullscreen_control) : m_window_info(wi) @@ -124,6 +154,144 @@ VkSurfaceKHR VKSwapChain::CreateVulkanSurface(VkInstance instance, VkPhysicalDev } #endif + // VK_KHR_display direct-to-monitor (kmsdrm handhelds). No compositor, + // no GBM, no native window handle from the frontend — the renderer + // enumerates displays itself. + if (wi->type == WindowInfo::Type::VulkanDirect) + { + u32 display_count = 0; + VkResult res = vkGetPhysicalDeviceDisplayPropertiesKHR(physical_device, &display_count, nullptr); + if (res != VK_SUCCESS || display_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPropertiesKHR (count) failed: "); + Console.Error("VK_KHR_display: no displays reported by ICD."); + return VK_NULL_HANDLE; + } + + std::vector displays(display_count); + res = vkGetPhysicalDeviceDisplayPropertiesKHR(physical_device, &display_count, displays.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + // Pick the first display. Multi-monitor handhelds are vanishingly + // rare; revisit if needed. + const VkDisplayKHR display = displays[0].display; + INFO_LOG("VK_KHR_display: using display '{}', physical {}x{} mm", + displays[0].displayName ? displays[0].displayName : "", + displays[0].physicalDimensions.width, displays[0].physicalDimensions.height); + + u32 mode_count = 0; + res = vkGetDisplayModePropertiesKHR(physical_device, display, &mode_count, nullptr); + if (res != VK_SUCCESS || mode_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetDisplayModePropertiesKHR (count) failed: "); + return VK_NULL_HANDLE; + } + + std::vector modes(mode_count); + res = vkGetDisplayModePropertiesKHR(physical_device, display, &mode_count, modes.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetDisplayModePropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + // Index 0 is the display's preferred (native) mode per spec. + // If the caller asked for a specific resolution, try to match it. + u32 best_mode_idx = 0; + if (wi->surface_width != 0 && wi->surface_height != 0) + { + for (u32 i = 0; i < mode_count; i++) + { + if (modes[i].parameters.visibleRegion.width == wi->surface_width && + modes[i].parameters.visibleRegion.height == wi->surface_height) + { + best_mode_idx = i; + break; + } + } + } + const VkDisplayModeKHR mode = modes[best_mode_idx].displayMode; + const VkExtent2D mode_extent = modes[best_mode_idx].parameters.visibleRegion; + INFO_LOG("VK_KHR_display: selected mode {}x{}@{}.{:03} Hz", + mode_extent.width, mode_extent.height, + modes[best_mode_idx].parameters.refreshRate / 1000, + modes[best_mode_idx].parameters.refreshRate % 1000); + + u32 plane_count = 0; + res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physical_device, &plane_count, nullptr); + if (res != VK_SUCCESS || plane_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR (count) failed: "); + return VK_NULL_HANDLE; + } + + std::vector planes(plane_count); + res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physical_device, &plane_count, planes.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + u32 selected_plane = UINT32_MAX; + for (u32 i = 0; i < plane_count; i++) + { + // Skip planes already bound to a different display. + if (planes[i].currentDisplay != VK_NULL_HANDLE && planes[i].currentDisplay != display) + continue; + + u32 supported_count = 0; + if (vkGetDisplayPlaneSupportedDisplaysKHR(physical_device, i, &supported_count, nullptr) != VK_SUCCESS || + supported_count == 0) + continue; + + std::vector supported(supported_count); + vkGetDisplayPlaneSupportedDisplaysKHR(physical_device, i, &supported_count, supported.data()); + if (std::find(supported.begin(), supported.end(), display) != supported.end()) + { + selected_plane = i; + break; + } + } + + if (selected_plane == UINT32_MAX) + { + Console.Error("VK_KHR_display: no compatible plane found for selected display."); + return VK_NULL_HANDLE; + } + + VkDisplaySurfaceCreateInfoKHR surface_create_info = {}; + surface_create_info.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR; + surface_create_info.displayMode = mode; + surface_create_info.planeIndex = selected_plane; + surface_create_info.planeStackIndex = planes[selected_plane].currentStackIndex; + surface_create_info.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + surface_create_info.globalAlpha = 1.0f; + surface_create_info.alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR; + surface_create_info.imageExtent = mode_extent; + + VkSurfaceKHR surface; + res = vkCreateDisplayPlaneSurfaceKHR(instance, &surface_create_info, nullptr, &surface); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkCreateDisplayPlaneSurfaceKHR failed: "); + return VK_NULL_HANDLE; + } + + // Reflect the actual selected mode back to the caller so the + // swapchain sizes correctly even when 0x0 was passed in. + wi->surface_width = mode_extent.width; + wi->surface_height = mode_extent.height; + wi->surface_refresh_rate = + static_cast(modes[best_mode_idx].parameters.refreshRate) / 1000.0f; + + return surface; + } + return VK_NULL_HANDLE; } @@ -202,6 +370,72 @@ std::optional VKSwapChain::SelectSurfaceFormat(VkSurfaceKHR return std::nullopt; } +VKSwapChain::PresentStats VKSwapChain::GetPresentStats() +{ + const u64 acquire_n = s_acquire_count.load(std::memory_order_relaxed); + const u64 present_n = s_present_count.load(std::memory_order_relaxed); + return PresentStats{ + acquire_n, + static_cast(s_acquire_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + static_cast(s_acquire_max_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + present_n, + static_cast(s_present_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + static_cast(s_present_max_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + s_suboptimal_count.load(std::memory_order_relaxed), + s_out_of_date_count.load(std::memory_order_relaxed), + }; +} + +void VKSwapChain::ResetPresentStats() +{ + s_acquire_count.store(0, std::memory_order_relaxed); + s_acquire_total_ns.store(0, std::memory_order_relaxed); + s_acquire_max_ns.store(0, std::memory_order_relaxed); + s_present_count.store(0, std::memory_order_relaxed); + s_present_total_ns.store(0, std::memory_order_relaxed); + s_present_max_ns.store(0, std::memory_order_relaxed); + s_suboptimal_count.store(0, std::memory_order_relaxed); + s_out_of_date_count.store(0, std::memory_order_relaxed); +} + +void VKSwapChain::SetPresentStatsEnabled(bool enabled) +{ + s_stats_enabled.store(enabled, std::memory_order_relaxed); +} + +bool VKSwapChain::IsPresentStatsEnabled() +{ + return s_stats_enabled.load(std::memory_order_relaxed); +} + +void VKSwapChain::NoteAcquire(double ms, VkResult res) +{ + if (!s_stats_enabled.load(std::memory_order_relaxed)) + return; + const u64 ns = static_cast(ms * 1'000'000.0); + s_acquire_count.fetch_add(1, std::memory_order_relaxed); + s_acquire_total_ns.fetch_add(ns, std::memory_order_relaxed); + UpdateMax(s_acquire_max_ns, ns); + if (res == VK_SUBOPTIMAL_KHR) + s_suboptimal_count.fetch_add(1, std::memory_order_relaxed); + else if (res == VK_ERROR_OUT_OF_DATE_KHR) + s_out_of_date_count.fetch_add(1, std::memory_order_relaxed); +} + +void VKSwapChain::NotePresent(double ms, VkResult res) +{ + if (!s_stats_enabled.load(std::memory_order_relaxed)) + return; + const u64 ns = static_cast(ms * 1'000'000.0); + s_present_count.fetch_add(1, std::memory_order_relaxed); + s_present_total_ns.fetch_add(ns, std::memory_order_relaxed); + UpdateMax(s_present_max_ns, ns); + if (res == VK_SUBOPTIMAL_KHR) + s_suboptimal_count.fetch_add(1, std::memory_order_relaxed); + else if (res == VK_ERROR_OUT_OF_DATE_KHR) + s_out_of_date_count.fetch_add(1, std::memory_order_relaxed); +} + static const char* PresentModeToString(VkPresentModeKHR mode) { switch (mode) @@ -325,8 +559,18 @@ bool VKSwapChain::CreateSwapChain() // Select number of images in swap chain, we prefer one buffer in the background to work on in triple-buffered mode. // maxImageCount can be zero, in which case there isn't an upper limit on the number of buffers. + // VK_KHR_display (VulkanDirect) + FIFO + 2 images stalls vkAcquireNextImageKHR + // for ~1.5 vsync intervals per frame waiting for the display engine to release + // the previously-presented image (measured on some tiler-class drivers). A third + // image lets the GPU work on N+2 while N is on-screen and N+1 is queued, recovering + // ~33% throughput. Default to 3 images for VulkanDirect, and for MAILBOX + // present mode regardless of WSI. + const bool use_triple = + (m_window_info.type == WindowInfo::Type::VulkanDirect) || + (m_present_mode == VK_PRESENT_MODE_MAILBOX_KHR); + const u32 desired_image_count = use_triple ? 3 : 2; u32 image_count = std::clamp( - (m_present_mode == VK_PRESENT_MODE_MAILBOX_KHR) ? 3 : 2, surface_capabilities.minImageCount, + desired_image_count, surface_capabilities.minImageCount, (surface_capabilities.maxImageCount == 0) ? std::numeric_limits::max() : surface_capabilities.maxImageCount); DEV_LOG("Creating a swap chain with {} images in present mode {}", image_count, PresentModeToString(m_present_mode)); @@ -343,6 +587,28 @@ bool VKSwapChain::CreateSwapChain() size.height = std::clamp(size.height, surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height); + // One-shot log of the resolved swapchain config — useful for WSI-path diagnosis + // (e.g. comparing VK_KHR_display vs a Wayland surface on the same device). + { + const char* wsi_name = "?"; + switch (m_window_info.type) + { + case WindowInfo::Type::Surfaceless: wsi_name = "Surfaceless"; break; + case WindowInfo::Type::Win32: wsi_name = "Win32"; break; + case WindowInfo::Type::X11: wsi_name = "X11"; break; + case WindowInfo::Type::Wayland: wsi_name = "Wayland"; break; + case WindowInfo::Type::MacOS: wsi_name = "MacOS"; break; + case WindowInfo::Type::VulkanDirect: wsi_name = "VulkanDirect"; break; + } + Console.WriteLnFmt( + "Vulkan: Swapchain {}x{} fmt={} colorspace={} present={} images={} (desired={} min={} max={}) wsi={}", + size.width, size.height, static_cast(surface_format->format), + static_cast(surface_format->colorSpace), + PresentModeToString(m_present_mode), image_count, + desired_image_count, surface_capabilities.minImageCount, surface_capabilities.maxImageCount, + wsi_name); + } + // Prefer identity transform if possible VkSurfaceTransformFlagBitsKHR transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; if (!(surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)) @@ -382,9 +648,18 @@ bool VKSwapChain::CreateSwapChain() // VK_EXT_swapchain_maintenance1 types/enums are aliases of VK_KHR_swapchain_maintenance1 types/enums. const VkSwapchainPresentModesCreateInfoKHR modes_info{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODES_CREATE_INFO_KHR, nullptr, 1u, &m_present_mode}; + // Some ARM Mali Vulkan drivers advertise VK_EXT_swapchain_maintenance1 but + // vkCreateSwapchainKHR errors VK_ERROR_INITIALIZATION_FAILED whenever this pNext is + // attached, regardless of present mode. Keep the extension enabled — the rest of its + // surface (present-fence-info, release-swapchain-images) works fine — and just skip + // the create-time pNext on ARM Mali. + const bool use_present_modes_pnext = + GSDeviceVK::GetInstance()->GetOptionalExtensions().vk_swapchain_maintenance1 && + !GSDeviceVK::GetInstance()->IsDeviceARM(); + // Now we can actually create the swap chain - VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, - GSDeviceVK::GetInstance()->GetOptionalExtensions().vk_swapchain_maintenance1 ? &modes_info : nullptr, 0, m_surface, + VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + use_present_modes_pnext ? &modes_info : nullptr, 0, m_surface, image_count, surface_format->format, surface_format->colorSpace, size, 1u, image_usage, VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, transform, alpha, m_present_mode, VK_TRUE, old_swap_chain}; std::array indices = {{ @@ -551,8 +826,16 @@ VkResult VKSwapChain::AcquireNextImage() // Use a different semaphore for each image. m_current_semaphore = (m_current_semaphore + 1) % static_cast(m_semaphores.size()); + const bool stats = s_stats_enabled.load(std::memory_order_relaxed); + const Common::Timer::Value t_start = stats ? Common::Timer::GetCurrentValue() : 0; const VkResult res = vkAcquireNextImageKHR(GSDeviceVK::GetInstance()->GetDevice(), m_swap_chain, UINT64_MAX, m_semaphores[m_current_semaphore].available_semaphore, VK_NULL_HANDLE, &m_current_image); + if (stats) + { + const double elapsed_ms = + Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - t_start); + NoteAcquire(elapsed_ms, res); + } m_image_acquire_result = res; return res; } diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h index ce43ea689d..bca8d40022 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h @@ -23,6 +23,29 @@ public: ~VKSwapChain(); + // Diagnostic counters for present/acquire timing, accumulated across all swapchains. + // Surfaces WSI-layer stalls (e.g. slow present/acquire on tiler-class drivers). + struct PresentStats + { + u64 acquire_count; + double acquire_total_ms; + double acquire_max_ms; + u64 present_count; + double present_total_ms; + double present_max_ms; + u64 suboptimal_count; + u64 out_of_date_count; + }; + static PresentStats GetPresentStats(); + static void ResetPresentStats(); + // Controls whether NoteAcquire/NotePresent record anything. Default off so + // the normal present path pays only one atomic-load + branch per call; + // diagnostic tools flip this on at startup. + static void SetPresentStatsEnabled(bool enabled); + static bool IsPresentStatsEnabled(); + static void NoteAcquire(double ms, VkResult res); + static void NotePresent(double ms, VkResult res); + // Creates a vulkan-renderable surface for the specified window handle. static VkSurfaceKHR CreateVulkanSurface(VkInstance instance, VkPhysicalDevice physical_device, WindowInfo* wi); @@ -46,6 +69,7 @@ public: __fi u32 GetCurrentImageIndex() const { return m_current_image; } __fi const u32* GetCurrentImageIndexPtr() const { return &m_current_image; } __fi u32 GetImageCount() const { return static_cast(m_images.size()); } + __fi VkPresentModeKHR GetPresentMode() const { return m_present_mode; } __fi const GSTextureVK* GetCurrentTexture() const { return m_images[m_current_image].get(); } __fi GSTextureVK* GetCurrentTexture() { return m_images[m_current_image].get(); } __fi VkSemaphore GetImageAvailableSemaphore() const diff --git a/pcsx2/ImGui/ImGuiManager.cpp b/pcsx2/ImGui/ImGuiManager.cpp index ae0d272bad..307abb4d57 100644 --- a/pcsx2/ImGui/ImGuiManager.cpp +++ b/pcsx2/ImGui/ImGuiManager.cpp @@ -183,8 +183,11 @@ bool ImGuiManager::Initialize() g.ConfigNavWindowingKeyPrev = ImGuiKey_None; g.ConfigNavWindowingWithGamepad = false; - s_window_width = static_cast(g_gs_device->GetWindowWidth()); - s_window_height = static_cast(g_gs_device->GetWindowHeight()); + { + const GSVector2i pres = g_gs_device->GetPresentationSize(); + s_window_width = static_cast(pres.x); + s_window_height = static_cast(pres.y); + } io.DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling io.DisplaySize = ImVec2(s_window_width, s_window_height); @@ -258,11 +261,12 @@ float ImGuiManager::GetWindowHeight() void ImGuiManager::WindowResized() { - const u32 new_width = g_gs_device ? g_gs_device->GetWindowWidth() : 0; - const u32 new_height = g_gs_device ? g_gs_device->GetWindowHeight() : 0; + GSVector2i new_size{}; + if (g_gs_device) + new_size = g_gs_device->GetPresentationSize(); - s_window_width = static_cast(new_width); - s_window_height = static_cast(new_height); + s_window_width = static_cast(new_size.x); + s_window_height = static_cast(new_size.y); ImGui::GetIO().DisplaySize = ImVec2(s_window_width, s_window_height); // Scale might have changed as a result of window resize. diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 8fa9ad8608..d70ea21c6f 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -658,6 +658,13 @@ const char* Pcsx2Config::GSOptions::FMVAspectRatioSwitchNames[(size_t)FMVAspectR "10:7", nullptr}; +const char* Pcsx2Config::GSOptions::DisplayRotationNames[(size_t)DisplayRotation::MaxCount + 1] = { + "0", + "90", + "180", + "270", + nullptr}; + const char* Pcsx2Config::GSOptions::BlendingLevelNames[] = { "Minimum", "Basic", @@ -727,6 +734,7 @@ Pcsx2Config::GSOptions::GSOptions() UseBlitSwapChain = false; DisableShaderCache = false; DisableFramebufferFetch = false; + DisablePS2DepthQuantization = false; DisableVertexShaderExpand = false; SkipDuplicateFrames = true; OsdMessagesPos = OsdOverlayPos::TopLeft; @@ -806,6 +814,7 @@ bool Pcsx2Config::GSOptions::operator==(const GSOptions& right) const OpEqu(AspectRatio) && OpEqu(FMVAspectRatioSwitch) && + OpEqu(Rotation) && OptionsAreEqual(right)); } @@ -916,6 +925,7 @@ bool Pcsx2Config::GSOptions::RestartOptionsAreEqual(const GSOptions& right) cons OpEqu(UseBlitSwapChain) && OpEqu(DisableShaderCache) && OpEqu(DisableFramebufferFetch) && + OpEqu(DisablePS2DepthQuantization) && OpEqu(DisableVertexShaderExpand) && OpEqu(OverrideTextureBarriers) && OpEqu(DepthFeedbackMode) && @@ -942,6 +952,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapEnumEx(AspectRatio, "AspectRatio", AspectRatioNames); SettingsWrapEnumEx(FMVAspectRatioSwitch, "FMVAspectRatioSwitch", FMVAspectRatioSwitchNames); + SettingsWrapEnumEx(Rotation, "DisplayRotation", DisplayRotationNames); SettingsWrapIntEnumEx(ScreenshotSize, "ScreenshotSize"); SettingsWrapIntEnumEx(ScreenshotFormat, "ScreenshotFormat"); SettingsWrapEntry(ScreenshotQuality); @@ -964,6 +975,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBool(UseBlitSwapChain); SettingsWrapBitBool(DisableShaderCache); SettingsWrapBitBool(DisableFramebufferFetch); + SettingsWrapBitBool(DisablePS2DepthQuantization); SettingsWrapBitBool(DisableVertexShaderExpand); SettingsWrapBitBool(SkipDuplicateFrames); SettingsWrapBitBool(OsdShowSpeed); From ff1abc36b88500674f67ce3b0f449685fd1bbfe6 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:56 -0700 Subject: [PATCH 013/292] arm64: SDL/kmsdrm frontend, headless runners, and handheld defaults (fork-only) pcsx2-sdl (SDL3/kmsdrm handheld frontend), pcsx2-eerunner / pcsx2-vurunner headless JIT regression+divergence tools, the gsrunner libmali CLI flags + Wayland scanner wiring, and the heterogeneous-CPU thread-pinning default. Fork-only tooling/frontends. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 28 + pcsx2-eerunner/CMakeLists.txt | 22 + pcsx2-eerunner/Main.cpp | 3038 +++++++++++++++++++++++++++++++++ pcsx2-gsrunner/CMakeLists.txt | 36 + pcsx2-gsrunner/Main.cpp | 321 +++- pcsx2-sdl/CMakeLists.txt | 35 + pcsx2-sdl/Main.cpp | 936 ++++++++++ pcsx2-vurunner/CMakeLists.txt | 43 + pcsx2-vurunner/Main.cpp | 1115 ++++++++++++ pcsx2/VMManager.cpp | 13 + 10 files changed, 5581 insertions(+), 6 deletions(-) create mode 100644 pcsx2-eerunner/CMakeLists.txt create mode 100644 pcsx2-eerunner/Main.cpp create mode 100644 pcsx2-sdl/CMakeLists.txt create mode 100644 pcsx2-sdl/Main.cpp create mode 100644 pcsx2-vurunner/CMakeLists.txt create mode 100644 pcsx2-vurunner/Main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 197a1e54fb..cb0cc63649 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,34 @@ else() add_subdirectory(pcsx2-gsrunner EXCLUDE_FROM_ALL) endif() +# eerunner — headless EE (R5900) JIT-vs-interpreter divergence localizer. +# Built on demand (`--target pcsx2-eerunner`); EXCLUDE_FROM_ALL otherwise. +if(ENABLE_EERUNNER) + add_subdirectory(pcsx2-eerunner) +else() + add_subdirectory(pcsx2-eerunner EXCLUDE_FROM_ALL) +endif() + +# vurunner — headless VU microprogram replayer for codegen iteration. Needs +# the recompiler test hooks since it shares the harness's plumbing. +if(ENABLE_VURUNNER) + if(NOT ENABLE_RECOMPILER_TEST_HOOKS) + message(FATAL_ERROR "ENABLE_VURUNNER requires ENABLE_RECOMPILER_TEST_HOOKS=ON.") + endif() + add_subdirectory(pcsx2-vurunner) +else() + add_subdirectory(pcsx2-vurunner EXCLUDE_FROM_ALL) +endif() + +# SDL3 / kmsdrm frontend (handheld target) +if(UNIX AND NOT APPLE) + if(ENABLE_SDL_FRONTEND) + add_subdirectory(pcsx2-sdl) + else() + add_subdirectory(pcsx2-sdl EXCLUDE_FROM_ALL) + endif() +endif() + #------------------------------------------------------------------------------- if(NOT IS_SUPPORTED_COMPILER) message(WARNING " diff --git a/pcsx2-eerunner/CMakeLists.txt b/pcsx2-eerunner/CMakeLists.txt new file mode 100644 index 0000000000..342d1ce38b --- /dev/null +++ b/pcsx2-eerunner/CMakeLists.txt @@ -0,0 +1,22 @@ +add_executable(pcsx2-eerunner) + +if (PACKAGE_MODE) + install(TARGETS pcsx2-eerunner DESTINATION ${CMAKE_INSTALL_BINDIR}) +else() + install(TARGETS pcsx2-eerunner DESTINATION ${CMAKE_SOURCE_DIR}/bin) +endif() + +target_sources(pcsx2-eerunner PRIVATE + Main.cpp +) + +target_include_directories(pcsx2-eerunner PRIVATE + "${CMAKE_BINARY_DIR}/common/include" + "${CMAKE_SOURCE_DIR}/pcsx2" + "${CMAKE_SOURCE_DIR}/tests/ctest/core/recompilers" +) + +target_link_libraries(pcsx2-eerunner PRIVATE + PCSX2_FLAGS + PCSX2 +) diff --git a/pcsx2-eerunner/Main.cpp b/pcsx2-eerunner/Main.cpp new file mode 100644 index 0000000000..74f54962f8 --- /dev/null +++ b/pcsx2-eerunner/Main.cpp @@ -0,0 +1,3038 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// pcsx2-eerunner — headless standalone full-system runner for EE (R5900) +// JIT-vs-interpreter divergence triage. +// +// Modeled closely on pcsx2-gsrunner: it does a full VMManager init on a +// dedicated CPU thread and provides the complete Host:: implementation surface +// a standalone binary needs to link against libpcsx2. Unlike gsrunner it runs +// fully headless (Null GS renderer, no window, synchronous GS, no audio) so the +// run is deterministic frame-to-frame. +// +// --selfcheck loads a savestate, runs N frames under the EE interpreter twice +// from the same savestate, and proves the two per-frame fingerprint streams are +// byte-identical. That run-to-run determinism is the gate before any +// JIT-vs-interp diff (--localize / --repro) is meaningful. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include "common/RedtapeWindows.h" +#endif + +#include "fmt/format.h" + +#include "common/Assertions.h" +#include "common/Console.h" +#include "common/CrashHandler.h" +#include "common/Error.h" +#include "common/FileSystem.h" +#include "common/MemorySettingsInterface.h" +#include "common/Path.h" +#include "common/ProgressCallback.h" +#include "common/StringUtil.h" + +#include "pcsx2/PrecompiledHeader.h" + +#include "pcsx2/Achievements.h" +#include "pcsx2/DebugTools/Debug.h" +#include "pcsx2/GS/GS.h" +#include "pcsx2/Host.h" +#include "pcsx2/INISettingsInterface.h" +#include "pcsx2/ImGui/FullscreenUI.h" +#include "pcsx2/ImGui/ImGuiFullscreen.h" +#include "pcsx2/ImGui/ImGuiManager.h" +#include "pcsx2/Hw.h" +#include "pcsx2/Input/InputManager.h" +#include "pcsx2/Memory.h" +#include "pcsx2/R5900.h" +#include "pcsx2/SIO/Pad/Pad.h" +#include "pcsx2/VMManager.h" + +#include "pcsx2/ee_divtrace.h" + +#include "svnrev.h" + +namespace EERunner +{ + static void InitializeConsole(); + static bool InitializeConfig(); + static void SettingsOverride(); + static bool ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params); +} // namespace EERunner + +enum class RunMode +{ + None, + SelfCheck, + Localize, + Repro, + StepDiff, + Vu0Diff, + ContMem, + SpeedhackDiff, + LiveRun, + Disasm, +}; + +static MemorySettingsInterface s_settings_interface; + +// Parsed command-line state, read by the CPU thread. +static RunMode s_mode = RunMode::None; +static std::string s_iso_path; +static std::string s_savestate_path; +static uint32_t s_frames = 300; +static bool s_no_console = false; +static bool s_contmem_vu0_interp = false; // --vu0-interp modifier for --contmem +static GSRendererType s_renderer = GSRendererType::Null; // --renderer (Null default; vk for Intel/headless) +static std::string s_memdump_prefix; // --memdump : write .{interp,jit}.bin at the last frame + +bool EERunner::InitializeConfig() +{ + EmuFolders::SetAppRoot(); + if (!EmuFolders::SetResourcesDirectory() || !EmuFolders::SetDataDirectory(nullptr)) + return false; + + CrashHandler::SetWriteDirectory(EmuFolders::DataRoot); + + const char* error; + if (!VMManager::PerformEarlyHardwareChecks(&error)) + return false; + + { + const std::string roboto_path = + EmuFolders::GetOverridableResourcePath("fonts" FS_OSPATH_SEPARATOR_STR "Roboto-Regular.ttf"); + const auto roboto_data = FileSystem::MapBinaryFileForRead(roboto_path.c_str()); + if (roboto_data.empty()) + { + Console.ErrorFmt("Failed to load font file '{}'.", roboto_path); + return false; + } + + std::vector fonts; + ImGuiManager::FontInfo fi{}; + fi.data = roboto_data; + fi.exclude_ranges = {}; + fi.face_name = nullptr; + fi.is_emoji_font = false; + fonts.push_back(fi); + + ImGuiManager::SetFonts(std::move(fonts)); + } + + // don't provide an ini path, or bother loading. settings are stored entirely in memory. + MemorySettingsInterface& si = s_settings_interface; + Host::Internal::SetBaseSettingsLayer(&si); + + VMManager::SetDefaultSettings(si, true, true, true, true, true); + + VMManager::Internal::LoadStartupSettings(); + return true; +} + +void Host::CommitBaseSettingChanges() +{ + // nothing to save, settings are entirely in memory +} + +void Host::LoadSettings(SettingsInterface& si, std::unique_lock& lock) +{ +} + +void Host::CheckForSettingsChanges(const Pcsx2Config& old_config) +{ +} + +bool Host::RequestResetSettings(bool folders, bool core, bool controllers, bool hotkeys, bool ui) +{ + // not running any UI, so no settings requests will come in + return false; +} + +void Host::SetDefaultUISettings(SettingsInterface& si) +{ + // nothing +} + +bool Host::LocaleCircleConfirm() +{ + // not running any UI, so no settings requests will come in + return false; +} + +std::unique_ptr Host::CreateHostProgressCallback() +{ + return ProgressCallback::CreateNullProgressCallback(); +} + +void Host::ReportInfoAsync(const std::string_view title, const std::string_view message) +{ + if (!title.empty() && !message.empty()) + INFO_LOG("ReportInfoAsync: {}: {}", title, message); + else if (!message.empty()) + INFO_LOG("ReportInfoAsync: {}", message); +} + +void Host::ReportErrorAsync(const std::string_view title, const std::string_view message) +{ + if (!title.empty() && !message.empty()) + ERROR_LOG("ReportErrorAsync: {}: {}", title, message); + else if (!message.empty()) + ERROR_LOG("ReportErrorAsync: {}", message); +} + +void Host::OpenURL(const std::string_view url) +{ + // noop +} + +bool Host::CopyTextToClipboard(const std::string_view text) +{ + return false; +} + +std::string Host::GetTextFromClipboard() +{ + return std::string(); +} + +void Host::BeginTextInput() +{ + // noop +} + +void Host::EndTextInput() +{ + // noop +} + +std::optional Host::GetTopLevelWindowInfo() +{ + // Headless — never present anything. + WindowInfo wi; + wi.type = WindowInfo::Type::Surfaceless; + return wi; +} + +void Host::OnInputDeviceConnected(const std::string_view identifier, const std::string_view device_name) +{ +} + +void Host::OnInputDeviceDisconnected(const InputBindingKey key, const std::string_view identifier) +{ +} + +void Host::SetMouseMode(bool relative_mode, bool hide_cursor) +{ +} + +void Host::SetMouseLock(bool state) +{ +} + +std::optional Host::AcquireRenderWindow(bool recreate_window) +{ + // Headless — the Null renderer doesn't need a surface. + WindowInfo wi; + wi.type = WindowInfo::Type::Surfaceless; + return wi; +} + +void Host::ReleaseRenderWindow() +{ +} + +void Host::BeginPresentFrame() +{ + // Headless — nothing to present. +} + +void Host::RequestResizeHostDisplay(s32 width, s32 height) +{ +} + +void Host::OnVMStarting() +{ +} + +void Host::OnVMStarted() +{ +} + +void Host::OnVMDestroyed() +{ +} + +void Host::OnVMPaused() +{ +} + +void Host::OnVMResumed() +{ +} + +void Host::OnGameChanged(const std::string& title, const std::string& elf_override, const std::string& disc_path, + const std::string& disc_serial, u32 disc_crc, u32 current_crc) +{ +} + +void Host::OnPerformanceMetricsUpdated() +{ +} + +void Host::OnSaveStateLoading(const std::string_view filename) +{ +} + +void Host::OnSaveStateLoaded(const std::string_view filename, bool was_successful) +{ +} + +void Host::OnSaveStateSaved(const std::string_view filename) +{ +} + +void Host::RunOnCPUThread(std::function function, bool block /* = false */) +{ + pxFailRel("Not implemented"); +} + +void Host::RefreshGameListAsync(bool invalidate_cache) +{ +} + +void Host::CancelGameListRefresh() +{ +} + +bool Host::IsFullscreen() +{ + return false; +} + +void Host::SetFullscreen(bool enabled) +{ +} + +void Host::OnCaptureStarted(const std::string& filename) +{ +} + +void Host::OnCaptureStopped() +{ +} + +void Host::RequestExitApplication(bool allow_confirm) +{ +} + +void Host::RequestExitBigPicture() +{ +} + +void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state) +{ + VMManager::SetState(VMState::Stopping); +} + +void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) +{ + // noop +} + +void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ + // noop +} + +void Host::OnAchievementsHardcoreModeChanged(bool enabled) +{ + // noop +} + +void Host::OnAchievementsRefreshed() +{ + // noop +} + +void Host::OnCoverDownloaderOpenRequested() +{ + // noop +} + +void Host::OnCreateMemoryCardOpenRequested() +{ + // noop +} + +bool Host::InBatchMode() +{ + return false; +} + +bool Host::InNoGUIMode() +{ + return false; +} + +bool Host::ShouldPreferHostFileSelector() +{ + return false; +} + +void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters, std::string_view initial_directory) +{ + callback(std::string()); +} + +int Host::LocaleSensitiveCompare(std::string_view lhs, std::string_view rhs) +{ + const int res = std::strncmp(lhs.data(), rhs.data(), std::min(lhs.size(), rhs.size())); + if (res != 0) + return res; + return lhs.size() > rhs.size() ? 1 : (lhs.size() < rhs.size() ? -1 : 0); +} + +std::optional InputManager::ConvertHostKeyboardStringToCode(const std::string_view str) +{ + return std::nullopt; +} + +std::optional InputManager::ConvertHostKeyboardCodeToString(u32 code) +{ + return std::nullopt; +} + +const char* InputManager::ConvertHostKeyboardCodeToIcon(u32 code) +{ + return nullptr; +} + +BEGIN_HOTKEY_LIST(g_host_hotkeys) +END_HOTKEY_LIST() + +void Host::PumpMessagesOnCPUThread() +{ + // Headless — no platform message pump. +} + +s32 Host::Internal::GetTranslatedStringImpl( + const std::string_view context, const std::string_view msg, char* tbuf, size_t tbuf_space) +{ + if (msg.size() > tbuf_space) + return -1; + else if (msg.empty()) + return 0; + + std::memcpy(tbuf, msg.data(), msg.size()); + return static_cast(msg.size()); +} + +std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count) +{ + TinyString count_str = TinyString::from_format("{}", count); + + std::string ret(msg); + for (;;) + { + std::string::size_type pos = ret.find("%n"); + if (pos == std::string::npos) + break; + + ret.replace(pos, 2, count_str.view()); + } + + return ret; +} + +static void PrintCommandLineVersion() +{ + std::fprintf(stderr, "PCSX2 EE Runner Version %s\n", GIT_REV); + std::fprintf(stderr, "https://pcsx2.net/\n"); + std::fprintf(stderr, "\n"); +} + +static void PrintCommandLineHelp(const char* progname) +{ + PrintCommandLineVersion(); + std::fprintf(stderr, "Usage: %s [--stepdiff|--contmem|--liverun|--vu0diff|--localize|--repro|--selfcheck] --savestate --frames N [--iso ] []\n", progname); + std::fprintf(stderr, "\n"); + std::fprintf(stderr, " --stepdiff: Checkpoint-anchored interp-vs-JIT diff (THE primary mode). Per frame, checkpoints\n"); + std::fprintf(stderr, " the VM and runs one frame interp-twice + JIT-once from the IDENTICAL state, so a\n"); + std::fprintf(stderr, " clean interp control + JIT divergence = a real EE JIT bug; then zooms to the block.\n"); + std::fprintf(stderr, " --contmem: Continuous-trajectory memory diff. Runs interp CONTINUOUSLY (x2, control) + JIT\n"); + std::fprintf(stderr, " CONTINUOUSLY, diffs per-frame memory hashes. Catches ACCUMULATION bugs --stepdiff\n"); + std::fprintf(stderr, " can't (it re-anchors to golden each frame). Add --vu0-interp to force VU0=interp in\n"); + std::fprintf(stderr, " all passes (isolate EE-FPU/integer from VU0/COP2). Run on x86 too for cross-arch.\n"); + std::fprintf(stderr, " --speedhack-diff: Speedhack-misfire differential. EE-jit throughout; baseline = all transparency-class\n"); + std::fprintf(stderr, " speedhacks OFF (run twice for the determinism floor), then sweeps each speedhack on its\n"); + std::fprintf(stderr, " own (WaitLoop/IntcStat/vuFlagHack/vu1Instant/fastCDVD) + all-on. A speedhack that claims\n"); + std::fprintf(stderr, " to be architecturally transparent must NOT change the EE-RAM trajectory before the\n"); + std::fprintf(stderr, " baseline control floor breaks; the first such divergence (with ReportMemDiff at it) is a\n"); + std::fprintf(stderr, " misfire (e.g. the Burnout-3 WaitLoop timeout-loop skip). Diffs EE main RAM + scratchpad.\n"); + std::fprintf(stderr, " --liverun: Reproduce the in-game HANG headlessly: single EE-jit pass with the LIVE subsystems\n"); + std::fprintf(stderr, " the diff modes suppress (real GS so GIF is consumed, MTVU on). A 10s no-frame-\n"); + std::fprintf(stderr, " progress watchdog samples the live EE PC to fingerprint the spin loop, then exits 42.\n"); + std::fprintf(stderr, " --vu0diff: Pin EE interp in both passes, toggle only VU0 micro engine; diff COP2 read-streams.\n"); + std::fprintf(stderr, " --localize / --repro: aliases of --stepdiff (the old jittery frame-boundary funnel was removed).\n"); + std::fprintf(stderr, " --selfcheck: Characterize run-to-run determinism (interp only). Expected to flag benign ~10-cycle\n"); + std::fprintf(stderr, " pause-point sampling jitter; use it to understand the noise floor, not as a gate.\n"); + std::fprintf(stderr, " --renderer : GS renderer (default null). Use vk on Intel GPUs / boxes where\n"); + std::fprintf(stderr, " the auto-check declines Vulkan and the surfaceless GL path fails to open GS.\n"); + std::fprintf(stderr, " --savestate : Savestate to load after Initialize (required).\n"); + std::fprintf(stderr, " --frames N: Number of frames to run (default 300).\n"); + std::fprintf(stderr, " --iso : Game ISO/disc to mount (required so the savestate has its disc).\n"); + std::fprintf(stderr, " -help: Displays this information and exits.\n"); + std::fprintf(stderr, " -version: Displays version information and exits.\n"); + std::fprintf(stderr, "\n"); +} + +void EERunner::InitializeConsole() +{ + const char* var = std::getenv("PCSX2_NOCONSOLE"); + s_no_console = (var && StringUtil::FromChars(var).value_or(false)); + if (!s_no_console) + Log::SetConsoleOutputLevel(LOGLEVEL_DEBUG); +} + +bool EERunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params) +{ + bool no_more_args = false; + for (int i = 1; i < argc; i++) + { + if (!no_more_args) + { +#define CHECK_ARG(str) !std::strcmp(argv[i], str) +#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc)) + + if (CHECK_ARG("-help") || CHECK_ARG("--help")) + { + PrintCommandLineHelp(argv[0]); + return false; + } + else if (CHECK_ARG("-version") || CHECK_ARG("--version")) + { + PrintCommandLineVersion(); + return false; + } + else if (CHECK_ARG("--selfcheck")) + { + s_mode = RunMode::SelfCheck; + continue; + } + else if (CHECK_ARG("--localize")) + { + s_mode = RunMode::Localize; + continue; + } + else if (CHECK_ARG("--repro")) + { + s_mode = RunMode::Repro; + continue; + } + else if (CHECK_ARG("--stepdiff")) + { + s_mode = RunMode::StepDiff; + continue; + } + else if (CHECK_ARG("--vu0diff")) + { + s_mode = RunMode::Vu0Diff; + continue; + } + else if (CHECK_ARG("--contmem")) + { + s_mode = RunMode::ContMem; + continue; + } + else if (CHECK_ARG("--speedhack-diff")) + { + s_mode = RunMode::SpeedhackDiff; + continue; + } + else if (CHECK_ARG("--liverun")) + { + s_mode = RunMode::LiveRun; + continue; + } + else if (CHECK_ARG("--disasm")) + { + // --disasm: load the savestate, then disassemble EE code in + // [EERUNNER_DIS_LO, EERUNNER_DIS_HI] with the correct R5900 disassembler + // (generic MIPS disassemblers garble R5900 COP2/MMI opcodes). No run. + s_mode = RunMode::Disasm; + continue; + } + else if (CHECK_ARG("--vu0-interp")) + { + s_contmem_vu0_interp = true; + continue; + } + else if (CHECK_ARG_PARAM("--memdump")) + { + s_memdump_prefix = StringUtil::StripWhitespace(argv[++i]); + continue; + } + else if (CHECK_ARG_PARAM("--renderer")) + { + const std::string_view r = StringUtil::StripWhitespace(argv[++i]); + if (r == "null" || r == "Null") s_renderer = GSRendererType::Null; + else if (r == "vk" || r == "vulkan") s_renderer = GSRendererType::VK; + else if (r == "ogl" || r == "gl" || r == "opengl") s_renderer = GSRendererType::OGL; + else if (r == "sw" || r == "software") s_renderer = GSRendererType::SW; + else + { + Console.Error("--renderer expects one of: null, vk, ogl, sw"); + return false; + } + continue; + } + else if (CHECK_ARG_PARAM("--iso")) + { + s_iso_path = StringUtil::StripWhitespace(argv[++i]); + continue; + } + else if (CHECK_ARG_PARAM("--savestate")) + { + s_savestate_path = StringUtil::StripWhitespace(argv[++i]); + continue; + } + else if (CHECK_ARG_PARAM("--frames")) + { + const auto v = StringUtil::FromChars(argv[++i]); + if (!v.has_value() || v.value() == 0) + { + Console.Error("Invalid --frames value."); + return false; + } + s_frames = v.value(); + continue; + } + else if (CHECK_ARG("--")) + { + no_more_args = true; + continue; + } + else if (argv[i][0] == '-') + { + Console.Error("Unknown parameter: '%s'", argv[i]); + return false; + } + +#undef CHECK_ARG +#undef CHECK_ARG_PARAM + } + + // Positional argument = the ISO/disc. + if (s_iso_path.empty()) + s_iso_path = argv[i]; + else + { + Console.Error("Unexpected extra positional argument: '%s'", argv[i]); + return false; + } + } + + if (s_mode == RunMode::None) + { + Console.Error("No mode specified (use --stepdiff, --contmem, --speedhack-diff, --liverun, --vu0diff, --selfcheck, --localize, or --repro)."); + return false; + } + + if (s_savestate_path.empty()) + { + Console.Error("No savestate provided (use --savestate )."); + return false; + } + if (!FileSystem::FileExists(s_savestate_path.c_str())) + { + Console.ErrorFmt("Savestate '{}' does not exist.", s_savestate_path); + return false; + } + + if (s_iso_path.empty()) + { + Console.Error("No ISO provided (use --iso ); the savestate needs its disc mounted."); + return false; + } + if (!FileSystem::FileExists(s_iso_path.c_str())) + { + Console.ErrorFmt("ISO '{}' does not exist.", s_iso_path); + return false; + } + + params.filename = s_iso_path; + return true; +} + +void EERunner::SettingsOverride() +{ + // Headless + deterministic: Null GS renderer, synchronous GS on the CPU + // thread, no MTVU, no audio, no time-stretch. Keep wall-clock out of the + // system clock so two runs from the same savestate produce identical + // architectural state. + + // GS renderer. Default Null (no GS draws) — but Null is NOT self-contained: GS's + // GetAPIForRenderer() has no Null case, so it falls through to GetPreferredRenderer() + // for the HOST device API. On Asahi/AMD/NVIDIA that resolves to Vulkan (works with + // the Surfaceless window we hand back); on an Intel box the auto-check declines Intel + // Vulkan and picks OpenGL, which can't make a context for a Surfaceless window -> GS + // fails to open. There, pass `--renderer vk` to force Vulkan (surfaceless-capable). + // The renderer is irrelevant to EE/--contmem results (both passes on a box use the + // same one, so GS cancels out in the jit-vs-interp diff). + // LiveRun (the in-game hang repro) needs the live subsystems the deterministic + // diff modes suppress: a REAL GS (so GIF/PATH3 is actually consumed, not dropped + // like Null) and MTVU. Null GS is meaningless for it, so force VK if unset. + const bool live = (s_mode == RunMode::LiveRun); + GSRendererType rend = s_renderer; + if (live && rend == GSRendererType::Null) + rend = GSRendererType::VK; + s_settings_interface.SetIntValue("EmuCore/GS", "Renderer", static_cast(rend)); + + // Run GS synchronously on the CPU thread (no MTGS). Key is "SynchronousMTGS" + // (Pcsx2Config::GSOptions::SynchronousMTGS; DEVBUILD-only — Devel defines it). + // Diff modes keep it forced true. For LiveRun, default true but gate on + // EERUNNER_SYNCMTGS: real async play uses SyncMTGS=false, so this lets us test + // whether the MTVU+SyncMTGS combo (which can't occur in normal play) is itself + // the deadlock trigger (harness artifact) vs a genuine hang reproducible async. + bool sync_mtgs = true; + if (live) + { + if (const char* e = std::getenv("EERUNNER_SYNCMTGS")) + sync_mtgs = (e[0] != '0'); + } + s_settings_interface.SetBoolValue("EmuCore/GS", "SynchronousMTGS", sync_mtgs); + + // MTVU off for the deterministic diff modes; ON for LiveRun by default. Gate on + // EERUNNER_MTVU so the wedge can be retested MTVU-off without a rebuild: if the + // hang reproduces identically MTVU-off, it is NOT an MTVU/MTGS thread deadlock + // (rules out "EE thread blocked") and is a genuine EE-rec cycle/event-test bug. + bool live_mtvu = live; + if (live) + { + if (const char* e = std::getenv("EERUNNER_MTVU")) + live_mtvu = (e[0] != '0'); + } + s_settings_interface.SetBoolValue("EmuCore/Speedhacks", "vuThread", live_mtvu); + + // EE recompiler ON by default for LiveRun; gate on EERUNNER_EE=interp (or 0) to + // run the clean EE-interpreter control pass — for jit-vs-interp comparison of the + // SAME live hang (interp clean, jit hangs). Diff modes leave EnableEE untouched. + if (live) + { + bool ee_jit = true; + if (const char* e = std::getenv("EERUNNER_EE")) + ee_jit = !(e[0] == 'i' || e[0] == 'I' || e[0] == '0'); + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableEE", ee_jit); + } + + // EERUNNER_FPUFULL=1 forces CHECK_FPU_FULL (eeClampMode:3) so the EE-FPU JIT uses + // the double-precision ADD/SUB/MUL/MADD paths that match the interp's fpuDouble() + // math. In --liverun: decisive test of the "EE-FPU 1-ULP precision is the hang + // cause" hypothesis (proven NEGATIVE — still hangs with FPUFULL on). In --stepdiff: + // converges the benign mul/add 1-ULP so the lockstep differ walks past it to the + // next (non-precision) divergence. Applied to ALL modes (set before VMManager + // init). recDIV_S stays single even in FULL, so div divergences still surface. + if (const char* e = std::getenv("EERUNNER_FPUFULL")) + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "fpuFullMode", e[0] != '0'); + + // EERUNNER_NOFASTMEM=1 disables EE/VTLB fastmem (the 4 GB signal-backpatch fast + // path). REQUIRED when running this binary under x86 emulation (FEX inside the + // muvm 4K-page microVM, for single-machine cross-arch jit-vs-jit): PCSX2 fastmem + // catches its OWN SIGSEGV to backpatch VTLB accesses, but FEX intercepts SIGSEGV + // for its x86->arm64 translation, so the guest fault never reaches PCSX2's handler + // -> unhandled SIGSEGV right after "Resetting fastmem mappings". Fastmem off routes + // every load/store through the explicit VTLB call path: EE architectural state is + // IDENTICAL (fastmem only changes speed), so memdump/contmem results are unaffected. + // Native arm64 leaves it on (default). Applied to ALL modes (set before VM init). + if (const char* e = std::getenv("EERUNNER_NOFASTMEM")) + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableFastmem", e[0] == '0'); + + // EERUNNER_DIVCHOP=1 forces EE FPU DIV.S/SQRT.S to round toward zero (chop) by + // setting FPUDiv.Roundmode = ChopZero (3), making FPUDivFPCR == FPUFPCR so the + // arm64 recDIV_S FPCR round-mode swap-to-Nearest becomes a no-op. Diagnostic for + // the Burnout-3 hang: the JIT div rounds Nearest (default FPUDivFPCR), interp + // rounds chop (ambient FPUFPCR), and one game div (0.1*2560) lands on the + // 255.9999847/256.0 boundary -> cvt.w.s gives 255 vs 256, a 1-off count that + // corrupts the GIF scratchpad buffer bookkeeping. Roundmode: 0=Nearest 3=ChopZero. + if (const char* e = std::getenv("EERUNNER_DIVCHOP")) + s_settings_interface.SetIntValue("EmuCore/CPU", "FPUDiv.Roundmode", e[0] != '0' ? 3 : 0); + + // No audio output, and no time-stretch sync (wall-clock-driven). Keys live in + // Pcsx2Config::SPU2Options::LoadSave under [SPU2/Output]: Backend / SyncMode. + s_settings_interface.SetStringValue("SPU2/Output", "Backend", "Null"); + s_settings_interface.SetStringValue("SPU2/Output", "SyncMode", "Disabled"); + + // No frameskip. + s_settings_interface.SetBoolValue("EmuCore/GS", "FrameSkipEnable", false); + s_settings_interface.SetIntValue("EmuCore/GS", "FramesToDraw", 1); + s_settings_interface.SetIntValue("EmuCore/GS", "FramesToSkip", 0); + + // Don't limit speed (also set on the VM via SetLimiterMode after init). + s_settings_interface.SetBoolValue("EmuCore/GS", "FrameLimitEnable", false); + s_settings_interface.SetIntValue("EmuCore/GS", "VsyncEnable", 0); + + // Disable input sources; we drive nothing. + s_settings_interface.SetBoolValue("InputSources", "SDL", false); + s_settings_interface.SetBoolValue("InputSources", "XInput", false); + Pad::ClearPortBindings(s_settings_interface, 0); + s_settings_interface.ClearSection("Hotkeys"); + + // Logging. + s_settings_interface.SetBoolValue("Logging", "EnableSystemConsole", !s_no_console); + s_settings_interface.SetBoolValue("Logging", "EnableTimestamps", false); + s_settings_interface.SetBoolValue("Logging", "EnableVerbose", true); + + // Remove memory cards, so we don't have sharing violations. + for (u32 i = 0; i < 2; i++) + { + s_settings_interface.SetBoolValue("MemoryCards", fmt::format("Slot{}_Enable", i + 1).c_str(), false); + s_settings_interface.SetStringValue("MemoryCards", fmt::format("Slot{}_Filename", i + 1).c_str(), ""); + } +} + +// Snapshot live cpuRegs/fpuRegs into a FullSnap (frame-boundary capture; pc/cycle +// from the live registers). +static ee_divtrace::FullSnap CaptureFullSnap() +{ + ee_divtrace::FullSnap fs; + std::memcpy(&fs.cpu, &cpuRegs, sizeof(cpuRegisters)); + std::memcpy(&fs.fpu, &fpuRegs, sizeof(fpuRegisters)); + fs.cycle = cpuRegs.cycle; + fs.pc = cpuRegs.pc; + fs._pad = 0; + return fs; +} + +// Per-frame selfcheck record: full register snapshot + the frame memory hash. +struct SelfCheckFrame +{ + ee_divtrace::FullSnap snap; + uint64_t memhash; +}; + +// Field-level diff of two FullSnaps (defined later); empty == identical regs. +static std::vector DiffFullSnaps(const ee_divtrace::FullSnap& jit, + const ee_divtrace::FullSnap& interp); + +// Advance the loaded VM by `count` frames, discarding output (defined later). +static void AdvanceFrames(uint32_t count); + +// Run s_frames frames in the current CPU mode, retaining a full register +// snapshot + memory hash per frame so selfcheck can field-diff the first +// divergent frame. (Snapshots are ~2 KB each; bounded by s_frames.) +static std::vector RunAndSnapshot() +{ + std::vector out; + out.reserve(s_frames); + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + VMManager::FrameAdvance(1); + VMManager::Execute(); // returns after one frame (paused) + out.push_back({CaptureFullSnap(), ee_divtrace::HashMemory()}); + } + return out; +} + +// Re-run from a fresh savestate load to the state captured at frame index +// `frame` (== AdvanceFrames(frame+1)), returning EE main RAM + scratchpad bytes +// for offline region diffing. Used to localize a store-path divergence. +static std::vector RunToFrameCaptureMem(uint32_t frame) +{ + Error error; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("RunToFrameCaptureMem: load failed: {}", error.GetDescription()); + return {}; + } + AdvanceFrames(frame + 1); + std::vector out(Ps2MemSize::MainRam + Ps2MemSize::Scratch); + std::memcpy(out.data(), eeMem->Main, Ps2MemSize::MainRam); + std::memcpy(out.data() + Ps2MemSize::MainRam, eeMem->Scratch, Ps2MemSize::Scratch); + return out; +} + +// Count of differing 4 KB pages / bytes between two EE-memory captures. +struct MemDiffCount +{ + size_t pages = 0; + size_t bytes = 0; +}; + +// Diff two EE-memory captures; print the first few differing 4 KB pages with a +// little content from each side so the divergent region/device can be reasoned +// about (main RAM byte offset == EE physical address for the first 32 MB). +// Returns the total differing page/byte counts (used by --speedhack-diff to size +// a divergence against the baseline determinism floor). verbose=false suppresses +// the per-page detail + summary line (for the many quiet trajectory samples). +static MemDiffCount ReportMemDiff(const std::vector& a, const std::vector& b, bool verbose = true) +{ + if (a.size() != b.size() || a.empty()) + { + if (verbose) + Console.ErrorFmt(" mem capture size mismatch ({} vs {})", a.size(), b.size()); + return {}; + } + const size_t pageSize = 0x1000; + size_t shown = 0, diffPages = 0, diffBytes = 0; + for (size_t off = 0; off < a.size(); off += pageSize) + { + const size_t end = std::min(off + pageSize, a.size()); + size_t firstDiff = SIZE_MAX, pageDiffBytes = 0; + for (size_t i = off; i < end; ++i) + if (a[i] != b[i]) + { + if (firstDiff == SIZE_MAX) + firstDiff = i; + ++pageDiffBytes; + } + if (firstDiff == SIZE_MAX) + continue; + ++diffPages; + diffBytes += pageDiffBytes; + if (verbose && shown < 8) + { + ++shown; + const char* region = (firstDiff < Ps2MemSize::MainRam) ? "Main" : "Scratch"; + const size_t addr = (firstDiff < Ps2MemSize::MainRam) + ? firstDiff : (firstDiff - Ps2MemSize::MainRam); + Console.ErrorFmt(" {} @ {:#010x}: {} bytes differ in page; A=[{:02x} {:02x} {:02x} {:02x}] B=[{:02x} {:02x} {:02x} {:02x}]", + region, addr, pageDiffBytes, + a[firstDiff], a[firstDiff + 1 < a.size() ? firstDiff + 1 : firstDiff], + a[firstDiff + 2 < a.size() ? firstDiff + 2 : firstDiff], a[firstDiff + 3 < a.size() ? firstDiff + 3 : firstDiff], + b[firstDiff], b[firstDiff + 1 < b.size() ? firstDiff + 1 : firstDiff], + b[firstDiff + 2 < b.size() ? firstDiff + 2 : firstDiff], b[firstDiff + 3 < b.size() ? firstDiff + 3 : firstDiff]); + } + } + if (verbose) + Console.ErrorFmt(" mem diff summary: {} pages, {} bytes differ (of {} captured)", diffPages, diffBytes, a.size()); + return {diffPages, diffBytes}; +} + +// Compare two per-frame snapshot streams; print the first few divergent frames +// with field detail. Returns true if identical. `la`/`lb` label the two streams. +static bool CompareStreams(const std::vector& a, + const std::vector& b, const char* la, const char* lb) +{ + if (a.size() != b.size()) + { + Console.ErrorFmt(" {} vs {}: frame count differs ({} vs {})", la, lb, a.size(), b.size()); + return false; + } + + size_t first_regdiff = SIZE_MAX, first_memdiff = SIZE_MAX, first_pcdiff = SIZE_MAX; + size_t diverged_frames = 0; + for (size_t f = 0; f < a.size(); ++f) + { + const auto regdiffs = DiffFullSnaps(a[f].snap, b[f].snap); + const bool memdiff = a[f].memhash != b[f].memhash; + const bool pcdiff = a[f].snap.pc != b[f].snap.pc; + if (regdiffs.empty() && !memdiff && !pcdiff) + continue; + + ++diverged_frames; + if (!regdiffs.empty() && first_regdiff == SIZE_MAX) + first_regdiff = f; + if (memdiff && first_memdiff == SIZE_MAX) + first_memdiff = f; + if (pcdiff && first_pcdiff == SIZE_MAX) + first_pcdiff = f; + + if (diverged_frames <= 4) + { + Console.ErrorFmt(" [{} vs {}] frame {}: pc {:#010x}/{:#010x} cycle {}/{} (dcyc={}) mem {}", + la, lb, f, a[f].snap.pc, b[f].snap.pc, a[f].snap.cycle, b[f].snap.cycle, + (int64_t)a[f].snap.cycle - (int64_t)b[f].snap.cycle, + memdiff ? "DIFFERS" : "same"); + for (const auto& d : regdiffs) + Console.ErrorFmt(" {}", d); // here "JIT="==la, "INTERP="==lb + } + } + + if (diverged_frames == 0) + { + Console.WriteLn(fmt::format(" {} vs {}: identical ({} frames)", la, lb, a.size())); + return true; + } + + Console.ErrorFmt(" {} vs {}: {}/{} frames diverged. first reg={} mem={} pc={}", + la, lb, diverged_frames, a.size(), + first_regdiff == SIZE_MAX ? -1 : (int64_t)first_regdiff, + first_memdiff == SIZE_MAX ? -1 : (int64_t)first_memdiff, + first_pcdiff == SIZE_MAX ? -1 : (int64_t)first_pcdiff); + return false; +} + +// --selfcheck: N interpreter passes from the same savestate must produce +// byte-identical per-frame snapshot streams. Running 3 passes (not 2) lets us +// distinguish a cold-cache/first-run artifact (B==C but A differs) from genuine +// per-run host nondeterminism (all three differ). Returns process exit code. +static int RunSelfCheck() +{ + Error error; + const int kPasses = 3; + std::vector> runs; + + for (int p = 0; p < kPasses; ++p) + { + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("Failed to load savestate (pass {}): {}", p, error.GetDescription()); + return EXIT_FAILURE; + } + Console.WriteLn(fmt::format("eerunner: pass {} — running {} frames (interp)...", p, s_frames)); + runs.push_back(RunAndSnapshot()); + } + + static const char* const names[3] = {"A", "B", "C"}; + bool all_match = true; + for (int p = 1; p < kPasses; ++p) + all_match &= CompareStreams(runs[p - 1], runs[p], names[p - 1], names[p]); + + if (all_match) + { + Console.WriteLn(fmt::format("SELFCHECK PASS ({} passes × {} frames identical)", kPasses, s_frames)); + return EXIT_SUCCESS; + } + + // Localize the first store-path divergence between the two WARM runs (B,C): + // these have no cold-cache asymmetry, so a memory diff there is genuine + // per-run nondeterminism. Re-run twice to that frame and report the region. + size_t warm_memdiff = SIZE_MAX; + for (size_t f = 0; f < runs[1].size() && f < runs[2].size(); ++f) + if (runs[1][f].memhash != runs[2][f].memhash) + { + warm_memdiff = f; + break; + } + if (warm_memdiff != SIZE_MAX) + { + Console.ErrorFmt("warm-run (B,C) memory first diverges at frame {} — localizing region:", warm_memdiff); + const auto m1 = RunToFrameCaptureMem((uint32_t)warm_memdiff); + const auto m2 = RunToFrameCaptureMem((uint32_t)warm_memdiff); + ReportMemDiff(m1, m2); + } + + Console.Error("SELFCHECK FAIL — see per-pair divergence above."); + Console.Error(" (B vs C identical but A differs => cold-cache/first-run artifact; warm up before the golden.)"); + Console.Error(" (all pairs differ => per-run host nondeterminism; hunt the wall-clock/thread source.)"); + return EXIT_FAILURE; +} + +// =========================================================================== +// --localize : the three-level divergence funnel. +// +// Level 1 (per-FRAME, whole run): run interp golden, then JIT, recording one +// (regfp, memhash) per frame. First differing frame F is the divergent +// frame. If only memhash differs there, it's a store-only divergence (a +// bad memory write that no register has read back yet) — reported at frame +// granularity; finer memory localization is intentionally out of scope (it +// is a slice-tracking tarpit). +// Level 2 (per-OP, frame F only): re-run interp (dense, one Sample/op) and +// JIT (sparse, one Sample per block entry) for frame F with the capture +// sites enabled. Align by walking the JIT block-entry stream against the +// dense interp op-stream — EE rec blocks are single basic blocks, so the +// next interp op with pc == jit[k].pc is unambiguously that block entry. +// First fp mismatch localizes the offending JIT block (the one that ran +// between the last matching entry and the mismatch). +// Level 3 (full register snapshot): re-run frame F with a 1-entry detail +// window at the divergent index on each side, and diff the full +// cpuRegisters/fpuRegisters to name the exact divergent field(s). +// =========================================================================== + +// Switch the EE core between interpreter (jit=false) and recompiler (jit=true). +// Consumed by the next VMManager::Execute() (cpu-impl-changed -> cache clear). +static void SetEeMode(bool jit) +{ + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableEE", jit); + VMManager::ApplySettings(); +} + +// --vu0diff axis: pin the EE INTERPRETER (deterministic, identical in both +// passes) and toggle only the VU0 micro engine. The two passes then run EE-interp +// in lockstep until mVU0-jit first hands the EE a different result than the VU0 +// interpreter — isolating a VU0-jit-vs-interp value bug with no EE-rec or +// cross-arch noise. +static void SetVu0Mode(bool vu0_jit) +{ + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableEE", false); + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableVU0", vu0_jit); + VMManager::ApplySettings(); +} + +// Advance the (already savestate-loaded) VM by `count` frames, discarding output. +static void AdvanceFrames(uint32_t count) +{ + for (uint32_t f = 0; f < count && VMManager::GetState() != VMState::Shutdown; ++f) + { + VMManager::FrameAdvance(1); + VMManager::Execute(); + } +} + +// Checkpoint-anchored fine pass: run exactly ONE frame from the CURRENT VM state +// (a freshly-loaded checkpoint) with the dense/sparse capture sites enabled. No +// AdvanceFrames — the caller positioned the VM, so there is no cross-pass drift. +static std::vector RunFineFpFromHere() +{ + ee_divtrace::Reset(); + ee_divtrace::ReserveStream(16u * 1024u * 1024u); + ee_divtrace::ConfigureFullWindow(0, 0); // fingerprints only + ee_divtrace::g_enabled.store(true, std::memory_order_release); + VMManager::FrameAdvance(1); + VMManager::Execute(); + ee_divtrace::g_enabled.store(false, std::memory_order_release); + return ee_divtrace::g_stream; +} + +// Checkpoint-anchored detail pass: one frame from the current VM state with a +// 1-entry full-snapshot window at stream index `idx`. +static ee_divtrace::FullSnap RunFineSnapAtFromHere(uint32_t idx) +{ + ee_divtrace::Reset(); + ee_divtrace::ReserveStream(16u * 1024u * 1024u); + ee_divtrace::ConfigureFullWindow(idx, 1); + ee_divtrace::g_enabled.store(true, std::memory_order_release); + VMManager::FrameAdvance(1); + VMManager::Execute(); + ee_divtrace::g_enabled.store(false, std::memory_order_release); + if (ee_divtrace::g_snaps.empty()) + return ee_divtrace::FullSnap{}; + return ee_divtrace::g_snaps.front(); +} + +struct AlignResult +{ + bool found = false; + bool control_flow = false; // true: JIT reached a block interp never did + uint32_t jit_idx = 0; // divergent JIT block-entry index + uint32_t interp_idx = 0; // matched interp op index + uint32_t pc = 0; // block entry where divergence was observed + uint32_t prev_pc = 0; // entry of the JIT block that produced it +}; + +// Walk the sparse JIT block-entry stream against the dense interp op-stream. +// +// Semantics: a JIT block-entry sample is (pc=block start, fp=state ABOUT TO +// execute that block). An interp sample is recorded AFTER each op with +// pc=cpuRegs.pc (== the NEXT op) and fp=state after the op == state about to +// execute that next pc. So an interp sample (pc=P, fp=S) means "about to execute +// P with state S" — directly comparable to a JIT block-entry (P, S). +// +// Exception: JIT block-entry #0 is the FRAME START (state == the shared +// checkpoint), and the interp stream has no pre-first-op sample for it (the first +// interp sample with pc==entry0 is one loop iteration later). Entry #0 is equal +// by construction, so we anchor on it without comparing, and begin the real +// divergence search at entry #1. +// Walk from a given resume point (start_k JIT entry, start_ii interp position, +// start_prev_pc the entry that precedes start_k). Returns the first divergence +// at or after start_k, or {found=false} if the streams agree to the end. +static AlignResult AlignFrom(const std::vector& interp, + const std::vector& jit, size_t start_k, size_t start_ii, uint32_t start_prev_pc) +{ + size_t ii = start_ii; + uint32_t prev_pc = start_prev_pc; + for (size_t k = start_k; k < jit.size(); ++k) + { + size_t scan = ii; + while (scan < interp.size() && interp[scan].pc != jit[k].pc) + ++scan; + if (scan >= interp.size()) + return {true, true, static_cast(k), static_cast(ii), jit[k].pc, prev_pc}; + if (interp[scan].fp != jit[k].fp) + return {true, false, static_cast(k), static_cast(scan), jit[k].pc, prev_pc}; + ii = scan + 1; + prev_pc = jit[k].pc; + } + return {}; // streams agree across every JIT block entry +} + +static AlignResult Align(const std::vector& interp, + const std::vector& jit) +{ + return AlignFrom(interp, jit, 1, 0, jit.empty() ? 0 : jit[0].pc); +} + +// After a data divergence at (div_k, div_ii) that the caller has classified +// benign (a cycle-derived timer read taints one or more GPRs), walk forward to +// where the two streams RE-CONVERGE — the first JIT block-entry whose pc matches +// the interp op-stream AND whose full fingerprint agrees again (the tainted +// value has been overwritten / washed out). From there a strict walk is sound. +struct ResyncResult +{ + bool reconverged = false; + uint32_t k = 0; // resume start_k for AlignFrom + uint32_t ii = 0; // resume start_ii + uint32_t prev_pc = 0; // resume start_prev_pc + uint32_t blind = 0; // JIT block-entries skipped while contaminated (a blind window) + bool ran_off = false; // pc-match failed mid-window (timing perturbed control flow) +}; + +static ResyncResult ResyncAfter(const std::vector& interp, + const std::vector& jit, uint32_t div_k, uint32_t div_ii) +{ + size_t ii = static_cast(div_ii) + 1; + uint32_t blind = 0; + for (size_t k = div_k + 1; k < jit.size(); ++k) + { + size_t scan = ii; + while (scan < interp.size() && interp[scan].pc != jit[k].pc) + ++scan; + if (scan >= interp.size()) + { + ResyncResult r; + r.ran_off = true; + r.blind = blind; + return r; // JIT reached a block interp never did within the window + } + if (interp[scan].fp == jit[k].fp) + { + // Full state matches again — resume strict align AFTER this entry. + ResyncResult r; + r.reconverged = true; + r.k = static_cast(k) + 1; + r.ii = static_cast(scan) + 1; + r.prev_pc = jit[k].pc; + r.blind = blind; + return r; + } + ii = scan + 1; + ++blind; + } + ResyncResult r; // walked to end-of-frame still divergent (taint never washed out) + r.blind = blind; + return r; +} + +// True if any non-GPR architectural field differs (HI/LO, CP0 except the +// dispatcher counters, FPR, ACC, sa) — i.e. the divergence is more than just +// tainted GPRs, so it can't be dismissed as a pure timer-read artifact. +static bool NonGprDiffers(const ee_divtrace::FullSnap& jit, const ee_divtrace::FullSnap& interp) +{ + if (jit.cpu.HI.UD[0] != interp.cpu.HI.UD[0] || jit.cpu.HI.UD[1] != interp.cpu.HI.UD[1] || + jit.cpu.LO.UD[0] != interp.cpu.LO.UD[0] || jit.cpu.LO.UD[1] != interp.cpu.LO.UD[1]) + return true; + for (int i = 0; i < 32; ++i) + { + if (i == 1 || i == 9 || i == 11 || i == 13 || i == 14) // +Cause/EPC: interrupt-phase noise + continue; + if (jit.cpu.CP0.r[i] != interp.cpu.CP0.r[i]) + return true; + } + if (!ee_divtrace::g_fp_exclude) + { + for (int i = 0; i < 32; ++i) + if (jit.fpu.fpr[i].UL != interp.fpu.fpr[i].UL) + return true; + if (jit.fpu.ACC.UL != interp.fpu.ACC.UL) + return true; + } + if (jit.cpu.sa != interp.cpu.sa) + return true; + return false; +} + +// Field-level diff of two full snapshots, mirroring DiffEe's ignored set +// (CP0 Random/Count/Compare, FPU control regs, cycle bookkeeping). +static std::vector DiffFullSnaps(const ee_divtrace::FullSnap& jit, + const ee_divtrace::FullSnap& interp) +{ + std::vector out; + static const char* const gpr_names[32] = { + "zero", "at", "v0", "v1", "a0", "a1", "a2", "a3", + "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", + "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", + "t8", "t9", "k0", "k1", "gp", "sp", "s8", "ra"}; + auto d64 = [&](const std::string& n, u64 a, u64 b) { + if (a != b) + out.push_back(fmt::format("{}: JIT={:#018x} INTERP={:#018x}", n, a, b)); + }; + auto d32 = [&](const std::string& n, u32 a, u32 b) { + if (a != b) + out.push_back(fmt::format("{}: JIT={:#010x} INTERP={:#010x}", n, a, b)); + }; + for (int i = 0; i < 32; ++i) + { + d64(std::string(gpr_names[i]) + ".lo", jit.cpu.GPR.r[i].UD[0], interp.cpu.GPR.r[i].UD[0]); + d64(std::string(gpr_names[i]) + ".hi", jit.cpu.GPR.r[i].UD[1], interp.cpu.GPR.r[i].UD[1]); + } + d64("hi.lo", jit.cpu.HI.UD[0], interp.cpu.HI.UD[0]); + d64("hi.hi", jit.cpu.HI.UD[1], interp.cpu.HI.UD[1]); + d64("lo.lo", jit.cpu.LO.UD[0], interp.cpu.LO.UD[0]); + d64("lo.hi", jit.cpu.LO.UD[1], interp.cpu.LO.UD[1]); + for (int i = 0; i < 32; ++i) + { + if (i == 1 || i == 9 || i == 11 || i == 13 || i == 14) // +Cause/EPC: interrupt-phase noise + continue; + d32(fmt::format("cp0[{}]", i), jit.cpu.CP0.r[i], interp.cpu.CP0.r[i]); + } + if (!ee_divtrace::g_fp_exclude) + { + for (int i = 0; i < 32; ++i) + d32(fmt::format("fpr[{}]", i), jit.fpu.fpr[i].UL, interp.fpu.fpr[i].UL); + d32("ACC", jit.fpu.ACC.UL, interp.fpu.ACC.UL); + } + d32("sa", jit.cpu.sa, interp.cpu.sa); + return out; +} + +// True for EE branch/jump primary opcodes (so we can stop disassembling a +// single basic block after its terminating branch + delay slot). +static bool IsEeBranchOpcode(u32 code) +{ + const u32 op = code >> 26; + if (op == 0) // SPECIAL — JR (8) / JALR (9) + { + const u32 fn = code & 0x3f; + return fn == 8 || fn == 9; + } + if (op == 1) // REGIMM — BLTZ/BGEZ/BLTZAL/... + return true; + if (op == 2 || op == 3) // J / JAL + return true; + if (op >= 4 && op <= 7) // BEQ/BNE/BLEZ/BGTZ + return true; + if (op >= 0x14 && op <= 0x17) // BEQL/BNEL/BLEZL/BGTZL + return true; + if (op == 0x10 || op == 0x11 || op == 0x12) // COP0/1/2 — may be BCxF/T + return ((code >> 21) & 0x1f) == 0x08; + return false; +} + +// Disassemble a single EE basic block starting at `pc` to the console (stops a +// couple of instructions past the first branch, or at maxInsns). Read on the +// CPU thread where guest memory is live. +static void DisasmBlock(u32 pc, u32 maxInsns = 48) +{ + Console.WriteLn(fmt::format(" --- block disasm @ {:#010x} ---", pc)); + bool saw_branch = false; + u32 after_branch = 0; + for (u32 i = 0; i < maxInsns; ++i) + { + const u32 addr = pc + i * 4; + const u32 code = memRead32(addr); + std::string line; + R5900::disR5900Fasm(line, code, addr, /*simplify=*/false); + Console.WriteLn(fmt::format(" {:#010x}: {:08x} {}", addr, code, line)); + if (saw_branch && ++after_branch >= 1) // include the delay slot, then stop + break; + if (IsEeBranchOpcode(code)) + saw_branch = true; + } +} + +// Recognize the cycle-derived-MMIO divergence class: a divergent GPR whose value +// originates — directly OR through arithmetic — from an EE timer COUNT register +// read inside the offending block. The EE timers (T0..T3 @ 0x1000_0000 / _0800 / +// _1000 / _1800, COUNT at +0) advance with cpuRegs.cycle, and the JIT vs interp +// differ by a few ticks at any mid-block read because they sync accumulated +// block-cycles at different granularity (per-block vs per-op). That is a TIMING +// artifact, not a codegen bug — a classic timing-taint trap — so we tag it rather +// than presenting it as a real bug. +// +// Two layers run over the block: +// * const-prop (lui/ori/addiu chains) resolves each load's effective address, +// so we can spot a load from a timer COUNT; +// * taint-propagation tracks a per-GPR "cycle-derived" bit: set on a COUNT +// load, propagated through pure-dataflow ALU ops (subu/addu/daddu/sll/and/…) +// whose source is tainted, cleared on a non-timer load / lui / unmodeled +// writer. This catches the common software-timer accumulator shape (read +// COUNT, subtract prior COUNT for a delta, daddu it into a 64-bit virtual +// clock) where the divergence flows into registers that were never the load +// destination. Under-tainting is the SAFE failure mode (the divergence is +// reported as real and a human looks); we only propagate through ops modeled +// as pure dataflow, so we never mark a real-bug value as benign. +// +// Returns a human description for each divergent GPR proven cycle-derived (and +// pushes its index into classified_out), or empty. +static std::vector ClassifyCycleDerivedLoads(u32 block_pc, + const std::vector& divergent_gprs, std::vector* classified_out = nullptr, + u32 maxInsns = 48) +{ + std::vector out; + if (divergent_gprs.empty()) + return out; + + u32 regval[32] = {0}; + bool known[32] = {false}; + bool tainted[32] = {false}; + std::string taint_src[32]; // origin description carried with the taint + known[0] = true; // $zero + + auto setTaint = [&](u32 d, bool t, const std::string& src) { + tainted[d] = t; + taint_src[d] = t ? src : std::string(); + }; + + bool saw_branch = false; + for (u32 i = 0; i < maxInsns; ++i) + { + const u32 addr = block_pc + i * 4; + const u32 code = memRead32(addr); + const u32 op = code >> 26; + const u32 rs = (code >> 21) & 0x1f; + const u32 rt = (code >> 16) & 0x1f; + const u32 rd = (code >> 11) & 0x1f; + const u32 fn = code & 0x3f; + const s32 simm = static_cast(code & 0xffff); + const u32 uimm = code & 0xffff; + + auto isTimerCount = [](u32 a) { + // COUNT register of any of the four EE timers (offset 0 within the + // 0x800-strided bank). Flag the whole COUNT word. + return (a == 0x10000000 || a == 0x10000800 || a == 0x10001000 || a == 0x10001800); + }; + + // Word/dword loads — a timer-COUNT load TAINTS rt; any other load gives rt + // a fresh untainted value. + if (op == 0x23 /*LW*/ || op == 0x27 /*LWU*/ || op == 0x37 /*LD*/ || op == 0x1e /*LQ*/) + { + if (known[rs] && isTimerCount(regval[rs] + static_cast(simm))) + { + const u32 ea = regval[rs] + static_cast(simm); + const int timer = (ea - 0x10000000) / 0x800; + setTaint(rt, true, fmt::format("EE Timer {} COUNT ({:#010x}) read at pc={:#010x}", timer, ea, addr)); + } + else + { + setTaint(rt, false, {}); + } + known[rt] = false; // loaded value is not a tracked const + } + else if (op == 0x10 /*COP0*/ && rs == 0x00 /*MFC0*/ && rd == 9 /*Count*/) + { + // mfc0 rt, $9 reads the COP0 cycle counter directly into rt. Like the + // EE-timer COUNT MMIO loads, its value differs between the JIT and interp + // by the per-block-vs-per-op cycle-sync granularity, so it taints rt. The + // shape here is a Count-based timeout busy-wait (mfc0 Count; dsll32/dsra32 + // sign-extend; sltu vs a deadline) - benign cycle phase, not a codegen bug. + setTaint(rt, true, fmt::format("COP0 Count (mfc0 $9) read at pc={:#010x}", addr)); + known[rt] = false; + } + else if (op == 0x0f /*LUI*/) + { + regval[rt] = uimm << 16; known[rt] = true; + setTaint(rt, false, {}); // immediate — untainted + } + else if (op == 0x0d /*ORI*/) + { + if (known[rs]) { regval[rt] = regval[rs] | uimm; known[rt] = true; } else known[rt] = false; + setTaint(rt, tainted[rs], taint_src[rs]); + } + else if (op == 0x09 /*ADDIU*/ || op == 0x19 /*DADDIU*/ || op == 0x08 /*ADDI*/ || op == 0x18 /*DADDI*/) + { + if (known[rs]) { regval[rt] = regval[rs] + static_cast(simm); known[rt] = true; } else known[rt] = false; + setTaint(rt, tainted[rs], taint_src[rs]); + } + else if (op == 0x0a /*SLTI*/ || op == 0x0b /*SLTIU*/ || op == 0x0c /*ANDI*/ || op == 0x0e /*XORI*/) + { + known[rt] = false; // not const-tracked, but taint flows from rs + setTaint(rt, tainted[rs], taint_src[rs]); + } + else if (op == 0x00 /*SPECIAL*/) + { + // R-type pure-dataflow ALU: dst rd, tainted iff any source operand is. + // Shift-immediate forms (sll/srl/sra/dsll*/dsrl*/dsra*) take only rt. + const bool isShiftImm = + (fn == 0x00 || fn == 0x02 || fn == 0x03 || + fn == 0x38 || fn == 0x3a || fn == 0x3b || + fn == 0x3c || fn == 0x3e || fn == 0x3f); + const bool isAlu = + (fn >= 0x20 && fn <= 0x2f) || // add/addu/sub/subu/and/or/xor/nor/slt/sltu/dadd..dsubu + isShiftImm || + (fn == 0x04 || fn == 0x06 || fn == 0x07) || // sllv/srlv/srav + (fn == 0x14 || fn == 0x16 || fn == 0x17); // dsllv/dsrlv/dsrav + if (isAlu) + { + const bool srcT = isShiftImm ? tainted[rt] : (tainted[rs] || tainted[rt]); + const std::string& src = tainted[rs] ? taint_src[rs] : taint_src[rt]; + setTaint(rd, srcT, src); + // Keep the existing OR const-prop; other R-ops invalidate rd's const. + if (fn == 0x25 /*OR*/ && known[rs] && known[rt]) { regval[rd] = regval[rs] | regval[rt]; known[rd] = true; } + else known[rd] = false; + } + else + { + // Unmodeled SPECIAL GPR writer (mfhi/mflo/movz/…): clear rd taint + // (under-taint = safe). jr/jalr/sync have rd=0, harmless. + setTaint(rd, false, {}); + } + } + // (Stores, branches, COP ops write no GPR we model — taint left intact. + // Any other GPR-writing op we don't recognize is an under-taint, which is + // the safe direction: the divergence is reported as real, not skipped.) + + if (saw_branch) + break; + if (IsEeBranchOpcode(code)) + saw_branch = true; + } + + // Classify each divergent GPR whose FINAL value is cycle-derived. + for (int dr : divergent_gprs) + { + if (tainted[dr]) + { + out.push_back(fmt::format( + "${} is cycle-derived from {} — JIT/interp cycle-sync granularity makes a few-tick delta " + "EXPECTED, almost certainly NOT a codegen bug.", + dr, taint_src[dr])); + if (classified_out) + classified_out->push_back(dr); + } + } + return out; +} + +// Returns the backward-branch target if the basic block at `block_pc` terminates +// in a PC-relative branch that loops back to at/near its own entry (a self-loop), +// else 0. Only PC-relative families (REGIMM, BEQ/BNE/BLEZ/BGTZ, their likely +// variants, COPx BCxF/T) encode a reachable backward target; J/JAL/JR/JALR are +// absolute and not treated as self-loops here. When found, `*branch_addr_out` +// receives the address of the terminating branch (so the caller can bound the +// loop body's pc range, delay slot included). +static u32 BlockBackwardBranchTarget(u32 block_pc, u32* branch_addr_out = nullptr, u32 maxInsns = 64) +{ + for (u32 i = 0; i < maxInsns; ++i) + { + const u32 addr = block_pc + i * 4; + const u32 code = memRead32(addr); + if (!IsEeBranchOpcode(code)) + continue; + const u32 op = code >> 26; + const bool pcrel = (op == 1) || (op >= 4 && op <= 7) || (op >= 0x14 && op <= 0x17) || + ((op == 0x10 || op == 0x11 || op == 0x12) && ((code >> 21) & 0x1f) == 0x08); + if (!pcrel) + return 0; // first terminating branch is absolute — not a self-loop + const s32 off = static_cast(code & 0xffff); + const u32 target = addr + 4 + (static_cast(off) << 2); + // Self-loop: backward branch whose target lands in this block's head + // region (at the entry or a few words before/within it). + if (target <= addr && target + 8 >= block_pc) + { + if (branch_addr_out) + *branch_addr_out = addr; + return target; + } + return 0; // first terminating branch is forward / out-of-block + } + return 0; +} + +// Resync past an ENTIRE self-loop run, not one iteration at a time. The JIT +// records one block-entry per loop iteration (all at the loop head pc), so we +// skip every consecutive JIT entry whose pc is in the loop body range +// [loop_lo, loop_hi] to land on the loop EXIT entry — the first JIT entry past +// the loop, carrying the loop's final architectural state. We then find that +// exact exit in the dense interp stream by (pc AND fingerprint) match. +// +// The fingerprint match (not a pc-range skip) is essential AND is the soundness +// check: a branch's delay slot is sampled by the interpreter with pc = branch+8, +// which aliases the loop's fall-through exit pc and recurs EVERY iteration — so a +// pc-only scan can't tell a mid-loop delay slot from the real exit. Only the true +// exit carries the loop's final state, so pc+fp pins it unambiguously. A pure +// sampling-phase artifact reaches an exit state identical to the JIT's; a real +// loop-body codegen bug changes the exit state or trip count, so the JIT exit +// fingerprint never appears in interp → reported as a real lead. Collapses what +// iteration-at-a-time resync would spend the whole benign-skip budget on into a +// single jump. +static ResyncResult ResyncPastSelfLoop(const std::vector& interp, + const std::vector& jit, uint32_t div_k, uint32_t div_ii, + u32 loop_lo, u32 loop_hi) +{ + auto inLoop = [&](u32 pc) { return pc >= loop_lo && pc <= loop_hi; }; + uint32_t blind = 0; + size_t k = div_k; + while (k < jit.size() && inLoop(jit[k].pc)) { ++k; ++blind; } + if (k >= jit.size()) + { + ResyncResult r; // loop never exited within the frame + r.blind = blind; + return r; + } + const u32 exit_pc = jit[k].pc; + const u64 exit_fp = jit[k].fp; + size_t ii = div_ii; + while (ii < interp.size() && !(interp[ii].pc == exit_pc && interp[ii].fp == exit_fp)) + ++ii; + if (ii >= interp.size()) + { + ResyncResult r; // JIT's loop-exit state never appears in interp — real lead + r.blind = blind; + return r; + } + ResyncResult r; + r.reconverged = true; + r.k = static_cast(k) + 1; + r.ii = static_cast(ii) + 1; + r.prev_pc = exit_pc; + r.blind = blind; + return r; +} + +// Checkpoint-anchored zoom: given a checkpoint file holding the state at the +// START of the divergent frame, localize the offending JIT block and the exact +// divergent register field(s). interp (dense per-op) and JIT (sparse per-block) +// both run ONE frame from the SAME checkpoint, so the alignment is clean — no +// cross-pass drift. Prints the block disasm + field diff + fixture next-step. +// Returns true if the walk found something worth STOPPING for (a real codegen +// divergence, a control-flow split, or an inconclusive lead), false if every +// divergence this frame was a benign cycle-derived timer artifact the walk could +// resync past — in which case the caller should keep scanning later frames. +static bool ZoomFromCheckpoint(const std::string& ckpt) +{ + Error error; + auto reload = [&](bool jit) -> bool { + if (!VMManager::LoadState(ckpt.c_str(), &error)) + { + Console.ErrorFmt("zoom: load checkpoint failed: {}", error.GetDescription()); + return false; + } + SetEeMode(jit); + return true; + }; + + Console.WriteLn("STEPDIFF zoom — dense interp pass (one frame from checkpoint)..."); + if (!reload(false)) + return true; + const auto interp_fine = RunFineFpFromHere(); + + Console.WriteLn("STEPDIFF zoom — sparse JIT pass (one frame from checkpoint)..."); + if (!reload(true)) + return true; + const auto jit_fine = RunFineFpFromHere(); + + Console.WriteLn(fmt::format("STEPDIFF zoom — interp {} ops, JIT {} block entries.", + interp_fine.size(), jit_fine.size())); + + // Iteratively localize. Find the first divergence and classify it: a data + // divergence whose ONLY differing fields are GPRs loaded from EE timer COUNT + // registers is the cycle-sync timing artifact — tag it, resync past where the + // tainted value washes out, and keep hunting. Anything else is a real lead and + // stops the walk. A cap bounds the per-skip re-run cost. + size_t k = 1, ii = 0; + uint32_t prev_pc = jit_fine.empty() ? 0 : jit_fine[0].pc; + // Two skip budgets. Timer skips each take TWO full-frame re-runs to snapshot + // the divergent registers, so they are capped tightly. Self-loop phase skips + // are pure fingerprint-stream walks (no re-run), so they get a far larger cap + // — a single frame can legitimately contain dozens of short phase-misaligned + // string/scan loops, and stopping at 32 would falsely report the 33rd. + int timer_skipped = 0; + int selfloop_skipped = 0; + int interrupt_skipped = 0; + const int kTimerCap = 32; + const int kSelfLoopCap = 4096; + const int kInterruptCap = 4096; // stream-only resync, like self-loops + + while (true) + { + const int benign_skipped = timer_skipped + selfloop_skipped + interrupt_skipped; + const AlignResult ar = AlignFrom(interp_fine, jit_fine, k, ii, prev_pc); + if (!ar.found) + { + if (benign_skipped > 0) + { + Console.WriteLn(fmt::format( + "STEPDIFF zoom: no CODEGEN divergence this frame — walked past {} benign divergence(s) " + "({} cycle-derived timer, {} self-loop phase, {} interrupt-handler phase); the streams " + "otherwise agree to end-of-frame.", + benign_skipped, timer_skipped, selfloop_skipped, interrupt_skipped)); + return false; // benign — caller keeps scanning + } + Console.WriteLn("STEPDIFF zoom: registers diverged at frame granularity but per-op alignment found " + "no block-entry mismatch. The divergence likely lands on state written after the " + "last block boundary (event-test / COP path) — inspect the frame-boundary diff."); + return true; // inconclusive lead — stop for a human look + } + + if (ar.control_flow) + { + // control_flow means jit[k].pc was not found in interp's op-stream + // AFTER the alignment point. Disambiguate a genuine wrong-target branch + // from a benign spin-wait PHASE offset: if that pc appears ANYWHERE in + // interp's stream this frame, the interpreter DID execute it (just at a + // different iteration of a producer/consumer poll loop — e.g. the GIF + // double-buffer wait at 0x1f24e0) and the JIT is merely further ahead at + // the vsync cutoff. Benign — the end-of-frame MEMORY gate (caller) has + // already proven this frame's persisted state differs only by timer/phase + // noise, so a poll-loop iteration imbalance here carries no real signal. + // If the pc appears NOWHERE in interp's stream, the JIT branched to a + // block the interpreter never reached → a real control-flow codegen bug. + bool interp_reached = false; + for (const auto& s : interp_fine) + if (s.pc == ar.pc) { interp_reached = true; break; } + if (interp_reached) + { + Console.WriteLn(fmt::format( + "STEPDIFF zoom: spin-wait PHASE offset entering pc={:#010x} (offending block {:#010x}) — the " + "interpreter DID reach this pc elsewhere this frame; the JIT sits at a different poll-loop " + "iteration at the vsync cutoff (benign, JIT ran off the end of interp's stream). Continuing the hunt.", + ar.pc, ar.prev_pc)); + return false; // benign phase — caller keeps scanning later frames + } + Console.WriteLn(fmt::format( + "STEPDIFF zoom: CONTROL-FLOW divergence — JIT dispatched to block pc={:#010x} the interpreter " + "NEVER reached this frame. Offending JIT block: pc={:#010x} (terminating branch went to the wrong target).", + ar.pc, ar.prev_pc)); + if (ar.prev_pc) + DisasmBlock(ar.prev_pc); + return true; + } + + // Self-loop PHASE misalignment — checked FIRST and CHEAPLY (no snapshot). + // The offending block is a tight backward self-loop; the JIT folds the + // loop's first iteration into the preceding block, so its loop-head sample + // runs one iteration ahead of the interpreter's dense per-op samples. Both + // cores compute the SAME results — a sampling-phase artifact, not a codegen + // bug. ResyncPastSelfLoop works on the fingerprint streams ALONE: it skips + // the whole loop run and proves convergence at the loop EXIT (final state + // identical). A real loop-body bug changes the exit state or trip count and + // will NOT converge → it falls through to the snapshot + real report below. + // Because this needs no re-run, it gets the large self-loop cap, so a frame + // full of short scan loops doesn't exhaust the tight timer budget. + { + u32 loop_branch_addr = 0; + const u32 loop_target = BlockBackwardBranchTarget(ar.pc, &loop_branch_addr); + if (loop_target != 0 && selfloop_skipped < kSelfLoopCap) + { + const u32 loop_lo = loop_target; + const u32 loop_hi = loop_branch_addr + 4; // include the branch's delay slot + const ResyncResult rs = ResyncPastSelfLoop( + interp_fine, jit_fine, ar.jit_idx, ar.interp_idx, loop_lo, loop_hi); + if (rs.reconverged) + { + Console.WriteLn(fmt::format( + "STEPDIFF zoom: skipping self-loop phase misalignment entering pc={:#010x} (loop {:#010x}..{:#010x}, " + "branches back to {:#010x}). The JIT folds the loop's first iteration into the preceding block, so " + "its loop-head sample runs one iteration ahead of interp; skipped the whole {}-entry loop run and " + "converged at the exit. Continuing the hunt.", + ar.pc, loop_lo, loop_hi, loop_target, rs.blind)); + ++selfloop_skipped; + k = rs.k; + ii = rs.ii; + prev_pc = rs.prev_pc; + continue; + } + // Structural self-loop but the exit did NOT converge — not a mere + // phase offset. Fall through to the snapshot + real-divergence report. + } + } + + // EXCEPTION/INTERRUPT-HANDLER PHASE — checked CHEAPLY (stream-only, no + // snapshot) before the expensive data path. The divergence is entered inside + // the EE kernel exception handler (kseg0, pc >= 0x8000_0000): the JIT and + // interp took the same vblank/timer interrupt a few cycles apart in an idle + // poll loop, so EPC differs by an instruction and that value propagates into + // whatever scratch GPRs the handler touches (k0→t0→…). The handler dispatches + // on Cause (not EPC), saves+restores the FULL user register context, and + // ERETs to EPC — so on return to user code every user GPR is restored + // IDENTICAL and only EPC (excluded from the fingerprint) differs. ResyncAfter + // therefore reconverges the instant the handler returns. Gating on + // reconvergence is the safety net: a genuine kernel-codegen bug changes the + // post-return state and will NOT reconverge, falling through to the report. + if ((ar.pc >= 0x80000000u || ar.prev_pc >= 0x80000000u) && interrupt_skipped < kInterruptCap) + { + const ResyncResult rs = ResyncAfter(interp_fine, jit_fine, ar.jit_idx, ar.interp_idx); + if (rs.reconverged) + { + ++interrupt_skipped; + k = rs.k; + ii = rs.ii; + prev_pc = rs.prev_pc; + continue; + } + // Did not reconverge within the frame — not a benign handler phase. + // Fall through to the snapshot + real-divergence report. + } + + // DATA divergence — snapshot both streams at the entry (per-stream indices: + // interp dense vs JIT sparse) and classify. + if (!reload(false)) + return true; + const auto isnap = RunFineSnapAtFromHere(ar.interp_idx); + if (!reload(true)) + return true; + const auto jsnap = RunFineSnapAtFromHere(ar.jit_idx); + + std::vector divergent_gprs; + for (int i = 0; i < 32; ++i) + if (jsnap.cpu.GPR.r[i].UD[0] != isnap.cpu.GPR.r[i].UD[0] || + jsnap.cpu.GPR.r[i].UD[1] != isnap.cpu.GPR.r[i].UD[1]) + divergent_gprs.push_back(i); + + std::vector classified; + const auto timer_notes = ClassifyCycleDerivedLoads(ar.prev_pc, divergent_gprs, &classified); + + const bool fully_benign = + !divergent_gprs.empty() && + classified.size() == divergent_gprs.size() && + !NonGprDiffers(jsnap, isnap); + + if (fully_benign && timer_skipped < kTimerCap) + { + const ResyncResult rs = ResyncAfter(interp_fine, jit_fine, ar.jit_idx, ar.interp_idx); + if (rs.ran_off) + { + Console.WriteLn(fmt::format( + "STEPDIFF zoom: benign cycle-derived timer divergence entering pc={:#010x} ({}), but control " + "flow then perturbed within the blind window ({} entries) — the timer value propagated into a " + "branch. Can't cleanly look past it here; re-run from a later savestate.", + ar.pc, timer_notes.front(), rs.blind)); + if (ar.prev_pc) + DisasmBlock(ar.prev_pc); + return true; + } + if (!rs.reconverged) + { + Console.WriteLn(fmt::format( + "STEPDIFF zoom: benign cycle-derived timer divergence entering pc={:#010x} ({}); state never " + "re-converged before end-of-frame ({} entries stayed tainted). The timing difference persisted " + "— re-run from a later savestate to look past it.", + ar.pc, timer_notes.front(), rs.blind)); + return true; + } + Console.WriteLn(fmt::format( + "STEPDIFF zoom: skipping benign timer divergence entering pc={:#010x} (offending block {:#010x}; {}). " + "Resynced after a {}-entry blind window; continuing the hunt.", + ar.pc, ar.prev_pc, timer_notes.front(), rs.blind)); + ++timer_skipped; + k = rs.k; + ii = rs.ii; + prev_pc = rs.prev_pc; + continue; + } + + // REAL divergence (or benign-cap reached): full report + stop. + Console.WriteLn(fmt::format( + "STEPDIFF zoom: DATA divergence observed entering block pc={:#010x} (JIT block-entry #{}). " + "Offending JIT block: entry pc={:#010x} — its body produced register state differing from interp.", + ar.pc, ar.jit_idx, ar.prev_pc)); + if (ar.prev_pc) + DisasmBlock(ar.prev_pc); + + const auto diffs = DiffFullSnaps(jsnap, isnap); + if (diffs.empty()) + { + Console.WriteLn("STEPDIFF zoom: (detail re-run did not reproduce the field diff at the entry — the " + "divergence may be mid-block / in memory; the block disasm above is the lead.)"); + } + else + { + Console.WriteLn(fmt::format("STEPDIFF zoom: divergent register field(s) entering block pc={:#010x}:", ar.pc)); + for (const auto& d : diffs) + Console.WriteLn(fmt::format(" {}", d)); + if (!timer_notes.empty()) + { + Console.WriteLn("STEPDIFF zoom: SUSPECTED TIMING (some divergent GPRs are cycle-derived timer reads, " + "but NOT all divergent state is — see above):"); + for (const auto& n : timer_notes) + Console.WriteLn(fmt::format(" {}", n)); + } + } + if (timer_skipped >= kTimerCap) + Console.WriteLn(fmt::format( + "STEPDIFF zoom: NOTE — hit the timer-skip cap ({}); this report may itself be another timer " + "artifact. Re-run from a later savestate if so.", kTimerCap)); + else if (timer_skipped + selfloop_skipped > 0) + Console.WriteLn(fmt::format( + "STEPDIFF zoom: (walked past {} benign divergence(s) — {} timer, {} self-loop phase — before " + "reaching this one.)", timer_skipped + selfloop_skipped, timer_skipped, selfloop_skipped)); + Console.WriteLn(fmt::format( + "STEPDIFF zoom: offending block pc={:#010x} — capture a single-block EE fixture there to pin the opcode.", + ar.prev_pc)); + return true; + } +} + + +// =========================================================================== +// --stepdiff : checkpoint-anchored per-frame interp-vs-JIT comparison. +// +// The frame-boundary funnel (--localize) runs interp and JIT as two separate +// full passes and diffs them at frame boundaries. That conflates two things: +// real codegen divergence, and the ~10-cycle async sampling jitter at the +// frame-advance pause point (proven by --selfcheck: two warm interp runs +// already disagree at the boundary, yet committed memory re-captures identical). +// Accumulated over a pass, that jitter looks exactly like a JIT bug — a +// diagnostic tarpit. +// +// --stepdiff removes the accumulation: at each frame it CHECKPOINTS the VM +// (in-flight savestate), then runs ONE frame three times from that IDENTICAL +// state — interp twice (a determinism control) and JIT once. From a shared +// checkpoint: +// * interp-vs-interp divergence => async sampling jitter (this frame is noisy) +// * interp-vs-interp clean + interp-vs-JIT divergence => a REAL EE JIT bug +// The golden timeline is advanced one interp frame between checkpoints, so the +// scan walks the whole run while every comparison starts from a clean state. +// =========================================================================== +// --contmem : continuous-trajectory memory diff. The checkpoint-anchored +// --stepdiff re-anchors to the golden interp state every frame, so it can NEVER +// reproduce an ACCUMULATION bug (drift that builds over ~1s of CONTINUOUS JIT — +// the Burnout 3 physics-explosion shape); it also fights LoadState pause-point +// jitter. This instead runs interp CONTINUOUSLY for the whole window +// (deterministic, per --selfcheck) twice as a control + JIT CONTINUOUSLY once, +// then diffs the per-frame memory-hash trajectories. The first frame whose +// hashes differ WITH a clean interp control is where continuous JIT first +// deviates; on it the EE-RAM region is localized via a byte diff. Cross-arch: +// run the SAME invocation on x86 — if x86 also diverges early the divergence is +// shared benign timing, if x86 stays clean it's an arm64-specific EE-JIT bug. +// `--vu0-interp` forces VU0=interp in every pass so the only cross-pass +// difference stays the EE engine (isolates EE-COP1-FPU/integer from VU0/COP2). +static int RunContinuousMemTrajectory() +{ + Error error; + const bool force_vu0_interp = s_contmem_vu0_interp; + auto runPass = [&](bool jit, std::vector* cycles = nullptr) -> std::vector { + std::vector hashes; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("contmem: load failed: {}", error.GetDescription()); + return hashes; + } + SetEeMode(jit); + if (force_vu0_interp) + { + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableVU0", false); + VMManager::ApplySettings(); + } + hashes.reserve(s_frames); + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + VMManager::FrameAdvance(1); + VMManager::Execute(); + hashes.push_back(ee_divtrace::HashMemory()); + if (cycles) + cycles->push_back(static_cast(cpuRegs.cycle)); + } + return hashes; + }; + + // Cycle-drift trajectory: EE cpuRegs.cycle at each frame boundary, JIT vs interp. + // This is DETERMINISTIC (unlike the chaotic memory diff): if the JIT's per-block + // emitted cycle cost matches the interpreter's, the two cycle counts stay locked; + // a growing |jit.cycle - interp.cycle| is the EE-JIT cycle-accounting drift that + // shifts every cycle-clocked subsystem (DMA/VIF/timers) out of phase. Cross-arch: + // if arm64's per-frame drift >> x86's, arm64 EE block-cycle costs are the bug. + std::vector ic, jc; + Console.WriteLn("CONTMEM: interp pass 1 (continuous)..."); + const auto i1 = runPass(false, &ic); + Console.WriteLn("CONTMEM: interp pass 2 (continuous, determinism control)..."); + const auto i2 = runPass(false); + Console.WriteLn("CONTMEM: JIT pass (continuous)..."); + const auto j = runPass(true, &jc); + + // Cycle-drift report (deterministic; compare arm64's vs x86's numbers cross-arch). + { + const size_t cn = std::min(ic.size(), jc.size()); + int64_t maxabs = 0; + size_t maxf = 0; + for (size_t f = 0; f < cn; ++f) + { + const int64_t d = (int64_t)jc[f] - (int64_t)ic[f]; + if (std::llabs(d) > std::llabs(maxabs)) { maxabs = d; maxf = f; } + } + Console.WriteLn("CONTMEM CYCLE-DRIFT (EE cpuRegs.cycle, jit - interp, per frame):"); + for (size_t f = 0; f < cn; ++f) + { + const int64_t d = (int64_t)jc[f] - (int64_t)ic[f]; + if (f < 12 || f + 4 >= cn || std::llabs(d) == std::llabs(maxabs)) + Console.WriteLn(fmt::format(" frame {:3}: interp.cycle={} jit.cycle={} drift={:+d}", f, ic[f], jc[f], d)); + } + Console.WriteLn(fmt::format("CONTMEM CYCLE-DRIFT SUMMARY: max |drift| = {:+d} EE cycles at frame {} (of {} frames).", + maxabs, maxf, cn)); + } + + const size_t n = std::min({i1.size(), i2.size(), j.size()}); + int first_ctrl = -1, first_real = -1; + for (size_t f = 0; f < n; ++f) + { + const bool ctrl_div = i1[f] != i2[f]; + const bool jit_div = i1[f] != j[f]; + if (ctrl_div && first_ctrl < 0) + first_ctrl = (int)f; + if (jit_div && !ctrl_div && first_real < 0) + first_real = (int)f; + Console.WriteLn(fmt::format("CONTMEM frame {:3}: interp1={:#018x} interp2={:#018x} jit={:#018x} ctrl={} jit-vs-interp={}", + f, i1[f], i2[f], j[f], ctrl_div ? "DIFF" : "ok", jit_div ? "DIFF" : "ok")); + } + Console.WriteLn(fmt::format( + "CONTMEM SUMMARY: {} frames; interp determinism first breaks at frame {} ; " + "continuous JIT-vs-interp memory first diverges (with clean interp control) at frame {}.", + n, first_ctrl, first_real)); + // Capture EE main RAM + scratchpad after running `frame+1` continuous frames + // in the given mode (honors --vu0-interp), for an interp-vs-JIT byte-region diff. + auto capMem = [&](bool jit, uint32_t frame) -> std::vector { + std::vector out; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) return out; + SetEeMode(jit); + if (force_vu0_interp) + { + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableVU0", false); + VMManager::ApplySettings(); + } + AdvanceFrames(frame + 1); + out.resize(Ps2MemSize::MainRam + Ps2MemSize::Scratch); + std::memcpy(out.data(), eeMem->Main, Ps2MemSize::MainRam); + std::memcpy(out.data() + Ps2MemSize::MainRam, eeMem->Scratch, Ps2MemSize::Scratch); + return out; + }; + + if (first_real >= 0) + { + Console.WriteLn(fmt::format("CONTMEM: EE-RAM region @ FIRST clean-control JIT divergence (frame {}) — divergence onset:", first_real)); + ReportMemDiff(capMem(false, (uint32_t)first_real), capMem(true, (uint32_t)first_real)); + } + // Magnitude trajectory: diff again at the LAST frame. A BOUNDED drift stays a + // similar page/byte count to the onset frame (→ the divergence is benign timing + // phase, the explosion is elsewhere); an EXPLOSION balloons to thousands of + // pages of NaN/garbage physics (→ this divergence IS the corruption). Interp's + // own late-frame nondeterminism adds only a small page-count floor, far below an + // explosion's footprint. + if (n >= 2) + { + const uint32_t lastf = (uint32_t)n - 1; + Console.WriteLn(fmt::format("CONTMEM: interp-vs-interp CONTROL region @ LAST frame ({}) — nondeterminism noise floor:", lastf)); + ReportMemDiff(capMem(false, lastf), capMem(false, lastf)); + Console.WriteLn(fmt::format("CONTMEM: EE-RAM region @ LAST frame ({}) — JIT-vs-interp; subtract the control floor above:", lastf)); + ReportMemDiff(capMem(false, lastf), capMem(true, lastf)); + } + + // --memdump: write the raw EE main RAM + scratchpad of the interp and jit passes + // at the LAST frame, for a cross-machine JIT-vs-JIT diff. Interp is bit-identical + // cross-arch (deterministic IEEE) and EE cycles are locked, so diffing arm64's + // .jit.bin vs x86's .jit.bin isolates the arm64-specific COMPUTATIONAL EE-JIT + // divergence directly. Use a SMALL --frames (e.g. 2-3) so chaotic amplification + // hasn't spread the seed yet. The .interp.bin pair should be byte-identical + // cross-arch (a sanity check on cross-arch determinism). + if (!s_memdump_prefix.empty() && n >= 1) + { + const uint32_t lastf = (uint32_t)n - 1; + auto dump = [&](bool jit, const char* tag) { + const std::vector m = capMem(jit, lastf); + const std::string path = fmt::format("{}.{}.bin", s_memdump_prefix, tag); + std::ofstream f(path, std::ios::binary | std::ios::trunc); + if (!f) { Console.ErrorFmt("CONTMEM: failed to open {}", path); return; } + f.write(reinterpret_cast(m.data()), static_cast(m.size())); + Console.WriteLn(fmt::format("CONTMEM: wrote {} ({} bytes, frame {}).", path, m.size(), lastf)); + }; + dump(false, "interp"); + dump(true, "jit"); + } + return EXIT_SUCCESS; +} + +// --speedhack-diff : speedhack-misfire differential. +// +// Speedhacks are silent, runtime-gated divergences: each one CLAIMS to skip only +// dead work (a spin loop, an intc_stat poll, a redundant flag update) and leave +// the EE/VU architectural result unchanged. When that claim is wrong — as the EE +// recompiler's WaitLoop timeout-loop skip was for Burnout 3's DMA-display-list +// build loop — the game corrupts state and hangs, with EE=interp clean and +// EE=jit broken. No existing state-diff tool catches this because they compare +// jit-vs-interp at ONE speedhack config; the bug lived in a config axis no test +// varied. +// +// This mode varies that axis. It runs the savestate forward in EE-jit throughout +// (the speedhacks are jit-gated), establishes a baseline with every +// transparency-class speedhack OFF (run twice, for the run-to-run determinism +// floor), then sweeps each speedhack on its own and all-on. A transparent +// speedhack must NOT change the per-frame EE-RAM hash before the baseline control +// floor breaks; the FIRST such clean-control divergence is a misfire, and +// ReportMemDiff at that frame localizes the corrupted region (e.g. the GIF DMA +// chain). No core instrumentation — reuses HashMemory/ReportMemDiff. This is the +// system-level positive-side coverage the unit tests can't give (a fired skip +// diverges from naive interp BY DESIGN; only equality-against-an-honest-baseline +// validates it). +// +// Excluded by construction: vuThread/MTVU (nondeterministic — thread races defeat +// an equality diff) and EECycleRate/EECycleSkip (deliberately lossy cycle scaling +// — they change results on purpose). Diffs EE main RAM + scratchpad (where +// DMA-chain / display-list corruption lands), the same surface as --contmem. +static int RunSpeedhackDiff() +{ + Error error; + + // The swept speedhacks. equalityClass = CLAIMS bit-exact equivalence (skips + // provably-dead work); a sustained divergence from those is off-spec. The + // others are deliberate timing approximations that legitimately change a few + // async-phase bytes — only a runaway EXPLOSION flags them. Bit i = knob i ON. + struct Knob { const char* name; const char* key; bool equalityClass; }; + static const Knob kKnobs[] = { + {"WaitLoop", "WaitLoop", true}, // EE timeout/idle-loop skip (recSkipTimeoutLoop) — the Burnout-3 culprit + {"IntcStat", "IntcStat", true}, // fast-forward through intc_stat poll waits (skip-to-event) + {"vuFlagHack", "vuFlagHack", true}, // microVU status/mac flag elision (redundant-write skip) + {"vu1Instant", "vu1Instant", false}, // run VU1 to completion instantly — lossy timing approximation + {"fastCDVD", "fastCDVD", false}, // shorten CDVD access latency — lossy timing approximation + }; + constexpr size_t kNumKnobs = std::size(kKnobs); + const uint32_t kAllOn = (1u << kNumKnobs) - 1u; + + auto maskLabel = [&](uint32_t mask) -> std::string { + if (mask == 0) + return "baseline(all-off)"; + if (mask == kAllOn) + return "all-on"; + std::string s; + for (size_t k = 0; k < kNumKnobs; ++k) + if ((mask >> k) & 1u) + s += (s.empty() ? "" : "+") + std::string(kKnobs[k].name); + return s; + }; + + auto applyConfig = [&](uint32_t mask) { + for (size_t k = 0; k < kNumKnobs; ++k) + s_settings_interface.SetBoolValue("EmuCore/Speedhacks", kKnobs[k].key, ((mask >> k) & 1u) != 0u); + // Force the excluded knobs to their neutral / off state so they never + // contaminate the differential. + s_settings_interface.SetBoolValue("EmuCore/Speedhacks", "vuThread", false); + s_settings_interface.SetIntValue("EmuCore/Speedhacks", "EECycleRate", 0); + s_settings_interface.SetIntValue("EmuCore/Speedhacks", "EECycleSkip", 0); + // Speedhacks bite the EE recompiler; run it (not interp) in every pass. + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableEE", true); + VMManager::ApplySettings(); + }; + + auto runPass = [&](uint32_t mask, std::vector* cyc = nullptr) -> std::vector { + std::vector hashes; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("speedhack-diff: load failed: {}", error.GetDescription()); + return hashes; + } + applyConfig(mask); + hashes.reserve(s_frames); + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + VMManager::FrameAdvance(1); + VMManager::Execute(); + hashes.push_back(ee_divtrace::HashMemory()); + if (cyc) + cyc->push_back(static_cast(cpuRegs.cycle)); + } + return hashes; + }; + + auto capMem = [&](uint32_t mask, uint32_t frame) -> std::vector { + std::vector out; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + return out; + applyConfig(mask); + AdvanceFrames(frame + 1); + out.resize(Ps2MemSize::MainRam + Ps2MemSize::Scratch); + std::memcpy(out.data(), eeMem->Main, Ps2MemSize::MainRam); + std::memcpy(out.data() + Ps2MemSize::MainRam, eeMem->Scratch, Ps2MemSize::Scratch); + return out; + }; + + // Baseline determinism sanity: run all-off twice and report how long the + // per-frame hash stays bit-identical. If this breaks immediately the + // savestate/renderer setup is nondeterministic and the verdicts below are + // unreliable (reduce --frames or check the setup). + Console.WriteLn("SPEEDHACK-DIFF: baseline determinism pass A (all speedhacks OFF)..."); + const auto baseA = runPass(0); + Console.WriteLn("SPEEDHACK-DIFF: baseline determinism pass B (all speedhacks OFF)..."); + const auto baseB = runPass(0); + if (baseA.empty() || baseB.empty()) + return EXIT_FAILURE; + { + const size_t bn = std::min(baseA.size(), baseB.size()); + int floorFrame = -1; + for (size_t f = 0; f < bn; ++f) + if (baseA[f] != baseB[f]) { floorFrame = static_cast(f); break; } + Console.WriteLn(fmt::format( + "SPEEDHACK-DIFF: baseline (all-off) stays run-to-run bit-identical through frame {} (of {}).", + floorFrame < 0 ? static_cast(bn) : floorFrame, bn)); + } + + // The verdict is NOT "any byte differs". Two distinct speedhack classes exist: + // - equality-class (WaitLoop/IntcStat/vuFlagHack): skip provably-dead work + // (a spin loop to its next event, a redundant flag) and CLAIM bit-exact + // equivalence. Any sustained divergence from all-off is suspect. + // - lossy-timing (vu1Instant/fastCDVD): deliberate approximations that DO + // change cycle timing (and therefore a few async-phase bytes) on purpose. + // Only a runaway EXPLOSION matters for these. + // The clean cross-class discriminator for the corruption/hang family (the + // Burnout-3 WaitLoop misfire) is GROWTH: corruption balloons across the + // window; a legit timing offset stays bounded. So we sample the EE-RAM + // divergence magnitude at several frames and compare its growth + absolute + // size against the determinism floor (two independent all-off runs). Same + // bounded-vs-explosion logic --contmem uses, swept over the speedhack axis. + std::vector samples; + { + const uint32_t N = s_frames; + auto add = [&](uint32_t f) { + if (f < N && std::find(samples.begin(), samples.end(), f) == samples.end()) + samples.push_back(f); + }; + add(N >= 8 ? N / 4 : 0); + add(N / 2); + add((3u * N) / 4u); + if (N >= 1) + add(N - 1); + std::sort(samples.begin(), samples.end()); + } + + // Reference (all-off run #1) and an independent all-off run #2 for the floor, + // captured once per sample frame and reused across every config. + std::vector> baseCaps, floorCaps; + std::vector floorTraj; + for (uint32_t sf : samples) + { + baseCaps.push_back(capMem(0, sf)); + floorCaps.push_back(capMem(0, sf)); + floorTraj.push_back(ReportMemDiff(baseCaps.back(), floorCaps.back(), /*verbose=*/false)); + } + { + std::string s; + for (size_t i = 0; i < samples.size(); ++i) + s += fmt::format("{}f={}p ", samples[i], floorTraj[i].pages); + Console.WriteLn("SPEEDHACK-DIFF: determinism floor trajectory (all-off vs all-off): " + s); + } + const MemDiffCount floorLast = floorTraj.empty() ? MemDiffCount{} : floorTraj.back(); + + // Is every speedhack in `mask` an equality-class one (and mask non-empty)? + auto pureEqualityClass = [&](uint32_t mask) -> bool { + if (mask == 0) + return false; + for (size_t k = 0; k < kNumKnobs; ++k) + if (((mask >> k) & 1u) && !kKnobs[k].equalityClass) + return false; + return true; + }; + + struct Verdict { uint32_t mask; std::vector traj; bool explosion; }; + std::vector verdicts; + + auto evalConfig = [&](uint32_t mask) { + std::vector traj; + for (size_t i = 0; i < samples.size(); ++i) + traj.push_back(ReportMemDiff(baseCaps[i], capMem(mask, samples[i]), /*verbose=*/false)); + const MemDiffCount first = traj.front(); + const MemDiffCount last = traj.back(); + + // EXPLOSION = the corruption/hang signature: a page spread well past the + // determinism floor that GREW across the window (a misfire's corruption + // onsets mid-run and runs away, e.g. buggy WaitLoop 14p->10p->10p->206p). + // This is class-agnostic and the ONLY automated verdict — every swept + // speedhack perturbs SOME bounded state by design (a correct WaitLoop/IntcStat + // leaves the abandoned spin counter + a few async-phase bytes; + // vu1Instant/fastCDVD shift timing), so "diverges at all" is not a bug; only + // runaway GROWTH is. + // + // Absolute magnitude alone cannot discriminate: chaos-amplifying 3D titles + // (e.g. GTA San Andreas) amplify a 110-byte sub-ULP seed (the unavoidable + // arm64 fmadd/-ffp-contract JIT-vs-interp difference) into a 576p intrinsic + // chaos floor with NO speedhack at all (see --contmem); a 303p WaitLoop + // divergence there is *below* that floor, same scattered-1-ULP-FP character, + // and is NOT a misfire. Magnitude (absolute OR relative-to-floor) can't + // separate that from a real misfire (Burnout-3 buggy 206p was only 1.2x its + // 166p fixed floor); the late-onset GROWTH shape can: + // GTA's flat-high 282->303 fails `grew`, Burnout's 14->206 passes it. + // Empirically validated: Burnout-3 buggy WaitLoop flagged, fixed not; + // GTA SA / R&C UYA WaitLoop+IntcStat correctly NOT flagged. + const size_t floorPad = floorLast.pages * 4 + 16; + const bool grew = last.pages >= first.pages * 4; + const bool explosion = last.pages >= 32 && last.pages > floorPad && grew; + + std::string trajStr; + for (size_t i = 0; i < samples.size(); ++i) + trajStr += fmt::format("{}f={}p/{}b ", samples[i], traj[i].pages, traj[i].bytes); + // Only an EQUALITY-class config (WaitLoop/IntcStat/vuFlagHack — claims + // bit-exactness) exploding is an actionable misfire. Lossy-class configs + // (vu1Instant/fastCDVD/all-on) are DESIGNED to diverge — fastCDVD shortens + // disc latency, so on a slot that's actively streaming it legitimately loads + // MBs of assets earlier (seen as a multi-hundred-page explosion). That is + // expected, not a bug, so it must not read as "investigate as misfire". + const bool isEquality = (mask != 0) && pureEqualityClass(mask); + const char* cls = (mask == 0) ? "-" : (isEquality ? "equality" : "lossy/mixed"); + const char* verdictStr = !explosion ? "bounded (expected)" + : (isEquality ? "EXPLOSION (likely misfire/corruption)" + : "EXPLOSION (expected — lossy timing class, not a misfire)"); + Console.WriteLn(fmt::format("SPEEDHACK-DIFF [{}] class={}: {}", maskLabel(mask), cls, verdictStr)); + Console.WriteLn(" trajectory (baseline-all-off vs config): " + trajStr + + fmt::format("(floor_last={}p)", floorLast.pages)); + if (explosion) + { + Console.WriteLn(fmt::format(" EE-RAM region @ frame {} — baseline(all-off) vs [{}]:", + samples.back(), maskLabel(mask))); + ReportMemDiff(baseCaps.back(), capMem(mask, samples.back()), /*verbose=*/true); + } + verdicts.push_back({mask, std::move(traj), explosion}); + }; + + // Sweep each speedhack on its own, then all-on (interaction check). + for (size_t k = 0; k < kNumKnobs; ++k) + evalConfig(1u << k); + evalConfig(kAllOn); + + // Summary. + Console.WriteLn(fmt::format("SPEEDHACK-DIFF SUMMARY (floor_last = {} pages / {} bytes @ frame {}):", + floorLast.pages, floorLast.bytes, samples.empty() ? 0u : samples.back())); + int misfires = 0, lossyExplosions = 0; + for (const auto& v : verdicts) + { + const bool isEquality = (v.mask != 0) && pureEqualityClass(v.mask); + const bool misfire = v.explosion && isEquality; + misfires += misfire ? 1 : 0; + lossyExplosions += (v.explosion && !isEquality) ? 1 : 0; + const MemDiffCount last = v.traj.empty() ? MemDiffCount{} : v.traj.back(); + const char* tag = !v.explosion ? "bounded" + : (misfire ? "EXPLOSION <-- likely misfire" : "EXPLOSION (expected lossy)"); + Console.WriteLn(fmt::format(" {:<28} last {:>6} pages / {:>8} bytes {}", + maskLabel(v.mask), last.pages, last.bytes, tag)); + } + Console.WriteLn(fmt::format( + "SPEEDHACK-DIFF: {} equality-class MISFIRE(s) — the actionable signal — plus {} expected lossy-class explosion(s).", + misfires, lossyExplosions)); + return EXIT_SUCCESS; +} + +static int RunStepDiff() +{ + Error error; + const std::string ckpt = Path::Combine(EmuFolders::Cache, "eerunner_stepdiff.p2s"); + + auto saveCkpt = [&]() -> bool { + bool ok = true; + VMManager::SaveState(ckpt.c_str(), /*zip_on_thread=*/false, /*backup_old_state=*/false, + [&](const std::string& e) { ok = false; Console.ErrorFmt("stepdiff: save failed: {}", e); }); + VMManager::WaitForSaveStateFlush(); + return ok; + }; + auto loadCkpt = [&]() -> bool { + if (!VMManager::LoadState(ckpt.c_str(), &error)) + { + Console.ErrorFmt("stepdiff: load checkpoint failed: {}", error.GetDescription()); + return false; + } + return true; + }; + // Run one frame in the given mode from the just-loaded checkpoint, returning + // the end-of-frame full snapshot + memory hash. + auto runOne = [&](bool jit, ee_divtrace::FullSnap& snap, uint64_t& memhash) -> bool { + if (!loadCkpt()) + return false; + SetEeMode(jit); + AdvanceFrames(1); + snap = CaptureFullSnap(); + memhash = ee_divtrace::HashMemory(); + return true; + }; + + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("stepdiff: initial load failed: {}", error.GetDescription()); + return EXIT_FAILURE; + } + SetEeMode(false); // golden timeline is interp + + int benign_frames = 0; + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + if (!saveCkpt()) + return EXIT_FAILURE; + + ee_divtrace::FullSnap i1, i2, j; + uint64_t i1m = 0, i2m = 0, jm = 0; + if (!runOne(false, i1, i1m) || !runOne(false, i2, i2m) || !runOne(true, j, jm)) + return EXIT_FAILURE; + + const auto ii = DiffFullSnaps(i1, i2); + const bool ii_clean = ii.empty() && i1m == i2m && i1.pc == i2.pc; + const auto ij = DiffFullSnaps(j, i1); // labels: JIT=j, INTERP=i1 + const bool ij_diverged = !ij.empty() || (i1m != jm) || (i1.pc != j.pc); + + if (ij_diverged) + { + Console.WriteLn(fmt::format( + "STEPDIFF frame {}: interp-vs-JIT DIVERGES (pc interp={:#010x} jit={:#010x}, mem {}); " + "interp-vs-interp control = {}", + f, i1.pc, j.pc, (i1m != jm) ? "DIFFERS" : "same", + ii_clean ? "CLEAN" : "ALSO DIVERGES (async jitter)")); + for (const auto& d : ij) + Console.WriteLn(fmt::format(" {}", d)); + if (ii_clean && i1m == jm) + { + // End-of-frame MEMORY identical — only live registers / pc differ. + // The EE is parked in a producer/consumer spin-wait (the GIF double- + // buffer poll at 0x1f24e0) and the two cores are sampled at different + // iteration counts at the vsync boundary. No architectural state + // PERSISTED differently, so this CANNOT be the divergence we hunt: a + // real EE-JIT computational bug stores its wrong value, which would + // show as a memory difference. Skip the zoom (it would only re-find + // the benign spin phase) and keep scanning. + Console.WriteLn(fmt::format( + " => frame {}: end-of-frame MEMORY identical (only live regs/pc differ) — benign spin-wait " + "phase at the vsync boundary; continuing scan.", f)); + ++benign_frames; + } + else if (ii_clean) + { + // Candidate real divergence (clean interp control, MEMORY differs). + // Zoom in INLINE to classify: the checkpoint still holds this frame's + // start (the zoom only LoadState-reads it). If the zoom resolves it + // all to benign cycle-derived timer reads / spin phase, keep scanning + // later frames; otherwise it's a real lead and we stop here. + Console.Error(" => candidate REAL EE JIT divergence (clean interp control, MEMORY differs) — zooming to classify..."); + const bool stop = ZoomFromCheckpoint(ckpt); + if (stop) + { + FileSystem::DeleteFilePath(ckpt.c_str()); + return EXIT_FAILURE; + } + ++benign_frames; + Console.WriteLn(fmt::format( + " => frame {}: the frame-boundary divergence resolved to benign timing only; continuing scan.", f)); + } + else + { + Console.WriteLn(" => discounted: interp control also diverges, so this is sampling jitter, " + "not a codegen bug."); + for (const auto& d : ii) + Console.WriteLn(fmt::format(" [ctrl] {}", d)); + } + } + + // Advance the golden interp timeline by one frame for the next checkpoint. + // (After an inline zoom the VM is wherever the last re-run left it; loadCkpt + // restores this frame's start, then AdvanceFrames steps to the next.) + if (!loadCkpt()) + return EXIT_FAILURE; + SetEeMode(false); + AdvanceFrames(1); + } + + FileSystem::DeleteFilePath(ckpt.c_str()); + if (benign_frames > 0) + Console.WriteLn(fmt::format( + "STEPDIFF: no real JIT divergence over {} frames ({} frame(s) had benign cycle-derived timer " + "divergences that the zoom walked past).", s_frames, benign_frames)); + else + Console.WriteLn(fmt::format("STEPDIFF: no real JIT divergence over {} frames (interp control clean throughout).", + s_frames)); + return EXIT_SUCCESS; +} + +// =================================================================== +// --vu0diff : live VU0-jit-vs-interp COP2-read value diff +// =================================================================== +// Pin the EE INTERPRETER in both passes and toggle ONLY the VU0 micro engine. +// Each pass loads the same checkpoint, runs one frame, and records every COP2 +// read (QMFC2 reads VF[fs], CFC2 reads VI[fs]) the EE interpreter performs of +// VU0 state, in execution order. Diffing the two read-streams pins the FIRST +// VU0 program output the micro JIT computes differently from the VU0 +// interpreter — live, with the real EE<->VU0 interleave the offline +// capture-replay harness can't reproduce. +// +// CAVEAT (read first): this is SINGLE-ARCH jit-vs-interp. It is the right tool +// for an arithmetic VALUE bug (the JIT computes a wrong number), but a +// divergence in the flag / Q / cycle pipeline INSTANCE is usually +// shared-with-x86 and NOT arch-specific — the FMAND-flag and cycle-bubble red +// herrings of the Burnout 3 hunt all collapsed this way. Confirm any lead from +// here with an arm64-jit-vs-x86-jit diff before trusting it. + +// Capture hooks live in pcsx2/VU0.cpp; null in production. +typedef void (*Cop2ReadHook)(u32 ee_pc, u32 op, u32 fs, const u32* lanes); +typedef void (*Cop2StateHook)(u32 tpc, u32 q, u32 mac, u32 status, u32 clip); +extern Cop2ReadHook g_cop2ReadHook; +extern Cop2StateHook g_cop2StateHook; + +namespace +{ +struct Cop2Read +{ + u32 ee_pc, op, fs; + u32 lanes[4]; + u32 tpc, q, mac, status, clip; +}; +std::vector s_cop2_sink; + +void Cop2ReadCapture(u32 ee_pc, u32 op, u32 fs, const u32* lanes) +{ + Cop2Read r{}; + r.ee_pc = ee_pc; + r.op = op; + r.fs = fs; + // QMFC2 (op 0) reads a full 128-bit VF; CFC2 (op 1) reads one 32-bit VI. + r.lanes[0] = lanes[0]; + r.lanes[1] = (op == 0) ? lanes[1] : 0; + r.lanes[2] = (op == 0) ? lanes[2] : 0; + r.lanes[3] = (op == 0) ? lanes[3] : 0; + s_cop2_sink.push_back(r); +} + +void Cop2StateCapture(u32 tpc, u32 q, u32 mac, u32 status, u32 clip) +{ + if (s_cop2_sink.empty()) + return; + Cop2Read& r = s_cop2_sink.back(); + r.tpc = tpc; + r.q = q; + r.mac = mac; + r.status = status; + r.clip = clip; +} +} // namespace + +// Diff two per-COP2-read VU0-value streams (a=interp golden, b=jit candidate) and +// report the FIRST read whose VU0 value differs. This pins the exact COP2 read — and +// thus the VU0 program output — where the micro JIT first diverges from the interp, +// upstream of the geometry-buffer corruption. VI flag regs read by CFC2 (16/17/18/ +// 22/23/26) are stopping-point/flag noise (shared block-overshoot); they're shown but +// the FIRST-QMFC2-divergence line is the actionable signal. +static void ReportCop2ReadDiff(const std::vector& a, const std::vector& b) +{ + auto asf = [](u32 u) { float f; std::memcpy(&f, &u, 4); return f; }; + auto is_flag_vi = [](u32 fs) { + return fs == 16 || fs == 17 || fs == 18 || fs == 22 || fs == 23 || fs == 26; + }; + const size_t n = std::min(a.size(), b.size()); + Console.WriteLn(fmt::format(" COP2-read streams: interp={} reads, jit={} reads", a.size(), b.size())); + int shown = 0; + bool first_qmfc2 = false; + for (size_t i = 0; i < n; ++i) + { + const Cop2Read& x = a[i]; + const Cop2Read& y = b[i]; + if (x.op != y.op || x.fs != y.fs || x.ee_pc != y.ee_pc) + { + Console.WriteLn(fmt::format( + " read #{}: STRUCTURAL divergence interp(pc={:#010x} op={} fs={}) jit(pc={:#010x} op={} fs={})", + i, x.ee_pc, x.op, x.fs, y.ee_pc, y.op, y.fs)); + break; // control flow split — everything after is unaligned + } + const bool diff = x.lanes[0] != y.lanes[0] || x.lanes[1] != y.lanes[1] || + x.lanes[2] != y.lanes[2] || x.lanes[3] != y.lanes[3]; + if (!diff) + continue; + const char* tag = (x.op == 0) ? "QMFC2 VF" : (is_flag_vi(x.fs) ? "CFC2 VI(flag)" : "CFC2 VI"); + if (x.op == 0) + { + if (!first_qmfc2) + { + Console.WriteLn(fmt::format( + " >>> FIRST QMFC2 VF divergence at read #{} (pc={:#010x} VF{:02}):", i, x.ee_pc, x.fs)); + first_qmfc2 = true; + } + Console.WriteLn(fmt::format( + " read #{} {} {:02} pc={:#010x}\n" + " interp {:08x}_{:08x}_{:08x}_{:08x} ({:g} {:g} {:g} {:g})\n" + " jit {:08x}_{:08x}_{:08x}_{:08x} ({:g} {:g} {:g} {:g})", + i, tag, x.fs, x.ee_pc, + x.lanes[0], x.lanes[1], x.lanes[2], x.lanes[3], asf(x.lanes[0]), asf(x.lanes[1]), asf(x.lanes[2]), asf(x.lanes[3]), + y.lanes[0], y.lanes[1], y.lanes[2], y.lanes[3], asf(y.lanes[0]), asf(y.lanes[1]), asf(y.lanes[2]), asf(y.lanes[3]))); + } + else + { + Console.WriteLn(fmt::format( + " read #{} {} {:02} pc={:#010x} interp={:08x} jit={:08x}", + i, tag, x.fs, x.ee_pc, x.lanes[0], y.lanes[0])); + } + if (++shown >= 40) + { + Console.WriteLn(" ... (40 divergent reads shown; truncating)"); + break; + } + } + if (a.size() != b.size()) + Console.WriteLn(fmt::format(" NOTE: read-count differs (interp={} jit={}) — VU0-jit drove a different COP2 path", + a.size(), b.size())); +} + +static int RunVu0Diff() +{ + Error error; + const std::string ckpt = Path::Combine(EmuFolders::Cache, "eerunner_vu0diff.p2s"); + + auto saveCkpt = [&]() -> bool { + bool ok = true; + VMManager::SaveState(ckpt.c_str(), /*zip_on_thread=*/false, /*backup_old_state=*/false, + [&](const std::string& e) { ok = false; Console.ErrorFmt("vu0diff: save failed: {}", e); }); + VMManager::WaitForSaveStateFlush(); + return ok; + }; + // One pass: load the checkpoint, set VU0 mode, capture the COP2 read-stream + // for exactly one frame from the just-loaded checkpoint. + auto runOne = [&](bool vu0_jit, std::vector& out) -> bool { + if (!VMManager::LoadState(ckpt.c_str(), &error)) + { + Console.ErrorFmt("vu0diff: load checkpoint failed: {}", error.GetDescription()); + return false; + } + SetVu0Mode(vu0_jit); + s_cop2_sink.clear(); + g_cop2ReadHook = &Cop2ReadCapture; + g_cop2StateHook = &Cop2StateCapture; + AdvanceFrames(1); + g_cop2ReadHook = nullptr; + g_cop2StateHook = nullptr; + out = s_cop2_sink; + return true; + }; + + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("vu0diff: initial load failed: {}", error.GetDescription()); + return EXIT_FAILURE; + } + SetVu0Mode(false); // golden timeline = VU0 interp + + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + if (!saveCkpt()) + return EXIT_FAILURE; + + std::vector interp, jit; + if (!runOne(false, interp) || !runOne(true, jit)) + return EXIT_FAILURE; + + // Did any QMFC2 VF value diverge? (The actionable signal; flag-VI noise + // is reported by ReportCop2ReadDiff but doesn't gate the per-frame header.) + bool qmfc2_diverged = false; + const size_t n = std::min(interp.size(), jit.size()); + for (size_t i = 0; i < n && !qmfc2_diverged; ++i) + { + const Cop2Read& x = interp[i]; + const Cop2Read& y = jit[i]; + if (x.op == 0 && (x.lanes[0] != y.lanes[0] || x.lanes[1] != y.lanes[1] || + x.lanes[2] != y.lanes[2] || x.lanes[3] != y.lanes[3])) + qmfc2_diverged = true; + } + + Console.WriteLn(fmt::format("VU0DIFF frame {}: {} ({} interp / {} jit COP2 reads)", f, + qmfc2_diverged ? "QMFC2 VF DIVERGES" : "no QMFC2 value divergence", interp.size(), jit.size())); + if (qmfc2_diverged || interp.size() != jit.size()) + ReportCop2ReadDiff(interp, jit); + + // Advance the golden interp timeline by one frame for the next checkpoint. + if (!VMManager::LoadState(ckpt.c_str(), &error)) + return EXIT_FAILURE; + SetVu0Mode(false); + AdvanceFrames(1); + } + + FileSystem::DeleteFilePath(ckpt.c_str()); + Console.WriteLn(fmt::format("VU0DIFF: scanned {} frame(s).", s_frames)); + return EXIT_SUCCESS; +} + +// --liverun: reproduce the in-game HANG headlessly. Unlike the deterministic diff +// modes, this enables the live subsystems (real GS so GIF is consumed, MTVU on) and +// runs a single straight EE-jit pass. If the EE wedges in a spin/sync loop (the +// frozen-frame + looping-audio symptom), VMManager::Execute() never returns for a +// frame; a watchdog thread notices the stalled frame counter, samples the live EE +// PC (cpuRegs.pc) to fingerprint the spin loop, prints a PC histogram, and exits +// with code 42. A clean completion (all frames, code 0) means we did NOT reproduce +// the hang in this configuration. +// --disasm: load the savestate and disassemble EE code in [EERUNNER_DIS_LO, +// EERUNNER_DIS_HI] (default 0x100000..0x100040) with the correct R5900 disassembler. +// Generic MIPS disassemblers garble R5900 COP2/MMI ops; this tool does not. +static int RunDisasm() +{ + Error error; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("disasm: load failed: {}", error.GetDescription()); + return EXIT_FAILURE; + } + u32 lo = 0x100000, hi = 0x100040; + if (const char* e = std::getenv("EERUNNER_DIS_LO")) lo = (u32)strtoul(e, nullptr, 0); + if (const char* e = std::getenv("EERUNNER_DIS_HI")) hi = (u32)strtoul(e, nullptr, 0); + Console.WriteLn(fmt::format("DISASM 0x{:08x}..0x{:08x}:", lo, hi)); + for (u32 a = lo; a <= hi; a += 4) + { + const u32 code = memRead32(a); + std::string line; + R5900::disR5900Fasm(line, code, a, false); + Console.WriteLn(fmt::format(" {:#010x}: {:08x} {}", a, code, line)); + } + return EXIT_SUCCESS; +} + +static std::atomic s_liverun_frame{0}; +static std::atomic s_liverun_done{false}; + +// Step-0 wedge classifier: snapshot the EE event/interrupt scheduler state. The +// recompiler and interpreter SHARE all of _cpuEventTest_Shared / intc / dmac / +// scheduling — the only jit-specific variables are (a) when a block re-enters the +// event test and (b) the cpuRegs.cycle value it has accumulated. So a "this IRQ +// never fires under jit" wedge is one of three modes, distinguishable here: +// A cycle FROZEN between snapshots -> spin block costs 0 cycles / RECCYCLE stuck +// B cycle advances, INTC/DMAC pending+unmasked but never serviced -> arm64 codegen (event-test/x25) +// C cycle advances, nothing pending -> IRQ never scheduled (IOP/SIF/DMA upstream) +// Racy reads of globals from the watchdog thread — fine for a stuck loop. +static void DumpEeEventState(int snap) +{ + const u64 cyc = cpuRegs.cycle; + const u64 nextE = cpuRegs.nextEventCycle; + const u64 lastE = cpuRegs.lastEventCycle; + const u32 ints = cpuRegs.interrupt; + const u32 stat = cpuRegs.CP0.n.Status.val; + const u32 intcS = psHu32(INTC_STAT); + const u32 intcM = psHu32(INTC_MASK); + const u16 dmacS = psHu16(0xe012); + const u16 dmacM = psHu16(0xe010); + Console.Error(fmt::format( + " [snap {}] cycle={} nextEvent={} (dEvt={}) lastEvent={} | interrupt=0x{:08x} branch={}", + snap, cyc, nextE, (s64)(nextE - cyc), lastE, ints, cpuRegs.branch)); + Console.Error(fmt::format( + " CP0.Status=0x{:08x} (EIE={} ERL={} EXL={} IE={} IM_INTC={} IM_DMAC={})", + stat, (stat >> 16) & 1, (stat >> 2) & 1, (stat >> 1) & 1, stat & 1, + (stat >> 10) & 1, (stat >> 11) & 1)); + Console.Error(fmt::format( + " INTC_STAT=0x{:08x} INTC_MASK=0x{:08x} (pending&unmasked=0x{:08x}) | DMAC_STAT=0x{:04x} DMAC_MASK=0x{:04x} (pend=0x{:04x})", + intcS, intcM, intcS & intcM, dmacS, dmacM, (u16)(dmacS & dmacM))); + // Per-source scheduled EE interrupts: which channels have a deadline, and has cycle passed it? + if (ints) + { + std::string sched; + for (int n = 0; n < 32; n++) + { + if (!(ints & (1u << n))) + continue; + const u64 deadline = cpuRegs.sCycle[n] + cpuRegs.eCycle[n]; + sched += fmt::format(" int[{}]: sCycle={} eCycle={} deadline={} ({}); ", + n, cpuRegs.sCycle[n], cpuRegs.eCycle[n], deadline, + (s64)(deadline - cyc) <= 0 ? "DUE" : "future"); + } + Console.Error(fmt::format(" scheduled:{}", sched)); + } +} + +static int RunLiveRun() +{ + Error error; + if (!VMManager::LoadState(s_savestate_path.c_str(), &error)) + { + Console.ErrorFmt("liverun: load failed: {}", error.GetDescription()); + return EXIT_FAILURE; + } + + Console.WriteLn(fmt::format( + "LIVERUN: EE=jit, MTVU=on, real GS — running up to {} frames (10s no-progress watchdog)...", + s_frames)); + + std::thread watchdog([]() { + uint32_t last = 0; + int stalled = 0; + while (!s_liverun_done.load(std::memory_order_relaxed)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if (s_liverun_done.load(std::memory_order_relaxed)) + return; + const uint32_t cur = s_liverun_frame.load(std::memory_order_relaxed); + if (cur != last) + { + last = cur; + stalled = 0; + continue; + } + if (++stalled < 20) // 20 * 500ms = ~10s of no frame progress + continue; + + // Wedged: the EE has not finished a frame in ~10s. Sample the live EE PC + // to fingerprint the spin/sync loop (racy read of a global u32 — fine for + // a stuck loop whose PC sits in a tiny range). + std::map hist; + u32 pcmin = ~0u, pcmax = 0; + const int N = 4000; + for (int i = 0; i < N; i++) + { + const u32 pc = cpuRegs.pc; + hist[pc]++; + pcmin = std::min(pcmin, pc); + pcmax = std::max(pcmax, pc); + std::this_thread::sleep_for(std::chrono::microseconds(250)); + } + Console.Error(fmt::format( + "LIVERUN WEDGE: no frame completed past frame {} for ~10s. " + "EE spin PC range [0x{:08x} .. 0x{:08x}], {} distinct PCs over {} samples:", + last, pcmin, pcmax, hist.size(), N)); + std::vector> top(hist.begin(), hist.end()); + std::sort(top.begin(), top.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + for (size_t i = 0; i < top.size() && i < 16; i++) + Console.Error(fmt::format(" pc=0x{:08x} {:5d} ({:4.1f}%)", + top[i].first, top[i].second, 100.0 * top[i].second / N)); + // Disassemble the dominant spin block so the poll/branch + the awaited + // memory operand are visible. The EE is stuck looping in this one block, + // so its code bytes are stable to read from here. + if (!top.empty()) + DisasmBlock(top[0].first); + + // Step-0 wedge classifier: two snapshots of the EE event/interrupt + // scheduler ~200ms apart. Whether cpuRegs.cycle MOVES between them, and + // whether an INTC/DMAC source is pending-but-unserviced, classifies the + // wedge into mode A (cycle frozen), B (codegen: pending never delivered), + // or C (never scheduled — upstream). See DumpEeEventState(). + Console.Error("LIVERUN WEDGE: EE event/interrupt scheduler state:"); + DumpEeEventState(0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + DumpEeEventState(1); + + std::fflush(stdout); + std::fflush(stderr); + std::_Exit(42); // distinct from EXIT_FAILURE; bypass GS teardown deliberately + } + }); + + // Soft-freeze probe: the frame-count watchdog can't see this hang, because the EE + // keeps ticking vblank so frames KEEP completing (frozen frames). Instead watch a + // guest EE-RAM word (EERUNNER_WATCH_ADDR) that should keep changing during normal + // play; if it stays constant for many frames WHILE the frame counter advances, the + // game is frozen even though the harness "completes" frames. For Burnout 3 the + // idle-loop consumer pointer is 0x4e2838 (waits to equal 0x4e283c == addr+4). + u32 watch_addr = 0; + if (const char* e = std::getenv("EERUNNER_WATCH_ADDR")) + watch_addr = static_cast(strtoul(e, nullptr, 0)); + auto eeR32 = [](u32 paddr) -> u32 { + return *reinterpret_cast(reinterpret_cast(eeMem->Main) + (paddr & 0x1ffffffu)); + }; + u32 watch_prev = 0; + bool watch_init = false; + int watch_stuck = 0; + + for (uint32_t f = 0; f < s_frames && VMManager::GetState() != VMState::Shutdown; ++f) + { + VMManager::FrameAdvance(1); + VMManager::Execute(); // blocks for one frame; if the EE wedges, never returns + s_liverun_frame.store(f + 1, std::memory_order_relaxed); + + if (watch_addr) + { + const u32 v = eeR32(watch_addr); + if (watch_init && v == watch_prev) + { + if (++watch_stuck == 120) // ~2s of frozen game while frames advanced + { + const u32 vb = eeR32(watch_addr + 4); + Console.Error(fmt::format( + "LIVERUN SOFT-FREEZE: watch[0x{:08x}]=0x{:08x} unchanged for {} frames " + "(frame counter reached {}); neighbor[+4]=0x{:08x}. Game frozen, vblank still ticking.", + watch_addr, v, watch_stuck, f + 1, vb)); + Console.Error("LIVERUN SOFT-FREEZE: EE event/interrupt scheduler state:"); + DumpEeEventState(0); + // Fingerprint the EE spin PCs (game idle loop). + std::map hist; + for (int i = 0; i < 2000; i++) + { + hist[cpuRegs.pc]++; + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } + std::vector> top(hist.begin(), hist.end()); + std::sort(top.begin(), top.end(), [](auto& a, auto& b) { return a.second > b.second; }); + for (size_t i = 0; i < top.size() && i < 8; i++) + Console.Error(fmt::format(" pc=0x{:08x} {:5d}", top[i].first, top[i].second)); + // Disasm a window around the hottest spin PC — the loop the game + // is actually stuck in — so this is game-agnostic. Override the + // range with EERUNNER_DIS_LO..EERUNNER_DIS_HI when chasing a + // divergence whose deciding branch sits outside the spin window. + { + const u32 center = top.empty() ? cpuRegs.pc : top[0].first; + u32 lo = center - 0x40, hi = center + 0x40; + if (const char* e = std::getenv("EERUNNER_DIS_LO")) lo = (u32)strtoul(e, nullptr, 0); + if (const char* e = std::getenv("EERUNNER_DIS_HI")) hi = (u32)strtoul(e, nullptr, 0); + Console.Error(fmt::format("LIVERUN SOFT-FREEZE: disasm 0x{:08x}..0x{:08x}:", lo, hi)); + for (u32 a = lo; a <= hi; a += 4) + { + const u32 code = memRead32(a); + std::string line; + R5900::disR5900Fasm(line, code, a, false); + Console.Error(fmt::format(" {:#010x}: {:08x} {}", a, code, line)); + } + } + std::fflush(stdout); + std::fflush(stderr); + std::_Exit(43); // distinct from 42 (frame-count wedge) + } + } + else + { + watch_stuck = 0; + } + watch_prev = v; + watch_init = true; + } + } + + s_liverun_done.store(true, std::memory_order_relaxed); + watchdog.join(); + Console.WriteLn(fmt::format( + "LIVERUN: completed {} frames with NO wedge — this config did not reproduce the hang.", + s_frames)); + return EXIT_SUCCESS; +} + +#ifdef _WIN32 +// Unicode filenames require wmain on Win32; use the ascii main() with this workaround. +#define main real_main +#endif + +static void CPUThreadMain(VMBootParameters* params, std::atomic* ret) +{ + ret->store(EXIT_FAILURE); + + if (VMManager::Internal::CPUThreadInitialize()) + { + // apply new settings (e.g. pick up renderer change) + VMManager::ApplySettings(); + + if (VMManager::Initialize(*params) == VMBootResult::StartupSuccess) + { + // Run unlimited — the runner steps frame-by-frame and needs no + // wall-clock pacing. + VMManager::SetLimiterMode(LimiterModeType::Unlimited); + VMManager::SetState(VMState::Paused); + + int code = EXIT_FAILURE; + switch (s_mode) + { + case RunMode::SelfCheck: + code = RunSelfCheck(); + break; + + // --localize / --repro / --stepdiff all run the robust + // checkpoint-anchored comparison. The frame-boundary pass + // conflates codegen bugs with the ~10-cycle async pause-point + // sampling jitter (see --selfcheck, which characterizes that + // jitter). --repro is the fast iteration verb (point it at a + // savestate already narrowed to the bug); + // --localize/--stepdiff are aliases. + case RunMode::Localize: + case RunMode::Repro: + case RunMode::StepDiff: + code = RunStepDiff(); + break; + + case RunMode::Vu0Diff: + code = RunVu0Diff(); + break; + + case RunMode::ContMem: + code = RunContinuousMemTrajectory(); + break; + + case RunMode::SpeedhackDiff: + code = RunSpeedhackDiff(); + break; + + case RunMode::LiveRun: + code = RunLiveRun(); + break; + + case RunMode::Disasm: + code = RunDisasm(); + break; + + default: + break; + } + + VMManager::Shutdown(false); + ret->store(code); + } + else + { + Console.Error("eerunner: VMManager::Initialize failed."); + } + } + + VMManager::Internal::CPUThreadShutdown(); +} + +int main(int argc, char* argv[]) +{ + CrashHandler::Install(); + EERunner::InitializeConsole(); + + std::signal(SIGINT, [](int) { VMManager::SetState(VMState::Stopping); }); + std::signal(SIGTERM, [](int) { VMManager::SetState(VMState::Stopping); }); + + if (!EERunner::InitializeConfig()) + { + Console.Error("Failed to initialize config."); + return EXIT_FAILURE; + } + + VMBootParameters params; + if (!EERunner::ParseCommandLineArgs(argc, argv, params)) + return EXIT_FAILURE; + + SysMemory::ReserveMemory(); + + // --selfcheck and --vu0diff force the EE interpreter. Must be set BEFORE + // VMManager::Initialize. (--vu0diff toggles only VU0; the EE stays interp in + // both passes so the only moving part is the VU0 micro engine.) + if (s_mode == RunMode::SelfCheck || s_mode == RunMode::Vu0Diff) + s_settings_interface.SetBoolValue("EmuCore/CPU/Recompiler", "EnableEE", false); + + // The checkpoint-anchored modes emit the per-block divtrace hook into every + // JIT block prologue (used by the zoom's sparse JIT stream). g_emit_block_hook + // is read at block-compile time, so it must be set before the recompiler is + // initialized; it stays true for the whole process (the interp passes simply + // never compile EE blocks, and the hook is a g_enabled-gated no-op otherwise). + // Production builds never set it, so they emit nothing. + if (s_mode == RunMode::Localize || s_mode == RunMode::Repro || s_mode == RunMode::StepDiff) + ee_divtrace::g_emit_block_hook = true; + + // Mirror the JIT's recSYSCALL FlushCache/iFlushCache skip in the golden interp + // passes so both timelines stay bit-identical across that ABI-benign divergence + // (otherwise the JIT skip vs interp-runs-handler shows up as a register + kernel- + // stack diff that masks real bugs downstream). Harmless for --selfcheck (both + // interp runs skip identically), so we set it for every mode the runner uses. + ee_divtrace::g_skip_flushcache_syscall = true; + + // EERUNNER_NOFP=1: drop the FPU register file + ACC from the alignment + // fingerprint and the diff helpers, so the zoom walks PAST FP-register + // divergences to hunt a non-FP (integer / control-flow) divergence. Use when the + // FP path is known benign (Burnout 3: hang persists with the EE-FPU fully + // converged to interp — the real bug is the integer cond_b/pointer math, and the + // pervasive 1-ULP div.s noise was masking it). Combine with EERUNNER_FPUFULL=1 to + // also converge mul/add and minimize FP laundered into GPRs via store/reload. + if (const char* e = std::getenv("EERUNNER_NOFP")) + ee_divtrace::g_fp_exclude = (e[0] != '0'); + + // Override settings that shouldn't be picked up from defaults or INIs. + EERunner::SettingsOverride(); + + std::atomic thread_ret; + std::thread cputhread(CPUThreadMain, ¶ms, &thread_ret); + cputhread.join(); + + return thread_ret.load(); +} + +#ifdef _WIN32 + +int wmain(int argc, wchar_t** argv) +{ + std::vector u8_args; + u8_args.reserve(static_cast(argc)); + for (int i = 0; i < argc; i++) + u8_args.push_back(StringUtil::WideStringToUTF8String(argv[i])); + + std::vector u8_argptrs; + u8_argptrs.reserve(u8_args.size()); + for (int i = 0; i < argc; i++) + u8_argptrs.push_back(u8_args[i].data()); + u8_argptrs.push_back(nullptr); + + return real_main(argc, u8_argptrs.data()); +} + +#endif // _WIN32 diff --git a/pcsx2-gsrunner/CMakeLists.txt b/pcsx2-gsrunner/CMakeLists.txt index 9c69e4cf94..63f31081cf 100644 --- a/pcsx2-gsrunner/CMakeLists.txt +++ b/pcsx2-gsrunner/CMakeLists.txt @@ -19,3 +19,39 @@ target_link_libraries(pcsx2-gsrunner PRIVATE PCSX2_FLAGS PCSX2 ) + +if(WAYLAND_API AND UNIX AND NOT APPLE) + find_program(WAYLAND_SCANNER_EXECUTABLE NAMES wayland-scanner REQUIRED) + + # pkg_get_variable() needs CMake 3.18; the project minimum is 3.16 + # (Ubuntu 20.04 LTS ships 3.16.3), so query pkg-config directly. + execute_process( + COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=pkgdatadir wayland-protocols + OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT WAYLAND_PROTOCOLS_DIR) + message(FATAL_ERROR + "wayland-protocols not found via pkg-config. " + "Install wayland-protocols-devel (Fedora) / libwayland-dev (Debian).") + endif() + + set(_xdg_xml "${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml") + set(_xdg_h "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-client-protocol.h") + set(_xdg_c "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-protocol.c") + + add_custom_command(OUTPUT ${_xdg_h} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${_xdg_xml} ${_xdg_h} + DEPENDS ${_xdg_xml} VERBATIM) + add_custom_command(OUTPUT ${_xdg_c} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${_xdg_xml} ${_xdg_c} + DEPENDS ${_xdg_xml} VERBATIM) + + target_sources(pcsx2-gsrunner PRIVATE ${_xdg_h} ${_xdg_c}) + target_include_directories(pcsx2-gsrunner PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + # Generated C file; skip the C++ PCH which #includes . + set_source_files_properties(${_xdg_c} PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + + pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client) + target_link_libraries(pcsx2-gsrunner PRIVATE ${WAYLAND_CLIENT_LIBRARIES}) + target_include_directories(pcsx2-gsrunner PRIVATE ${WAYLAND_CLIENT_INCLUDE_DIRS}) +endif() diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index 17bf1c398f..1952d8585c 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -49,7 +49,7 @@ #include "svnrev.h" // Down here because X11 has a lot of defines that can conflict -#if defined(__linux__) +#if defined(__linux__) && defined(X11_API) #include #include #include @@ -106,8 +106,10 @@ static u64 s_total_draws_rov = 0; static u64 s_total_barriers_rov = 0; static u32 s_total_frames = 0; static u32 s_total_drawn_frames = 0; +static std::vector s_extended_stats_snapshot; static bool s_perf_enable = false; +static bool s_force_vsync = false; static float s_perf_updates = 0.0f; static float s_perf_sum_fps = 0.0f; static float s_perf_sum_internal_fps = 0.0f; @@ -511,6 +513,14 @@ static void PrintCommandLineHelp(const char* progname) std::fprintf(stderr, " -logfile : Writes emu log to filename.\n"); std::fprintf(stderr, " -noshadercache: Disables the shader cache (useful for parallel runs).\n"); std::fprintf(stderr, " -perf: Enable frame timing performance stats.\n"); + std::fprintf(stderr, " -vsync: Force vsync on (FIFO present mode). Workaround for libmali Wayland WSI which " + "advertises MAILBOX support but errors VK_ERROR_INITIALIZATION_FAILED on swapchain create.\n"); + std::fprintf(stderr, " -no-fb-fetch: Disable Vulkan framebuffer fetch (VK_EXT_rasterization_order_attachment_access). " + "Use to A/B against drivers that mishandle subpass self-dependencies (e.g. libmali).\n"); + std::fprintf(stderr, " -no-vs-expand: Disable vertex-shader point/line/sprite expansion (storage-buffer path). " + "Falls back to hardware/geometry expansion.\n"); + std::fprintf(stderr, " -no-tex-barriers: Force OverrideTextureBarriers=0. Disables the texture-barrier render-pass pattern " + "and the framebuffer-fetch / depth-feedback paths that build on it.\n"); std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" " parameters make up the filename. Use when the filename contains\n" " spaces or starts with a dash.\n"); @@ -803,6 +813,30 @@ bool GSRunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& pa s_perf_enable = true; continue; } + else if (CHECK_ARG("-vsync")) + { + Console.WriteLn("Forcing vsync on (FIFO present mode). Use on libmali Wayland where MAILBOX errors VK_ERROR_INITIALIZATION_FAILED."); + s_force_vsync = true; + continue; + } + else if (CHECK_ARG("-no-fb-fetch")) + { + Console.WriteLn("Disabling framebuffer fetch (VK_EXT_rasterization_order_attachment_access)"); + s_settings_interface.SetBoolValue("EmuCore/GS", "DisableFramebufferFetch", true); + continue; + } + else if (CHECK_ARG("-no-vs-expand")) + { + Console.WriteLn("Disabling vertex-shader point/line/sprite expansion"); + s_settings_interface.SetBoolValue("EmuCore/GS", "DisableVertexShaderExpand", true); + continue; + } + else if (CHECK_ARG("-no-tex-barriers")) + { + Console.WriteLn("Forcing texture barriers off (OverrideTextureBarriers=0)"); + s_settings_interface.SetIntValue("EmuCore/GS", "OverrideTextureBarriers", 0); + continue; + } else if (CHECK_ARG("-debugdevice")) { Console.WriteLn("Enable debug device"); @@ -871,8 +905,12 @@ bool GSRunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& pa void GSRunner::SettingsOverride() { // complete as quickly as possible - s_settings_interface.SetBoolValue("EmuCore/GS", "FrameLimitEnable", false); - s_settings_interface.SetIntValue("EmuCore/GS", "VsyncEnable", false); + s_settings_interface.SetBoolValue("EmuCore/GS", "FrameLimitEnable", s_force_vsync); + s_settings_interface.SetIntValue("EmuCore/GS", "VsyncEnable", s_force_vsync); + // -vsync needs DisableMailboxPresentation too: GetEffectiveVSyncMode() returns + // Mailbox when VsyncEnable=true unless this is set. + if (s_force_vsync) + s_settings_interface.SetBoolValue("EmuCore/GS", "DisableMailboxPresentation", true); // Force screenshot quality settings to something more performant, overriding any defaults good for users. s_settings_interface.SetIntValue("EmuCore/GS", "ScreenshotFormat", static_cast(GSScreenshotFormat::PNG)); @@ -932,6 +970,8 @@ void GSRunner::DumpStats() Console.WriteLn(fmt::format("@HWSTAT@ Average GS Thread Time: {:.3f} ms", s_perf_sum_gs_thread_time / s_perf_updates)); Console.WriteLn(fmt::format("@HWSTAT@ Average GPU Time: {:.3f} ms", s_perf_sum_gpu_time / s_perf_updates)); } + for (const std::string& line : s_extended_stats_snapshot) + Console.WriteLn(fmt::format("@HWSTAT@ {}", line)); Console.WriteLn("============================================"); } @@ -955,6 +995,9 @@ static void CPUThreadMain(VMBootParameters* params, std::atomic* ret) // run until end GSDumpReplayer::SetLoopCount(s_loop_count); VMManager::SetState(VMState::Running); + // gsrunner is diagnostic-by-design; always collect extended stats so DumpStats has data. + if (g_gs_device) + g_gs_device->EnableExtendedStats(true); if (s_perf_enable) { VMManager::SetLimiterMode(LimiterModeType::Unlimited); @@ -962,6 +1005,9 @@ static void CPUThreadMain(VMBootParameters* params, std::atomic* ret) } while (VMManager::GetState() == VMState::Running) VMManager::Execute(); + // Snapshot backend-specific stats before the GS device is destroyed. + if (g_gs_device) + s_extended_stats_snapshot = g_gs_device->GetExtendedStats(); VMManager::Shutdown(false); GSRunner::DumpStats(); ret->store(EXIT_SUCCESS); @@ -972,11 +1018,22 @@ static void CPUThreadMain(VMBootParameters* params, std::atomic* ret) GSRunner::StopPlatformMessagePump(); } +// Set by the SIGINT/SIGTERM handlers (async-signal-safe: just an atomic store) +// and consumed on the CPU thread in PumpMessagesOnCPUThread(), which issues the +// actual VMManager::SetState(Stopping). Calling SetState() from signal context +// is not async-signal-safe — it can assert/log, take mutexes, and WaitGS/WaitVU. +static std::atomic s_signal_stop_requested{false}; + int main(int argc, char* argv[]) { CrashHandler::Install(); GSRunner::InitializeConsole(); + // Clean SIGINT/SIGTERM → VM stop, so DumpStats() still fires on ^C or SIGTERM during -loop 0. + // Defer the actual stop to the CPU thread (see s_signal_stop_requested). + std::signal(SIGINT, [](int) { s_signal_stop_requested.store(true); }); + std::signal(SIGTERM, [](int) { s_signal_stop_requested.store(true); }); + if (!GSRunner::InitializeConfig()) { Console.Error("Failed to initialize config."); @@ -1008,6 +1065,11 @@ int main(int argc, char* argv[]) void Host::PumpMessagesOnCPUThread() { + // Honor a pending ^C / SIGTERM here, on the CPU thread, where SetState() is + // safe to call. exchange() makes the transition fire exactly once. + if (s_signal_stop_requested.exchange(false)) + VMManager::SetState(VMState::Stopping); + // update GS thread copy of frame number MTGS::RunOnGSThread([frame_number = GSDumpReplayer::GetFrameNumber()]() { s_dump_frame_number = frame_number; }); MTGS::RunOnGSThread([loop_number = GSDumpReplayer::GetLoopCount()]() { s_loop_number = loop_number; }); @@ -1036,7 +1098,7 @@ std::string Host::TranslatePluralToString(const char* context, const char* msg, if (pos == std::string::npos) break; - ret.replace(pos, pos + 2, count_str.view()); + ret.replace(pos, 2, count_str.view()); } return ret; @@ -1215,7 +1277,206 @@ void GSRunner::StopPlatformMessagePump() CocoaTools::StopMainThreadEventLoop(); } -#elif defined(__linux__) +#elif defined(__linux__) && defined(WAYLAND_API) +// Wayland frontend for gsrunner. Used on handheld targets where the GPU's +// libmali variant is built for Wayland WSI (vkCreateWaylandSurfaceKHR) and +// VK_KHR_display is half-implemented (returns present_supported=false on the +// sole queue family). Runs as a normal Wayland client alongside the running +// compositor — no need to stop sway/weston. + +#include +#include "xdg-shell-client-protocol.h" +#include +#include + +static wl_display* s_display = nullptr; +static wl_registry* s_registry = nullptr; +static wl_compositor* s_compositor = nullptr; +static xdg_wm_base* s_wm_base = nullptr; +static wl_surface* s_surface = nullptr; +static xdg_surface* s_xdg_surface = nullptr; +static xdg_toplevel* s_xdg_toplevel = nullptr; +static WindowInfo s_wi; +static std::atomic s_shutdown_requested{false}; +static bool s_initial_configure_received = false; + +static void wl_wm_base_ping(void*, xdg_wm_base* wm_base, uint32_t serial) +{ + xdg_wm_base_pong(wm_base, serial); +} +static const xdg_wm_base_listener s_wm_base_listener = {wl_wm_base_ping}; + +static void wl_xdg_surface_configure(void*, xdg_surface* xs, uint32_t serial) +{ + xdg_surface_ack_configure(xs, serial); + s_initial_configure_received = true; +} +static const xdg_surface_listener s_xdg_surface_listener = {wl_xdg_surface_configure}; + +static void wl_xdg_toplevel_configure(void*, xdg_toplevel*, int32_t width, int32_t height, wl_array*) +{ + if (width > 0 && height > 0) + { + s_wi.surface_width = static_cast(width); + s_wi.surface_height = static_cast(height); + } +} +static void wl_xdg_toplevel_close(void*, xdg_toplevel*) +{ + s_shutdown_requested.store(true); +} +// Stubs for the newer xdg_toplevel_listener slots. These struct members exist +// only when the wayland-scanner-generated header was built against a new enough +// xdg-shell (configure_bounds: protocol v4 / wayland-protocols >= 1.20; +// wm_capabilities: v5 / >= 1.26). Guard both the stubs and their initializer +// slots on the matching SINCE_VERSION macros so the aggregate initializer always +// matches the generated struct's member count — without the guards this is a hard +// "too many initializers" build break on older protocol headers. +#ifdef XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION +static void wl_xdg_toplevel_configure_bounds(void*, xdg_toplevel*, int32_t, int32_t) {} +#endif +#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION +static void wl_xdg_toplevel_wm_capabilities(void*, xdg_toplevel*, wl_array*) {} +#endif +static const xdg_toplevel_listener s_xdg_toplevel_listener = { + wl_xdg_toplevel_configure, + wl_xdg_toplevel_close, +#ifdef XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION + wl_xdg_toplevel_configure_bounds, +#endif +#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION + wl_xdg_toplevel_wm_capabilities, +#endif +}; + +static void wl_registry_global(void*, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) +{ + if (std::strcmp(interface, wl_compositor_interface.name) == 0) + { + s_compositor = static_cast( + wl_registry_bind(registry, name, &wl_compositor_interface, std::min(version, 4u))); + } + else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) + { + s_wm_base = static_cast( + wl_registry_bind(registry, name, &xdg_wm_base_interface, std::min(version, 4u))); + xdg_wm_base_add_listener(s_wm_base, &s_wm_base_listener, nullptr); + } +} +static void wl_registry_global_remove(void*, wl_registry*, uint32_t) {} +static const wl_registry_listener s_registry_listener = {wl_registry_global, wl_registry_global_remove}; + +bool GSRunner::CreatePlatformWindow() +{ + pxAssertRel(!s_display && !s_surface, "Tried to create window when there already was one!"); + + s_display = wl_display_connect(nullptr); + if (!s_display) + { + Console.Error("wl_display_connect failed (check $WAYLAND_DISPLAY)"); + return false; + } + + s_registry = wl_display_get_registry(s_display); + wl_registry_add_listener(s_registry, &s_registry_listener, nullptr); + wl_display_roundtrip(s_display); + + if (!s_compositor || !s_wm_base) + { + Console.Error("Wayland compositor missing wl_compositor or xdg_wm_base"); + DestroyPlatformWindow(); + return false; + } + + s_surface = wl_compositor_create_surface(s_compositor); + s_xdg_surface = xdg_wm_base_get_xdg_surface(s_wm_base, s_surface); + xdg_surface_add_listener(s_xdg_surface, &s_xdg_surface_listener, nullptr); + s_xdg_toplevel = xdg_surface_get_toplevel(s_xdg_surface); + xdg_toplevel_add_listener(s_xdg_toplevel, &s_xdg_toplevel_listener, nullptr); + xdg_toplevel_set_title(s_xdg_toplevel, "PCSX2 GS Runner"); + xdg_toplevel_set_app_id(s_xdg_toplevel, "net.pcsx2.gsrunner"); + + wl_surface_commit(s_surface); + // Round-trip until the compositor acks our initial configure, so the + // Vulkan WSI sees a properly-sized surface from the first swapchain. + while (!s_initial_configure_received) + { + if (wl_display_dispatch(s_display) < 0) + { + Console.Error("wl_display_dispatch failed during initial configure"); + DestroyPlatformWindow(); + return false; + } + } + + s_wi.type = WindowInfo::Type::Wayland; + s_wi.display_connection = s_display; + s_wi.window_handle = s_surface; + if (s_wi.surface_width == 0) + s_wi.surface_width = WINDOW_WIDTH; + if (s_wi.surface_height == 0) + s_wi.surface_height = WINDOW_HEIGHT; + s_wi.surface_scale = 1.0f; + return true; +} + +void GSRunner::DestroyPlatformWindow() +{ + if (s_xdg_toplevel) { xdg_toplevel_destroy(s_xdg_toplevel); s_xdg_toplevel = nullptr; } + if (s_xdg_surface) { xdg_surface_destroy(s_xdg_surface); s_xdg_surface = nullptr; } + if (s_surface) { wl_surface_destroy(s_surface); s_surface = nullptr; } + if (s_wm_base) { xdg_wm_base_destroy(s_wm_base); s_wm_base = nullptr; } + if (s_compositor) { wl_compositor_destroy(s_compositor); s_compositor = nullptr; } + if (s_registry) { wl_registry_destroy(s_registry); s_registry = nullptr; } + if (s_display) { wl_display_disconnect(s_display); s_display = nullptr; } +} + +std::optional GSRunner::GetPlatformWindowInfo() +{ + WindowInfo wi; + if (s_display && s_surface) + wi = s_wi; + else + wi.type = WindowInfo::Type::Surfaceless; + return wi; +} + +void GSRunner::PumpPlatformMessages(bool forever) +{ + if (!s_display) + return; + + if (!forever) + { + wl_display_flush(s_display); + wl_display_dispatch_pending(s_display); + return; + } + + const int fd = wl_display_get_fd(s_display); + while (!s_shutdown_requested.load()) + { + wl_display_flush(s_display); + pollfd pfd = {fd, POLLIN, 0}; + const int p = poll(&pfd, 1, 16); // cap so we keep checking shutdown + if (p > 0 && (pfd.revents & POLLIN)) + { + if (wl_display_dispatch(s_display) < 0) + break; + } + else + { + wl_display_dispatch_pending(s_display); + } + } +} + +void GSRunner::StopPlatformMessagePump() +{ + s_shutdown_requested.store(true); +} + +#elif defined(__linux__) && defined(X11_API) static Display* s_display = nullptr; static Window s_window = None; static WindowInfo s_wi; @@ -1329,4 +1590,52 @@ void GSRunner::StopPlatformMessagePump() { s_shutdown_requested.store(true); } -#endif // _WIN32 / __APPLE__ + +#elif defined(__linux__) +// No X11/Wayland on this build (handheld kmsdrm target). Vulkan VK_KHR_display +// owns the screen; VulkanDirect is reported with the requested resolution and +// the GS device's display backend enumerates the monitor itself. Mirrors +// pcsx2-sdl/Main.cpp::BuildWindowInfo. +static std::atomic s_shutdown_requested{false}; + +bool GSRunner::CreatePlatformWindow() +{ + return true; +} + +void GSRunner::DestroyPlatformWindow() +{ +} + +std::optional GSRunner::GetPlatformWindowInfo() +{ + WindowInfo wi; + if (s_use_window.value_or(true)) + { + wi.type = WindowInfo::Type::VulkanDirect; + wi.surface_width = WINDOW_WIDTH; + wi.surface_height = WINDOW_HEIGHT; + wi.surface_scale = 1.0f; + } + else + { + wi.type = WindowInfo::Type::Surfaceless; + } + return wi; +} + +void GSRunner::PumpPlatformMessages(bool forever) +{ + if (!forever) + return; + + while (!s_shutdown_requested.load()) + std::this_thread::sleep_for(std::chrono::milliseconds(16)); +} + +void GSRunner::StopPlatformMessagePump() +{ + s_shutdown_requested.store(true); +} + +#endif // _WIN32 / __APPLE__ / __linux__ diff --git a/pcsx2-sdl/CMakeLists.txt b/pcsx2-sdl/CMakeLists.txt new file mode 100644 index 0000000000..1ddcf8406a --- /dev/null +++ b/pcsx2-sdl/CMakeLists.txt @@ -0,0 +1,35 @@ +add_executable(pcsx2-sdl) + +if (PACKAGE_MODE) + install(TARGETS pcsx2-sdl DESTINATION ${CMAKE_INSTALL_BINDIR}) +else() + install(TARGETS pcsx2-sdl DESTINATION ${CMAKE_SOURCE_DIR}/bin) +endif() + +target_sources(pcsx2-sdl PRIVATE + Main.cpp +) + +target_include_directories(pcsx2-sdl PRIVATE + "${CMAKE_BINARY_DIR}/common/include" + "${CMAKE_SOURCE_DIR}/pcsx2" +) + +target_link_libraries(pcsx2-sdl PRIVATE + PCSX2_FLAGS + PCSX2 + SDL3::SDL3 +) + +# Deterministic process layout for the persisted-JIT VU program cache: a +# non-PIE executable (ET_EXEC) loads at its fixed link address even with +# ASLR enabled, so every libpcsx2 symbol address is run-invariant — +# paired with the image-anchored fixed-base arena reservation in +# SysMemory::AllocateVirtualMemory. This is what lets cached VU code +# reload across boots on the handheld without repatching baked +# addresses. Linux-only; libpcsx2's -fPIC objects link into an ET_EXEC +# fine. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_target_properties(pcsx2-sdl PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_link_options(pcsx2-sdl PRIVATE -no-pie) +endif() diff --git a/pcsx2-sdl/Main.cpp b/pcsx2-sdl/Main.cpp new file mode 100644 index 0000000000..aabd816901 --- /dev/null +++ b/pcsx2-sdl/Main.cpp @@ -0,0 +1,936 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// pcsx2-sdl — SDL3 frontend for kmsdrm-only handhelds. +// +// Boots a game from a CLI ISO path, or wires PCSX2's existing FullscreenUI +// (ImGui) for in-game settings, game-picker, and configuration. With no ISO +// supplied (and the "UI"/"StartBigPictureMode" flag set, which is on by +// default for this frontend), comes up directly into the FullscreenUI +// game-picker. +// +// Display surface is acquired via Vulkan VK_KHR_display, so no Wayland/X11 +// compositor is needed. The Vulkan renderer enumerates monitors itself; this +// frontend only reports the requested resolution back through WindowInfo. +// +// Audio + input come from SDL3 via the existing SDLAudioStream / SDLInputSource +// modules in the core (already linked, already non-Qt). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "fmt/format.h" + +#include "common/Assertions.h" +#include "common/Console.h" +#include "common/CrashHandler.h" +#include "common/Error.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/ProgressCallback.h" +#include "common/StringUtil.h" +#include "common/Threading.h" + +#include "pcsx2/PrecompiledHeader.h" + +#include "pcsx2/Achievements.h" +#include "pcsx2/CDVD/CDVDcommon.h" +#include "pcsx2/GS.h" +#include "pcsx2/GameList.h" +#include "pcsx2/Host.h" +#include "pcsx2/INISettingsInterface.h" +#include "pcsx2/ImGui/FullscreenUI.h" +#include "pcsx2/ImGui/ImGuiFullscreen.h" +#include "pcsx2/ImGui/ImGuiManager.h" +#include "pcsx2/Input/InputManager.h" +#include "pcsx2/MTGS.h" +#include "pcsx2/PerformanceMetrics.h" +#include "pcsx2/SIO/Pad/Pad.h" +#include "pcsx2/VMManager.h" + +#include "svnrev.h" + +namespace Pcsx2SDL +{ + static bool InitializeConfig(); + static bool ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params); + static void InstallSignalHandler(); + static std::optional BuildWindowInfo(); + static void CPUThreadMain(VMBootParameters initial_params, bool start_in_fsui, std::atomic* ret); + static void DrainCPUThreadQueue(); + static void StopGameListRefreshThread(); +} // namespace Pcsx2SDL + +// Settings persistence (INI on disk). +static std::unique_ptr s_base_settings; +static std::unique_ptr s_secrets_settings; + +// Shutdown signal from SIGTERM/SIGINT or VM exit. +static std::atomic s_shutdown_requested{false}; + +// Display mode requested via --fullscreen-mode (0 = let the renderer pick the +// display's preferred mode). +static u32 s_requested_width = 0; +static u32 s_requested_height = 0; + +// Pending CPU-thread callbacks queued by Host::RunOnCPUThread (FullscreenUI +// uses this to schedule work back from the GS thread, e.g. "user picked an +// ISO from the game list, please VMManager::Initialize it on the CPU thread"). +// Drained by Host::PumpMessagesOnCPUThread which the VM polls every vsync; +// also drained on a 16ms tick by the idle loop while no VM is running. +static std::mutex s_cpu_queue_lock; +static std::deque> s_cpu_queue; +static std::condition_variable s_cpu_queue_cv; +// Set once the CPU thread is up; used to detect "RunOnCPUThread(block=true)" +// being called from the CPU thread itself (which would self-deadlock). +static std::atomic s_cpu_thread_id{}; + +// Background game-list scanner. FullscreenUI's GameList page calls +// Host::RefreshGameListAsync, which spawns a single worker thread to +// GameList::Refresh. The thread is joined (blocking) at shutdown and at +// restart, to avoid two scans racing. +static std::thread s_gamelist_thread; +static std::atomic s_gamelist_running{false}; + +////////////////////////////////////////////////////////////////////////// +// Settings + lifecycle +////////////////////////////////////////////////////////////////////////// + +bool Pcsx2SDL::InitializeConfig() +{ + EmuFolders::SetAppRoot(); + if (!EmuFolders::SetResourcesDirectory() || !EmuFolders::SetDataDirectory(nullptr)) + return false; + + CrashHandler::SetWriteDirectory(EmuFolders::DataRoot); + + const char* hw_check_error = nullptr; + if (!VMManager::PerformEarlyHardwareChecks(&hw_check_error)) + { + Console.ErrorFmt("Early hardware check failed: {}", hw_check_error ? hw_check_error : "unknown"); + return false; + } + + // Load Roboto for OSD / FullscreenUI (font is bundled in resources). + { + const std::string roboto_path = + EmuFolders::GetOverridableResourcePath("fonts" FS_OSPATH_SEPARATOR_STR "Roboto-Regular.ttf"); + const auto roboto_data = FileSystem::MapBinaryFileForRead(roboto_path.c_str()); + if (roboto_data.empty()) + { + Console.ErrorFmt("Failed to load font file '{}'.", roboto_path); + return false; + } + + std::vector fonts; + ImGuiManager::FontInfo fi{}; + fi.data = roboto_data; + fi.exclude_ranges = {}; + fi.face_name = nullptr; + fi.is_emoji_font = false; + fonts.push_back(fi); + ImGuiManager::SetFonts(std::move(fonts)); + } + + // Open / create the persistent INI at $XDG_CONFIG_HOME/PCSX2/PCSX2.ini. + const std::string ini_path = Path::Combine(EmuFolders::Settings, "PCSX2.ini"); + const bool ini_exists = FileSystem::FileExists(ini_path.c_str()); + Console.WriteLnFmt("Loading config from {}.", ini_path); + + s_base_settings = std::make_unique(ini_path); + Host::Internal::SetBaseSettingsLayer(s_base_settings.get()); + + if (!ini_exists || !s_base_settings->Load() || !VMManager::Internal::CheckSettingsVersion()) + { + Console.WriteLnFmt("Initialising fresh config at {}.", ini_path); + VMManager::SetDefaultSettings(*s_base_settings, true, true, true, true, true); + } + + // Secrets layer (achievements credentials etc). Created on first run; + // failure to create is non-fatal — just log and continue. + const std::string secrets_path = Path::Combine(EmuFolders::Settings, "secrets.ini"); + s_secrets_settings = std::make_unique(secrets_path); + Host::Internal::SetSecretsSettingsLayer(s_secrets_settings.get()); + if (FileSystem::FileExists(secrets_path.c_str())) + s_secrets_settings->Load(); + + // Apply handheld-frontend defaults. These override anything the INI + // might have for fields that don't make sense without a desktop: + // - Vulkan renderer (only one with VK_KHR_display direct-display path). + // - Fullscreen always (no window manager to support windowed mode). + // - SDL audio + SDL gamepad input (no Qt-coupled keyboard input layer). + { + auto lock = Host::GetSettingsLock(); + s_base_settings->SetBoolValue("InputSources", "SDL", true); + // Don't disable user-set values they may have customised — only fill + // in missing defaults for first-run. + if (!s_base_settings->ContainsValue("EmuCore/GS", "Renderer")) + s_base_settings->SetIntValue("EmuCore/GS", "Renderer", + static_cast(GSRendererType::VK)); + if (!s_base_settings->ContainsValue("SPU2/Output", "OutputModule")) + s_base_settings->SetStringValue("SPU2/Output", "OutputModule", "sdl"); + if (!s_base_settings->ContainsValue("EmuCore/GS", "FullscreenMode")) + s_base_settings->SetBoolValue("EmuCore/GS", "FullscreenMode", true); + } + + // Persist any first-run defaults so the user can hand-edit + // the INI between sessions. + Error save_error; + if (!s_base_settings->Save(&save_error)) + Console.ErrorFmt("Failed to save config: {}", save_error.GetDescription()); + + VMManager::Internal::LoadStartupSettings(); + return true; +} + +bool Pcsx2SDL::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params) +{ + for (int i = 1; i < argc; i++) + { +#define ARG(s) (!std::strcmp(argv[i], s)) +#define ARG_PARAM(s) (!std::strcmp(argv[i], s) && (i + 1) < argc) + + if (ARG("--help") || ARG("-h")) + { + std::fprintf(stderr, "PCSX2 SDL frontend %s\n", GIT_REV); + std::fprintf(stderr, "Usage: %s [options] \n\n", argv[0]); + std::fprintf(stderr, " --fullscreen-mode WxH Request a specific display mode\n"); + std::fprintf(stderr, " (default: monitor's preferred mode)\n"); + std::fprintf(stderr, " --bios-only Boot the BIOS without loading a disc\n"); + std::fprintf(stderr, " --state-from-file PATH Resume from a save state file\n"); + std::fprintf(stderr, " --no-fast-boot Skip fast boot (run full BIOS animation)\n"); + std::fprintf(stderr, " -h, --help Show this help and exit\n"); + std::fprintf(stderr, " --version Show version and exit\n"); + return false; + } + if (ARG("--version")) + { + std::fprintf(stderr, "PCSX2 SDL frontend %s\n", GIT_REV); + return false; + } + if (ARG_PARAM("--fullscreen-mode")) + { + const char* mode = argv[++i]; + const char* x_pos = std::strchr(mode, 'x'); + if (!x_pos) + { + Console.ErrorFmt("Invalid --fullscreen-mode '{}', expected WxH (e.g. 1280x720).", mode); + return false; + } + s_requested_width = StringUtil::FromChars(std::string_view(mode, x_pos - mode)).value_or(0); + s_requested_height = StringUtil::FromChars(x_pos + 1).value_or(0); + if (s_requested_width == 0 || s_requested_height == 0) + { + Console.ErrorFmt("Invalid --fullscreen-mode '{}'.", mode); + return false; + } + continue; + } + if (ARG("--bios-only")) + { + params.source_type = CDVD_SourceType::NoDisc; + continue; + } + if (ARG_PARAM("--state-from-file")) + { + params.save_state = argv[++i]; + continue; + } + if (ARG("--no-fast-boot")) + { + params.fast_boot = false; + continue; + } + if (argv[i][0] == '-') + { + Console.ErrorFmt("Unknown argument: '{}'", argv[i]); + return false; + } + + // Positional: ISO path. + if (!params.filename.empty()) + { + Console.Error("Multiple ISO paths supplied; expected exactly one."); + return false; + } + params.filename = argv[i]; + +#undef ARG +#undef ARG_PARAM + } + + // Empty positional + no --bios-only is allowed: the frontend boots into + // FullscreenUI's game-picker if "UI/StartBigPictureMode" is set + // (the default for this frontend; see Host::SetDefaultUISettings). + // The decision happens in main() after settings are loaded. + + if (!params.fast_boot.has_value()) + params.fast_boot = true; + if (!params.fullscreen.has_value()) + params.fullscreen = true; + + return true; +} + +static void HandleSignal(int) +{ + s_shutdown_requested.store(true, std::memory_order_release); + if (VMManager::HasValidVM()) + VMManager::SetState(VMState::Stopping); +} + +void Pcsx2SDL::InstallSignalHandler() +{ + std::signal(SIGTERM, &HandleSignal); + std::signal(SIGINT, &HandleSignal); + std::signal(SIGHUP, &HandleSignal); +} + +std::optional Pcsx2SDL::BuildWindowInfo() +{ + WindowInfo wi; + wi.type = WindowInfo::Type::VulkanDirect; + wi.surface_width = s_requested_width; + wi.surface_height = s_requested_height; + wi.surface_scale = 1.0f; + wi.display_connection = nullptr; + wi.window_handle = nullptr; + wi.surface_handle = nullptr; + return wi; +} + +////////////////////////////////////////////////////////////////////////// +// Host:: callbacks +////////////////////////////////////////////////////////////////////////// + +void Host::CommitBaseSettingChanges() +{ + if (!s_base_settings) + return; + Error err; + if (!s_base_settings->Save(&err)) + Console.ErrorFmt("Failed to save settings: {}", err.GetDescription()); +} + +void Host::LoadSettings(SettingsInterface& si, std::unique_lock& lock) +{ + // No host-specific settings layer to merge in — everything lives in the + // base INI. +} + +void Host::CheckForSettingsChanges(const Pcsx2Config& old_config) +{ +} + +bool Host::RequestResetSettings(bool folders, bool core, bool controllers, bool hotkeys, bool ui) +{ + // FullscreenUI will trigger this; no UI is wired to drive it yet. + return false; +} + +void Host::SetDefaultUISettings(SettingsInterface& si) +{ + // Handheld defaults — start straight into FullscreenUI when no game is + // loaded, hide pointer (no mouse), confirm via cross/A button. + si.SetBoolValue("UI", "StartBigPictureMode", true); +} + +bool Host::LocaleCircleConfirm() +{ + return false; +} + +std::unique_ptr Host::CreateHostProgressCallback() +{ + return ProgressCallback::CreateNullProgressCallback(); +} + +void Host::ReportInfoAsync(const std::string_view title, const std::string_view message) +{ + if (!title.empty() && !message.empty()) + INFO_LOG("{}: {}", title, message); + else if (!message.empty()) + INFO_LOG("{}", message); +} + +void Host::ReportErrorAsync(const std::string_view title, const std::string_view message) +{ + if (!title.empty() && !message.empty()) + ERROR_LOG("{}: {}", title, message); + else if (!message.empty()) + ERROR_LOG("{}", message); +} + +void Host::OpenURL(const std::string_view url) +{ +} + +bool Host::CopyTextToClipboard(const std::string_view text) +{ + return false; +} + +std::string Host::GetTextFromClipboard() +{ + return std::string(); +} + +void Host::BeginTextInput() +{ +} + +void Host::EndTextInput() +{ +} + +std::optional Host::GetTopLevelWindowInfo() +{ + return Pcsx2SDL::BuildWindowInfo(); +} + +void Host::OnInputDeviceConnected(const std::string_view identifier, const std::string_view device_name) +{ + INFO_LOG("Input device connected: {} ({})", identifier, device_name); +} + +void Host::OnInputDeviceDisconnected(const InputBindingKey key, const std::string_view identifier) +{ + INFO_LOG("Input device disconnected: {}", identifier); +} + +void Host::SetMouseMode(bool relative_mode, bool hide_cursor) +{ +} + +void Host::SetMouseLock(bool state) +{ +} + +std::optional Host::AcquireRenderWindow(bool recreate_window) +{ + return Pcsx2SDL::BuildWindowInfo(); +} + +void Host::ReleaseRenderWindow() +{ +} + +void Host::BeginPresentFrame() +{ +} + +void Host::RequestResizeHostDisplay(s32 width, s32 height) +{ + // VK_KHR_display provides a fixed mode for the lifetime of the surface; + // resize requests from the core are advisory only. +} + +void Host::OnVMStarting() +{ +} + +void Host::OnVMStarted() +{ +} + +void Host::OnVMDestroyed() +{ +} + +void Host::OnVMPaused() +{ +} + +void Host::OnVMResumed() +{ +} + +void Host::OnGameChanged(const std::string& title, const std::string& elf_override, const std::string& disc_path, + const std::string& disc_serial, u32 disc_crc, u32 current_crc) +{ + if (!title.empty()) + INFO_LOG("Game changed: {} (serial {}, CRC {:08X})", title, disc_serial, current_crc); +} + +void Host::OnPerformanceMetricsUpdated() +{ +} + +void Host::OnSaveStateLoading(const std::string_view filename) +{ +} + +void Host::OnSaveStateLoaded(const std::string_view filename, bool was_successful) +{ +} + +void Host::OnSaveStateSaved(const std::string_view filename) +{ +} + +void Pcsx2SDL::DrainCPUThreadQueue() +{ + for (;;) + { + std::function fn; + { + std::lock_guard lock(s_cpu_queue_lock); + if (s_cpu_queue.empty()) + return; + fn = std::move(s_cpu_queue.front()); + s_cpu_queue.pop_front(); + } + fn(); + } +} + +void Host::PumpMessagesOnCPUThread() +{ + // Honour SIGTERM / SIGINT picked up by the signal handler. + if (s_shutdown_requested.load(std::memory_order_acquire) && VMManager::HasValidVM()) + VMManager::SetState(VMState::Stopping); + + Pcsx2SDL::DrainCPUThreadQueue(); +} + +void Host::RunOnCPUThread(std::function function, bool block) +{ + if (block) + { + // Inline if already on the CPU thread to avoid self-deadlock. + if (s_cpu_thread_id.load(std::memory_order_acquire) == std::this_thread::get_id()) + { + function(); + return; + } + + std::mutex done_lock; + std::condition_variable done_cv; + bool done = false; + auto wrapped = [&function, &done_lock, &done_cv, &done]() { + function(); + std::lock_guard lk(done_lock); + done = true; + done_cv.notify_all(); + }; + { + std::lock_guard lock(s_cpu_queue_lock); + s_cpu_queue.emplace_back(std::move(wrapped)); + } + s_cpu_queue_cv.notify_all(); + std::unique_lock lk(done_lock); + done_cv.wait(lk, [&done]() { return done; }); + return; + } + + { + std::lock_guard lock(s_cpu_queue_lock); + s_cpu_queue.emplace_back(std::move(function)); + } + s_cpu_queue_cv.notify_all(); +} + +void Pcsx2SDL::StopGameListRefreshThread() +{ + if (!s_gamelist_thread.joinable()) + return; + s_gamelist_thread.join(); +} + +void Host::RefreshGameListAsync(bool invalidate_cache) +{ + // Only one scan at a time — FullscreenUI's "rescan" button can fire + // multiple times in quick succession; coalesce by joining the previous + // scan first. + Pcsx2SDL::StopGameListRefreshThread(); + + s_gamelist_running.store(true, std::memory_order_release); + s_gamelist_thread = std::thread([invalidate_cache]() { + Threading::SetNameOfCurrentThread("GameList Refresh"); + GameList::Refresh(invalidate_cache, false, nullptr); + s_gamelist_running.store(false, std::memory_order_release); + }); +} + +void Host::CancelGameListRefresh() +{ + Pcsx2SDL::StopGameListRefreshThread(); +} + +bool Host::IsFullscreen() +{ + return true; +} + +void Host::SetFullscreen(bool enabled) +{ + // No-op: VK_KHR_display is always fullscreen; there's no compositor to + // host a windowed mode. +} + +void Host::OnCaptureStarted(const std::string& filename) +{ +} + +void Host::OnCaptureStopped() +{ +} + +void Host::RequestExitApplication(bool allow_confirm) +{ + s_shutdown_requested.store(true, std::memory_order_release); + if (VMManager::HasValidVM()) + VMManager::SetState(VMState::Stopping); +} + +void Host::RequestExitBigPicture() +{ + // FullscreenUI exit — shut down the application. + Host::RequestExitApplication(false); +} + +void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state) +{ + VMManager::SetState(VMState::Stopping); +} + +void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) +{ +} + +void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) +{ +} + +void Host::OnAchievementsHardcoreModeChanged(bool enabled) +{ +} + +void Host::OnAchievementsRefreshed() +{ +} + +void Host::OnCoverDownloaderOpenRequested() +{ +} + +void Host::OnCreateMemoryCardOpenRequested() +{ +} + +bool Host::InBatchMode() +{ + return false; +} + +bool Host::InNoGUIMode() +{ + return false; +} + +bool Host::ShouldPreferHostFileSelector() +{ + return false; +} + +void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters, std::string_view initial_directory) +{ + // No native file picker on a kmsdrm-only handheld. FullscreenUI's own + // game list / file browser handles this path. + callback(std::string()); +} + +int Host::LocaleSensitiveCompare(std::string_view lhs, std::string_view rhs) +{ + const int res = std::strncmp(lhs.data(), rhs.data(), std::min(lhs.size(), rhs.size())); + if (res != 0) + return res; + return lhs.size() > rhs.size() ? 1 : (lhs.size() < rhs.size() ? -1 : 0); +} + +s32 Host::Internal::GetTranslatedStringImpl( + const std::string_view context, const std::string_view msg, char* tbuf, size_t tbuf_space) +{ + if (msg.size() > tbuf_space) + return -1; + if (msg.empty()) + return 0; + + std::memcpy(tbuf, msg.data(), msg.size()); + return static_cast(msg.size()); +} + +std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count) +{ + TinyString count_str = TinyString::from_format("{}", count); + + std::string ret(msg); + for (;;) + { + std::string::size_type pos = ret.find("%n"); + if (pos == std::string::npos) + break; + ret.replace(pos, 2, count_str.view()); + } + return ret; +} + +std::optional InputManager::ConvertHostKeyboardStringToCode(const std::string_view str) +{ + return std::nullopt; +} + +std::optional InputManager::ConvertHostKeyboardCodeToString(u32 code) +{ + return std::nullopt; +} + +const char* InputManager::ConvertHostKeyboardCodeToIcon(u32 code) +{ + return nullptr; +} + +BEGIN_HOTKEY_LIST(g_host_hotkeys) +END_HOTKEY_LIST() + +////////////////////////////////////////////////////////////////////////// +// CPU thread + main +////////////////////////////////////////////////////////////////////////// + +void Pcsx2SDL::CPUThreadMain(VMBootParameters initial_params, bool start_in_fsui, std::atomic* ret) +{ + ret->store(EXIT_FAILURE); + s_cpu_thread_id.store(std::this_thread::get_id(), std::memory_order_release); + + if (!VMManager::Internal::CPUThreadInitialize()) + { + Console.Error("CPU thread init failed."); + VMManager::Internal::CPUThreadShutdown(); + return; + } + + VMManager::ApplySettings(); + + // SDL doesn't enumerate already-connected gamepads at init time — the + // initial SDL_EVENT_GAMEPAD_ADDED events sit in SDL's queue until + // something calls SDL_PollEvent. Drain them now and rebind, so any + // SDL2-style A/B/X/Y face-button bindings get migrated to SDL3 positional + // names (FaceSouth/East/West/North) before use. Qt sidesteps this via its + // background-poll QTimer; the SDL frontend must drain explicitly here. + InputManager::ReloadDevices(); + VMManager::ReloadInputBindings(true); + + // Bring up the GS thread + display surface before booting anything when + // starting straight into FullscreenUI. With a VM-on-startup path the + // VMManager::Initialize call below will open MTGS itself; with the + // bootless FSUI path it must be opened manually so the game-picker is + // visible before the user selects a game. + if (start_in_fsui) + { + ImGuiManager::InitializeFullscreenUI(); + if (!MTGS::WaitForOpen()) + { + Console.Error("Failed to open MTGS for FullscreenUI startup."); + VMManager::Internal::CPUThreadShutdown(); + return; + } + MTGS::SetRunIdle(true); + } + + // The "initial" boot is the ISO/state passed on the CLI. When launching + // straight into FullscreenUI instead, this stays empty and the user picks + // a game from the game-list page (which queues a VMManager::Initialize + // call back via Host::RunOnCPUThread). + std::optional pending_boot; + if (!start_in_fsui) + pending_boot = std::move(initial_params); + + bool clean_shutdown = true; + + // Main CPU thread state-machine loop. Exits on shutdown_requested OR on + // VM shutdown when no FullscreenUI session is active to fall back to. + while (!s_shutdown_requested.load(std::memory_order_acquire)) + { + // Drain RunOnCPUThread callbacks (also done inside Execute via + // PumpMessagesOnCPUThread, but must be drained in the no-VM idle + // state too). + Pcsx2SDL::DrainCPUThreadQueue(); + + const VMState state = VMManager::GetState(); + switch (state) + { + case VMState::Initializing: + // Transient — just spin until VMManager moves on. + continue; + + case VMState::Running: + VMManager::Execute(); + continue; + + case VMState::Resetting: + VMManager::Reset(); + continue; + + case VMState::Stopping: + VMManager::Shutdown(false); + // After a clean shutdown, fall through to Shutdown / Paused. + continue; + + case VMState::Paused: + case VMState::Shutdown: + { + // If a CLI-supplied boot is pending, kick it now. + if (pending_boot.has_value()) + { + VMBootParameters bp = std::move(pending_boot.value()); + pending_boot.reset(); + const VMBootResult br = VMManager::Initialize(bp); + if (br != VMBootResult::StartupSuccess) + { + Console.ErrorFmt("VMManager::Initialize failed (result {}).", + static_cast(br)); + clean_shutdown = false; + s_shutdown_requested.store(true, std::memory_order_release); + break; + } + VMManager::SetState(VMState::Running); + continue; + } + + // Nothing pending. If no FSUI session is running, exit cleanly + // — the user's game finished and there is no UI to show next, + // so the frontend's job is done. + if (!start_in_fsui) + { + s_shutdown_requested.store(true, std::memory_order_release); + break; + } + + // FSUI idle: pump input so the gamepad can drive the menus. + // Qt does this from a background QTimer; here it has to run + // inline. Do it *outside* s_cpu_queue_lock — FSUI menu + // callbacks can call Host::RunOnCPUThread, which takes the + // same lock. + VMManager::IdlePollUpdate(); + + // Then wait for either a queued RunOnCPUThread (which might + // Initialize a new VM) or a state change. Bounded timeout so + // the shutdown flag is rechecked promptly. + std::unique_lock lock(s_cpu_queue_lock); + s_cpu_queue_cv.wait_for(lock, std::chrono::milliseconds(16), + []() { return !s_cpu_queue.empty(); }); + continue; + } + + default: + continue; + } + } + + // Tear down in reverse order of setup. MTGS may already have been closed + // by VMManager::Shutdown if VMManager opened it; if opened for FSUI + // directly it will still be open here. + if (VMManager::HasValidVM()) + VMManager::Shutdown(false); + if (MTGS::IsOpen()) + { + MTGS::SetRunIdle(false); + MTGS::WaitForClose(); + } + + Pcsx2SDL::StopGameListRefreshThread(); + + VMManager::Internal::CPUThreadShutdown(); + s_cpu_thread_id.store(std::thread::id{}, std::memory_order_release); + ret->store(clean_shutdown ? EXIT_SUCCESS : EXIT_FAILURE); +} + +int main(int argc, char* argv[]) +{ + // Short-circuit --help/--version before any heavyweight init so they + // work even on a system where the resources dir hasn't been laid down. + for (int i = 1; i < argc; i++) + { + if (!std::strcmp(argv[i], "--help") || !std::strcmp(argv[i], "-h")) + { + std::fprintf(stderr, "PCSX2 SDL frontend %s\n", GIT_REV); + std::fprintf(stderr, "Usage: %s [options] \n\n", argv[0]); + std::fprintf(stderr, " --fullscreen-mode WxH Request a specific display mode\n"); + std::fprintf(stderr, " (default: monitor's preferred mode)\n"); + std::fprintf(stderr, " --bios-only Boot the BIOS without loading a disc\n"); + std::fprintf(stderr, " --state-from-file PATH Resume from a save state file\n"); + std::fprintf(stderr, " --no-fast-boot Skip fast boot (run full BIOS animation)\n"); + std::fprintf(stderr, " -h, --help Show this help and exit\n"); + std::fprintf(stderr, " --version Show version and exit\n"); + return EXIT_SUCCESS; + } + if (!std::strcmp(argv[i], "--version")) + { + std::fprintf(stderr, "PCSX2 SDL frontend %s\n", GIT_REV); + return EXIT_SUCCESS; + } + } + + CrashHandler::Install(); + Log::SetConsoleOutputLevel(LOGLEVEL_INFO); + + if (!Pcsx2SDL::InitializeConfig()) + { + Console.Error("Failed to initialize config."); + return EXIT_FAILURE; + } + + VMBootParameters params; + if (!Pcsx2SDL::ParseCommandLineArgs(argc, argv, params)) + return EXIT_FAILURE; + + const bool have_boot_target = !params.filename.empty() || params.source_type.has_value(); + bool start_in_fsui = false; + if (!have_boot_target) + { + // No ISO supplied and no --bios-only — only valid if the + // StartBigPictureMode flag is set, in which case the frontend starts + // in the FullscreenUI game-picker. + if (Host::GetBaseBoolSettingValue("UI", "StartBigPictureMode", true)) + { + start_in_fsui = true; + } + else + { + Console.Error("No ISO path supplied. Use --bios-only to boot the BIOS, " + "or set UI/StartBigPictureMode=true in PCSX2.ini for the game-picker."); + return EXIT_FAILURE; + } + } + + Pcsx2SDL::InstallSignalHandler(); + SysMemory::ReserveMemory(); + + std::atomic thread_ret{EXIT_FAILURE}; + std::thread cpu_thread([&]() { + Pcsx2SDL::CPUThreadMain(std::move(params), start_in_fsui, &thread_ret); + }); + + // VK_KHR_display has no host event loop; SDL3 input pumping happens + // inside InputManager on the CPU thread; signals are async. The main + // thread just waits for shutdown. + cpu_thread.join(); + + return thread_ret.load(); +} diff --git a/pcsx2-vurunner/CMakeLists.txt b/pcsx2-vurunner/CMakeLists.txt new file mode 100644 index 0000000000..43f94f789d --- /dev/null +++ b/pcsx2-vurunner/CMakeLists.txt @@ -0,0 +1,43 @@ +add_executable(pcsx2-vurunner) + +if(PACKAGE_MODE) + install(TARGETS pcsx2-vurunner DESTINATION ${CMAKE_INSTALL_BINDIR}) +else() + install(TARGETS pcsx2-vurunner DESTINATION ${CMAKE_SOURCE_DIR}/bin) +endif() + +# vurunner reuses the recompiler test harness's environment + replay driver. +# Both the harness env and StubHost are also linked by the gtest binary; they +# are compiled in here directly (rather than as a shared library) to mirror +# pcsx2-gsrunner's single-binary-no-extra-libs pattern. +target_sources(pcsx2-vurunner PRIVATE + Main.cpp + ${CMAKE_SOURCE_DIR}/tests/ctest/core/StubHost.cpp + ${CMAKE_SOURCE_DIR}/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp + ${CMAKE_SOURCE_DIR}/tests/ctest/core/recompilers/harness/VuSnapshot.cpp + ${CMAKE_SOURCE_DIR}/tests/ctest/core/recompilers/harness/VuReplay.cpp +) + +target_include_directories(pcsx2-vurunner PRIVATE + "${CMAKE_BINARY_DIR}/common/include" + "${CMAKE_SOURCE_DIR}/pcsx2" + "${CMAKE_SOURCE_DIR}/tests/ctest/core/recompilers" +) + +target_link_libraries(pcsx2-vurunner PRIVATE + PCSX2_FLAGS + PCSX2 +) + +# Deterministic process layout for the persisted-JIT VU program cache: a +# non-PIE executable (ET_EXEC) loads at its fixed link address even with +# ASLR enabled, so every libpcsx2 symbol address is run-invariant — +# paired with the image-anchored fixed-base arena reservation in +# SysMemory::AllocateVirtualMemory. Verify with `pcsx2-vurunner +# --print-bases` (two runs must print identical addresses). Linux-only; +# macOS has no non-PIE executables. libpcsx2's -fPIC objects link into an +# ET_EXEC fine. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_target_properties(pcsx2-vurunner PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_link_options(pcsx2-vurunner PRIVATE -no-pie) +endif() diff --git a/pcsx2-vurunner/Main.cpp b/pcsx2-vurunner/Main.cpp new file mode 100644 index 0000000000..e7ec420f8f --- /dev/null +++ b/pcsx2-vurunner/Main.cpp @@ -0,0 +1,1115 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// pcsx2-vurunner — headless VU microprogram replayer for codegen iteration. +// +// Loads .vucap files (produced by the live capture probe in mVUexecute, +// see pcsx2/vu_capture.h) and replays them through both the microVU JIT +// and the VU interpreter. Operating modes: +// +// --diff (default) — run JIT and interp, gtest-equivalent divergence diff +// --bench — run JIT only, measure cycles/insns/branch-misses per +// iter via PMU counters; report median + median-abs-dev +// --dump-asm — template-JIT host-code disasm to .codegen.s +// +// Modes can be combined; the binary runs each capture through each +// requested mode in turn. + +#include "harness/RecompilerTestEnvironment.h" +#include "harness/VuReplay.h" +#include "harness/VuSnapshot.h" + +#include "vu_capture.h" +#include "Config.h" +#include "Memory.h" +#include "VU.h" +#include "VUmicro.h" +#include "Gif_Unit.h" +#include "microVU_Divtrace.h" +#include "arm64/microVU_Persist-arm64.h" +#include "arm64/microVU_ProgCache-arm64.h" + +#include "DebugTools/Debug.h" +#include "common/FPControl.h" +#include "common/PmuCounters.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + +struct Options +{ + u32 iters = 1; + bool diff = false; + bool bench = false; + bool dump_asm = false; + bool dump_microcode = false; + bool divtrace = false; + bool bench_no_reprime = false; + bool print_bases = false; + bool no_progcache = false; // determinism gate: force program cache + recording off + u32 dump_count = 64; + u32 cycle_override = 0; // 0 = use captured budget + std::string cache_dir; // empty = persisted-JIT program cache off + std::vector files; +}; + +void PrintUsage(const char* argv0) +{ + std::fprintf(stderr, + "Usage: %s [options] file.vucap [file.vucap ...]\n" + "\n" + "Modes (combinable; default is --diff):\n" + " --diff Run JIT + interp, report architectural divergences.\n" + " --bench Run JIT only, measure PMU cycles/insns per iter.\n" + " --dump-asm Dump the TEMPLATE JIT's emitted host ARM64 to\n" + " .codegen.s. (ARM64 JIT only).\n" + " --dump-microcode Disassemble VU microcode starting at start_pc to stdout.\n" + " --divtrace Per-microvu-op state-snapshot diff between JIT and interp;\n" + " report the FIRST divergent op with full context. ARM64 only.\n" + " --print-bases Print the process-layout anchors (image base, data/code\n" + " arenas, VU rec slabs) and exit. Two consecutive runs of a\n" + " non-PIE build must print identical addresses — the\n" + " determinism gate for the persisted-JIT program cache.\n" + "\n" + "Options:\n" + " --cache-dir D Enable the persisted-JIT VU program cache rooted at D\n" + " (D/vu_jit/vu{0,1}/...). Turns on emit-time fixup\n" + " recording; programs compiled this run are saved as\n" + " .vuprog payloads at every block-cache reset, and\n" + " programs already on disk hydrate instead of\n" + " recompiling. Run twice with the same D for the\n" + " cross-process gate: the 2nd run must report\n" + " payloadHits>0 / blockCompiles=0 per capture.\n" + " --no-progcache Force the persisted-JIT program cache and emit-time\n" + " recording off for the whole process (overrides\n" + " --cache-dir) so a JIT-vs-interp diff run is\n" + " byte-reproducible.\n" + " --iters N Number of replay iterations per capture (default 1).\n" + " Bench: throw out the first iter (JIT compile cost).\n" + " --dump-count N Number of microinstructions to dump (default 64).\n" + " --cycles N Override captured cycle budget (0 = keep original, default).\n" + " --help Show this message.\n" + " -- Treat all subsequent args as filenames.\n" + "\n" + "Captures are produced by running pcsx2-qt under PCSX2_VU_CAPTURE_DIR=.\n", + argv0); +} + +bool ParseArgs(int argc, char** argv, Options& opts) +{ + bool end_of_options = false; + for (int i = 1; i < argc; ++i) + { + const std::string a = argv[i]; + if (end_of_options || a.empty() || a[0] != '-') + { + opts.files.push_back(a); + continue; + } + if (a == "--") + { + end_of_options = true; + } + else if (a == "--help" || a == "-h") + { + PrintUsage(argv[0]); + std::exit(0); + } + else if (a == "--diff") + { + opts.diff = true; + } + else if (a == "--bench") + { + opts.bench = true; + } + else if (a == "--dump-asm") + { + opts.dump_asm = true; + } + else if (a == "--dump-microcode") + { + opts.dump_microcode = true; + } + else if (a == "--divtrace") + { + opts.divtrace = true; + } + else if (a == "--bench-no-reprime") + { + opts.bench_no_reprime = true; + } + else if (a == "--print-bases") + { + opts.print_bases = true; + } + else if (a == "--no-progcache") + { + opts.no_progcache = true; + } + else if (a == "--cache-dir") + { + if (i + 1 >= argc) + { + std::fprintf(stderr, "vurunner: --cache-dir requires an argument\n"); + return false; + } + opts.cache_dir = argv[++i]; + if (opts.cache_dir.empty()) + { + std::fprintf(stderr, "vurunner: --cache-dir argument is empty\n"); + return false; + } + } + else if (a == "--dump-count") + { + if (i + 1 >= argc) + { + std::fprintf(stderr, "vurunner: --dump-count requires an argument\n"); + return false; + } + const long n = std::strtol(argv[++i], nullptr, 10); + if (n <= 0 || n > (1 << 16)) + { + std::fprintf(stderr, "vurunner: --dump-count must be in (0, 65536]\n"); + return false; + } + opts.dump_count = static_cast(n); + } + else if (a == "--cycles") + { + if (i + 1 >= argc) + { + std::fprintf(stderr, "vurunner: --cycles requires an argument\n"); + return false; + } + const long n = std::strtol(argv[++i], nullptr, 10); + if (n < 0) + { + std::fprintf(stderr, "vurunner: --cycles must be >= 0\n"); + return false; + } + opts.cycle_override = static_cast(n); + } + else if (a == "--iters") + { + if (i + 1 >= argc) + { + std::fprintf(stderr, "vurunner: --iters requires an argument\n"); + return false; + } + const long n = std::strtol(argv[++i], nullptr, 10); + if (n <= 0 || n > (1 << 24)) + { + std::fprintf(stderr, "vurunner: --iters must be in (0, 16M]\n"); + return false; + } + opts.iters = static_cast(n); + } + else + { + std::fprintf(stderr, "vurunner: unknown option '%s'\n", a.c_str()); + return false; + } + } + if (opts.files.empty() && !opts.print_bases) + { + std::fprintf(stderr, "vurunner: no input files\n"); + return false; + } + if (!opts.diff && !opts.bench && !opts.dump_asm + && !opts.dump_microcode && !opts.divtrace && !opts.print_bases) + opts.diff = true; + return true; +} + +int RunDumpMicrocode(const std::vector& records, + const std::vector& names, + u32 dump_count) +{ + for (size_t fi = 0; fi < records.size(); ++fi) + { + const auto& rec = records[fi]; + const u32 prog_size = static_cast(rec.microcode.size()); + const u32 limit = (rec.vu_index == 0) ? 0xFFFu : 0x3FFFu; + std::printf("// %s vu%u start_pc=0x%08X dump=%u insns\n", + names[fi].c_str(), rec.vu_index, rec.start_pc, dump_count); + + u32 pc = rec.start_pc & ~7u; + for (u32 i = 0; i < dump_count; ++i) + { + const u32 wrapped = pc & limit; + if (wrapped + 8 > prog_size) + break; + u32 lo = 0, up = 0; + std::memcpy(&lo, rec.microcode.data() + wrapped + 0, 4); + std::memcpy(&up, rec.microcode.data() + wrapped + 4, 4); + const char* upper_s = (rec.vu_index == 0) ? + disVU0MicroUF(up, pc + 4) : disVU1MicroUF(up, pc + 4); + std::string upper_copy = upper_s ? upper_s : "?"; + const char* lower_s = (rec.vu_index == 0) ? + disVU0MicroLF(lo, pc) : disVU1MicroLF(lo, pc); + std::printf(" %04X %08X %08X %-32s | %s\n", + wrapped, up, lo, upper_copy.c_str(), lower_s ? lower_s : "?"); + // Stop on E-bit (bit 30 of upper word) — end of program. + if (up & (1u << 30)) + { + std::printf(" // [E-bit] — program end\n"); + break; + } + pc += 8; + } + std::printf("\n"); + } + return 0; +} + +// Median + MAD of one PMU counter across samples (skipping the first sample +// when skip_warmup is set, since the JIT compile cost dominates iter 0). +struct Stat +{ + u64 median = 0; + u64 mad = 0; + u64 min = 0; + u64 max = 0; +}; + +Stat Summarize(const std::vector& xs) +{ + Stat s; + if (xs.empty()) + return s; + std::vector sorted = xs; + std::sort(sorted.begin(), sorted.end()); + s.min = sorted.front(); + s.max = sorted.back(); + s.median = sorted[sorted.size() / 2]; + + std::vector dev; + dev.reserve(sorted.size()); + for (u64 x : sorted) + dev.push_back(x > s.median ? x - s.median : s.median - x); + std::sort(dev.begin(), dev.end()); + s.mad = dev[dev.size() / 2]; + return s; +} + +// Returns the byte offset of the first byte that differs between two +// vectors, or -1 if they are byte-identical (including matching length). +ssize_t FirstByteDiff(const std::vector& a, const std::vector& b) +{ + const size_t n = std::min(a.size(), b.size()); + for (size_t i = 0; i < n; ++i) + if (a[i] != b[i]) + return static_cast(i); + if (a.size() != b.size()) + return static_cast(n); + return -1; +} + +void PrintPathDiff(const std::vector& jit, const std::vector& interp) +{ + if (jit == interp) + return; + std::printf(" PATH1: jit=%zu B interp=%zu B (lengths %s)\n", + jit.size(), interp.size(), jit.size() == interp.size() ? "match" : "DIFFER"); + const ssize_t at = FirstByteDiff(jit, interp); + if (at < 0) + return; + const size_t qw_off = static_cast(at) & ~size_t{15}; + std::printf(" PATH1: first byte diff at offset 0x%zx (qword 0x%zx)\n", + static_cast(at), qw_off); + auto dumpqw = [](const char* label, const std::vector& v, size_t off) { + if (off + 16 > v.size()) + { + std::printf(" %-6s [out of range, len=%zu]\n", label, v.size()); + return; + } + std::printf(" %-6s ", label); + for (int i = 15; i >= 0; --i) + std::printf("%02x", v[off + i]); + std::printf("\n"); + }; + for (int qi = -1; qi <= 1; ++qi) + { + const ssize_t off = static_cast(qw_off) + qi * 16; + if (off < 0) + continue; + std::printf(" [qw 0x%zx]\n", static_cast(off)); + dumpqw("jit", jit, static_cast(off)); + dumpqw("interp", interp, static_cast(off)); + } +} + +int RunDiff(const std::vector& records, + const std::vector& names, + u32 iters, + u32 cycle_override) +{ + int diverged_files = 0; + int path_diverged_files = 0; + for (size_t fi = 0; fi < records.size(); ++fi) + { + const auto& rec = records[fi]; + std::printf("[diff] %s (vu%u start_pc=0x%08X cycles=%u)\n", + names[fi].c_str(), rec.vu_index, rec.start_pc, rec.cycle_budget); + bool any_diverged = false; + bool path_diverged = false; + const char* term = "unknown"; + for (u32 i = 0; i < iters; ++i) + { + const auto r = recompiler_tests::ReplayCapture(rec, + recompiler_tests::VuDiffMode::PipelinePermissive, cycle_override); + if (!r.ok) + { + std::printf(" iter %u: replay setup failed\n", i); + any_diverged = true; + break; + } + // Termination signal (interp = oracle): ebit = program ran to its + // E-bit; budget = truncated by cycle budget (loop noise, not a bug). + if (i == 0) + term = r.interp_ebit ? "ebit" : "budget"; + const bool path_diff = (r.path1_packets_jit != r.path1_packets_interp); + if (r.diverged || path_diff) + { + any_diverged = true; + path_diverged = path_diff; + std::printf(" iter %u: %zu register divergence(s)%s\n", + i, r.diff_lines.size(), path_diff ? " [PATH1 DIFFERS]" : ""); + for (const auto& d : r.diff_lines) + std::printf(" %s\n", d.c_str()); + if (path_diff) + PrintPathDiff(r.path1_packets_jit, r.path1_packets_interp); + break; + } + } + std::printf(" term=%s\n", term); + if (!any_diverged) + std::printf(" ok (%u iters)\n", iters); + else + { + ++diverged_files; + if (path_diverged) + ++path_diverged_files; + } + } + if (diverged_files) + std::printf("[diff] %d of %zu captures diverged (%d with PATH1 byte diff)\n", + diverged_files, records.size(), path_diverged_files); + return diverged_files == 0 ? 0 : 2; +} + +int RunBench(const std::vector& records, + const std::vector& names, + u32 iters, + bool no_reprime) +{ + const u32 effective_iters = std::max(2u, iters); // need at least 1 warmup + 1 sample + std::printf("# bench: iters=%u (iter 0 dropped as warmup), counters: cycles instructions branch-misses, reprime=%s\n", + effective_iters, no_reprime ? "off" : "on"); + std::printf("%-50s %12s %12s %12s\n", "capture", "cycles_med", "insns_med", "br_miss_med"); + for (size_t fi = 0; fi < records.size(); ++fi) + { + const auto& rec = records[fi]; + const auto samples = recompiler_tests::BenchJit(rec, effective_iters, 0, !no_reprime); + if (samples.size() < 2) + { + std::printf("%-50s bench unavailable (PMU open failed?)\n", names[fi].c_str()); + continue; + } + std::vector cycles, insns, brmiss; + for (size_t i = 1; i < samples.size(); ++i) // skip warmup + { + cycles.push_back(samples[i][PmuCounters::CpuCycles]); + insns.push_back(samples[i][PmuCounters::InstructionsRetired]); + brmiss.push_back(samples[i][PmuCounters::BranchMisses]); + } + const Stat sc = Summarize(cycles); + const Stat si = Summarize(insns); + const Stat sb = Summarize(brmiss); + std::printf("%-50s %12llu %12llu %12llu (mad %llu/%llu/%llu)\n", + names[fi].c_str(), + (unsigned long long)sc.median, + (unsigned long long)si.median, + (unsigned long long)sb.median, + (unsigned long long)sc.mad, + (unsigned long long)si.mad, + (unsigned long long)sb.mad); + } + return 0; +} + +// Clamp/NaN by-design classifier. mVU broadcast FMACs leave operands unclamped, +// so the JIT can produce a raw NaN/Inf (exponent all-ones) where the +// interpreter's vuDouble clamps the operand first and then computes a large +// FINITE result — typically max-exponent (0xFE), e.g. 0xff7ffffe, NOT exactly +// ±FLT_MAX. mVUclamp1 also sign-strips NaN, so two NaN lanes can differ only in +// sign/payload. This whole family is shared with the x86 path and is NOT a bug. +// +// The signature is that the diverging lane is an *extreme saturation value* on +// at least one side: NaN/Inf (exponent all-ones), OR exactly ±maxfloat +// (0x?f7fffff) — the literal mVUclamp1 saturation output. The latter case +// surfaces when a NaN *operand* (which both engines hold identically) is +// consumed by a broadcast FMAC: the JIT clamps the NaN result to ±maxfloat +// while the interp's vuDouble path lands elsewhere (often 0). The result lanes +// are then both finite (e.g. JIT=0xff7fffff vs INTERP=0x0), so a NaN/Inf-only +// test misses it — but JIT==±maxfloat is the tell. Both-finite-and- +// neither-saturated is a genuine arithmetic divergence and stays real. +// +// A divergence is "clamp-only" iff every value-diff line is a vf/acc float lane +// matching this pattern, there is at least one such lane, and there are NO +// integer (viN), memory, or pipeline diffs. A single non-clamp diff (the +// FCGET→vi buckets, an integer op) keeps the whole divergence real. +static bool IsClampNanLane(u32 jit, u32 interp) +{ + // Signed-zero: both sides are zero, differing only in the sign bit. mVU + // flushes denormals via the hardware FPCR FZ bit while the interp's vuDouble + // flushes in software, so a denormal-underflow MUL/ADD lands at +0 vs -0 — + // arithmetically equal, a cosmetic sign-of-zero difference shared with x86 + // (same family as the documented NaN-sign divergence). + if ((jit & 0x7fffffffu) == 0 && (interp & 0x7fffffffu) == 0) + return true; + auto isExtreme = [](u32 v) { + if ((v & 0x7f800000u) == 0x7f800000u) return true; // NaN or Inf + if ((v & 0x7fffffffu) == 0x7f7fffffu) return true; // ±maxfloat (mVUclamp1) + return false; + }; + // CONSERVATIVE: only the JIT side being the saturated extreme is the + // mVU-clamp / unclamped-broadcast signature (mVU is the clamper). If the + // JIT produced a clean finite non-maxfloat value while the interp is the + // extreme, that is NOT the by-design pattern — it is the cleanest possible + // real-bug signature (the JIT computed something wrong) and must stay in the + // genuine queue. The one exception is both-sides-NaN/Inf, which is the + // documented mVUclamp1 NaN sign/payload-strip divergence. + if (isExtreme(jit)) + return true; + const bool jit_naninf = (jit & 0x7f800000u) == 0x7f800000u; + const bool int_naninf = (interp & 0x7f800000u) == 0x7f800000u; + return jit_naninf && int_naninf; // both NaN/Inf → clamp1 sign-strip +} + +// Returns true if all architectural diffs are the by-design clamp/NaN family. +// diff_lines come from DiffVu: value lines are ": JIT=0x.. INTERP=0x..". +// Q-disagree/P-disagree informational lines (appended later, no "JIT=0x") are +// ignored. Returns false if there are no value diffs at all. +static bool ClassifyClampOnly(const std::vector& diff_lines) +{ + bool saw_clamp_lane = false; + for (const auto& line : diff_lines) + { + const size_t jpos = line.find("JIT=0x"); + const size_t ipos = line.find("INTERP=0x"); + if (jpos == std::string::npos || ipos == std::string::npos) + continue; // informational line (Q-disagree etc.) — skip + // Only vf/acc float lanes can be clamp-pattern. ACC renders as "vf-1.*". + const bool is_vf = line.rfind("vf", 0) == 0; + if (!is_vf) + return false; // viN / vumem / pipeline diff → genuine bug + const u32 jit = static_cast(std::strtoul(line.c_str() + jpos + 6, nullptr, 16)); + const u32 interp = static_cast(std::strtoul(line.c_str() + ipos + 9, nullptr, 16)); + if (!IsClampNanLane(jit, interp)) + return false; // a vf lane that isn't the clamp pattern → genuine bug + saw_clamp_lane = true; + } + return saw_clamp_lane; +} + +// vudivtrace driver. For each capture: enable divtrace mode, replay (which +// runs JIT then interp under divtrace, populating per-op snapshot streams), +// then walk the streams to find the FIRST op where JIT and interp disagree +// on architectural state. Print microvu PC + decoded mnemonic + field diffs. +int RunDivTrace(const std::vector& records, + const std::vector& names, + u32 cycle_override) +{ + int diverged_files = 0; + for (size_t fi = 0; fi < records.size(); ++fi) + { + const auto& rec = records[fi]; + const int idx = static_cast(rec.vu_index); + std::printf("[divtrace] %s (vu%d start_pc=0x%08X cycles=%u)\n", + names[fi].c_str(), idx, rec.start_pc, rec.cycle_budget); + + // VIs the JIT defers writeback on (flag pipeline + TPC): STATUS=16, + // MAC=17, CLIP=18, TPC=26. flushAll doesn't push these. Q (vi22) and + // P (vi23) round-trip through current/pending lanes. The fingerprint + // (FingerprintRegs) and DiffVu both ignore exactly this set so a + // flagged divergence is real architectural state, not flag/Q/P noise. + const std::vector ignored_vi = {16, 17, 18, 22, 23, 26}; + + // Pass 1 — fingerprints only (zero-length full-snapshot window). This + // scales to multi-million-op vumain loops where the 3 KB/op full + // StateSnap stream overflows. + mvu_divtrace::EnterMode(idx); + mvu_divtrace::ConfigureFullWindow(0, 0); + const auto r = recompiler_tests::ReplayCapture(rec, + recompiler_tests::VuDiffMode::PipelinePermissive, cycle_override); + const u32 jit_count = mvu_divtrace::g_jit_snap_idx.load(std::memory_order_acquire); + const u32 interp_count = mvu_divtrace::g_interp_op_idx; + const u32 meta_count = static_cast(mvu_divtrace::g_meta.size()); + + if (!r.ok) + { + std::printf(" replay setup failed\n"); + mvu_divtrace::ExitMode(); + ++diverged_files; + continue; + } + + std::printf(" ops compiled: %u jit-executed: %u interp-executed: %u\n", + meta_count, jit_count, interp_count); + const char* term = r.interp_ebit ? "ebit" : "budget"; + std::printf(" term=%s\n", term); + + // Scan fingerprints for the first divergent execution index (and up to + // 40 divergent indices for the pattern summary). + const u32 cmp_n = std::min(jit_count, interp_count); + u32 first_diff = cmp_n; + std::vector divergent_idx; + for (u32 i = 0; i < cmp_n; ++i) + { + if (mvu_divtrace::g_jit_fps[i] != mvu_divtrace::g_interp_fps[i]) + { + if (first_diff == cmp_n) + first_diff = i; + divergent_idx.push_back(i); + if (divergent_idx.size() >= 40) break; + } + } + + if (first_diff == cmp_n) + { + // The entire common prefix matched architecturally. The two sides + // only differ in HOW MANY ops they ran. Classify that delta: + // + // - counts equal → fully clean. + // - counts differ, interp stopped on the CYCLE BUDGET (not E-bit), + // and the JIT ran at least as far → benign block-boundary + // overshoot. VU0 runs in EE-driven partial chunks, so every VU0 + // capture is budget-truncated mid-stream; the interp stops at the + // exact cycle while the JIT can only stop at its next atomic block + // boundary, so it runs a few extra ops of the SAME control flow + // interp would have continued into. Not a bug — this is the + // dominant VU0 false positive. + // + // Anything else with a clean prefix IS a real divergence: interp hit + // an E-bit terminator the JIT ran past (missed terminator), or the + // JIT stopped earlier than interp (premature terminator). Fall + // through to the COUNT_MISMATCH report. + if (jit_count == interp_count) + { + std::printf(" ok (%u ops matched architecturally)\n", cmp_n); + mvu_divtrace::ExitMode(); + continue; + } + const bool interp_budget_truncated = !r.interp_ebit; + if (interp_budget_truncated && jit_count >= interp_count) + { + std::printf(" ok (%u common ops matched; +%u JIT op(s) past the " + "interp budget cutoff — benign block-boundary overshoot)\n", + cmp_n, jit_count - interp_count); + mvu_divtrace::ExitMode(); + continue; + } + } + + ++diverged_files; + std::vector diff_lines; + if (first_diff < cmp_n) + { + // Pass 2 — re-run with a small full-snapshot window around the + // first divergence so the detailed report has real register state. + // Window length is CTX+TAIL+1 regardless of program size (it's + // [first_diff-CTX, first_diff+TAIL]), so a large CTX is cheap and + // lets the snapshot-scan below catch roots the fingerprint localizer + // mis-reports. + const u32 CTX = 1024, TAIL = 8; + const u32 wlo = first_diff > CTX ? first_diff - CTX : 0; + const u32 wlen = (first_diff - wlo) + 1 + TAIL; + mvu_divtrace::Reset(); + mvu_divtrace::ConfigureFullWindow(wlo, wlen); + recompiler_tests::ReplayCapture(rec, + recompiler_tests::VuDiffMode::PipelinePermissive, cycle_override); + mvu_divtrace::ExitMode(); + + auto JS = [&](u32 g) -> const mvu_divtrace::StateSnap& { + return mvu_divtrace::g_jit_snaps[g - wlo]; }; + auto ISn = [&](u32 g) -> const mvu_divtrace::StateSnap& { + return mvu_divtrace::g_interp_snaps[g - wlo]; }; + + const auto& js = JS(first_diff); + const auto& is = ISn(first_diff); + + // Recompute the actual field diffs at the first divergent op. + recompiler_tests::VuSnapshot jsnap, isnap; + jsnap.index = idx; jsnap.regs = js.regs; + isnap.index = idx; isnap.regs = is.regs; + diff_lines = recompiler_tests::DiffVu(jsnap, isnap, + recompiler_tests::VuDiffMode::PipelinePermissive, ignored_vi); + // Q/P multiset info (appended — doesn't drive grouping, the + // fingerprint already excluded Q/P). + { + const u32 ji_q = js.regs.VI[REG_Q].UL, ji_pq = js.regs.pending_q; + const u32 in_q = is.regs.VI[REG_Q].UL; + if (in_q != ji_q && in_q != ji_pq) + { + char line[160]; + std::snprintf(line, sizeof(line), + "Q-disagree: INT=%08x not in JIT{%08x,%08x}", + in_q, ji_q, ji_pq); + diff_lines.push_back(line); + } + if (idx == 1) + { + const u32 ji_p = js.regs.VI[REG_P].UL, ji_pp = js.regs.pending_p; + const u32 in_p = is.regs.VI[REG_P].UL; + if (in_p != ji_p && in_p != ji_pp) + { + char line[160]; + std::snprintf(line, sizeof(line), + "P-disagree: INT=%08x not in JIT{%08x,%08x}", + in_p, ji_p, ji_pp); + diff_lines.push_back(line); + } + } + } + + std::printf("\n === FIRST DIVERGENCE at op #%u ===\n", first_diff); + std::printf(" JIT meta_idx=%u pre_xPC=0x%04X\n", js.meta_idx, js.pre_xPC); + std::printf(" INT meta_idx=%u pre_xPC=0x%04X\n", is.meta_idx, is.pre_xPC); + if (js.pre_xPC != is.pre_xPC) + { + std::printf(" ⚠ pre_xPC mismatch — control-flow divergence;\n"); + std::printf(" JIT and interp executed different ops at this step.\n"); + } + + const u32 microvu_pc = is.pre_xPC; // interp's xPC is authoritative + const u32 prog_size = static_cast(rec.microcode.size()); + const u32 limit = (idx == 0) ? 0xFFFu : 0x3FFFu; + const u32 wrapped = microvu_pc & limit; + // Mnemonics for the machine-readable [group] line below. First + // whitespace-delimited token only (no operands) so the same op + // buckets identically across programs. + std::string grp_upper = "?", grp_lower = "?"; + int up_fs = -1, up_ft = -1, up_bc = -1; + if (wrapped + 8 <= prog_size) + { + u32 lo = 0, up = 0; + std::memcpy(&lo, rec.microcode.data() + wrapped + 0, 4); + std::memcpy(&up, rec.microcode.data() + wrapped + 4, 4); + up_fs = (up >> 11) & 0x1F; + up_ft = (up >> 16) & 0x1F; + up_bc = up & 0x3; + // disVU?Micro?F share a static output buffer, so copy upper's + // result before calling the lower decoder. + const char* upper_s = (idx == 0) + ? disVU0MicroUF(up, microvu_pc + 4) : disVU1MicroUF(up, microvu_pc + 4); + const std::string upper_copy = upper_s ? upper_s : "?"; + const char* lower_s = (idx == 0) + ? disVU0MicroLF(lo, microvu_pc) : disVU1MicroLF(lo, microvu_pc); + std::printf(" microcode: %08X %08X\n", up, lo); + std::printf(" upper: %s\n", upper_copy.c_str()); + std::printf(" lower: %s\n", lower_s ? lower_s : "?"); + + // Disasm format is " : " — the + // mnemonic is the first token after ": ". Stop at space/comma. + auto mnemonic = [](const std::string& s) -> std::string { + size_t c = s.find(": "); + size_t start = (c == std::string::npos) ? 0 : c + 2; + while (start < s.size() && (s[start] == ' ' || s[start] == '\t')) + ++start; + size_t e = start; + while (e < s.size() && s[e] != ' ' && s[e] != '\t' && s[e] != ',') + ++e; + return e > start ? s.substr(start, e - start) : s; + }; + grp_upper = mnemonic(upper_copy); + if (lower_s) + grp_lower = mnemonic(lower_s); + } + + // field "class": leading prefix of the first diff line up to the + // first '.', ':', '[', '-', or digit (vf12.x→vf, vi02→vi, + // Q-disagree→Q, mem[..]→mem). ACC renders as "vf-1.x" — special- + // cased to "acc" — so cross-program diffs on the same register + // class collapse into one bucket. + std::string grp_field = "?"; + if (!diff_lines.empty()) + { + const std::string& fl = diff_lines.front(); + if (fl.rfind("vf-1", 0) == 0) + { + grp_field = "acc"; + } + else + { + size_t e = 0; + while (e < fl.size() && fl[e] != '.' && fl[e] != ':' + && fl[e] != '[' && fl[e] != '-' + && !(fl[e] >= '0' && fl[e] <= '9')) + ++e; + grp_field = e ? fl.substr(0, e) : fl; + } + } + + // By-design clamp/NaN family detector — strips the documented + // mVU-FMAC NaN↔±FLT_MAX divergence off the real-bug queue. + const bool clamp_only = ClassifyClampOnly(diff_lines); + + // Machine-readable grouping key for triage.py --group. One line + // per diverging capture; bucket by (vu, mnem, field, term, clamp). + std::printf(" [group] vu=%d mnem=%s/%s field=%s term=%s clamp=%d\n", + idx, grp_upper.c_str(), grp_lower.c_str(), grp_field.c_str(), + term, clamp_only ? 1 : 0); + + std::printf("\n state diff (PipelinePermissive):\n"); + for (const auto& d : diff_lines) + std::printf(" %s\n", d.c_str()); + + // Show 5 ops of pre-divergence context with vi01/vi04/vi06/vi07 + // (commonly tested by IBxxx branches, so useful for spotting the + // branch input that decided the split). + std::printf("\n pre-divergence context (5 ops + key VIs):\n"); + const u32 ctx_start = first_diff >= 5 ? first_diff - 5 : 0; + for (u32 i = ctx_start; i < first_diff; ++i) + { + const auto& jss = JS(i); + const auto& iss = ISn(i); + std::printf(" op #%-4u xPC=0x%04X JIT vi01=%04x vi04=%04x vi06=%04x vi07=%04x INT vi01=%04x vi04=%04x vi06=%04x vi07=%04x\n", + i, jss.pre_xPC, + jss.regs.VI[1].UL & 0xFFFF, jss.regs.VI[4].UL & 0xFFFF, + jss.regs.VI[6].UL & 0xFFFF, jss.regs.VI[7].UL & 0xFFFF, + iss.regs.VI[1].UL & 0xFFFF, iss.regs.VI[4].UL & 0xFFFF, + iss.regs.VI[6].UL & 0xFFFF, iss.regs.VI[7].UL & 0xFFFF); + } + + // Print vf00 + vf01 (DIV inputs in the 0x408 case) and Q lanes. + std::printf("\n context (vf00, vf01, Q+pending_q):\n"); + std::printf(" JIT vf00.w=%08x vf01.w=%08x Q=%08x pendQ=%08x\n", + js.regs.VF[0].UL[3], js.regs.VF[1].UL[3], + js.regs.VI[REG_Q].UL, js.regs.pending_q); + std::printf(" INT vf00.w=%08x vf01.w=%08x Q=%08x pendQ=%08x\n", + is.regs.VF[0].UL[3], is.regs.VF[1].UL[3], + is.regs.VI[REG_Q].UL, is.regs.pending_q); + + // If the first divergence is on a VF reg, dump that VF's full + // pre-op and post-op state on both sides — the JIT/interp PRE + // values reveal whether divergence is bad input vs bad emit. + if (!diff_lines.empty() && diff_lines.front().rfind("vf", 0) == 0) + { + const std::string& fl = diff_lines.front(); + size_t dot = fl.find('.'); + const std::string num = fl.substr(2, (dot == std::string::npos ? fl.size() : dot) - 2); + char* end = nullptr; + const long n = std::strtol(num.c_str(), &end, 10); + if (end != num.c_str() && n >= 0 && n < 32) + { + std::printf("\n diverging VF%ld lanes (xyzw):\n", n); + if (first_diff > 0) + { + const auto& jp = JS(first_diff - 1); + const auto& ip = ISn(first_diff - 1); + std::printf(" JIT pre : %08x %08x %08x %08x\n", + jp.regs.VF[n].UL[0], jp.regs.VF[n].UL[1], + jp.regs.VF[n].UL[2], jp.regs.VF[n].UL[3]); + std::printf(" INT pre : %08x %08x %08x %08x\n", + ip.regs.VF[n].UL[0], ip.regs.VF[n].UL[1], + ip.regs.VF[n].UL[2], ip.regs.VF[n].UL[3]); + } + std::printf(" JIT post : %08x %08x %08x %08x\n", + js.regs.VF[n].UL[0], js.regs.VF[n].UL[1], + js.regs.VF[n].UL[2], js.regs.VF[n].UL[3]); + std::printf(" INT post : %08x %08x %08x %08x\n", + is.regs.VF[n].UL[0], is.regs.VF[n].UL[1], + is.regs.VF[n].UL[2], is.regs.VF[n].UL[3]); + } + } + + // ACC divergence (rendered "vf-1.*"): the VF-lane dump above guards + // n>=0 and skips ACC, so dump ACC pre/post here plus the op's runtime + // Fs/Ft operands. ACC is fingerprint-tracked, so at the FIRST + // divergence ACC pre MUST agree — a post-only ACC divergence with + // JIT=±maxfloat (0xff7fffff / 0x7f7fffff) is the documented + // broadcast-unclamped overflow→clamp family, not a new emit bug. + if (!diff_lines.empty() && diff_lines.front().rfind("vf-1", 0) == 0) + { + std::printf("\n diverging ACC lanes (xyzw):\n"); + if (first_diff > 0) + { + const auto& jp = JS(first_diff - 1); + const auto& ip = ISn(first_diff - 1); + std::printf(" JIT pre : %08x %08x %08x %08x\n", + jp.regs.ACC.UL[0], jp.regs.ACC.UL[1], jp.regs.ACC.UL[2], jp.regs.ACC.UL[3]); + std::printf(" INT pre : %08x %08x %08x %08x\n", + ip.regs.ACC.UL[0], ip.regs.ACC.UL[1], ip.regs.ACC.UL[2], ip.regs.ACC.UL[3]); + auto dumpVf = [&](const char* who, const VURegs& r, int v) { + if (v >= 0 && v < 32) + std::printf(" %s vf%02d : %08x %08x %08x %08x\n", who, v, + r.VF[v].UL[0], r.VF[v].UL[1], r.VF[v].UL[2], r.VF[v].UL[3]); + }; + std::printf(" op operands (pre): Fs=vf%d Ft=vf%d bc=%c\n", + up_fs, up_ft, "xyzw"[up_bc & 3]); + dumpVf("JIT Fs", jp.regs, up_fs); + dumpVf("INT Fs", ip.regs, up_fs); + dumpVf("JIT Ft", jp.regs, up_ft); + dumpVf("INT Ft", ip.regs, up_ft); + } + std::printf(" JIT post : %08x %08x %08x %08x\n", + js.regs.ACC.UL[0], js.regs.ACC.UL[1], js.regs.ACC.UL[2], js.regs.ACC.UL[3]); + std::printf(" INT post : %08x %08x %08x %08x\n", + is.regs.ACC.UL[0], is.regs.ACC.UL[1], is.regs.ACC.UL[2], is.regs.ACC.UL[3]); + } + + // Integer (viN) divergence: dump VI[0..15] pre (op N-1) and post + // (op N) on both sides — shows which integer reg split and whether it + // split AT this op (emit bug) or was already diverged in the pre-snap + // (integer-pipeline / snapshot-alignment artifact). + if (!diff_lines.empty() && diff_lines.front().rfind("vi", 0) == 0 + && diff_lines.front().rfind("vf", 0) != 0) + { + auto dumpVI = [](const char* who, const VURegs& r) { + std::printf(" %s:", who); + for (int v = 0; v < 16; ++v) + std::printf(" %x=%04x", v, r.VI[v].UL & 0xFFFF); + std::printf("\n"); + }; + std::printf("\n VI[0..15] pre (op #%u) / post (op #%u):\n", + first_diff > 0 ? first_diff - 1 : 0, first_diff); + if (first_diff > 0) + { + dumpVI("JIT pre ", JS(first_diff - 1).regs); + dumpVI("INT pre ", ISn(first_diff - 1).regs); + } + dumpVI("JIT post", js.regs); + dumpVI("INT post", is.regs); + + // Trace the single diverging integer reg across the whole window + // to see if it RECONVERGES (integer-pipeline-boundary artifact) or + // PROPAGATES (genuine hazard/emit bug). + // Snapshot-based first-divergence over ALL VI[0..15] within the + // window. The fingerprint first_diff can mis-localize badly when + // the JIT/interp op streams desync (count mismatch) — the + // fingerprint may report a downstream op while the true integer + // root is much earlier. This scan walks the captured window from + // its start and reports the earliest VI split, with the writing + // op's microcode so the root op is identifiable. + std::printf("\n snapshot-scan: earliest VI[0..15] split in window:\n"); + bool found = false; + for (u32 i = wlo; i < wlo + wlen && !found; ++i) + for (int v = 0; v < 16; ++v) + if ((JS(i).regs.VI[v].UL & 0xFFFF) != (ISn(i).regs.VI[v].UL & 0xFFFF)) + { + const u32 xpc = ISn(i).pre_xPC; + const u32 w = xpc & ((idx == 0) ? 0xFFFu : 0x3FFFu); + u32 lw = 0; + if (w + 4 <= rec.microcode.size()) + std::memcpy(&lw, rec.microcode.data() + w, 4); + std::printf(" op #%u xPC=0x%04X vi%d: JIT=%04x INT=%04x (lower=0x%08x)\n", + i, xpc, v, JS(i).regs.VI[v].UL & 0xFFFF, + ISn(i).regs.VI[v].UL & 0xFFFF, lw); + found = true; + break; + } + if (!found) + std::printf(" (no VI split inside the %u-op window — root is further back)\n", wlen); + } + + if (divergent_idx.size() > 1) + { + std::printf("\n divergent ops summary (first %zu of the " + "fingerprint stream; xPC only — out of detail window):\n", + divergent_idx.size()); + for (u32 g : divergent_idx) + std::printf(" op #%-7u xPC=0x%04X%s\n", g, + mvu_divtrace::g_interp_xpc[g], + g == first_diff ? " <= first" : ""); + } + } + else + { + mvu_divtrace::ExitMode(); + // Reached only for a GENUINE terminator divergence: the common + // prefix is architecturally clean, but the count delta is NOT the + // benign budget-overshoot filtered above. Either interp hit an E-bit + // the JIT ran past, or the JIT terminated before interp did. + const char* kind = (jit_count > interp_count) + ? "JIT RAN PAST interp's terminator (missed E-bit / over-run)" + : "JIT STOPPED BEFORE interp (premature terminator)"; + std::printf(" [group] vu=%d mnem=TERMINATOR_DIVERGENCE/- field=count term=%s clamp=0\n", + idx, term); + std::printf("\n ⚠ TERMINATOR DIVERGENCE — clean common prefix (%u ops), but\n", cmp_n); + std::printf(" counts disagree (jit=%u interp=%u, interp_term=%s): %s.\n", + jit_count, interp_count, term, kind); + std::printf(" The divergence is the terminator/branch at op #%u; inspect it.\n", + cmp_n); + } + std::printf("\n"); + } + if (diverged_files) + std::printf("[divtrace] %d of %zu captures diverged\n", + diverged_files, records.size()); + return diverged_files == 0 ? 0 : 5; +} + + +// Print the process-layout anchors the persisted-JIT program cache depends +// on. In a non-PIE build with the fixed-base arena reservation, every line +// must be identical across runs (the determinism gate); the image anchor is +// a libpcsx2 .text address standing in for every baked C-helper/global +// reference, the arena lines cover the rec slabs the cached code lives in. +int RunPrintBases() +{ + std::printf("[print-bases] image_anchor %p (SysMemory::GetDataPtr)\n", + reinterpret_cast(&SysMemory::GetDataPtr)); + std::printf("[print-bases] data_arena %p\n", SysMemory::GetDataPtr(0)); + std::printf("[print-bases] code_arena %p\n", SysMemory::GetCodePtr(0)); + std::printf("[print-bases] vu0_rec %p\n", SysMemory::GetVU0Rec()); + std::printf("[print-bases] vu1_rec %p\n", SysMemory::GetVU1Rec()); + std::printf("[print-bases] vu_mem %p\n", SysMemory::GetVUMem()); + return 0; +} + +int RunDumpAsm(const std::vector& records, + const std::vector& names) +{ + int failures = 0; + for (size_t fi = 0; fi < records.size(); ++fi) + { + const std::string out_path = names[fi] + ".codegen.s"; + const bool ok = recompiler_tests::DumpJitAsm(records[fi], out_path); + std::printf("[dump-asm] %s -> %s: %s\n", + names[fi].c_str(), out_path.c_str(), ok ? "ok" : "FAILED"); + if (!ok) + ++failures; + } + return failures == 0 ? 0 : 4; +} + +} // namespace + +int main(int argc, char** argv) +{ + Options opts; + if (!ParseArgs(argc, argv, opts)) + { + PrintUsage(argv[0]); + return 1; + } + + if (opts.no_progcache) + mVUPersist::SetProcessDisable(true); + + if (!opts.cache_dir.empty()) + { + // Must be wired before Initialize: mVUinit runs the ProgCache VERSION + // handshake against EmuFolders::Cache, and recording changes emitted + // code forms from the very first compile. The config bool is what + // production gates the whole feature on (vurunner doesn't go through + // VMManager, so set it directly); SetRecordingEnabled mixes the + // recording state into the options sentinel, so a --cache-dir run + // can never collide with a recording-off cache of the same programs. + if (mVUPersist::IsProcessDisabled()) + { + std::fprintf(stderr, "vurunner: --cache-dir ignored — " + "--no-progcache is set\n"); + } + else + { + EmuFolders::Cache = opts.cache_dir; + EmuConfig.Cpu.Recompiler.EnableVUProgramCache = true; + mVUPersist::SetRecordingEnabled(true); + } + } + + if (!recompiler_tests::RecompilerTestEnvironment::Initialize()) + { + std::fprintf(stderr, "vurunner: RecompilerTestEnvironment::Initialize failed\n"); + return 3; + } + + if (opts.print_bases) + { + const int rc = RunPrintBases(); + if (opts.files.empty()) + { + recompiler_tests::RecompilerTestEnvironment::Shutdown(); + return rc; + } + } + + std::vector records; + std::vector names; + records.reserve(opts.files.size()); + names.reserve(opts.files.size()); + for (const auto& path : opts.files) + { + vu_capture::CaptureRecord rec; + if (!vu_capture::ReadFromFile(path, rec)) + { + std::fprintf(stderr, "vurunner: failed to read %s (bad magic/version/sizes?)\n", + path.c_str()); + recompiler_tests::RecompilerTestEnvironment::Shutdown(); + return 1; + } + records.push_back(std::move(rec)); + names.push_back(path); + } + + int exit_code = 0; + if (opts.dump_microcode) + exit_code |= RunDumpMicrocode(records, names, opts.dump_count); + if (opts.diff) + exit_code |= RunDiff(records, names, opts.iters, opts.cycle_override); + if (opts.bench) + exit_code |= RunBench(records, names, opts.iters, opts.bench_no_reprime); + if (opts.dump_asm) + exit_code |= RunDumpAsm(records, names); + if (opts.divtrace) + exit_code |= RunDivTrace(records, names, opts.cycle_override); + + if (!opts.cache_dir.empty()) + { + // Flush still-live programs to disk so their saves show in the + // summary (mirrors the mVUclose-side save Shutdown would do). + recompiler_tests::RecompilerTestEnvironment::ResetVuBlockCache(0); + recompiler_tests::RecompilerTestEnvironment::ResetVuBlockCache(1); + for (u32 vu = 0; vu < 2; vu++) + { + const auto c = mVUProgCache::GetStats(vu); + const auto p = mVUPersist::GetStats(vu); + std::printf("[cache] vu%u: entries=%llu payloadWrites=%llu payloadHits=%llu " + "payloadMissing=%llu payloadRejects=%llu preloaded=%llu preloadHits=%llu " + "programsHydrated=%llu blocksHydrated=%llu chunksRecorded=%llu " + "chunksDropped=%llu blockCompiles=%llu\n", + vu, + (unsigned long long)c.entries, + (unsigned long long)c.payloadWrites, + (unsigned long long)c.payloadHits, + (unsigned long long)c.payloadMissing, + (unsigned long long)c.payloadRejects, + (unsigned long long)c.preloadedPayloads, + (unsigned long long)c.preloadHits, + (unsigned long long)p.programsHydrated, + (unsigned long long)p.blocksHydrated, + (unsigned long long)p.chunksRecorded, + (unsigned long long)p.chunksDropped, + (unsigned long long)mVUPersist::GetBlockCompileCount(vu)); + } + } + + recompiler_tests::RecompilerTestEnvironment::Shutdown(); + return exit_code; +} diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index c0dc49c3d3..9c11c73987 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -3617,6 +3617,19 @@ void VMManager::SetHardwareDependentDefaultSettings(SettingsInterface& si) const int extra_threads = (core_count > 3) ? 3 : 2; Console.WriteLn(fmt::format(" Setting Extra Software Rendering Threads to {}.", extra_threads)); si.SetIntValue("EmuCore/GS", "extrathreads", extra_threads); + + // Enable thread pinning by default on heterogeneous CPUs (big.LITTLE). + // Without it, the kernel may migrate the EE / VU / GS threads to E-cores + // mid-frame — even one such migration is enough to miss the 60fps deadline + // on Apple Silicon under Asahi and Intel Alder Lake-class hybrid systems. + // SetEmuThreadAffinities already sorts processors by frequency, so pinning + // to indices 0..2 lands on the fastest cores. + if (cpuinfo_get_clusters_count() > 1 && core_count >= 3) + { + Console.WriteLn(fmt::format(" Heterogeneous CPU detected ({} clusters); enabling thread pinning.", + cpuinfo_get_clusters_count())); + si.SetBoolValue("EmuCore", "EnableThreadPinning", true); + } } #elif defined(__APPLE__) From 5a8a666dc3ff4d175651b019554dc0ddf9297156 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sun, 21 Jun 2026 12:35:44 -0700 Subject: [PATCH 014/292] arm64: park LWL/LWR loaded word before Rt alloc (FlatOut 2 hang) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recUnalignedWord (LWL/LWR) left the loaded word in w0 across the _allocArm64GPR(rt) that follows the read, then used w0 in the merge. Under register pressure that alloc spills a guest reg (or lands Rt in x0) and clobbers w0, so the unaligned load returns garbage. Single-op LWL/LWR tests stay clean — it only bites with several unaligned loads live at once. This is the 32-bit twin of the Black (SLUS-21376) LDL/LDR bug: when that fix landed it patched recUnalignedLoadDouble only; recUnalignedWord had the identical hazard. FlatOut 2 (SLUS-21251) tripped it — its property- table parser reads packed name pointers via LWL/LWR under heavy pressure, so the corrupted pointer made the game's own "PropertyDb: mapped buffer too small" assert fire (break 0,1), whose BIOS handler clears Status (IE=0) and strands the in-flight SIF0 completion, wedging the EE. Fix mirrors recUnalignedLoadDouble: park the loaded word in a callee- saved temp (allocated before the read) and use it in the merge. Test: EeRecLoadStore.MultiRegUnalignedWordCopyBlock — an 8-word unaligned memcpy via LWL/LWR + SWL/SWR across 4x4 src/dst alignments; red on the unfixed path, green with the fix. 1004 recompiler_tests pass. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/recVTLB-arm64.cpp | 25 ++++++--- .../recompilers/ee_rec_loadstore_tests.cpp | 56 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/pcsx2/arm64/recVTLB-arm64.cpp b/pcsx2/arm64/recVTLB-arm64.cpp index 1cfc8ba91b..f66b08234f 100644 --- a/pcsx2/arm64/recVTLB-arm64.cpp +++ b/pcsx2/arm64/recVTLB-arm64.cpp @@ -825,9 +825,14 @@ static void recUnalignedWord(bool is_lwl) if (_Imm_ != 0) armAsm->Add(a64::w9, a64::w9, _Imm_); - // shift8 lives in a callee-saved temp so it survives vtlb's slow-path - // C call (fastmem backpatch thunk OR softmem slow path). + // shift8 and the loaded word both live in callee-saved temps. shift8 must + // survive vtlb's slow-path C call (fastmem backpatch thunk OR softmem slow + // path); the loaded word must survive the Rt alloc below — under register + // pressure _allocArm64GPR can spill a guest reg or land Rt in x0, clobbering + // w0 between the read and the merge. Same hazard fixed in + // recUnalignedLoadDouble; single-op tests miss it (it needs the pressure). const int shift8 = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); + const int memTemp = _allocArm64GPR(ARM64TYPE_TEMP, 0, MODE_CALLEESAVED); armAsm->And(armWRegister(shift8), a64::w9, 3); armAsm->Lsl(armWRegister(shift8), armWRegister(shift8), 3); @@ -841,10 +846,13 @@ static void recUnalignedWord(bool is_lwl) if (!_Rt_) { + _freeArm64GPR(memTemp); _freeArm64GPR(shift8); return; } + armAsm->Mov(armWRegister(memTemp), a64::w0); // park loaded (x0 unsafe across Rt alloc) + const int rt = _allocArm64GPR(ARM64TYPE_GPR, _Rt_, MODE_READ | MODE_WRITE); if (is_lwl) @@ -857,10 +865,10 @@ static void recUnalignedWord(bool is_lwl) // shifted_loaded = loaded << (24 - shift8); reuse RWSCRATCH as shift amount. armAsm->Mov(RWSCRATCH, 24); armAsm->Sub(RWSCRATCH, RWSCRATCH, armWRegister(shift8)); - armAsm->Lsl(a64::w0, a64::w0, RWSCRATCH); + armAsm->Lsl(armWRegister(memTemp), armWRegister(memTemp), RWSCRATCH); // Merge and sign-extend the 32-bit result into the 64-bit guest reg. - armAsm->Orr(armWRegister(rt), armWRegister(rt), a64::w0); + armAsm->Orr(armWRegister(rt), armWRegister(rt), armWRegister(memTemp)); armAsm->Sxtw(armXRegister(rt), armWRegister(rt)); } else @@ -875,20 +883,21 @@ static void recUnalignedWord(bool is_lwl) armAsm->Lsl(RSCRATCHADDR.W(), RSCRATCHADDR.W(), RWSCRATCH); armAsm->And(RWSCRATCH, armWRegister(rt), RSCRATCHADDR.W()); - armAsm->Lsr(a64::w0, a64::w0, armWRegister(shift8)); - armAsm->Orr(a64::w0, a64::w0, RWSCRATCH); + armAsm->Lsr(armWRegister(memTemp), armWRegister(memTemp), armWRegister(shift8)); + armAsm->Orr(armWRegister(memTemp), armWRegister(memTemp), RWSCRATCH); // Per interp: when shift8 != 0, only Rt[31:0] changes; upper 32 preserved. - armAsm->Bfi(armXRegister(rt), a64::x0, 0, 32); + armAsm->Bfi(armXRegister(rt), armXRegister(memTemp), 0, 32); armAsm->B(&done); // shift8 == 0 (aligned): straight sign-extend, full 64-bit overwrite. armAsm->Bind(&nomask); - armAsm->Sxtw(armXRegister(rt), a64::w0); + armAsm->Sxtw(armXRegister(rt), armWRegister(memTemp)); armAsm->Bind(&done); } + _freeArm64GPR(memTemp); _freeArm64GPR(shift8); } diff --git a/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp b/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp index 8e051b455b..e40d9bce37 100644 --- a/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_loadstore_tests.cpp @@ -577,3 +577,59 @@ TEST(EeRecLoadStore, MultiRegUnalignedDwordCopyBlock) } } } + +// Word twin of the dword pressure test above: an unaligned 32-byte memcpy built +// from LWL/LWR (load) + SWL/SWR (store), eight words in flight at once so the Rt +// allocation for each LWL/LWR spills a guest reg under pressure. recUnalignedWord +// left the loaded word in w0 across that alloc; the spill (or Rt landing in x0) +// clobbered it, so the loaded bytes came out wrong — only ever visible under +// pressure (single-op LWL/LWR above stay clean). This is the FlatOut 2 boot hang: +// the property-table parser reads packed name pointers via LWL/LWR and got +// garbage. Mirror of the Black LDL/LDR fix in recUnalignedLoadDouble. +TEST(EeRecLoadStore, MultiRegUnalignedWordCopyBlock) +{ + constexpr u32 kSrc = kScratch; + constexpr u32 kDst = kScratch + 256; + for (u32 sa = 0; sa < 4; ++sa) + { + for (u32 da = 0; da < 4; ++da) + { + SCOPED_TRACE(testing::Message() << "src_align=" << sa << " dst_align=" << da); + EeRecTestHarness h; + const u32 src = kSrc + sa; + const u32 dst = kDst + da; + // 32 distinct source bytes so any mis-shift/clobber is visible. + for (u32 i = 0; i < 32; ++i) + h.WriteU8(src + i, static_cast(0x10 + i)); + for (u32 i = 0; i < 40; ++i) + h.WriteU8(kDst + i, 0xA5); // sentinel; bytes outside [0,32) must survive + h.SetGpr64(reg::v1, src); + h.SetGpr64(reg::a0, dst); + h.TrackMemWindow(kDst, 40); + // Eight unaligned words loaded into eight regs (all live across each + // other's LWL/LWR Rt alloc), then stored back — the register pressure + // that exposes the w0 clobber. + h.LoadProgram({ + LWL(reg::v0, 3, reg::v1), LWR(reg::v0, 0, reg::v1), + LWL(reg::a2, 7, reg::v1), LWR(reg::a2, 4, reg::v1), + LWL(reg::a3, 11, reg::v1), LWR(reg::a3, 8, reg::v1), + LWL(reg::t0, 15, reg::v1), LWR(reg::t0, 12, reg::v1), + LWL(reg::t1, 19, reg::v1), LWR(reg::t1, 16, reg::v1), + LWL(reg::t2, 23, reg::v1), LWR(reg::t2, 20, reg::v1), + LWL(reg::t3, 27, reg::v1), LWR(reg::t3, 24, reg::v1), + LWL(reg::t4, 31, reg::v1), LWR(reg::t4, 28, reg::v1), + SWL(reg::v0, 3, reg::a0), SWR(reg::v0, 0, reg::a0), + SWL(reg::a2, 7, reg::a0), SWR(reg::a2, 4, reg::a0), + SWL(reg::a3, 11, reg::a0), SWR(reg::a3, 8, reg::a0), + SWL(reg::t0, 15, reg::a0), SWR(reg::t0, 12, reg::a0), + SWL(reg::t1, 19, reg::a0), SWR(reg::t1, 16, reg::a0), + SWL(reg::t2, 23, reg::a0), SWR(reg::t2, 20, reg::a0), + SWL(reg::t3, 27, reg::a0), SWR(reg::t3, 24, reg::a0), + SWL(reg::t4, 31, reg::a0), SWR(reg::t4, 28, reg::a0), + }); + h.Run(); // auto-diffs JIT vs interp (GPRs + tracked dest memory) + for (u32 i = 0; i < 32; ++i) + EXPECT_EQ(h.ReadU8(dst + i), static_cast(0x10 + i)) << "copied byte " << i; + } + } +} From 78f84827e6a42bb33572a585a90ba6d8bc7326e8 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sun, 21 Jun 2026 14:19:13 -0700 Subject: [PATCH 015/292] 3rdparty: re-vendor rapidyaml in-tree (revert upstream un-bundling) Upstream 0beb18c9e ("Deps: Update rapidyaml to v0.11.1", 2026-03-29) deleted the vendored 3rdparty/rapidyaml and switched to find_package(ryml REQUIRED), making ryml a system/external dependency. That breaks self-contained and cross builds (e.g. the rocknix handheld toolchain) which have no system ryml available. Restore the in-tree copy (v0.10.0, from 0beb18c9e^) and point the build back at it: add_subdirectory(3rdparty/rapidyaml) instead of find_package, and link rapidyaml::rapidyaml. The C++ side needs no changes: common/YAML.cpp is already version-guarded (0804b68fc "Common: Fix build with older ryml", RYML_VERSION_MINOR >= 11) so it compiles against v0.10 unchanged. Also drop ryml.dll from the Windows dep-copy list since it's now statically linked. This is a deliberate fork-local divergence from upstream's un-bundling, kept so downstream builders don't have to provision ryml themselves. Configures without a system ryml and builds end-to-end (common + pcsx2-qt) using the in-tree library. Co-Authored-By: Claude Opus 4.8 (1M context) --- 3rdparty/rapidyaml/CMakeLists.txt | 91 + 3rdparty/rapidyaml/include/c4/base64.hpp | 141 + 3rdparty/rapidyaml/include/c4/blob.hpp | 71 + 3rdparty/rapidyaml/include/c4/c4core.natvis | 168 + 3rdparty/rapidyaml/include/c4/charconv.hpp | 2670 ++++++ 3rdparty/rapidyaml/include/c4/compiler.hpp | 120 + 3rdparty/rapidyaml/include/c4/config.hpp | 39 + 3rdparty/rapidyaml/include/c4/cpu.hpp | 205 + 3rdparty/rapidyaml/include/c4/dump.hpp | 798 ++ 3rdparty/rapidyaml/include/c4/error.hpp | 440 + 3rdparty/rapidyaml/include/c4/export.hpp | 18 + 3rdparty/rapidyaml/include/c4/format.hpp | 1058 +++ 3rdparty/rapidyaml/include/c4/language.hpp | 358 + 3rdparty/rapidyaml/include/c4/memory_util.hpp | 782 ++ 3rdparty/rapidyaml/include/c4/platform.hpp | 46 + .../rapidyaml/include/c4/preprocessor.hpp | 123 + 3rdparty/rapidyaml/include/c4/std/std.hpp | 11 + 3rdparty/rapidyaml/include/c4/std/std_fwd.hpp | 10 + 3rdparty/rapidyaml/include/c4/std/string.hpp | 97 + .../rapidyaml/include/c4/std/string_fwd.hpp | 59 + .../rapidyaml/include/c4/std/string_view.hpp | 71 + 3rdparty/rapidyaml/include/c4/std/tuple.hpp | 184 + 3rdparty/rapidyaml/include/c4/std/vector.hpp | 88 + .../rapidyaml/include/c4/std/vector_fwd.hpp | 70 + 3rdparty/rapidyaml/include/c4/substr.hpp | 2290 +++++ 3rdparty/rapidyaml/include/c4/substr_fwd.hpp | 16 + 3rdparty/rapidyaml/include/c4/szconv.hpp | 68 + 3rdparty/rapidyaml/include/c4/types.hpp | 507 + 3rdparty/rapidyaml/include/c4/utf.hpp | 73 + 3rdparty/rapidyaml/include/c4/windows.hpp | 10 + 3rdparty/rapidyaml/include/c4/windows_pop.hpp | 41 + .../rapidyaml/include/c4/windows_push.hpp | 102 + 3rdparty/rapidyaml/include/c4/yml/common.hpp | 660 ++ .../include/c4/yml/detail/checks.hpp | 200 + .../include/c4/yml/detail/dbgprint.hpp | 129 + .../rapidyaml/include/c4/yml/detail/print.hpp | 184 + .../rapidyaml/include/c4/yml/detail/stack.hpp | 291 + .../rapidyaml/include/c4/yml/emit.def.hpp | 1084 +++ 3rdparty/rapidyaml/include/c4/yml/emit.hpp | 909 ++ .../include/c4/yml/event_handler_stack.hpp | 194 + .../include/c4/yml/event_handler_tree.hpp | 768 ++ 3rdparty/rapidyaml/include/c4/yml/export.hpp | 18 + .../include/c4/yml/filter_processor.hpp | 512 + 3rdparty/rapidyaml/include/c4/yml/fwd.hpp | 24 + 3rdparty/rapidyaml/include/c4/yml/node.hpp | 1679 ++++ .../rapidyaml/include/c4/yml/node_type.hpp | 282 + 3rdparty/rapidyaml/include/c4/yml/parse.hpp | 324 + .../include/c4/yml/parse_engine.def.hpp | 8317 +++++++++++++++++ .../rapidyaml/include/c4/yml/parse_engine.hpp | 799 ++ .../rapidyaml/include/c4/yml/parser_state.hpp | 212 + .../rapidyaml/include/c4/yml/preprocess.hpp | 97 + .../include/c4/yml/reference_resolver.hpp | 88 + 3rdparty/rapidyaml/include/c4/yml/std/map.hpp | 46 + 3rdparty/rapidyaml/include/c4/yml/std/std.hpp | 8 + .../rapidyaml/include/c4/yml/std/string.hpp | 9 + .../rapidyaml/include/c4/yml/std/vector.hpp | 59 + 3rdparty/rapidyaml/include/c4/yml/tag.hpp | 83 + 3rdparty/rapidyaml/include/c4/yml/tree.hpp | 1561 ++++ 3rdparty/rapidyaml/include/c4/yml/version.hpp | 25 + 3rdparty/rapidyaml/include/c4/yml/writer.hpp | 195 + 3rdparty/rapidyaml/include/c4/yml/yml.hpp | 16 + 3rdparty/rapidyaml/include/ryml-gdbtypes.py | 391 + 3rdparty/rapidyaml/include/ryml.hpp | 11 + 3rdparty/rapidyaml/include/ryml.natvis | 304 + 3rdparty/rapidyaml/include/ryml_std.hpp | 6 + 3rdparty/rapidyaml/rapidyaml.vcxproj | 139 + 3rdparty/rapidyaml/src/c4/base64.cpp | 226 + 3rdparty/rapidyaml/src/c4/error.cpp | 234 + 3rdparty/rapidyaml/src/c4/format.cpp | 64 + 3rdparty/rapidyaml/src/c4/language.cpp | 16 + 3rdparty/rapidyaml/src/c4/memory_util.cpp | 32 + 3rdparty/rapidyaml/src/c4/utf.cpp | 114 + 3rdparty/rapidyaml/src/c4/yml/common.cpp | 149 + 3rdparty/rapidyaml/src/c4/yml/node.cpp | 30 + 3rdparty/rapidyaml/src/c4/yml/node_type.cpp | 215 + 3rdparty/rapidyaml/src/c4/yml/parse.cpp | 146 + 3rdparty/rapidyaml/src/c4/yml/preprocess.cpp | 112 + .../src/c4/yml/reference_resolver.cpp | 333 + 3rdparty/rapidyaml/src/c4/yml/tag.cpp | 316 + 3rdparty/rapidyaml/src/c4/yml/tree.cpp | 1978 ++++ 3rdparty/rapidyaml/src/c4/yml/version.cpp | 27 + cmake/SearchForStuff.cmake | 4 +- common/CMakeLists.txt | 2 +- pcsx2/CMakeLists.txt | 2 +- 84 files changed, 34446 insertions(+), 3 deletions(-) create mode 100644 3rdparty/rapidyaml/CMakeLists.txt create mode 100644 3rdparty/rapidyaml/include/c4/base64.hpp create mode 100644 3rdparty/rapidyaml/include/c4/blob.hpp create mode 100644 3rdparty/rapidyaml/include/c4/c4core.natvis create mode 100644 3rdparty/rapidyaml/include/c4/charconv.hpp create mode 100644 3rdparty/rapidyaml/include/c4/compiler.hpp create mode 100644 3rdparty/rapidyaml/include/c4/config.hpp create mode 100644 3rdparty/rapidyaml/include/c4/cpu.hpp create mode 100644 3rdparty/rapidyaml/include/c4/dump.hpp create mode 100644 3rdparty/rapidyaml/include/c4/error.hpp create mode 100644 3rdparty/rapidyaml/include/c4/export.hpp create mode 100644 3rdparty/rapidyaml/include/c4/format.hpp create mode 100644 3rdparty/rapidyaml/include/c4/language.hpp create mode 100644 3rdparty/rapidyaml/include/c4/memory_util.hpp create mode 100644 3rdparty/rapidyaml/include/c4/platform.hpp create mode 100644 3rdparty/rapidyaml/include/c4/preprocessor.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/std.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/std_fwd.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/string.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/string_fwd.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/string_view.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/tuple.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/vector.hpp create mode 100644 3rdparty/rapidyaml/include/c4/std/vector_fwd.hpp create mode 100644 3rdparty/rapidyaml/include/c4/substr.hpp create mode 100644 3rdparty/rapidyaml/include/c4/substr_fwd.hpp create mode 100644 3rdparty/rapidyaml/include/c4/szconv.hpp create mode 100644 3rdparty/rapidyaml/include/c4/types.hpp create mode 100644 3rdparty/rapidyaml/include/c4/utf.hpp create mode 100644 3rdparty/rapidyaml/include/c4/windows.hpp create mode 100644 3rdparty/rapidyaml/include/c4/windows_pop.hpp create mode 100644 3rdparty/rapidyaml/include/c4/windows_push.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/common.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/detail/checks.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/detail/dbgprint.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/detail/print.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/detail/stack.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/emit.def.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/emit.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/event_handler_stack.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/event_handler_tree.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/export.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/filter_processor.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/fwd.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/node.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/node_type.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/parse.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/parse_engine.def.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/parse_engine.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/parser_state.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/preprocess.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/reference_resolver.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/std/map.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/std/std.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/std/string.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/std/vector.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/tag.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/tree.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/version.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/writer.hpp create mode 100644 3rdparty/rapidyaml/include/c4/yml/yml.hpp create mode 100644 3rdparty/rapidyaml/include/ryml-gdbtypes.py create mode 100644 3rdparty/rapidyaml/include/ryml.hpp create mode 100644 3rdparty/rapidyaml/include/ryml.natvis create mode 100644 3rdparty/rapidyaml/include/ryml_std.hpp create mode 100644 3rdparty/rapidyaml/rapidyaml.vcxproj create mode 100644 3rdparty/rapidyaml/src/c4/base64.cpp create mode 100644 3rdparty/rapidyaml/src/c4/error.cpp create mode 100644 3rdparty/rapidyaml/src/c4/format.cpp create mode 100644 3rdparty/rapidyaml/src/c4/language.cpp create mode 100644 3rdparty/rapidyaml/src/c4/memory_util.cpp create mode 100644 3rdparty/rapidyaml/src/c4/utf.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/common.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/node.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/node_type.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/parse.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/preprocess.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/reference_resolver.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/tag.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/tree.cpp create mode 100644 3rdparty/rapidyaml/src/c4/yml/version.cpp diff --git a/3rdparty/rapidyaml/CMakeLists.txt b/3rdparty/rapidyaml/CMakeLists.txt new file mode 100644 index 0000000000..b2375d8cf8 --- /dev/null +++ b/3rdparty/rapidyaml/CMakeLists.txt @@ -0,0 +1,91 @@ +add_library(pcsx2-rapidyaml + include/c4/base64.hpp + include/c4/blob.hpp + include/c4/charconv.hpp + include/c4/compiler.hpp + include/c4/config.hpp + include/c4/cpu.hpp + include/c4/dump.hpp + include/c4/error.hpp + include/c4/export.hpp + include/c4/format.hpp + include/c4/language.hpp + include/c4/memory_util.hpp + include/c4/platform.hpp + include/c4/preprocessor.hpp + include/c4/std/std.hpp + include/c4/std/std_fwd.hpp + include/c4/std/string.hpp + include/c4/std/string_fwd.hpp + include/c4/std/string_view.hpp + include/c4/std/tuple.hpp + include/c4/std/vector.hpp + include/c4/std/vector_fwd.hpp + include/c4/substr.hpp + include/c4/substr_fwd.hpp + include/c4/szconv.hpp + include/c4/types.hpp + include/c4/utf.hpp + include/c4/windows.hpp + include/c4/windows_pop.hpp + include/c4/windows_push.hpp + include/c4/yml/common.hpp + include/c4/yml/detail/dbgprint.hpp + include/c4/yml/detail/stack.hpp + include/c4/yml/emit.def.hpp + include/c4/yml/emit.hpp + include/c4/yml/event_handler_stack.hpp + include/c4/yml/event_handler_tree.hpp + include/c4/yml/filter_processor.hpp + include/c4/yml/fwd.hpp + include/c4/yml/export.hpp + include/c4/yml/node.hpp + include/c4/yml/node_type.hpp + include/c4/yml/parse.hpp + include/c4/yml/parse_engine.def.hpp + include/c4/yml/parse_engine.hpp + include/c4/yml/parser_state.hpp + include/c4/yml/reference_resolver.hpp + include/c4/yml/preprocess.hpp + include/c4/yml/std/map.hpp + include/c4/yml/std/std.hpp + include/c4/yml/std/string.hpp + include/c4/yml/std/vector.hpp + include/c4/yml/tag.hpp + include/c4/yml/tree.hpp + include/c4/yml/version.hpp + include/c4/yml/writer.hpp + include/c4/yml/yml.hpp + include/ryml.hpp + include/ryml_std.hpp + src/c4/base64.cpp + src/c4/error.cpp + src/c4/format.cpp + src/c4/language.cpp + src/c4/memory_util.cpp + src/c4/utf.cpp + src/c4/yml/common.cpp + src/c4/yml/node.cpp + src/c4/yml/parse.cpp + src/c4/yml/preprocess.cpp + src/c4/yml/tree.cpp + src/c4/yml/node_type.cpp + src/c4/yml/reference_resolver.cpp + src/c4/yml/tag.cpp + src/c4/yml/version.cpp +) + +target_include_directories(pcsx2-rapidyaml PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/../fast_float/include" +) +target_include_directories(pcsx2-rapidyaml INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include" +) + +target_compile_definitions(pcsx2-rapidyaml PUBLIC + "C4_NO_DEBUG_BREAK" +) + +add_library(rapidyaml::rapidyaml ALIAS pcsx2-rapidyaml) diff --git a/3rdparty/rapidyaml/include/c4/base64.hpp b/3rdparty/rapidyaml/include/c4/base64.hpp new file mode 100644 index 0000000000..4456c0ce83 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/base64.hpp @@ -0,0 +1,141 @@ +#ifndef _C4_BASE64_HPP_ +#define _C4_BASE64_HPP_ + +/** @file base64.hpp encoding/decoding for base64. + * @see https://en.wikipedia.org/wiki/Base64 + * @see https://www.base64encode.org/ + * */ + +#include "c4/charconv.hpp" +#include "c4/blob.hpp" + +namespace c4 { + +/** @defgroup doc_base64 Base64 encoding/decoding + * @see https://en.wikipedia.org/wiki/Base64 + * @see https://www.base64encode.org/ + * @{ */ + +/** check that the given buffer is a valid base64 encoding + * @see https://en.wikipedia.org/wiki/Base64 */ +C4CORE_EXPORT bool base64_valid(csubstr encoded); + + +/** base64-encode binary data. + * @param encoded [out] output buffer for encoded data + * @param data [in] the input buffer with the binary data + * + * @return the number of bytes needed to return the output (ie the + * required size for @p encoded). No writes occur beyond the end of + * the output buffer, so it is safe to do a speculative call where the + * encoded buffer is empty, or maybe too small. The caller should + * ensure that the returned size is smaller than the size of the + * encoded buffer. + * + * @note the result depends on endianness. If transfer between + * little/big endian systems is desired, the caller should normalize + * @p data before encoding. + * + * @see https://en.wikipedia.org/wiki/Base64 */ +C4CORE_EXPORT size_t base64_encode(substr encoded, cblob data); + + +/** decode the base64 encoding in the given buffer + * @param encoded [in] the encoded base64 + * @param data [out] the output buffer + * + * @return the number of bytes needed to return the output (ie the + * required size for @p data). No writes occur beyond the end of the + * output buffer, so it is safe to do a speculative call where the + * data buffer is empty, or maybe too small. The caller should ensure + * that the returned size is smaller than the size of the data buffer. + * + * @note the result depends on endianness. If transfer between + * little/big endian systems is desired, the caller should normalize + * @p data after decoding. + * + * @see https://en.wikipedia.org/wiki/Base64 */ +C4CORE_EXPORT size_t base64_decode(csubstr encoded, blob data); + +/** @} */ // base64 + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_base64_fmt Base64 + * @{ */ + +template +struct base64_wrapper_ +{ + blob_ data; + base64_wrapper_() : data() {} + base64_wrapper_(blob_ blob) : data(blob) {} +}; +/** a tag type to mark a payload as base64-encoded */ +using const_base64_wrapper = base64_wrapper_; +/** a tag type to mark a payload to be encoded as base64 */ +using base64_wrapper = base64_wrapper_; + + +/** mark a variable to be written in base64 format */ +template +C4_ALWAYS_INLINE const_base64_wrapper cbase64(Args const& C4_RESTRICT ...args) +{ + return const_base64_wrapper(cblob(args...)); +} +/** mark a csubstr to be written in base64 format */ +C4_ALWAYS_INLINE const_base64_wrapper cbase64(csubstr s) +{ + return const_base64_wrapper(cblob(s.str, s.len)); +} +/** mark a variable to be written in base64 format */ +template +C4_ALWAYS_INLINE const_base64_wrapper base64(Args const& C4_RESTRICT ...args) +{ + return const_base64_wrapper(cblob(args...)); +} +/** mark a csubstr to be written in base64 format */ +C4_ALWAYS_INLINE const_base64_wrapper base64(csubstr s) +{ + return const_base64_wrapper(cblob(s.str, s.len)); +} + +/** mark a variable to be read in base64 format */ +template +C4_ALWAYS_INLINE base64_wrapper base64(Args &... args) +{ + return base64_wrapper(blob(args...)); +} +/** mark a variable to be read in base64 format */ +C4_ALWAYS_INLINE base64_wrapper base64(substr s) +{ + return base64_wrapper(blob(s.str, s.len)); +} + +/** @} */ // base64_fmt + +/** @} */ // format_specifiers + +} // namespace fmt + + +/** write a variable in base64 format + * @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, fmt::const_base64_wrapper b) +{ + return base64_encode(buf, b.data); +} + +/** read a variable in base64 format + * @ingroup doc_from_chars */ +inline size_t from_chars(csubstr buf, fmt::base64_wrapper *b) +{ + return base64_decode(buf, b->data); +} + +} // namespace c4 + +#endif /* _C4_BASE64_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/blob.hpp b/3rdparty/rapidyaml/include/c4/blob.hpp new file mode 100644 index 0000000000..be241f2f4a --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/blob.hpp @@ -0,0 +1,71 @@ +#ifndef _C4_BLOB_HPP_ +#define _C4_BLOB_HPP_ + +#include "c4/types.hpp" +#include "c4/error.hpp" + +/** @file blob.hpp Mutable and immutable binary data blobs. +*/ + +namespace c4 { + +template +struct blob_; + +namespace detail { +template struct is_blob_type : std::integral_constant {}; +template struct is_blob_type> : std::integral_constant {}; +template struct is_blob_value_type : std::integral_constant::value || std::is_trivially_copyable::value)> {}; +} // namespace + +// NOLINTBEGIN(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) + +template +struct blob_ +{ + static_assert(std::is_same::value || std::is_same::value, "must be either byte or cbyte"); + static_assert(sizeof(T) == 1u, "must be either byte or cbyte"); + +public: + + T * buf; + size_t len; + +public: + + C4_ALWAYS_INLINE blob_() noexcept = default; + C4_ALWAYS_INLINE blob_(blob_ const& that) noexcept = default; + C4_ALWAYS_INLINE blob_(blob_ && that) noexcept = default; + C4_ALWAYS_INLINE blob_& operator=(blob_ && that) noexcept = default; + C4_ALWAYS_INLINE blob_& operator=(blob_ const& that) noexcept = default; + + template::value && std::is_same::type, T>::value, U>::type> C4_ALWAYS_INLINE blob_(blob_ const& that) noexcept : buf(that.buf), len(that.len) {} // NOLINT + template::value && std::is_same::type, T>::value, U>::type> C4_ALWAYS_INLINE blob_(blob_ && that) noexcept : buf(that.buf), len(that.len) {} // NOLINT + template::value && std::is_same::type, T>::value, U>::type> C4_ALWAYS_INLINE blob_& operator=(blob_ && that) noexcept { buf = that.buf; len = that.len; } // NOLINT + template::value && std::is_same::type, T>::value, U>::type> C4_ALWAYS_INLINE blob_& operator=(blob_ const& that) noexcept { buf = that.buf; len = that.len; } // NOLINT + + C4_ALWAYS_INLINE blob_(void *ptr, size_t n) noexcept : buf(reinterpret_cast(ptr)), len(n) {} // NOLINT + C4_ALWAYS_INLINE blob_(void const *ptr, size_t n) noexcept : buf(reinterpret_cast(ptr)), len(n) {} // NOLINT + + #define _C4_REQUIRE_BLOBTYPE(ty) class=typename std::enable_if<((!detail::is_blob_type::value) && (detail::is_blob_value_type::value)), T>::type + template C4_ALWAYS_INLINE blob_(U &var) noexcept : buf(reinterpret_cast(&var)), len(sizeof(U)) {} // NOLINT + template C4_ALWAYS_INLINE blob_(U *ptr, size_t n) noexcept : buf(reinterpret_cast(ptr)), len(sizeof(U) * n) { C4_ASSERT(is_aligned(ptr)); } // NOLINT + template C4_ALWAYS_INLINE blob_& operator= (U &var) noexcept { buf = reinterpret_cast(&var); len = sizeof(U); return *this; } // NOLINT + template C4_ALWAYS_INLINE blob_(U (&arr)[N]) noexcept : buf(reinterpret_cast(arr)), len(sizeof(U) * N) {} // NOLINT + template C4_ALWAYS_INLINE blob_& operator= (U (&arr)[N]) noexcept { buf = reinterpret_cast(arr); len = sizeof(U) * N; return *this; } // NOLINT + #undef _C4_REQUIRE_BLOBTYPE +}; + +// NOLINTEND(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) + +/** an immutable binary blob */ +using cblob = blob_; +/** a mutable binary blob */ +using blob = blob_< byte>; + +C4_MUST_BE_TRIVIAL_COPY(blob); +C4_MUST_BE_TRIVIAL_COPY(cblob); + +} // namespace c4 + +#endif // _C4_BLOB_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/c4core.natvis b/3rdparty/rapidyaml/include/c4/c4core.natvis new file mode 100644 index 0000000000..7cd138fe66 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/c4core.natvis @@ -0,0 +1,168 @@ + + + + + + + + {str,[len]} (sz={len}) + str,[len] + + len + + len + str + + + + + + {m_ptr,[m_size]} (sz={m_size}) + + m_size + + m_size + m_ptr + + + + + {m_ptr,[m_size]} (sz={m_size}, cap={m_capacity}) + + m_size + m_capacity + + m_size + m_ptr + + + + + + {m_ptr,[m_size]} (sz={m_size}) + m_ptr,[m_size] + + m_size + + m_size + m_ptr + + + + + {m_ptr,[m_size]} (sz={m_size}) + m_ptr,[m_size] + + m_size + + m_size + m_ptr + + + + + + {m_ptr,[m_size]} (sz={m_size}, cap={m_capacity}) + m_ptr,[m_size] + + m_size + m_capacity + + m_size + m_ptr + + + + + {m_ptr,[m_size]} (sz={m_size}, cap={m_capacity}) + m_ptr,[m_size] + + m_size + m_capacity + + m_size + m_ptr + + + + + + + {(($T3*)this)->m_str,[(($T3*)this)->m_size]} (sz={(($T3*)this)->m_size}) + (($T3*)this)->m_str,[(($T3*)this)->m_size] + + + {(($T3*)this)->m_str,[(($T3*)this)->m_size]} + (($T3*)this)->m_str,[(($T3*)this)->m_size] + + + {(($T3*)this)->m_size} + + + + + {m_str,[m_size]} (sz={m_size}) + m_str,[m_size] + + + {m_size} + + + + + {m_str,[m_size]} (sz={m_size},cap={m_capacity}) + m_str,[m_size] + + + {m_size} + + + {m_capacity} + + + {m_str,[m_capacity]} + m_str,[m_capacity] + + + + + {m_str,[m_size]} (sz={m_size},cap={m_capacity}) + m_str,[m_size] + + + {m_size} + + + {m_str,[m_capacity]} + m_str,[m_capacity] + + + + + + + {value} - {name} + + value + name + + + + {m_symbols,[m_num]} (sz={m_num}) + + m_num + + m_num + m_symbols + + + + + diff --git a/3rdparty/rapidyaml/include/c4/charconv.hpp b/3rdparty/rapidyaml/include/c4/charconv.hpp new file mode 100644 index 0000000000..d43bd17126 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/charconv.hpp @@ -0,0 +1,2670 @@ +#ifndef _C4_CHARCONV_HPP_ +#define _C4_CHARCONV_HPP_ + +/** @file charconv.hpp Lightweight generic type-safe wrappers for + * converting individual values to/from strings. + */ + +#include "c4/language.hpp" +#include +#include +#include +#include +#include + +#include "c4/config.hpp" +#include "c4/substr.hpp" +#include "c4/std/std_fwd.hpp" +#include "c4/memory_util.hpp" +#include "c4/szconv.hpp" + +#ifndef C4CORE_NO_FAST_FLOAT +# if (C4_CPP >= 17) +# if defined(_MSC_VER) +# if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019) // VS2017 and lower do not have these macros +# include +# define C4CORE_HAVE_STD_TOCHARS 1 +# define C4CORE_HAVE_STD_FROMCHARS 0 // prefer fast_float with MSVC +# define C4CORE_HAVE_FAST_FLOAT 1 +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# define C4CORE_HAVE_FAST_FLOAT 1 +# endif +# else +# if __has_include() +# include +# if defined(__cpp_lib_to_chars) +# define C4CORE_HAVE_STD_TOCHARS 1 +# define C4CORE_HAVE_STD_FROMCHARS 0 // glibc uses fast_float internally +# define C4CORE_HAVE_FAST_FLOAT 1 +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# define C4CORE_HAVE_FAST_FLOAT 1 +# endif +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# define C4CORE_HAVE_FAST_FLOAT 1 +# endif +# endif +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# define C4CORE_HAVE_FAST_FLOAT 1 +# endif +# if C4CORE_HAVE_FAST_FLOAT +#include "fast_float/fast_float.h" +# endif +#elif (C4_CPP >= 17) +# define C4CORE_HAVE_FAST_FLOAT 0 +# if defined(_MSC_VER) +# if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019) // VS2017 and lower do not have these macros +# include +# define C4CORE_HAVE_STD_TOCHARS 1 +# define C4CORE_HAVE_STD_FROMCHARS 1 +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# endif +# else +# if __has_include() +# include +# if defined(__cpp_lib_to_chars) +# define C4CORE_HAVE_STD_TOCHARS 1 +# define C4CORE_HAVE_STD_FROMCHARS 1 // glibc uses fast_float internally +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# endif +# else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# endif +# endif +#else +# define C4CORE_HAVE_STD_TOCHARS 0 +# define C4CORE_HAVE_STD_FROMCHARS 0 +# define C4CORE_HAVE_FAST_FLOAT 0 +#endif + + +#if !C4CORE_HAVE_STD_FROMCHARS +#include +#endif + + +#if defined(_MSC_VER) && !defined(__clang__) +# pragma warning(push) +# pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe +# if C4_MSVC_VERSION != C4_MSVC_VERSION_2017 +# pragma warning(disable: 4800) //'int': forcing value to bool 'true' or 'false' (performance warning) +# endif +#elif defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +# pragma clang diagnostic ignored "-Wformat-nonliteral" +# pragma clang diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +# pragma GCC diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision +# pragma GCC diagnostic ignored "-Wuseless-cast" +# pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + +#if defined(__clang__) +#define C4_NO_UBSAN_IOVRFLW __attribute__((no_sanitize("signed-integer-overflow"))) +#elif defined(__GNUC__) +#if __GNUC__ > 7 +#define C4_NO_UBSAN_IOVRFLW __attribute__((no_sanitize("signed-integer-overflow"))) +#else +#define C4_NO_UBSAN_IOVRFLW +#endif +#else +#define C4_NO_UBSAN_IOVRFLW +#endif + +// NOLINTBEGIN(hicpp-signed-bitwise) + +namespace c4 { + +/** @defgroup doc_charconv Charconv utilities + * + * Lightweight, very fast generic type-safe wrappers for converting + * individual values to/from strings. These are the main generic + * functions: + * - @ref doc_to_chars and its alias @ref xtoa(): implemented by calling @ref itoa() / @ref utoa() / @ref ftoa() / @ref dtoa() (or generically @ref xtoa()) + * - @ref doc_from_chars and its alias @ref atox(): implemented by calling @ref atoi() / @ref atou() / @ref atof() / @ref atod() (or generically @ref atox()) + * - @ref to_chars_sub() + * - @ref from_chars_first() + * - @ref xtoa() and @ref atox() are implemented in terms of @ref write_dec() / @ref read_dec() et al (see @ref doc_write / @ref doc_read()) + * + * And also some modest brag is in order: these functions are really + * fast: faster even than C++17 `std::to_chars()` and + * `std::to_chars()`, and many dozens of times faster than the + * iostream abominations. + * + * For example, here are some benchmark comparisons for @ref + * doc_from_chars (link leads to the main project README, where these + * results are shown more systematically). + * + * + * + *
atox,int64_t
g++12, linux Visual Studio 2019 + *
\image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-atox-mega_bytes_per_second-i64.png \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-atox-mega_bytes_per_second-i64.png + *
+ * + * + * + *
xtoa,int64_t
g++12, linux Visual Studio 2019 + *
\image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-xtoa-mega_bytes_per_second-i64.png \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-xtoa-mega_bytes_per_second-i64.png + *
+ * + * To parse floating point, c4core uses + * [fastfloat](https://github.com/fastfloat/fast_float), which is + * extremely fast, by an even larger factor: + * + * + * + *
atox,float
g++12, linux Visual Studio 2019 + *
\image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-atof-mega_bytes_per_second-float.png \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-atof-mega_bytes_per_second-float.png + *
+ * + * @{ + */ + +#if C4CORE_HAVE_STD_TOCHARS +/** @warning Use only the symbol. Do not rely on the type or naked value of this enum. */ +typedef enum : std::underlying_type::type { + /** print the real number in floating point format (like %f) */ + FTOA_FLOAT = static_cast::type>(std::chars_format::fixed), + /** print the real number in scientific format (like %e) */ + FTOA_SCIENT = static_cast::type>(std::chars_format::scientific), + /** print the real number in flexible format (like %g) */ + FTOA_FLEX = static_cast::type>(std::chars_format::general), + /** print the real number in hexadecimal format (like %a) */ + FTOA_HEXA = static_cast::type>(std::chars_format::hex), +} RealFormat_e; +#else +/** @warning Use only the symbol. Do not rely on the type or naked value of this enum. */ +typedef enum : char { + /** print the real number in floating point format (like %f) */ + FTOA_FLOAT = 'f', + /** print the real number in scientific format (like %e) */ + FTOA_SCIENT = 'e', + /** print the real number in flexible format (like %g) */ + FTOA_FLEX = 'g', + /** print the real number in hexadecimal format (like %a) */ + FTOA_HEXA = 'a', +} RealFormat_e; +#endif + +/** @cond dev */ +/** in some platforms, int,unsigned int + * are not any of int8_t...int64_t and + * long,unsigned long are not any of uint8_t...uint64_t */ +template +struct is_fixed_length +{ + enum : bool { + /** true if T is one of the fixed length signed types */ + value_i = (std::is_integral::value + && (std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value)), + /** true if T is one of the fixed length unsigned types */ + value_u = (std::is_integral::value + && (std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value)), + /** true if T is one of the fixed length signed or unsigned types */ + value = value_i || value_u + }; +}; +/** @endcond */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(__clang__) +# pragma warning(push) +#elif defined(__clang__) +# pragma clang diagnostic push +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wconversion" +# if __GNUC__ >= 6 +# pragma GCC diagnostic ignored "-Wnull-dereference" +# endif +#endif + +/** @cond dev */ +namespace detail { + +/* python command to get the values below: +def dec(v): + return str(v) +for bits in (8, 16, 32, 64): + imin, imax, umax = (-(1 << (bits - 1))), (1 << (bits - 1)) - 1, (1 << bits) - 1 + for vname, v in (("imin", imin), ("imax", imax), ("umax", umax)): + for f in (bin, oct, dec, hex): + print(f"{bits}b: {vname}={v} {f.__name__}: len={len(f(v)):2d}: {v} {f(v)}") +*/ + +// do not use the type as the template argument because in some +// platforms long!=int32 and long!=int64. Just use the numbytes +// which is more generic and spares lengthy SFINAE code. +template struct charconv_digits_; +template using charconv_digits = charconv_digits_::value>; + +template<> struct charconv_digits_<1u, true> // int8_t +{ + enum : size_t { + maxdigits_bin = 1 + 2 + 8, // -128==-0b10000000 + maxdigits_oct = 1 + 2 + 3, // -128==-0o200 + maxdigits_dec = 1 + 3, // -128 + maxdigits_hex = 1 + 2 + 2, // -128==-0x80 + maxdigits_bin_nopfx = 8, // -128==-0b10000000 + maxdigits_oct_nopfx = 3, // -128==-0o200 + maxdigits_dec_nopfx = 3, // -128 + maxdigits_hex_nopfx = 2, // -128==-0x80 + }; + // min values without sign! + static constexpr csubstr min_value_dec() noexcept { return csubstr("128"); } + static constexpr csubstr min_value_hex() noexcept { return csubstr("80"); } + static constexpr csubstr min_value_oct() noexcept { return csubstr("200"); } + static constexpr csubstr min_value_bin() noexcept { return csubstr("10000000"); } + static constexpr csubstr max_value_dec() noexcept { return csubstr("127"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 3) || (str.len == 3 && str[0] <= '1')); } +}; +template<> struct charconv_digits_<1u, false> // uint8_t +{ + enum : size_t { + maxdigits_bin = 2 + 8, // 255 0b11111111 + maxdigits_oct = 2 + 3, // 255 0o377 + maxdigits_dec = 3, // 255 + maxdigits_hex = 2 + 2, // 255 0xff + maxdigits_bin_nopfx = 8, // 255 0b11111111 + maxdigits_oct_nopfx = 3, // 255 0o377 + maxdigits_dec_nopfx = 3, // 255 + maxdigits_hex_nopfx = 2, // 255 0xff + }; + static constexpr csubstr max_value_dec() noexcept { return csubstr("255"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 3) || (str.len == 3 && str[0] <= '3')); } +}; +template<> struct charconv_digits_<2u, true> // int16_t +{ + enum : size_t { + maxdigits_bin = 1 + 2 + 16, // -32768 -0b1000000000000000 + maxdigits_oct = 1 + 2 + 6, // -32768 -0o100000 + maxdigits_dec = 1 + 5, // -32768 -32768 + maxdigits_hex = 1 + 2 + 4, // -32768 -0x8000 + maxdigits_bin_nopfx = 16, // -32768 -0b1000000000000000 + maxdigits_oct_nopfx = 6, // -32768 -0o100000 + maxdigits_dec_nopfx = 5, // -32768 -32768 + maxdigits_hex_nopfx = 4, // -32768 -0x8000 + }; + // min values without sign! + static constexpr csubstr min_value_dec() noexcept { return csubstr("32768"); } + static constexpr csubstr min_value_hex() noexcept { return csubstr("8000"); } + static constexpr csubstr min_value_oct() noexcept { return csubstr("100000"); } + static constexpr csubstr min_value_bin() noexcept { return csubstr("1000000000000000"); } + static constexpr csubstr max_value_dec() noexcept { return csubstr("32767"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 6)); } +}; +template<> struct charconv_digits_<2u, false> // uint16_t +{ + enum : size_t { + maxdigits_bin = 2 + 16, // 65535 0b1111111111111111 + maxdigits_oct = 2 + 6, // 65535 0o177777 + maxdigits_dec = 6, // 65535 65535 + maxdigits_hex = 2 + 4, // 65535 0xffff + maxdigits_bin_nopfx = 16, // 65535 0b1111111111111111 + maxdigits_oct_nopfx = 6, // 65535 0o177777 + maxdigits_dec_nopfx = 6, // 65535 65535 + maxdigits_hex_nopfx = 4, // 65535 0xffff + }; + static constexpr csubstr max_value_dec() noexcept { return csubstr("65535"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 6) || (str.len == 6 && str[0] <= '1')); } +}; +template<> struct charconv_digits_<4u, true> // int32_t +{ + enum : size_t { + maxdigits_bin = 1 + 2 + 32, // len=35: -2147483648 -0b10000000000000000000000000000000 + maxdigits_oct = 1 + 2 + 11, // len=14: -2147483648 -0o20000000000 + maxdigits_dec = 1 + 10, // len=11: -2147483648 -2147483648 + maxdigits_hex = 1 + 2 + 8, // len=11: -2147483648 -0x80000000 + maxdigits_bin_nopfx = 32, // len=35: -2147483648 -0b10000000000000000000000000000000 + maxdigits_oct_nopfx = 11, // len=14: -2147483648 -0o20000000000 + maxdigits_dec_nopfx = 10, // len=11: -2147483648 -2147483648 + maxdigits_hex_nopfx = 8, // len=11: -2147483648 -0x80000000 + }; + // min values without sign! + static constexpr csubstr min_value_dec() noexcept { return csubstr("2147483648"); } + static constexpr csubstr min_value_hex() noexcept { return csubstr("80000000"); } + static constexpr csubstr min_value_oct() noexcept { return csubstr("20000000000"); } + static constexpr csubstr min_value_bin() noexcept { return csubstr("10000000000000000000000000000000"); } + static constexpr csubstr max_value_dec() noexcept { return csubstr("2147483647"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 11) || (str.len == 11 && str[0] <= '1')); } +}; +template<> struct charconv_digits_<4u, false> // uint32_t +{ + enum : size_t { + maxdigits_bin = 2 + 32, // len=34: 4294967295 0b11111111111111111111111111111111 + maxdigits_oct = 2 + 11, // len=13: 4294967295 0o37777777777 + maxdigits_dec = 10, // len=10: 4294967295 4294967295 + maxdigits_hex = 2 + 8, // len=10: 4294967295 0xffffffff + maxdigits_bin_nopfx = 32, // len=34: 4294967295 0b11111111111111111111111111111111 + maxdigits_oct_nopfx = 11, // len=13: 4294967295 0o37777777777 + maxdigits_dec_nopfx = 10, // len=10: 4294967295 4294967295 + maxdigits_hex_nopfx = 8, // len=10: 4294967295 0xffffffff + }; + static constexpr csubstr max_value_dec() noexcept { return csubstr("4294967295"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 11) || (str.len == 11 && str[0] <= '3')); } +}; +template<> struct charconv_digits_<8u, true> // int64_t +{ + enum : size_t { + maxdigits_bin = 1 + 2 + 64, // len=67: -9223372036854775808 -0b1000000000000000000000000000000000000000000000000000000000000000 + maxdigits_oct = 1 + 2 + 22, // len=25: -9223372036854775808 -0o1000000000000000000000 + maxdigits_dec = 1 + 19, // len=20: -9223372036854775808 -9223372036854775808 + maxdigits_hex = 1 + 2 + 16, // len=19: -9223372036854775808 -0x8000000000000000 + maxdigits_bin_nopfx = 64, // len=67: -9223372036854775808 -0b1000000000000000000000000000000000000000000000000000000000000000 + maxdigits_oct_nopfx = 22, // len=25: -9223372036854775808 -0o1000000000000000000000 + maxdigits_dec_nopfx = 19, // len=20: -9223372036854775808 -9223372036854775808 + maxdigits_hex_nopfx = 16, // len=19: -9223372036854775808 -0x8000000000000000 + }; + static constexpr csubstr min_value_dec() noexcept { return csubstr("9223372036854775808"); } + static constexpr csubstr min_value_hex() noexcept { return csubstr("8000000000000000"); } + static constexpr csubstr min_value_oct() noexcept { return csubstr("1000000000000000000000"); } + static constexpr csubstr min_value_bin() noexcept { return csubstr("1000000000000000000000000000000000000000000000000000000000000000"); } + static constexpr csubstr max_value_dec() noexcept { return csubstr("9223372036854775807"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 22)); } +}; +template<> struct charconv_digits_<8u, false> // uint64_t +{ + enum : size_t { + maxdigits_bin = 2 + 64, // len=66: 18446744073709551615 0b1111111111111111111111111111111111111111111111111111111111111111 + maxdigits_oct = 2 + 22, // len=24: 18446744073709551615 0o1777777777777777777777 + maxdigits_dec = 20, // len=20: 18446744073709551615 18446744073709551615 + maxdigits_hex = 2 + 16, // len=18: 18446744073709551615 0xffffffffffffffff + maxdigits_bin_nopfx = 64, // len=66: 18446744073709551615 0b1111111111111111111111111111111111111111111111111111111111111111 + maxdigits_oct_nopfx = 22, // len=24: 18446744073709551615 0o1777777777777777777777 + maxdigits_dec_nopfx = 20, // len=20: 18446744073709551615 18446744073709551615 + maxdigits_hex_nopfx = 16, // len=18: 18446744073709551615 0xffffffffffffffff + }; + static constexpr csubstr max_value_dec() noexcept { return csubstr("18446744073709551615"); } + static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 22) || (str.len == 22 && str[0] <= '1')); } +}; +} // namespace detail + +// Helper macros, undefined below +#define _c4append(c) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = static_cast(c); } else { ++pos; } } +#define _c4appendhex(i) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = hexchars[i]; } else { ++pos; } } + +/** @endcond */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** @defgroup doc_digits Get number of digits + * + * @note At first sight this code may look heavily branchy and + * therefore inefficient. However, measurements revealed this to be + * the fastest among the alternatives. + * + * @see https://github.com/biojppm/c4core/pull/77 + * + * @{ + */ + +/** decimal digits for 8 bit integers */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE +auto digits_dec(T v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + return ((v >= 100) ? 3u : ((v >= 10) ? 2u : 1u)); +} + +/** decimal digits for 16 bit integers */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE +auto digits_dec(T v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + return ((v >= 10000) ? 5u : (v >= 1000) ? 4u : (v >= 100) ? 3u : (v >= 10) ? 2u : 1u); +} + +/** decimal digits for 32 bit integers */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE +auto digits_dec(T v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + return ((v >= 1000000000) ? 10u : (v >= 100000000) ? 9u : (v >= 10000000) ? 8u : + (v >= 1000000) ? 7u : (v >= 100000) ? 6u : (v >= 10000) ? 5u : + (v >= 1000) ? 4u : (v >= 100) ? 3u : (v >= 10) ? 2u : 1u); +} + +/** decimal digits for 64 bit integers */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE +auto digits_dec(T v) noexcept + -> typename std::enable_if::type +{ + // thanks @fargies!!! + // https://github.com/biojppm/c4core/pull/77#issuecomment-1063753568 + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + if(v >= 1000000000) // 10 + { + if(v >= 100000000000000) // 15 [15-20] range + { + if(v >= 100000000000000000) // 18 (15 + (20 - 15) / 2) + { + if((typename std::make_unsigned::type)v >= 10000000000000000000u) // 20 + return 20u; + else + return (v >= 1000000000000000000) ? 19u : 18u; + } + else if(v >= 10000000000000000) // 17 + return 17u; + else + return(v >= 1000000000000000) ? 16u : 15u; + } + else if(v >= 1000000000000) // 13 + return (v >= 10000000000000) ? 14u : 13u; + else if(v >= 100000000000) // 12 + return 12; + else + return(v >= 10000000000) ? 11u : 10u; + } + else if(v >= 10000) // 5 [5-9] range + { + if(v >= 10000000) // 8 + return (v >= 100000000) ? 9u : 8u; + else if(v >= 1000000) // 7 + return 7; + else + return (v >= 100000) ? 6u : 5u; + } + else if(v >= 100) + return (v >= 1000) ? 4u : 3u; + else + return (v >= 10) ? 2u : 1u; +} + + +/** return the number of digits required to encode an hexadecimal number. */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_hex(T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + return v ? 1u + (msb((typename std::make_unsigned::type)v) >> 2u) : 1u; +} + +/** return the number of digits required to encode a binary number. */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_bin(T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + return v ? 1u + msb((typename std::make_unsigned::type)v) : 1u; +} + +/** return the number of digits required to encode an octal number. */ +template +C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_oct(T v_) noexcept +{ + // TODO: is there a better way? + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v_ >= 0); + using U = typename std::conditional::type>::type; + U v = (U) v_; // safe because we require v_ >= 0 // NOLINT + uint32_t __n = 1; + enum : U { + __b2 = 64u, + __b3 = 64u * 8u, + __b4 = 64u * 8u * 8u, + }; + while(true) + { + if(v < 8u) + return __n; + else if(v < __b2) + return __n + 1; + else if(v < __b3) + return __n + 2; + else if(v < __b4) + return __n + 3; + v /= (U) __b4; + __n += 4; + } +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +namespace detail { +C4_INLINE_CONSTEXPR const char hexchars[] = "0123456789abcdef"; +C4_INLINE_CONSTEXPR const char digits0099[] = + "0001020304050607080910111213141516171819" + "2021222324252627282930313233343536373839" + "4041424344454647484950515253545556575859" + "6061626364656667686970717273747576777879" + "8081828384858687888990919293949596979899"; +} // namespace detail +/** @endcond */ + +C4_SUPPRESS_WARNING_GCC_PUSH +C4_SUPPRESS_WARNING_GCC("-Warray-bounds") // gcc has false positives here +#if (defined(__GNUC__) && (__GNUC__ >= 7)) +C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow") // gcc has false positives here +#endif + +/** @defgroup doc_write_unchecked Write with known number of digits + * + * Writes a value without checking the buffer length with regards to + * the required number of digits to encode the value. It is the + * responsibility of the caller to ensure that the provided number of + * digits is enough to write the given value. Notwithstanding the + * name, assertions are liberally performed, so this code is safe. + * + * @{ */ + +template +C4_HOT C4_ALWAYS_INLINE +void write_dec_unchecked(substr buf, T v, unsigned digits_v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + C4_ASSERT(buf.len >= digits_v); + C4_XASSERT(digits_v == digits_dec(v)); + // in bm_xtoa: checkoncelog_singlediv_write2 + while(v >= T(100)) + { + T quo = v; + quo /= T(100); + const auto num = (v - quo * T(100)) << 1u; // NOLINT + v = quo; + buf.str[--digits_v] = detail::digits0099[num + 1]; + buf.str[--digits_v] = detail::digits0099[num]; + } + if(v >= T(10)) + { + C4_ASSERT(digits_v == 2); + const auto num = v << 1u; + buf.str[1] = detail::digits0099[num + 1]; + buf.str[0] = detail::digits0099[num]; + } + else + { + C4_ASSERT(digits_v == 1); + buf.str[0] = (char)('0' + v); + } +} + + +template +C4_HOT C4_ALWAYS_INLINE +void write_hex_unchecked(substr buf, T v, unsigned digits_v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + C4_ASSERT(buf.len >= digits_v); + C4_XASSERT(digits_v == digits_hex(v)); + do { + buf.str[--digits_v] = detail::hexchars[v & T(15)]; + v >>= 4; + } while(v); + C4_ASSERT(digits_v == 0); +} + + +template +C4_HOT C4_ALWAYS_INLINE +void write_oct_unchecked(substr buf, T v, unsigned digits_v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + C4_ASSERT(buf.len >= digits_v); + C4_XASSERT(digits_v == digits_oct(v)); + do { + buf.str[--digits_v] = (char)('0' + (v & T(7))); + v >>= 3; + } while(v); + C4_ASSERT(digits_v == 0); +} + + +template +C4_HOT C4_ALWAYS_INLINE +void write_bin_unchecked(substr buf, T v, unsigned digits_v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + C4_ASSERT(buf.len >= digits_v); + C4_XASSERT(digits_v == digits_bin(v)); + do { + buf.str[--digits_v] = (char)('0' + (v & T(1))); + v >>= 1; + } while(v); + C4_ASSERT(digits_v == 0); +} + +/** @} */ // write_unchecked + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_write Write a value + * + * Writes a value without checking the buffer length + * decimal number -- but asserting. + * + * @{ */ + +/** write an integer to a string in decimal format. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the required size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t write_dec(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + unsigned digits = digits_dec(v); + if(C4_LIKELY(buf.len >= digits)) + write_dec_unchecked(buf, v, digits); + return digits; +} + +/** write an integer to a string in hexadecimal format. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not prefix with 0x + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the required size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t write_hex(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + unsigned digits = digits_hex(v); + if(C4_LIKELY(buf.len >= digits)) + write_hex_unchecked(buf, v, digits); + return digits; +} + +/** write an integer to a string in octal format. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not prefix with 0o + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the required size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t write_oct(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + unsigned digits = digits_oct(v); + if(C4_LIKELY(buf.len >= digits)) + write_oct_unchecked(buf, v, digits); + return digits; +} + +/** write an integer to a string in binary format. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not prefix with 0b + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the required size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t write_bin(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(v >= 0); + unsigned digits = digits_bin(v); + C4_ASSERT(digits > 0); + if(C4_LIKELY(buf.len >= digits)) + write_bin_unchecked(buf, v, digits); + return digits; +} + + +/** @cond dev */ +namespace detail { +template using NumberWriter = size_t (*)(substr, U); +template writer> +size_t write_num_digits(substr buf, T v, size_t num_digits) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + const size_t ret = writer(buf, v); + if(ret >= num_digits) + return ret; + else if(ret >= buf.len || num_digits > buf.len) + return num_digits; + C4_ASSERT(num_digits >= ret); + const size_t delta = static_cast(num_digits - ret); // NOLINT + C4_ASSERT(ret + delta <= buf.len); + if(ret) + memmove(buf.str + delta, buf.str, ret); + if(delta) + memset(buf.str, '0', delta); + return num_digits; +} +} // namespace detail +/** @endcond */ + + +/** same as c4::write_dec(), but pad with zeroes on the left + * such that the resulting string is @p num_digits wide. + * If the given number is requires more than num_digits, then the number prevails. */ +template +C4_ALWAYS_INLINE size_t write_dec(substr buf, T val, size_t num_digits) noexcept +{ + return detail::write_num_digits>(buf, val, num_digits); +} + +/** same as c4::write_hex(), but pad with zeroes on the left + * such that the resulting string is @p num_digits wide. + * If the given number is requires more than num_digits, then the number prevails. */ +template +C4_ALWAYS_INLINE size_t write_hex(substr buf, T val, size_t num_digits) noexcept +{ + return detail::write_num_digits>(buf, val, num_digits); +} + +/** same as c4::write_bin(), but pad with zeroes on the left + * such that the resulting string is @p num_digits wide. + * If the given number is requires more than num_digits, then the number prevails. */ +template +C4_ALWAYS_INLINE size_t write_bin(substr buf, T val, size_t num_digits) noexcept +{ + return detail::write_num_digits>(buf, val, num_digits); +} + +/** same as c4::write_oct(), but pad with zeroes on the left + * such that the resulting string is @p num_digits wide. + * If the given number is requires more than num_digits, then the number prevails. */ +template +C4_ALWAYS_INLINE size_t write_oct(substr buf, T val, size_t num_digits) noexcept +{ + return detail::write_num_digits>(buf, val, num_digits); +} + +/** @} */ // write + +C4_SUPPRESS_WARNING_GCC_POP + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +C4_SUPPRESS_WARNING_MSVC_PUSH +C4_SUPPRESS_WARNING_MSVC(4365) // '=': conversion from 'int' to 'I', signed/unsigned mismatch + +/** @defgroup doc_read Read a value + * + * @{ */ + +/** read a decimal integer from a string. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note The string must be trimmed. Whitespace is not accepted. + * @note the string must not be empty + * @note there is no check for overflow; the value wraps around + * in a way similar to the standard C/C++ overflow behavior. + * For example, `read_dec("128", &val)` returns true + * and val will be set to 0 because 127 is the max i8 value. + * @see overflows() to find out if a number string overflows a type range + * @return true if the conversion was successful (no overflow check) */ +template +C4_NO_UBSAN_IOVRFLW +C4_ALWAYS_INLINE bool read_dec(csubstr s, I *C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(!s.empty()); + *v = 0; + for(char c : s) + { + if(C4_UNLIKELY(c < '0' || c > '9')) + return false; + *v = (*v) * I(10) + (I(c) - I('0')); + } + return true; +} + +/** read an hexadecimal integer from a string. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not accept leading 0x or 0X + * @note the string must not be empty + * @note the string must be trimmed. Whitespace is not accepted. + * @note there is no check for overflow; the value wraps around + * in a way similar to the standard C/C++ overflow behavior. + * For example, `read_hex("80", &val)` returns true + * and val will be set to 0 because 7f is the max i8 value. + * @see overflows() to find out if a number string overflows a type range + * @return true if the conversion was successful (no overflow check) */ +template +C4_NO_UBSAN_IOVRFLW +C4_ALWAYS_INLINE bool read_hex(csubstr s, I *C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(!s.empty()); + *v = 0; + for(char c : s) + { + I cv; + if(c >= '0' && c <= '9') + cv = I(c) - I('0'); + else if(c >= 'a' && c <= 'f') + cv = I(10) + (I(c) - I('a')); + else if(c >= 'A' && c <= 'F') + cv = I(10) + (I(c) - I('A')); + else + return false; + *v = (*v) * I(16) + cv; + } + return true; +} + +/** read a binary integer from a string. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not accept leading 0b or 0B + * @note the string must not be empty + * @note the string must be trimmed. Whitespace is not accepted. + * @note there is no check for overflow; the value wraps around + * in a way similar to the standard C/C++ overflow behavior. + * For example, `read_bin("10000000", &val)` returns true + * and val will be set to 0 because 1111111 is the max i8 value. + * @see overflows() to find out if a number string overflows a type range + * @return true if the conversion was successful (no overflow check) */ +template +C4_NO_UBSAN_IOVRFLW +C4_ALWAYS_INLINE bool read_bin(csubstr s, I *C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(!s.empty()); + *v = 0; + for(char c : s) + { + *v <<= 1; + if(c == '1') + *v |= 1; + else if(c != '0') + return false; + } + return true; +} + +/** read an octal integer from a string. This is the + * lowest level (and the fastest) function to do this task. + * @note does not accept negative numbers + * @note does not accept leading 0o or 0O + * @note the string must not be empty + * @note the string must be trimmed. Whitespace is not accepted. + * @note there is no check for overflow; the value wraps around + * in a way similar to the standard C/C++ overflow behavior. + * For example, `read_oct("200", &val)` returns true + * and val will be set to 0 because 177 is the max i8 value. + * @see overflows() to find out if a number string overflows a type range + * @return true if the conversion was successful (no overflow check) */ +template +C4_NO_UBSAN_IOVRFLW +C4_ALWAYS_INLINE bool read_oct(csubstr s, I *C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_ASSERT(!s.empty()); + *v = 0; + for(char c : s) + { + if(C4_UNLIKELY(c < '0' || c > '7')) + return false; + *v = (*v) * I(8) + (I(c) - I('0')); + } + return true; +} + +/** @} */ + +C4_SUPPRESS_WARNING_MSVC_POP + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wswitch-default") + +/** @cond dev */ +namespace detail { +inline size_t _itoa2buf(substr buf, size_t pos, csubstr val) noexcept +{ + C4_ASSERT(pos < buf.len); + C4_ASSERT(pos + val.len <= buf.len); + C4_ASSERT(val.len > 0); + memcpy(buf.str + pos, val.str, val.len); + return pos + val.len; +} +inline size_t _itoa2bufwithdigits(substr buf, size_t pos, size_t num_digits, csubstr val) noexcept +{ + num_digits = num_digits > val.len ? num_digits - val.len : 0; + C4_ASSERT(num_digits + val.len <= buf.len); + for(size_t i = 0; i < num_digits; ++i) + _c4append('0'); + return detail::_itoa2buf(buf, pos, val); +} +template +C4_NO_INLINE size_t _itoadec2buf(substr buf) noexcept +{ + using digits_type = detail::charconv_digits; + if(C4_UNLIKELY(buf.len < digits_type::maxdigits_dec)) + return digits_type::maxdigits_dec; + buf.str[0] = '-'; + return detail::_itoa2buf(buf, 1, digits_type::min_value_dec()); +} +template +C4_NO_INLINE size_t _itoa2buf(substr buf, I radix) noexcept +{ + using digits_type = detail::charconv_digits; + size_t pos = 0; + if(C4_LIKELY(buf.len > 0)) + buf.str[pos++] = '-'; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case I(10): + if(C4_UNLIKELY(buf.len < digits_type::maxdigits_dec)) + return digits_type::maxdigits_dec; + pos =_itoa2buf(buf, pos, digits_type::min_value_dec()); + break; + case I(16): + if(C4_UNLIKELY(buf.len < digits_type::maxdigits_hex)) + return digits_type::maxdigits_hex; + buf.str[pos++] = '0'; + buf.str[pos++] = 'x'; + pos = _itoa2buf(buf, pos, digits_type::min_value_hex()); + break; + case I( 2): + if(C4_UNLIKELY(buf.len < digits_type::maxdigits_bin)) + return digits_type::maxdigits_bin; + buf.str[pos++] = '0'; + buf.str[pos++] = 'b'; + pos = _itoa2buf(buf, pos, digits_type::min_value_bin()); + break; + case I( 8): + if(C4_UNLIKELY(buf.len < digits_type::maxdigits_oct)) + return digits_type::maxdigits_oct; + buf.str[pos++] = '0'; + buf.str[pos++] = 'o'; + pos = _itoa2buf(buf, pos, digits_type::min_value_oct()); + break; + } + return pos; +} +template +C4_NO_INLINE size_t _itoa2buf(substr buf, I radix, size_t num_digits) noexcept +{ + using digits_type = detail::charconv_digits; + size_t pos = 0; + size_t needed_digits = 0; + if(C4_LIKELY(buf.len > 0)) + buf.str[pos++] = '-'; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case I(10): + // add 1 to account for - + needed_digits = num_digits+1 > digits_type::maxdigits_dec ? num_digits+1 : digits_type::maxdigits_dec; + if(C4_UNLIKELY(buf.len < needed_digits)) + return needed_digits; + pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_dec()); + break; + case I(16): + // add 3 to account for -0x + needed_digits = num_digits+3 > digits_type::maxdigits_hex ? num_digits+3 : digits_type::maxdigits_hex; + if(C4_UNLIKELY(buf.len < needed_digits)) + return needed_digits; + buf.str[pos++] = '0'; + buf.str[pos++] = 'x'; + pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_hex()); + break; + case I(2): + // add 3 to account for -0b + needed_digits = num_digits+3 > digits_type::maxdigits_bin ? num_digits+3 : digits_type::maxdigits_bin; + if(C4_UNLIKELY(buf.len < needed_digits)) + return needed_digits; + C4_ASSERT(buf.len >= digits_type::maxdigits_bin); + buf.str[pos++] = '0'; + buf.str[pos++] = 'b'; + pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_bin()); + break; + case I(8): + // add 3 to account for -0o + needed_digits = num_digits+3 > digits_type::maxdigits_oct ? num_digits+3 : digits_type::maxdigits_oct; + if(C4_UNLIKELY(buf.len < needed_digits)) + return needed_digits; + C4_ASSERT(buf.len >= digits_type::maxdigits_oct); + buf.str[pos++] = '0'; + buf.str[pos++] = 'o'; + pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_oct()); + break; + } + return pos; +} +} // namespace detail +/** @endcond */ + + +/** @defgroup doc_itoa itoa: signed to chars + * + * @{ */ + +/** convert an integral signed decimal to a string. + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t itoa(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_signed::value); + if(v >= T(0)) + { + // write_dec() checks the buffer size, so no need to check here + return write_dec(buf, v); + } + // when T is the min value (eg i8: -128), negating it + // will overflow, so treat the min as a special case + if(C4_LIKELY(v != std::numeric_limits::min())) + { + v = -v; + unsigned digits = digits_dec(v); + if(C4_LIKELY(buf.len >= digits + 1u)) + { + buf.str[0] = '-'; + write_dec_unchecked(buf.sub(1), v, digits); + } + return digits + 1u; + } + return detail::_itoadec2buf(buf); +} + +/** convert an integral signed integer to a string, using a specific + * radix. The radix must be 2, 8, 10 or 16. + * + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t itoa(substr buf, T v, T radix) noexcept +{ + C4_STATIC_ASSERT(std::is_signed::value); + C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16); + C4_SUPPRESS_WARNING_GCC_PUSH + #if (defined(__GNUC__) && (__GNUC__ >= 7)) + C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow") // gcc has a false positive here + #endif + // when T is the min value (eg i8: -128), negating it + // will overflow, so treat the min as a special case + if(C4_LIKELY(v != std::numeric_limits::min())) + { + unsigned pos = 0; + if(v < 0) + { + v = -v; + if(C4_LIKELY(buf.len > 0)) + buf.str[pos] = '-'; + ++pos; + } + unsigned digits = 0; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case T(10): + digits = digits_dec(v); + if(C4_LIKELY(buf.len >= pos + digits)) + write_dec_unchecked(buf.sub(pos), v, digits); + break; + case T(16): + digits = digits_hex(v); + if(C4_LIKELY(buf.len >= pos + 2u + digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'x'; + write_hex_unchecked(buf.sub(pos + 2), v, digits); + } + digits += 2u; + break; + case T(2): + digits = digits_bin(v); + if(C4_LIKELY(buf.len >= pos + 2u + digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'b'; + write_bin_unchecked(buf.sub(pos + 2), v, digits); + } + digits += 2u; + break; + case T(8): + digits = digits_oct(v); + if(C4_LIKELY(buf.len >= pos + 2u + digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'o'; + write_oct_unchecked(buf.sub(pos + 2), v, digits); + } + digits += 2u; + break; + } + return pos + digits; + } + C4_SUPPRESS_WARNING_GCC_POP + // when T is the min value (eg i8: -128), negating it + // will overflow + return detail::_itoa2buf(buf, radix); +} + + +/** same as c4::itoa(), but pad with zeroes on the left such that the + * resulting string is @p num_digits wide, not accounting for radix + * prefix (0x,0o,0b). The @p radix must be 2, 8, 10 or 16. + * + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t itoa(substr buf, T v, T radix, size_t num_digits) noexcept +{ + C4_STATIC_ASSERT(std::is_signed::value); + C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16); + C4_SUPPRESS_WARNING_GCC_PUSH + #if (defined(__GNUC__) && (__GNUC__ >= 7)) + C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow") // gcc has a false positive here + #endif + // when T is the min value (eg i8: -128), negating it + // will overflow, so treat the min as a special case + if(C4_LIKELY(v != std::numeric_limits::min())) + { + unsigned pos = 0; + if(v < 0) + { + v = -v; + if(C4_LIKELY(buf.len > 0)) + buf.str[pos] = '-'; + ++pos; + } + unsigned total_digits = 0; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case T(10): + total_digits = digits_dec(v); + total_digits = pos + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + write_dec(buf.sub(pos), v, num_digits); + break; + case T(16): + total_digits = digits_hex(v); + total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'x'; + write_hex(buf.sub(pos + 2), v, num_digits); + } + break; + case T(2): + total_digits = digits_bin(v); + total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'b'; + write_bin(buf.sub(pos + 2), v, num_digits); + } + break; + case T(8): + total_digits = digits_oct(v); + total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[pos + 0] = '0'; + buf.str[pos + 1] = 'o'; + write_oct(buf.sub(pos + 2), v, num_digits); + } + break; + } + return total_digits; + } + C4_SUPPRESS_WARNING_GCC_POP + // when T is the min value (eg i8: -128), negating it + // will overflow + return detail::_itoa2buf(buf, radix, num_digits); +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_utoa utoa: unsigned to chars + * + * @{ */ + +/** convert an integral unsigned decimal to a string. + * + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t utoa(substr buf, T v) noexcept +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + // write_dec() does the buffer length check, so no need to check here + return write_dec(buf, v); +} + +/** convert an integral unsigned integer to a string, using a specific + * radix. The radix must be 2, 8, 10 or 16. + * + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t utoa(substr buf, T v, T radix) noexcept +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8); + unsigned digits = 0; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case T(10): + digits = digits_dec(v); + if(C4_LIKELY(buf.len >= digits)) + write_dec_unchecked(buf, v, digits); + break; + case T(16): + digits = digits_hex(v); + if(C4_LIKELY(buf.len >= digits+2u)) + { + buf.str[0] = '0'; + buf.str[1] = 'x'; + write_hex_unchecked(buf.sub(2), v, digits); + } + digits += 2u; + break; + case T(2): + digits = digits_bin(v); + if(C4_LIKELY(buf.len >= digits+2u)) + { + buf.str[0] = '0'; + buf.str[1] = 'b'; + write_bin_unchecked(buf.sub(2), v, digits); + } + digits += 2u; + break; + case T(8): + digits = digits_oct(v); + if(C4_LIKELY(buf.len >= digits+2u)) + { + buf.str[0] = '0'; + buf.str[1] = 'o'; + write_oct_unchecked(buf.sub(2), v, digits); + } + digits += 2u; + break; + } + return digits; +} + +/** same as c4::utoa(), but pad with zeroes on the left such that the + * resulting string is @p num_digits wide. The @p radix must be 2, + * 8, 10 or 16. + * + * @note the resulting string is NOT zero-terminated. + * @note it is ok to call this with an empty or too-small buffer; + * no writes will occur, and the needed size will be returned + * @return the number of characters required for the buffer. */ +template +C4_ALWAYS_INLINE size_t utoa(substr buf, T v, T radix, size_t num_digits) noexcept +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8); + unsigned total_digits = 0; + switch(radix) // NOLINT(hicpp-multiway-paths-covered) + { + case T(10): + total_digits = digits_dec(v); + total_digits = (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + write_dec(buf, v, num_digits); + break; + case T(16): + total_digits = digits_hex(v); + total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[0] = '0'; + buf.str[1] = 'x'; + write_hex(buf.sub(2), v, num_digits); + } + break; + case T(2): + total_digits = digits_bin(v); + total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[0] = '0'; + buf.str[1] = 'b'; + write_bin(buf.sub(2), v, num_digits); + } + break; + case T(8): + total_digits = digits_oct(v); + total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits); + if(C4_LIKELY(buf.len >= total_digits)) + { + buf.str[0] = '0'; + buf.str[1] = 'o'; + write_oct(buf.sub(2), v, num_digits); + } + break; + } + return total_digits; +} +C4_SUPPRESS_WARNING_GCC_POP + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_atoi atoi: chars to signed + * + * @{ */ + +/** Convert a trimmed string to a signed integral value. The input + * string can be formatted as decimal, binary (prefix 0b or 0B), octal + * (prefix 0o or 0O) or hexadecimal (prefix 0x or 0X). Strings with + * leading zeroes are considered as decimal and not octal (unlike the + * C/C++ convention). Every character in the input string is read for + * the conversion; the input string must not contain any leading or + * trailing whitespace. + * + * @return true if the conversion was successful. + * + * @note a positive sign is not accepted. ie, the string must not + * start with '+' + * + * @note overflow is not detected: the return status is true even if + * the conversion would return a value outside of the type's range, in + * which case the result will wrap around the type's range. This is + * similar to native behavior. See @ref doc_overflows and @ref + * doc_overflow_checked for overflow checking utilities. + * + * @see atoi_first() if the string is not trimmed to the value to read. */ +template +C4_NO_UBSAN_IOVRFLW +C4_ALWAYS_INLINE bool atoi(csubstr str, T * C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + C4_STATIC_ASSERT(std::is_signed::value); + + if(C4_UNLIKELY(str.len == 0)) + return false; + + C4_ASSERT(str.str[0] != '+'); + + T sign = 1; + size_t start = 0; + if(str.str[0] == '-') + { + if(C4_UNLIKELY(str.len == ++start)) + return false; + sign = -1; + } + + bool parsed_ok = true; + if(str.str[start] != '0') // this should be the common case, so put it first + { + parsed_ok = read_dec(str.sub(start), v); + } + else if(str.len > start + 1) + { + // starts with 0: is it 0x, 0o, 0b? + const char pfx = str.str[start + 1]; + if(pfx == 'x' || pfx == 'X') + parsed_ok = str.len > start + 2 && read_hex(str.sub(start + 2), v); + else if(pfx == 'b' || pfx == 'B') + parsed_ok = str.len > start + 2 && read_bin(str.sub(start + 2), v); + else if(pfx == 'o' || pfx == 'O') + parsed_ok = str.len > start + 2 && read_oct(str.sub(start + 2), v); + else + parsed_ok = read_dec(str.sub(start + 1), v); + } + else + { + parsed_ok = read_dec(str.sub(start), v); + } + if(C4_LIKELY(parsed_ok)) + *v *= sign; + return parsed_ok; +} + + +/** Select the next range of characters in the string that can be parsed + * as a signed integral value, and convert it using atoi(). Leading + * whitespace (space, newline, tabs) is skipped. + * @return the number of characters read for conversion, or csubstr::npos if the conversion failed + * @see atoi() if the string is already trimmed to the value to read. + * @see csubstr::first_int_span() */ +template +C4_ALWAYS_INLINE size_t atoi_first(csubstr str, T * C4_RESTRICT v) +{ + csubstr trimmed = str.first_int_span(); + if(trimmed.len == 0) + return csubstr::npos; + if(atoi(trimmed, v)) + return static_cast(trimmed.end() - str.begin()); + return csubstr::npos; +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_atou atou: chars to unsigned + * + * @{ */ + +/** Convert a trimmed string to an unsigned integral value. The string can be + * formatted as decimal, binary (prefix 0b or 0B), octal (prefix 0o or 0O) + * or hexadecimal (prefix 0x or 0X). Every character in the input string is read + * for the conversion; it must not contain any leading or trailing whitespace. + * + * @return true if the conversion was successful. + * + * @note overflow is not detected: the return status is true even if + * the conversion would return a value outside of the type's range, in + * which case the result will wrap around the type's range. See @ref + * doc_overflows and @ref doc_overflow_checked for overflow checking + * utilities. + * + * @note If the string has a minus character, the return status + * will be false. + * + * @see atou_first() if the string is not trimmed to the value to read. */ +template +bool atou(csubstr str, T * C4_RESTRICT v) noexcept +{ + C4_STATIC_ASSERT(std::is_integral::value); + + if(C4_UNLIKELY(str.len == 0 || str.front() == '-')) + return false; + + bool parsed_ok = true; + if(str.str[0] != '0') + { + parsed_ok = read_dec(str, v); + } + else + { + if(str.len > 1) + { + const char pfx = str.str[1]; + if(pfx == 'x' || pfx == 'X') + parsed_ok = str.len > 2 && read_hex(str.sub(2), v); + else if(pfx == 'b' || pfx == 'B') + parsed_ok = str.len > 2 && read_bin(str.sub(2), v); + else if(pfx == 'o' || pfx == 'O') + parsed_ok = str.len > 2 && read_oct(str.sub(2), v); + else + parsed_ok = read_dec(str, v); + } + else + { + *v = 0; // we know the first character is 0 + } + } + return parsed_ok; +} + + +/** Select the next range of characters in the string that can be parsed + * as an unsigned integral value, and convert it using atou(). Leading + * whitespace (space, newline, tabs) is skipped. + * @return the number of characters read for conversion, or csubstr::npos if the conversion faileds + * @see atou() if the string is already trimmed to the value to read. + * @see csubstr::first_uint_span() */ +template +C4_ALWAYS_INLINE size_t atou_first(csubstr str, T *v) +{ + csubstr trimmed = str.first_uint_span(); + if(trimmed.len == 0) + return csubstr::npos; + if(atou(trimmed, v)) + return static_cast(trimmed.end() - str.begin()); + return csubstr::npos; +} + + +/** @} */ + +#if defined(_MSC_VER) && !defined(__clang__) +# pragma warning(pop) +#elif defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +namespace detail { +inline bool check_overflow(csubstr str, csubstr limit) noexcept +{ + if(str.len != limit.len) + return str.len > limit.len; + for(size_t i = 0; i < limit.len; ++i) + { + if(str[i] < limit[i]) + return false; + else if(str[i] > limit[i]) + return true; + } + return false; +} +} // namespace detail +/** @endcond */ + + +/** @defgroup doc_overflows overflows: does a number string overflow a type + * + * @{ */ + +/** Test if the following string would overflow when converted to + * associated integral types; this function is dispatched with SFINAE + * to handle differently signed and unsigned types. + * @return true if number will overflow, false if it fits (or doesn't parse) + * @see doc_overflow_checked for format specifiers to enforce no-overflow reads + */ +template +auto overflows(csubstr str) noexcept + -> typename std::enable_if::value, bool>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + + if(C4_UNLIKELY(str.len == 0)) + { + return false; + } + else if(str.str[0] == '0') + { + if (str.len == 1) + return false; + switch (str.str[1]) + { + case 'x': + case 'X': + { + size_t fno = str.first_not_of('0', 2); + if (fno == csubstr::npos) + return false; + return !(str.len <= fno + (sizeof(T) * 2)); + } + case 'b': + case 'B': + { + size_t fno = str.first_not_of('0', 2); + if (fno == csubstr::npos) + return false; + return !(str.len <= fno +(sizeof(T) * 8)); + } + case 'o': + case 'O': + { + size_t fno = str.first_not_of('0', 2); + if(fno == csubstr::npos) + return false; + return detail::charconv_digits::is_oct_overflow(str.sub(fno)); + } + default: + { + size_t fno = str.first_not_of('0', 1); + if(fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::max_value_dec()); + } + } + } + else if(C4_UNLIKELY(str[0] == '-')) + { + return true; + } + else + { + return detail::check_overflow(str, detail::charconv_digits::max_value_dec()); + } +} + + +/** Test if the following string would overflow when converted to + * associated integral types; this function is dispatched with SFINAE + * to handle differently signed and unsigned types. + * + * @return true if number will overflow, false if it fits (or doesn't parse) + * @see doc_overflow_checked for format specifiers to enforce no-overflow reads + */ +template +auto overflows(csubstr str) noexcept + -> typename std::enable_if::value, bool>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + if(C4_UNLIKELY(str.len == 0)) + return false; + if(str.str[0] == '-') + { + if(str.str[1] == '0') + { + if(str.len == 2) + return false; + switch(str.str[2]) + { + case 'x': + case 'X': + { + size_t fno = str.first_not_of('0', 3); + if (fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::min_value_hex()); + } + case 'b': + case 'B': + { + size_t fno = str.first_not_of('0', 3); + if (fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::min_value_bin()); + } + case 'o': + case 'O': + { + size_t fno = str.first_not_of('0', 3); + if(fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::min_value_oct()); + } + default: + { + size_t fno = str.first_not_of('0', 2); + if(fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::min_value_dec()); + } + } + } + else + { + return detail::check_overflow(str.sub(1), detail::charconv_digits::min_value_dec()); + } + } + else if(str.str[0] == '0') + { + if (str.len == 1) + return false; + switch(str.str[1]) + { + case 'x': + case 'X': + { + size_t fno = str.first_not_of('0', 2); + if (fno == csubstr::npos) + return false; + const size_t len = str.len - fno; + return !((len < sizeof (T) * 2) || (len == sizeof(T) * 2 && str[fno] <= '7')); + } + case 'b': + case 'B': + { + size_t fno = str.first_not_of('0', 2); + if (fno == csubstr::npos) + return false; + return !(str.len <= fno + (sizeof(T) * 8 - 1)); + } + case 'o': + case 'O': + { + size_t fno = str.first_not_of('0', 2); + if(fno == csubstr::npos) + return false; + return detail::charconv_digits::is_oct_overflow(str.sub(fno)); + } + default: + { + size_t fno = str.first_not_of('0', 1); + if(fno == csubstr::npos) + return false; + return detail::check_overflow(str.sub(fno), detail::charconv_digits::max_value_dec()); + } + } + } + else + { + return detail::check_overflow(str, detail::charconv_digits::max_value_dec()); + } +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +namespace detail { + + +#if (!C4CORE_HAVE_STD_FROMCHARS) +/** @see http://www.exploringbinary.com/ for many good examples on float-str conversion */ +template +void get_real_format_str(char (& C4_RESTRICT fmt)[N], int precision, RealFormat_e formatting, const char* length_modifier="") +{ + int iret; + if(precision == -1) + iret = snprintf(fmt, sizeof(fmt), "%%%s%c", length_modifier, formatting); + else if(precision == 0) + iret = snprintf(fmt, sizeof(fmt), "%%.%s%c", length_modifier, formatting); + else + iret = snprintf(fmt, sizeof(fmt), "%%.%d%s%c", precision, length_modifier, formatting); + C4_ASSERT(iret >= 2 && size_t(iret) < sizeof(fmt)); + C4_UNUSED(iret); +} + + +/** @todo we're depending on snprintf()/sscanf() for converting to/from + * floating point numbers. Apparently, this increases the binary size + * by a considerable amount. There are some lightweight printf + * implementations: + * + * @see http://www.sparetimelabs.com/tinyprintf/tinyprintf.php (BSD) + * @see https://github.com/weiss/c99-snprintf + * @see https://github.com/nothings/stb/blob/master/stb_sprintf.h + * @see http://www.exploringbinary.com/ + * @see https://blog.benoitblanchon.fr/lightweight-float-to-string/ + * @see http://www.ryanjuckett.com/programming/printing-floating-point-numbers/ + */ +template +size_t print_one(substr str, const char* full_fmt, T v) +{ +#ifdef _MSC_VER + /** use _snprintf() to prevent early termination of the output + * for writing the null character at the last position + * @see https://msdn.microsoft.com/en-us/library/2ts7cx93.aspx */ + int iret = _snprintf(str.str, str.len, full_fmt, v); + if(iret < 0) + { + /* when buf.len is not enough, VS returns a negative value. + * so call it again with a negative value for getting an + * actual length of the string */ + iret = snprintf(nullptr, 0, full_fmt, v); + C4_ASSERT(iret > 0); + } + size_t ret = (size_t) iret; + return ret; +#else + int iret = snprintf(str.str, str.len, full_fmt, v); + C4_ASSERT(iret >= 0); + size_t ret = (size_t) iret; + if(ret >= str.len) + ++ret; /* snprintf() reserves the last character to write \0 */ + return ret; +#endif +} +#endif // (!C4CORE_HAVE_STD_FROMCHARS) + + +#if (!C4CORE_HAVE_STD_FROMCHARS) && (!C4CORE_HAVE_FAST_FLOAT) +/** scans a string using the given type format, while at the same time + * allowing non-null-terminated strings AND guaranteeing that the given + * string length is strictly respected, so that no buffer overflows + * might occur. */ +template +inline size_t scan_one(csubstr str, const char *type_fmt, T *v) +{ + /* snscanf() is absolutely needed here as we must be sure that + * str.len is strictly respected, because substr is + * generally not null-terminated. + * + * Alas, there is no snscanf(). + * + * So we fake it by using a dynamic format with an explicit + * field size set to the length of the given span. + * This trick is taken from: + * https://stackoverflow.com/a/18368910/5875572 */ + + /* this is the actual format we'll use for scanning */ + char fmt[16]; + + /* write the length into it. Eg "%12f". + * Also, get the number of characters read from the string. + * So the final format ends up as "%12f%n"*/ + int iret = std::snprintf(fmt, sizeof(fmt), "%%" "%zu" "%s" "%%n", str.len, type_fmt); + /* no nasty surprises, please! */ + C4_ASSERT(iret >= 0 && size_t(iret) < C4_COUNTOF(fmt)); + + /* now we scan with confidence that the span length is respected */ + int num_chars; + iret = std::sscanf(str.str, fmt, v, &num_chars); + /* scanf returns the number of successful conversions */ + if(iret != 1) return csubstr::npos; + C4_ASSERT(num_chars >= 0); + return (size_t)(num_chars); +} +#endif // (!C4CORE_HAVE_STD_FROMCHARS) && (!C4CORE_HAVE_FAST_FLOAT) + + +#if C4CORE_HAVE_STD_TOCHARS +template +C4_ALWAYS_INLINE size_t rtoa(substr buf, T v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept +{ + std::to_chars_result result; + size_t pos = 0; + if(formatting == FTOA_HEXA) + { + if(buf.len > size_t(2)) + { + buf.str[0] = '0'; + buf.str[1] = 'x'; + } + pos += size_t(2); + } + if(precision == -1) + result = std::to_chars(buf.str + pos, buf.str + buf.len, v, (std::chars_format)formatting); + else + result = std::to_chars(buf.str + pos, buf.str + buf.len, v, (std::chars_format)formatting, precision); + if(result.ec == std::errc()) + { + // all good, no errors. + C4_ASSERT(result.ptr >= buf.str); + ptrdiff_t delta = result.ptr - buf.str; + return static_cast(delta); + } + C4_ASSERT(result.ec == std::errc::value_too_large); + // This is unfortunate. + // + // When the result can't fit in the given buffer, + // std::to_chars() returns the end pointer it was originally + // given, which is useless because here we would like to know + // _exactly_ how many characters the buffer must have to fit + // the result. + // + // So we take the pessimistic view, and assume as many digits + // as could ever be required: + size_t ret = static_cast(std::numeric_limits::max_digits10); + return ret > buf.len ? ret : buf.len + 1; +} +#endif // C4CORE_HAVE_STD_TOCHARS + + +#if C4CORE_HAVE_FAST_FLOAT +template +C4_ALWAYS_INLINE bool scan_rhex(csubstr s, T *C4_RESTRICT val) noexcept +{ + C4_ASSERT(s.len > 0); + C4_ASSERT(s.str[0] != '-'); + C4_ASSERT(s.str[0] != '+'); + C4_ASSERT(!s.begins_with("0x")); + C4_ASSERT(!s.begins_with("0X")); + size_t pos = 0; + // integer part + for( ; pos < s.len; ++pos) + { + const char c = s.str[pos]; + if(c >= '0' && c <= '9') + *val = *val * T(16) + T(c - '0'); + else if(c >= 'a' && c <= 'f') + *val = *val * T(16) + T(c - 'a'); + else if(c >= 'A' && c <= 'F') + *val = *val * T(16) + T(c - 'A'); + else if(c == '.') + { + ++pos; + break; // follow on to mantissa + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power; // no mantissa given, jump to power // NOLINT + } + else + { + return false; + } + } + // mantissa + { + // 0.0625 == 1/16 == value of first digit after the comma + for(T digit = T(0.0625); pos < s.len; ++pos, digit /= T(16)) // NOLINT + { + const char c = s.str[pos]; + if(c >= '0' && c <= '9') + *val += digit * T(c - '0'); + else if(c >= 'a' && c <= 'f') + *val += digit * T(c - 'a'); + else if(c >= 'A' && c <= 'F') + *val += digit * T(c - 'A'); + else if(c == 'p' || c == 'P') + { + ++pos; + goto power; // mantissa finished, jump to power // NOLINT + } + else + { + return false; + } + } + } + return true; +power: + if(C4_LIKELY(pos < s.len)) + { + if(s.str[pos] == '+') // atoi() cannot handle a leading '+' + ++pos; + if(C4_LIKELY(pos < s.len)) + { + int16_t powval = {}; + if(C4_LIKELY(atoi(s.sub(pos), &powval))) + { + *val *= ipow(powval); + return true; + } + } + } + return false; +} +#endif + +} // namespace detail +/** @endcond */ + + +#undef _c4appendhex +#undef _c4append + + +/** @defgroup doc_ftoa ftoa: float32 to chars + * + * @{ */ + +/** Convert a single-precision real number to string. The string will + * in general be NOT null-terminated. For FTOA_FLEX, \p precision is + * the number of significand digits. Otherwise \p precision is the + * number of decimals. It is safe to call this function with an empty + * or too-small buffer. + * + * @return the size of the buffer needed to write the number + */ +C4_ALWAYS_INLINE size_t ftoa(substr str, float v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept +{ +#if C4CORE_HAVE_STD_TOCHARS + return detail::rtoa(str, v, precision, formatting); +#else + char fmt[16]; + detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/""); + return detail::print_one(str, fmt, v); +#endif +} + +/** @} */ + + +/** @defgroup doc_dtoa dtoa: float64 to chars + * + * @{ */ + +/** Convert a double-precision real number to string. The string will + * in general be NOT null-terminated. For FTOA_FLEX, \p precision is + * the number of significand digits. Otherwise \p precision is the + * number of decimals. It is safe to call this function with an empty + * or too-small buffer. + * + * @return the size of the buffer needed to write the number + */ +C4_ALWAYS_INLINE size_t dtoa(substr str, double v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept +{ +#if C4CORE_HAVE_STD_TOCHARS + return detail::rtoa(str, v, precision, formatting); +#else + char fmt[16]; + detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/"l"); + return detail::print_one(str, fmt, v); +#endif +} + +/** @} */ + + +/** @defgroup doc_atof atof: chars to float32 + * + * @{ */ + +/** Convert a string to a single precision real number. + * The input string must be trimmed to the value, ie + * no leading or trailing whitespace can be present. + * @return true iff the conversion succeeded + * @see atof_first() if the string is not trimmed + */ +C4_ALWAYS_INLINE bool atof(csubstr str, float * C4_RESTRICT v) noexcept +{ + C4_ASSERT(str.len > 0); + C4_ASSERT(str.triml(" \r\t\n").len == str.len); +#if C4CORE_HAVE_FAST_FLOAT + // fastfloat cannot parse hexadecimal floats + bool isneg = (str.str[0] == '-'); + csubstr rem = str.sub(isneg || str.str[0] == '+'); + if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X')))) + { + fast_float::from_chars_result result; + result = fast_float::from_chars(str.str, str.str + str.len, *v); + return result.ec == std::errc(); + } + else if(detail::scan_rhex(rem.sub(2), v)) + { + *v *= isneg ? -1.f : 1.f; + return true; + } + return false; +#elif C4CORE_HAVE_STD_FROMCHARS + std::from_chars_result result; + result = std::from_chars(str.str, str.str + str.len, *v); + return result.ec == std::errc(); +#else + csubstr rem = str.sub(str.str[0] == '-' || str.str[0] == '+'); + if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X')))) + return detail::scan_one(str, "f", v) != csubstr::npos; + else + return detail::scan_one(str, "a", v) != csubstr::npos; +#endif +} + + +/** Convert a string to a single precision real number. + * Leading whitespace is skipped until valid characters are found. + * @return the number of characters read from the string, or npos if + * conversion was not successful or if the string was empty */ +inline size_t atof_first(csubstr str, float * C4_RESTRICT v) noexcept +{ + csubstr trimmed = str.first_real_span(); + if(trimmed.len == 0) + return csubstr::npos; + if(atof(trimmed, v)) + return static_cast(trimmed.end() - str.begin()); + return csubstr::npos; +} + +/** @} */ + + +/** @defgroup doc_atod atod: chars to float64 + * + * @{ */ + +/** Convert a string to a double precision real number. + * The input string must be trimmed to the value, ie + * no leading or trailing whitespace can be present. + * @return true iff the conversion succeeded + * @see atod_first() if the string is not trimmed + */ +C4_ALWAYS_INLINE bool atod(csubstr str, double * C4_RESTRICT v) noexcept +{ + C4_ASSERT(str.len > 0); + C4_ASSERT(str.triml(" \r\t\n").len == str.len); +#if C4CORE_HAVE_FAST_FLOAT + // fastfloat cannot parse hexadecimal floats + bool isneg = (str.str[0] == '-'); + csubstr rem = str.sub(isneg || str.str[0] == '+'); + if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X')))) + { + fast_float::from_chars_result result; + result = fast_float::from_chars(str.str, str.str + str.len, *v); + return result.ec == std::errc(); + } + else if(detail::scan_rhex(rem.sub(2), v)) + { + *v *= isneg ? -1. : 1.; + return true; + } + return false; +#elif C4CORE_HAVE_STD_FROMCHARS + std::from_chars_result result; + result = std::from_chars(str.str, str.str + str.len, *v); + return result.ec == std::errc(); +#else + csubstr rem = str.sub(str.str[0] == '-' || str.str[0] == '+'); + if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X')))) + return detail::scan_one(str, "lf", v) != csubstr::npos; + else + return detail::scan_one(str, "la", v) != csubstr::npos; +#endif +} + + +/** Convert a string to a double precision real number. + * Leading whitespace is skipped until valid characters are found. + * @return the number of characters read from the string, or npos if + * conversion was not successful or if the string was empty */ +inline size_t atod_first(csubstr str, double * C4_RESTRICT v) noexcept +{ + csubstr trimmed = str.first_real_span(); + if(trimmed.len == 0) + return csubstr::npos; + if(atod(trimmed, v)) + return static_cast(trimmed.end() - str.begin()); + return csubstr::npos; +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// generic versions + +/** @cond dev */ +// on some platforms, (unsigned) int and (unsigned) long +// are not any of the fixed length types above +#define _C4_IF_NOT_FIXED_LENGTH_I(T, ty) typename std::enable_if::value && !is_fixed_length::value_i, ty> +#define _C4_IF_NOT_FIXED_LENGTH_U(T, ty) typename std::enable_if::value && !is_fixed_length::value_u, ty> +/** @endcond*/ + + +/** @defgroup doc_xtoa xtoa: generic value to chars + * + * Dispatches to the most appropriate and efficient conversion + * function + * + * @{ */ +C4_ALWAYS_INLINE size_t xtoa(substr s, uint8_t v) noexcept { return write_dec(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v) noexcept { return write_dec(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v) noexcept { return write_dec(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v) noexcept { return write_dec(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int8_t v) noexcept { return itoa(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int16_t v) noexcept { return itoa(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int32_t v) noexcept { return itoa(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int64_t v) noexcept { return itoa(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, float v) noexcept { return ftoa(s, v); } +C4_ALWAYS_INLINE size_t xtoa(substr s, double v) noexcept { return dtoa(s, v); } + +C4_ALWAYS_INLINE size_t xtoa(substr s, uint8_t v, uint8_t radix) noexcept { return utoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v, uint16_t radix) noexcept { return utoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v, uint32_t radix) noexcept { return utoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v, uint64_t radix) noexcept { return utoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int8_t v, int8_t radix) noexcept { return itoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int16_t v, int16_t radix) noexcept { return itoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int32_t v, int32_t radix) noexcept { return itoa(s, v, radix); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int64_t v, int64_t radix) noexcept { return itoa(s, v, radix); } + +C4_ALWAYS_INLINE size_t xtoa(substr s, uint8_t v, uint8_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v, uint16_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v, uint32_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v, uint64_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int8_t v, int8_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int16_t v, int16_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int32_t v, int32_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); } +C4_ALWAYS_INLINE size_t xtoa(substr s, int64_t v, int64_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); } + +C4_ALWAYS_INLINE size_t xtoa(substr s, float v, int precision, RealFormat_e formatting=FTOA_FLEX) noexcept { return ftoa(s, v, precision, formatting); } +C4_ALWAYS_INLINE size_t xtoa(substr s, double v, int precision, RealFormat_e formatting=FTOA_FLEX) noexcept { return dtoa(s, v, precision, formatting); } + +template C4_ALWAYS_INLINE auto xtoa(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return itoa(buf, v); } +template C4_ALWAYS_INLINE auto xtoa(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return write_dec(buf, v); } +template +C4_ALWAYS_INLINE size_t xtoa(substr s, T *v) noexcept { return itoa(s, (intptr_t)v, (intptr_t)16); } + +/** @} */ + +/** @defgroup doc_atox atox: generic chars to value + * + * Dispatches to the most appropriate and efficient conversion + * function + * + * @{ */ + +C4_ALWAYS_INLINE bool atox(csubstr s, uint8_t *C4_RESTRICT v) noexcept { return atou(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, uint16_t *C4_RESTRICT v) noexcept { return atou(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, uint32_t *C4_RESTRICT v) noexcept { return atou(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, uint64_t *C4_RESTRICT v) noexcept { return atou(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, int8_t *C4_RESTRICT v) noexcept { return atoi(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, int16_t *C4_RESTRICT v) noexcept { return atoi(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, int32_t *C4_RESTRICT v) noexcept { return atoi(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, int64_t *C4_RESTRICT v) noexcept { return atoi(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, float *C4_RESTRICT v) noexcept { return atof(s, v); } +C4_ALWAYS_INLINE bool atox(csubstr s, double *C4_RESTRICT v) noexcept { return atod(s, v); } + +template C4_ALWAYS_INLINE auto atox(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, bool)::type { return atoi(buf, v); } +template C4_ALWAYS_INLINE auto atox(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, bool)::type { return atou(buf, v); } +template +C4_ALWAYS_INLINE bool atox(csubstr s, T **v) noexcept { intptr_t tmp; bool ret = atox(s, &tmp); if(ret) { *v = (T*)tmp; } return ret; } + +/** @} */ + + +/** @defgroup doc_to_chars to_chars: generalized chars to value + * + * Convert the given value, writing into the string. The resulting + * string will NOT be null-terminated. Return the number of + * characters needed. This function is safe to call when the string + * is too small - no writes will occur beyond the string's last + * character. + * + * Dispatches to the most appropriate and efficient conversion + * function. + * + * @see write_dec, doc_utoa, doc_itoa, doc_ftoa, doc_dtoa + * + * @warning When serializing floating point values (float or double), + * be aware that because it uses defaults, to_chars() may cause a + * truncation of the precision. To enforce a particular precision, use + * for example @ref c4::fmt::real, or call directly @ref c4::ftoa or + * @ref c4::dtoa. + * + * @{ */ + +C4_ALWAYS_INLINE size_t to_chars(substr buf, uint8_t v) noexcept { return write_dec(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, uint16_t v) noexcept { return write_dec(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, uint32_t v) noexcept { return write_dec(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, uint64_t v) noexcept { return write_dec(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, int8_t v) noexcept { return itoa(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, int16_t v) noexcept { return itoa(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, int32_t v) noexcept { return itoa(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, int64_t v) noexcept { return itoa(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, float v) noexcept { return ftoa(buf, v); } +C4_ALWAYS_INLINE size_t to_chars(substr buf, double v) noexcept { return dtoa(buf, v); } + +template C4_ALWAYS_INLINE auto to_chars(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return itoa(buf, v); } +template C4_ALWAYS_INLINE auto to_chars(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return write_dec(buf, v); } +template +C4_ALWAYS_INLINE size_t to_chars(substr s, T *v) noexcept { return itoa(s, (intptr_t)v, (intptr_t)16); } + +/** @} */ + + +/** @defgroup doc_from_chars from_chars: generalized chars to value + * + * Read a value from the string, which must be trimmed to the value + * (ie, no leading/trailing whitespace). return true if the + * conversion succeeded. There is no check for overflow; the value + * wraps around in a way similar to the standard C/C++ overflow + * behavior. For example, from_chars("128", &val) returns true + * and val will be set tot 0. See @ref doc_overflows and @ref + * doc_overflow_checked for facilities enforcing no-overflow. + * + * Dispatches to the most appropriate and efficient conversion + * function + * + * @see doc_from_chars_first, atou, atoi, atof, atod + * @{ */ + +C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint8_t *C4_RESTRICT v) noexcept { return atou(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint16_t *C4_RESTRICT v) noexcept { return atou(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint32_t *C4_RESTRICT v) noexcept { return atou(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint64_t *C4_RESTRICT v) noexcept { return atou(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, int8_t *C4_RESTRICT v) noexcept { return atoi(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, int16_t *C4_RESTRICT v) noexcept { return atoi(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, int32_t *C4_RESTRICT v) noexcept { return atoi(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, int64_t *C4_RESTRICT v) noexcept { return atoi(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, float *C4_RESTRICT v) noexcept { return atof(buf, v); } +C4_ALWAYS_INLINE bool from_chars(csubstr buf, double *C4_RESTRICT v) noexcept { return atod(buf, v); } + +template C4_ALWAYS_INLINE auto from_chars(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, bool)::type { return atoi(buf, v); } +template C4_ALWAYS_INLINE auto from_chars(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, bool)::type { return atou(buf, v); } +template +C4_ALWAYS_INLINE bool from_chars(csubstr buf, T **v) noexcept { intptr_t tmp; bool ret = from_chars(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; } + +/** @defgroup doc_from_chars_first from_chars_first: generalized chars to value + * + * Read the first valid sequence of characters from the string, + * skipping leading whitespace, and convert it using @ref doc_from_chars . + * Return the number of characters read for converting. + * + * Dispatches to the most appropriate and efficient conversion + * function. + * + * @see atou_first, atoi_first, atof_first, atod_first + * @{ */ + +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint8_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint16_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint32_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint64_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int8_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int16_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int32_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int64_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, float *C4_RESTRICT v) noexcept { return atof_first(buf, v); } +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, double *C4_RESTRICT v) noexcept { return atod_first(buf, v); } + +template C4_ALWAYS_INLINE auto from_chars_first(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return atoi_first(buf, v); } +template C4_ALWAYS_INLINE auto from_chars_first(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return atou_first(buf, v); } +template +C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, T **v) noexcept { intptr_t tmp; bool ret = from_chars_first(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; } + +/** @} */ + +/** @} */ + +#undef _C4_IF_NOT_FIXED_LENGTH_I +#undef _C4_IF_NOT_FIXED_LENGTH_U + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** call to_chars() and return a substr consisting of the + * written portion of the input buffer. Ie, same as to_chars(), + * but return a substr instead of a size_t. + * Convert the given value to a string using to_chars(), and + * return the resulting string, up to and including the last + * written character. + * @ingroup doc_to_chars + * @see to_chars() */ +template +C4_ALWAYS_INLINE substr to_chars_sub(substr buf, T const& C4_RESTRICT v) noexcept +{ + size_t sz = to_chars(buf, v); + return buf.left_of(sz <= buf.len ? sz : buf.len); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// bool implementation + +/** @ingroup doc_to_chars */ +C4_ALWAYS_INLINE size_t to_chars(substr buf, bool v) noexcept +{ + int val = v; + return to_chars(buf, val); +} + +/** @ingroup doc_from_chars */ +inline bool from_chars(csubstr buf, bool * C4_RESTRICT v) noexcept +{ + if(buf == '0') + { + *v = false; return true; + } + else if(buf == '1') + { + *v = true; return true; + } + else if(buf == "false") + { + *v = false; return true; + } + else if(buf == "true") + { + *v = true; return true; + } + else if(buf == "False") + { + *v = false; return true; + } + else if(buf == "True") + { + *v = true; return true; + } + else if(buf == "FALSE") + { + *v = false; return true; + } + else if(buf == "TRUE") + { + *v = true; return true; + } + // fallback to c-style int bools + int val = 0; + bool ret = from_chars(buf, &val); + if(C4_LIKELY(ret)) + { + *v = (val != 0); + } + return ret; +} + +/** @ingroup doc_from_chars_first */ +inline size_t from_chars_first(csubstr buf, bool * C4_RESTRICT v) noexcept +{ + csubstr trimmed = buf.first_non_empty_span(); + if(trimmed.len == 0 || !from_chars(buf, v)) + return csubstr::npos; + return trimmed.len; +} + + +//----------------------------------------------------------------------------- +// single-char implementation + +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, char v) noexcept +{ + if(buf.len > 0) + { + C4_XASSERT(buf.str); + buf.str[0] = v; + } + return 1; +} + +/** extract a single character from a substring + * @note to extract a string instead and not just a single character, use the csubstr overload + * @ingroup doc_from_chars + * */ +inline bool from_chars(csubstr buf, char * C4_RESTRICT v) noexcept +{ + if(buf.len != 1) + return false; + C4_XASSERT(buf.str); + *v = buf.str[0]; + return true; +} + +/** @ingroup doc_from_chars_first */ +inline size_t from_chars_first(csubstr buf, char * C4_RESTRICT v) noexcept +{ + if(buf.len < 1) + return csubstr::npos; + *v = buf.str[0]; + return 1; +} + + +//----------------------------------------------------------------------------- +// csubstr implementation + +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, csubstr v) noexcept +{ + C4_ASSERT(!buf.overlaps(v)); + size_t len = buf.len < v.len ? buf.len : v.len; + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len) + { + C4_ASSERT(buf.str != nullptr); + C4_ASSERT(v.str != nullptr); + memcpy(buf.str, v.str, len); + } + return v.len; +} + +/** @ingroup doc_from_chars */ +inline bool from_chars(csubstr buf, csubstr *C4_RESTRICT v) noexcept +{ + *v = buf; + return true; +} + +/** @ingroup doc_from_chars_first */ +inline size_t from_chars_first(substr buf, csubstr * C4_RESTRICT v) noexcept +{ + csubstr trimmed = buf.first_non_empty_span(); + if(trimmed.len == 0) + return csubstr::npos; + *v = trimmed; + return static_cast(trimmed.end() - buf.begin()); +} + + +//----------------------------------------------------------------------------- +// substr + +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, substr v) noexcept +{ + C4_ASSERT(!buf.overlaps(v)); + size_t len = buf.len < v.len ? buf.len : v.len; + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len) + { + C4_ASSERT(buf.str != nullptr); + C4_ASSERT(v.str != nullptr); + memcpy(buf.str, v.str, len); + } + return v.len; +} + +/** @ingroup doc_from_chars */ +inline bool from_chars(csubstr buf, substr * C4_RESTRICT v) noexcept +{ + C4_ASSERT(!buf.overlaps(*v)); + // is the destination buffer wide enough? + if(v->len >= buf.len) + { + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(buf.len) + { + C4_ASSERT(buf.str != nullptr); + C4_ASSERT(v->str != nullptr); + memcpy(v->str, buf.str, buf.len); + } + v->len = buf.len; + return true; + } + return false; +} + +/** @ingroup doc_from_chars_first */ +inline size_t from_chars_first(csubstr buf, substr * C4_RESTRICT v) noexcept +{ + csubstr trimmed = buf.first_non_empty_span(); + C4_ASSERT(!trimmed.overlaps(*v)); + if(C4_UNLIKELY(trimmed.len == 0)) + return csubstr::npos; + size_t len = trimmed.len > v->len ? v->len : trimmed.len; + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len) + { + C4_ASSERT(buf.str != nullptr); + C4_ASSERT(v->str != nullptr); + memcpy(v->str, trimmed.str, len); + } + if(C4_UNLIKELY(trimmed.len > v->len)) + return csubstr::npos; + return static_cast(trimmed.end() - buf.begin()); +} + + +//----------------------------------------------------------------------------- + +/** @ingroup doc_to_chars */ +template +inline size_t to_chars(substr buf, const char (& C4_RESTRICT v)[N]) noexcept +{ + csubstr sp(v); + return to_chars(buf, sp); +} + +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, const char * C4_RESTRICT v) noexcept +{ + return to_chars(buf, to_csubstr(v)); +} + +/** @} */ + +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise) + +#if defined(_MSC_VER) && !defined(__clang__) +# pragma warning(pop) +#elif defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif /* _C4_CHARCONV_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/compiler.hpp b/3rdparty/rapidyaml/include/c4/compiler.hpp new file mode 100644 index 0000000000..07c5e91aa3 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/compiler.hpp @@ -0,0 +1,120 @@ +#ifndef _C4_COMPILER_HPP_ +#define _C4_COMPILER_HPP_ + +/** @file compiler.hpp Provides compiler information macros + * @ingroup basic_headers */ + +#include "c4/platform.hpp" + +// Compilers: +// C4_MSVC +// Visual Studio 2022: MSVC++ 17, 1930 +// Visual Studio 2019: MSVC++ 16, 1920 +// Visual Studio 2017: MSVC++ 15 +// Visual Studio 2015: MSVC++ 14 +// Visual Studio 2013: MSVC++ 13 +// Visual Studio 2013: MSVC++ 12 +// Visual Studio 2012: MSVC++ 11 +// Visual Studio 2010: MSVC++ 10 +// Visual Studio 2008: MSVC++ 09 +// Visual Studio 2005: MSVC++ 08 +// C4_CLANG +// C4_GCC +// C4_ICC (intel compiler) +/** @see http://sourceforge.net/p/predef/wiki/Compilers/ for a list of compiler identifier macros */ +/** @see https://msdn.microsoft.com/en-us/library/b0084kay.aspx for VS2013 predefined macros */ + +#if defined(_MSC_VER) && !defined(__clang__) +# define C4_MSVC +# define C4_MSVC_VERSION_2022 17 +# define C4_MSVC_VERSION_2019 16 +# define C4_MSVC_VERSION_2017 15 +# define C4_MSVC_VERSION_2015 14 +# define C4_MSVC_VERSION_2013 12 +# define C4_MSVC_VERSION_2012 11 +# if _MSC_VER >= 1930 +# define C4_MSVC_VERSION C4_MSVC_VERSION_2022 // visual studio 2022 +# define C4_MSVC_2022 +# elif _MSC_VER >= 1920 +# define C4_MSVC_VERSION C4_MSVC_VERSION_2019 // visual studio 2019 +# define C4_MSVC_2019 +# elif _MSC_VER >= 1910 +# define C4_MSVC_VERSION C4_MSVC_VERSION_2017 // visual studio 2017 +# define C4_MSVC_2017 +# elif _MSC_VER == 1900 +# define C4_MSVC_VERSION C4_MSVC_VERSION_2015 // visual studio 2015 +# define C4_MSVC_2015 +# elif _MSC_VER == 1800 +# error "MSVC version not supported" +# define C4_MSVC_VERSION C4_MSVC_VERSION_2013 // visual studio 2013 +# define C4_MSVC_2013 +# elif _MSC_VER == 1700 +# error "MSVC version not supported" +# define C4_MSVC_VERSION C4_MSVC_VERSION_2012 // visual studio 2012 +# define C4_MSVC_2012 +# elif _MSC_VER == 1600 +# error "MSVC version not supported" +# define C4_MSVC_VERSION 10 // visual studio 2010 +# define C4_MSVC_2010 +# elif _MSC_VER == 1500 +# error "MSVC version not supported" +# define C4_MSVC_VERSION 09 // visual studio 2008 +# define C4_MSVC_2008 +# elif _MSC_VER == 1400 +# error "MSVC version not supported" +# define C4_MSVC_VERSION 08 // visual studio 2005 +# define C4_MSVC_2005 +# else +# error "MSVC version not supported" +# endif // _MSC_VER +#else +# define C4_MSVC_VERSION 0 // visual studio not present +# define C4_GCC_LIKE +# ifdef __INTEL_COMPILER // check ICC before checking GCC, as ICC defines __GNUC__ too +# define C4_ICC +# define C4_ICC_VERSION __INTEL_COMPILER +# elif defined(__APPLE_CC__) +# define C4_XCODE +# if defined(__clang__) +# define C4_CLANG +# ifndef __apple_build_version__ +# define C4_CLANG_VERSION C4_VERSION_ENCODED(__clang_major__, __clang_minor__, __clang_patchlevel__) +# else +# define C4_CLANG_VERSION __apple_build_version__ +# endif +# else +# define C4_XCODE_VERSION __APPLE_CC__ +# endif +# elif defined(__clang__) +# define C4_CLANG +# ifndef __apple_build_version__ +# define C4_CLANG_VERSION C4_VERSION_ENCODED(__clang_major__, __clang_minor__, __clang_patchlevel__) +# else +# define C4_CLANG_VERSION __apple_build_version__ +# endif +# elif defined(__GNUC__) +# ifdef __MINGW32__ +# define C4_MINGW +# endif +# define C4_GCC +# if defined(__GNUC_PATCHLEVEL__) +# define C4_GCC_VERSION C4_VERSION_ENCODED(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +# else +# define C4_GCC_VERSION C4_VERSION_ENCODED(__GNUC__, __GNUC_MINOR__, 0) +# endif +# if __GNUC__ < 5 +# if __GNUC__ == 4 && __GNUC_MINOR__ >= 8 +// provided by cmake sub-project +# include "c4/gcc-4.8.hpp" +# else +// we do not support GCC < 4.8: +// * misses std::is_trivially_copyable +// * misses std::align +// * -Wshadow has false positives when a local function parameter has the same name as a method +# error "GCC < 4.8 is not supported" +# endif +# endif +# endif +#endif // defined(C4_WIN) && defined(_MSC_VER) + +#endif /* _C4_COMPILER_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/config.hpp b/3rdparty/rapidyaml/include/c4/config.hpp new file mode 100644 index 0000000000..bda8033b36 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/config.hpp @@ -0,0 +1,39 @@ +#ifndef _C4_CONFIG_HPP_ +#define _C4_CONFIG_HPP_ + +/** @defgroup basic_headers Basic headers + * @brief Headers providing basic macros, platform+cpu+compiler information, + * C++ facilities and basic typedefs. */ + +/** @file config.hpp Contains configuration defines and includes the basic_headers. + * @ingroup basic_headers */ + +//#define C4_DEBUG + +#define C4_ERROR_SHOWS_FILELINE +//#define C4_ERROR_SHOWS_FUNC +//#define C4_ERROR_THROWS_EXCEPTION +//#define C4_NO_ALLOC_DEFAULTS +//#define C4_REDEFINE_CPPNEW + +#ifndef C4_SIZE_TYPE +# define C4_SIZE_TYPE size_t +#endif + +#ifndef C4_STR_SIZE_TYPE +# define C4_STR_SIZE_TYPE C4_SIZE_TYPE +#endif + +#ifndef C4_TIME_TYPE +# define C4_TIME_TYPE double +#endif + +#include "c4/export.hpp" +#include "c4/preprocessor.hpp" +#include "c4/platform.hpp" +#include "c4/cpu.hpp" +#include "c4/compiler.hpp" +#include "c4/language.hpp" +#include "c4/types.hpp" + +#endif // _C4_CONFIG_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/cpu.hpp b/3rdparty/rapidyaml/include/c4/cpu.hpp new file mode 100644 index 0000000000..2c156d2ee4 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/cpu.hpp @@ -0,0 +1,205 @@ +#ifndef _C4_CPU_HPP_ +#define _C4_CPU_HPP_ + +/** @file cpu.hpp Provides processor information macros + * @ingroup basic_headers */ + +// see also https://sourceforge.net/p/predef/wiki/Architectures/ +// see also https://sourceforge.net/p/predef/wiki/Endianness/ +// see also https://github.com/googlesamples/android-ndk/blob/android-mk/hello-jni/jni/hello-jni.c +// see also http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/global/qprocessordetection.h + +#ifdef __ORDER_LITTLE_ENDIAN__ +# define _C4EL __ORDER_LITTLE_ENDIAN__ +#else +# define _C4EL 1234 +#endif + +#ifdef __ORDER_BIG_ENDIAN__ +# define _C4EB __ORDER_BIG_ENDIAN__ +#else +# define _C4EB 4321 +#endif + +// mixed byte order (eg, PowerPC or ia64) +#define _C4EM 1111 // NOLINT + + +// NOTE: to find defined macros in a platform, +// g++ -dM -E - = 8) \ + || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 8) +# define C4_CPU_ARMV8 +# elif defined(__ARM_ARCH_7__) || defined(_ARM_ARCH_7) \ + || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7R__) \ + || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__) \ + || defined(__ARM_ARCH_7EM__) \ + || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 7) \ + || (defined(_M_ARM) && _M_ARM >= 7) +# define C4_CPU_ARMV7 +# elif defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) \ + || defined(__ARM_ARCH_6T2__) || defined(__ARM_ARCH_6Z__) \ + || defined(__ARM_ARCH_6K__) || defined(__ARM_ARCH_6ZK__) \ + || defined(__ARM_ARCH_6M__) || defined(__ARM_ARCH_6KZ__) \ + || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 6) +# define C4_CPU_ARMV6 +# elif (defined(__ARM_ARCH) && __ARM_ARCH == 5) \ + || defined(__ARM_ARCH_5TEJ__) \ + || defined(__ARM_ARCH_5TE__) \ + || defined(__ARM_ARCH_5T__) \ + || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 5) +# define C4_CPU_ARMV5 +# elif (defined(__ARM_ARCH) && __ARM_ARCH == 4) \ + || defined(__ARM_ARCH_4T__) \ + || defined(__ARM_ARCH_4__) \ + || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 4) +# define C4_CPU_ARMV4 +# else +# error "unknown CPU architecture: ARM" +# endif +# endif +# if defined(__ARMEL__) || defined(__LITTLE_ENDIAN__) || defined(__AARCH64EL__) \ + || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) \ + || defined(_MSC_VER) // winarm64 does not provide any of the above macros, + // but advises little-endianess: + // https://docs.microsoft.com/en-us/cpp/build/overview-of-arm-abi-conventions?view=msvc-170 + // So if it is visual studio compiling, we'll assume little endian. +# define C4_BYTE_ORDER _C4EL +# elif defined(__ARMEB__) || defined(__BIG_ENDIAN__) || defined(__AARCH64EB__) \ + || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) +# define C4_BYTE_ORDER _C4EB +# elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_PDP_ENDIAN__) +# define C4_BYTE_ORDER _C4EM +# else +# error "unknown endianness" +# endif + +#elif defined(__ia64) || defined(__ia64__) || defined(_M_IA64) +# define C4_CPU_IA64 +# define C4_WORDSIZE 8 +# define C4_BYTE_ORDER _C4EM + // itanium is bi-endian - check byte order below + +#elif defined(__ppc__) || defined(__ppc) || defined(__powerpc__) \ + || defined(_ARCH_COM) || defined(_ARCH_PWR) || defined(_ARCH_PPC) \ + || defined(_M_MPPC) || defined(_M_PPC) +# if defined(__ppc64__) || defined(__powerpc64__) || defined(__64BIT__) +# define C4_CPU_PPC64 +# define C4_WORDSIZE 8 +# else +# define C4_CPU_PPC +# define C4_WORDSIZE 4 +# endif +# define C4_BYTE_ORDER _C4EM + // ppc is bi-endian - check byte order below + +#elif defined(__s390x__) || defined(__zarch__) || defined(__SYSC_ZARCH_) +# define C4_CPU_S390_X +# define C4_WORDSIZE 8 +# define C4_BYTE_ORDER _C4EB + +#elif defined(__xtensa__) || defined(__XTENSA__) +# define C4_CPU_XTENSA +# define C4_WORDSIZE 4 +// not sure about this... +# if defined(__XTENSA_EL__) || defined(__xtensa_el__) +# define C4_BYTE_ORDER _C4EL +# else +# define C4_BYTE_ORDER _C4EB +# endif + +#elif defined(__riscv) +# if __riscv_xlen == 64 +# define C4_CPU_RISCV64 +# define C4_WORDSIZE 8 +# else +# define C4_CPU_RISCV32 +# define C4_WORDSIZE 4 +# endif +# define C4_BYTE_ORDER _C4EL + +#elif defined(__EMSCRIPTEN__) +# define C4_BYTE_ORDER _C4EL +# define C4_WORDSIZE 4 + +#elif defined(__loongarch__) +# if defined(__loongarch64) +# define C4_CPU_LOONGARCH64 +# define C4_WORDSIZE 8 +# else +# define C4_CPU_LOONGARCH +# define C4_WORDSIZE 4 +# endif +# define C4_BYTE_ORDER _C4EL + +#elif defined(__mips__) || defined(_mips) || defined(mips) +# if defined(__mips) +# if __mips == 64 +# define C4_CPU_MIPS64 +# define C4_WORDSIZE 8 +# elif __mips == 32 +# define C4_CPU_MIPS32 +# define C4_WORDSIZE 4 +# endif +# elif defined(__arch64__) || (defined(__SIZE_WIDTH__) && __SIZE_WIDTH__ == 64) || (defined(__LP64__) && __LP64__) +# define C4_CPU_MIPS64 +# define C4_WORDSIZE 8 +# elif defined(__arch32__) || (defined(__SIZE_WIDTH__) && __SIZE_WIDTH__ == 32) || (defined(__LP32__) && __LP32__) +# define C4_CPU_MIPS32 +# define C4_WORDSIZE 4 +# else +# error "unknown mips architecture" +# endif +# if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +# define C4_BYTE_ORDER _C4EB +# elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define C4_BYTE_ORDER _C4EL +# else +# error "unknown mips endianness" +# endif + +#elif defined(__sparc__) || defined(__sparc) || defined(sparc) +# if defined(__arch64__) || (defined(__SIZE_WIDTH__) && __SIZE_WIDTH__ == 64) || (defined(__LP64__) && __LP64__) +# define C4_CPU_SPARC64 +# define C4_WORDSIZE 8 +# elif defined(__arch32__) || (defined(__SIZE_WIDTH__) && __SIZE_WIDTH__ == 32) || (defined(__LP32__) && __LP32__) +# define C4_CPU_SPARC32 +# define C4_WORDSIZE 4 +# else +# error "unknown sparc architecture" +# endif +# define C4_BYTE_ORDER _C4EB + +#elif defined(SWIG) +# error "please define CPU architecture macros when compiling with swig" + +#else +# error "unknown CPU architecture" +#endif + +#define C4_LITTLE_ENDIAN (C4_BYTE_ORDER == _C4EL) +#define C4_BIG_ENDIAN (C4_BYTE_ORDER == _C4EB) +#define C4_MIXED_ENDIAN (C4_BYTE_ORDER == _C4EM) + +#endif /* _C4_CPU_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/dump.hpp b/3rdparty/rapidyaml/include/c4/dump.hpp new file mode 100644 index 0000000000..1e3d15d4a3 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/dump.hpp @@ -0,0 +1,798 @@ +#ifndef C4_DUMP_HPP_ +#define C4_DUMP_HPP_ + +#include + +/** @file dump.hpp This file provides functions to dump several + * arguments as strings to a user-provided function sink, for example + * to implement a type-safe printf()-like function (where the sink + * would just be a plain call to putchars()). The function sink can be + * passed either by dynamic dispatching or by static dispatching (as a + * template argument). There are analogs to @ref c4::cat() (@ref + * c4::cat_dump() and @ref c4::cat_dump_resume()), @ref c4::catsep() + * (@ref catsetp_dump() and @ref catsep_dump_resume()) and @ref + * c4::format() (@ref c4::format_dump() and @ref + * c4::format_dump_resume()). The analogs have two types: immediate + * and resuming. An analog of immediate type cannot be retried when + * the work buffer is too small; this means that successful dumps in + * the first (successful) arguments will be dumped again in the + * subsequent attempt to call. An analog of resuming type will only + * ever dump as-yet-undumped arguments, through the use of @ref + * DumpResults return type. */ + +namespace c4 { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup dump_building_blocks Basic building blocks for dumping. + * + * The basic building block: given an argument and a + * buffer, serialize the argument to the buffer using @ref + * c4::to_chars(), and dump the buffer to the provided sink + * function. When the argument is a string, no serialization is + * performed, and the argument is dumped directly to the sink. + * + * @{ */ + + +/** Type of the function to be used as the sink. This function + * receives as its argument the string with characters to send to the + * sink. + * + * @warning the string passed to the sink may have zero length. If the + * user sink uses memcpy(), the call to memcpy() should be defended + * with a check for zero length (calling memcpy with zero length is + * undefined behavior). + * */ +using SinkPfn = void (*)(csubstr str); + + +/** a traits class to use in SFINAE with @ref c4::dump() to select if + * a type is treated as string type (which is dumped directly to the + * sink, using to_csubstr()), or if the type is treated as a value, + * which is first serialized to a buffer using to_chars(), and then + * the serialization serialized as */ +template struct dump_directly : public std::false_type {}; +template<> struct dump_directly : public std::true_type {}; +template<> struct dump_directly< substr> : public std::true_type {}; +template<> struct dump_directly : public std::true_type {}; +template<> struct dump_directly< char*> : public std::true_type {}; +template struct dump_directly : public std::true_type {}; +template struct dump_directly< char (&)[N]> : public std::true_type {}; +template struct dump_directly : public std::true_type {}; +template struct dump_directly< char[N]> : public std::true_type {}; + + +/** Dump a string-type object to the (statically dispatched) sink. The + * string is dumped directly, without any intermediate serialization. + * + * @return the number of bytes needed to serialize the string-type + * object, which is always 0 because there is no serialization + * + * @note the argument is considered a value when @ref + * dump_directly is a false type, which is the default. To enable + * the argument to be treated as a string type, which is dumped + * directly to the sink without intermediate serialization, define + * dump_directly to a true type. + * + * @warning the string passed to the sink may have zero length. If the + * user sink uses memcpy(), the call to memcpy() should be defended + * with a check for zero length (calling memcpy with zero length is + * undefined behavior). + * + * @see dump_directly + */ +template +inline auto dump(substr buf, Arg const& a) + -> typename std::enable_if::value, size_t>::type +{ + C4_ASSERT(!buf.overlaps(a)); + C4_UNUSED(buf); + // dump directly, no need to serialize to the buffer + sinkfn(to_csubstr(a)); + return 0; // no space was used in the buffer +} +/** Dump a string-type object to the (dynamically dispatched) + * sink. The string is dumped directly, without any intermediate + * serialization to the buffer. + * + * @return the number of bytes needed to serialize the string-type + * object, which is always 0 because there is no serialization + * + * @note the argument is considered a value when @ref + * dump_directly is a false type, which is the default. To enable + * the argument to be treated as a string type, which is dumped + * directly to the sink without intermediate serialization, define + * dump_directly to a true type. + * + * @warning the string passed to the sink may have zero length. If the + * user sink uses memcpy(), the call to memcpy() should be defended + * with a check for zero length (calling memcpy with zero length is + * undefined behavior). + * + * @see dump_directly + * */ +template +inline auto dump(SinkFn &&sinkfn, substr buf, Arg const& a) + -> typename std::enable_if::value, size_t>::type +{ + C4_UNUSED(buf); + C4_ASSERT(!buf.overlaps(a)); + // dump directly, no need to serialize to the buffer + std::forward(sinkfn)(to_csubstr(a)); + return 0; // no space was used in the buffer +} + + +/** Dump a value to the sink. Given an argument @p a and a buffer @p + * buf, serialize the argument to the buffer using @ref to_chars(), + * and then dump the buffer to the (statically dispatched) sink + * function passed as the template argument. If the buffer is too + * small to serialize the argument, the sink function is not called. + * + * @note the argument is considered a value when @ref + * dump_directly is a false type, which is the default. To enable + * the argument to be treated as a string type, which is dumped + * directly to the sink without intermediate serialization, define + * dump_directly to a true type. + * + * @see dump_directly + * + * @return the number of characters required to serialize the + * argument. */ +template +inline auto dump(substr buf, Arg const& a) + -> typename std::enable_if::value, size_t>::type +{ + // serialize to the buffer + const size_t sz = to_chars(buf, a); + // dump the buffer to the sink + if(C4_LIKELY(sz <= buf.len)) + { + // NOTE: don't do this: + //sinkfn(buf.first(sz)); + // ... but do this instead: + sinkfn({buf.str, sz}); + // ... this is needed because Release builds for armv5 and + // armv6 were failing for the first call, with the wrong + // buffer being passed into the function (!) + } + return sz; +} +/** Dump a value to the sink. Given an argument @p a and a buffer @p + * buf, serialize the argument to the buffer using @ref + * c4::to_chars(), and then dump the buffer to the (dynamically + * dispatched) sink function, passed as @p sinkfn. If the buffer is too + * small to serialize the argument, the sink function is not called. + * + * @note the argument is considered a value when @ref + * dump_directly is a false type, which is the default. To enable + * the argument to be treated as a string type, which is dumped + * directly to the sink without intermediate serialization, define + * dump_directly to a true type. + * + * @see @ref dump_directly + * + * @return the number of characters required to serialize the + * argument. */ +template +inline auto dump(SinkFn &&sinkfn, substr buf, Arg const& a) + -> typename std::enable_if::value, size_t>::type +{ + // serialize to the buffer + const size_t sz = to_chars(buf, a); + // dump the buffer to the sink + if(C4_LIKELY(sz <= buf.len)) + { + // NOTE: don't do this: + //std::forward(sinkfn)(buf.first(sz)); + // ... but do this instead: + std::forward(sinkfn)({buf.str, sz}); + // ... this is needed because Release builds for armv5 and + // armv6 were failing for the first call, with the wrong + // buffer being passed into the function (!) + } + return sz; +} + + +/** An opaque type used by resumeable dump functions like @ref + * cat_dump_resume(), @ref catsep_dump_resume() or @ref + * format_dump_resume(). */ +struct DumpResults +{ + enum : size_t { noarg = (size_t)-1 }; + size_t bufsize = 0; + size_t lastok = noarg; + bool success_until(size_t expected) const { return lastok == noarg ? false : lastok >= expected; } + bool write_arg(size_t arg) const { return lastok == noarg || arg > lastok; } + size_t argfail() const { return lastok + 1; } +}; + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** @defgroup cat_dump Dump several arguments to a sink, + * concatenated. This is the analog to @ref c4::cat(), with the + * significant difference that each argument is immediately sent to + * the sink (resulting in multiple calls to the sink function, once + * per argument), whereas equivalent usage of c4::cat() would first + * serialize all the arguments to the buffer, and then call the sink + * once at the end. As a consequence, the size needed for the buffer + * is only the maximum of the size needed for the arguments, whereas + * with c4::cat(), the size needed for the buffer would be the sum of + * the size needed for the arguments. When the size of dump + * + * @{ */ + +/// @cond dev +// terminates the variadic recursion +template +size_t cat_dump(SinkFn &&, substr) // NOLINT +{ + return 0; +} + +// terminates the variadic recursion +template +size_t cat_dump(substr) // NOLINT +{ + return 0; +} +/// @endcond + + +/** Dump several arguments to the (dynamically dispatched) sink + * function, as if through c4::cat(). For each argument, @ref dump() + * is called with the buffer and sink. If any of the arguments is too + * large for the buffer, no subsequent argument is sent to the sink, + * (but all the arguments are still processed to compute the size + * required for the buffer). This function can be safely called with an + * empty buffer. + * + * @return the size required for the buffer, which is the maximum size + * across all arguments + * + * @note subsequent calls with the same set of arguments will dump + * again the first successful arguments. If each argument must only be + * sent once to the sink (for example with printf-like behavior), use + * instead @ref cat_dump_resume(). */ +template +size_t cat_dump(SinkFn &&sinkfn, substr buf, Arg const& a, Args const& ...more) +{ + const size_t size_for_a = dump(std::forward(sinkfn), buf, a); + if(C4_UNLIKELY(size_for_a > buf.len)) + buf.len = 0; // ensure no more calls to the sink + const size_t size_for_more = cat_dump(std::forward(sinkfn), buf, more...); + return size_for_more > size_for_a ? size_for_more : size_for_a; +} + + +/** Dump several arguments to the (statically dispatched) sink + * function, as if through c4::cat(). For each argument, @ref dump() + * is called with the buffer and sink. If any of the arguments is too + * large for the buffer, no subsequent argument is sent to the sink, + * (but all the arguments are still processed to compute the size + * required for the buffer). This function can be safely called with an + * empty buffer. + * + * @return the size required for the buffer, which is the maximum size + * across all arguments + * + * @note subsequent calls with the same set of arguments will dump + * again the first successful arguments. If each argument must only be + * sent once to the sink (for example with printf-like behavior), use + * instead @ref cat_dump_resume(). */ +template +size_t cat_dump(substr buf, Arg const& a, Args const& ...more) +{ + const size_t size_for_a = dump(buf, a); + if(C4_UNLIKELY(size_for_a > buf.len)) + buf.len = 0; // ensure no more calls to the sink + const size_t size_for_more = cat_dump(buf, more...); + return size_for_more > size_for_a ? size_for_more : size_for_a; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev +namespace detail { + +// terminates the variadic recursion +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(size_t, DumpResults results, substr) +{ + return results; +} + +// terminates the variadic recursion +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(size_t, SinkFn &&, DumpResults results, substr) // NOLINT +{ + return results; +} + +template +DumpResults cat_dump_resume(size_t currarg, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + if(C4_LIKELY(results.write_arg(currarg))) + { + size_t sz = dump(buf, a); // yield to the specialized function + if(currarg == results.lastok + 1 && sz <= buf.len) + results.lastok = currarg; + results.bufsize = sz > results.bufsize ? sz : results.bufsize; + } + return detail::cat_dump_resume(currarg + 1u, results, buf, more...); +} + +template +DumpResults cat_dump_resume(size_t currarg, SinkFn &&sinkfn, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + if(C4_LIKELY(results.write_arg(currarg))) + { + size_t sz = dump(std::forward(sinkfn), buf, a); // yield to the specialized function + if(currarg == results.lastok + 1 && sz <= buf.len) + results.lastok = currarg; + results.bufsize = sz > results.bufsize ? sz : results.bufsize; + } + return detail::cat_dump_resume(currarg + 1u, std::forward(sinkfn), results, buf, more...); +} +} // namespace detail +/// @endcond + + +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + return detail::cat_dump_resume(0u, DumpResults{}, buf, a, more...); +} + +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(SinkFn &&sinkfn, substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + return detail::cat_dump_resume(0u, std::forward(sinkfn), DumpResults{}, buf, a, more...); +} + + +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + if(results.bufsize > buf.len) + return results; + return detail::cat_dump_resume(0u, results, buf, a, more...); +} + +template +C4_ALWAYS_INLINE DumpResults cat_dump_resume(SinkFn &&sinkfn, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& ...more) +{ + if(results.bufsize > buf.len) + return results; + return detail::cat_dump_resume(0u, std::forward(sinkfn), results, buf, a, more...); +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev +// terminate the recursion +template +size_t catsep_dump(SinkFn &&, substr, Sep const& C4_RESTRICT) // NOLINT +{ + return 0; +} + +// terminate the recursion +template +size_t catsep_dump(substr, Sep const& C4_RESTRICT) // NOLINT +{ + return 0; +} +/// @endcond + +/** take the function pointer as a function argument */ +template +size_t catsep_dump(SinkFn &&sinkfn, substr buf, Sep const& sep, Arg const& a, Args const& ...more) +{ + size_t sz = dump(std::forward(sinkfn), buf, a); + if(C4_UNLIKELY(sz > buf.len)) + buf.len = 0; // ensure no more calls + if C4_IF_CONSTEXPR (sizeof...(more) > 0) + { + size_t szsep = dump(std::forward(sinkfn), buf, sep); + if(C4_UNLIKELY(szsep > buf.len)) + buf.len = 0; // ensure no more calls + sz = sz > szsep ? sz : szsep; + } + size_t size_for_more = catsep_dump(std::forward(sinkfn), buf, sep, more...); + return size_for_more > sz ? size_for_more : sz; +} + +/** take the function pointer as a template argument */ +template +size_t catsep_dump(substr buf, Sep const& sep, Arg const& a, Args const& ...more) +{ + size_t sz = dump(buf, a); + if(C4_UNLIKELY(sz > buf.len)) + buf.len = 0; // ensure no more calls + if C4_IF_CONSTEXPR (sizeof...(more) > 0) + { + size_t szsep = dump(buf, sep); + if(C4_UNLIKELY(szsep > buf.len)) + buf.len = 0; // ensure no more calls + sz = sz > szsep ? sz : szsep; + } + size_t size_for_more = catsep_dump(buf, sep, more...); + return size_for_more > sz ? size_for_more : sz; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev +namespace detail { +template +void catsep_dump_resume_(size_t currarg, DumpResults *C4_RESTRICT results, substr *buf, Arg const& a) +{ + if(C4_LIKELY(results->write_arg(currarg))) + { + size_t sz = dump(*buf, a); + results->bufsize = sz > results->bufsize ? sz : results->bufsize; + if(C4_LIKELY(sz <= buf->len)) + results->lastok = currarg; + else + buf->len = 0; + } +} + +template +void catsep_dump_resume_(size_t currarg, SinkFn &&sinkfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Arg const& C4_RESTRICT a) +{ + if(C4_LIKELY(results->write_arg(currarg))) + { + size_t sz = dump(std::forward(sinkfn), *buf, a); + results->bufsize = sz > results->bufsize ? sz : results->bufsize; + if(C4_LIKELY(sz <= buf->len)) + results->lastok = currarg; + else + buf->len = 0; + } +} + +template +C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const&, Arg const& a) +{ + detail::catsep_dump_resume_(currarg, results, buf, a); +} + +template +C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, SinkFn &&sinkfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const&, Arg const& a) +{ + detail::catsep_dump_resume_(currarg, std::forward(sinkfn), results, buf, a); +} + +template +C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& sep, Arg const& a, Args const& ...more) +{ + detail::catsep_dump_resume_(currarg , results, buf, a); + detail::catsep_dump_resume_(currarg + 1u, results, buf, sep); + detail::catsep_dump_resume (currarg + 2u, results, buf, sep, more...); +} + +template +C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, SinkFn &&sinkfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& sep, Arg const& a, Args const& ...more) +{ + detail::catsep_dump_resume_(currarg , std::forward(sinkfn), results, buf, a); + detail::catsep_dump_resume_(currarg + 1u, std::forward(sinkfn), results, buf, sep); + detail::catsep_dump_resume (currarg + 2u, std::forward(sinkfn), results, buf, sep, more...); +} +} // namespace detail +/// @endcond + + +template +C4_ALWAYS_INLINE DumpResults catsep_dump_resume(substr buf, Sep const& sep, Args const& ...args) +{ + DumpResults results; + detail::catsep_dump_resume(0u, &results, &buf, sep, args...); + return results; +} + +template +C4_ALWAYS_INLINE DumpResults catsep_dump_resume(SinkFn &&sinkfn, substr buf, Sep const& sep, Args const& ...args) +{ + DumpResults results; + detail::catsep_dump_resume(0u, std::forward(sinkfn), &results, &buf, sep, args...); + return results; +} + + +template +C4_ALWAYS_INLINE DumpResults catsep_dump_resume(DumpResults results, substr buf, Sep const& sep, Args const& ...args) +{ + detail::catsep_dump_resume(0u, &results, &buf, sep, args...); + return results; +} + +template +C4_ALWAYS_INLINE DumpResults catsep_dump_resume(SinkFn &&sinkfn, DumpResults results, substr buf, Sep const& sep, Args const& ...args) +{ + detail::catsep_dump_resume(0u, std::forward(sinkfn), &results, &buf, sep, args...); + return results; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev +namespace detail { +// terminate the recursion +C4_ALWAYS_INLINE size_t _format_dump_compute_size() +{ + return 0u; +} +template +C4_ALWAYS_INLINE auto _format_dump_compute_size(T const&) + -> typename std::enable_if::value, size_t>::type +{ + return 0u; // no buffer needed +} +template +C4_ALWAYS_INLINE auto _format_dump_compute_size(T const& v) + -> typename std::enable_if::value, size_t>::type +{ + return to_chars(substr{}, v); +} +template +size_t _format_dump_compute_size(Arg const& a, Args const& ...more) +{ + const size_t sz = _format_dump_compute_size(a); // don't call to_chars() directly + const size_t rest = _format_dump_compute_size(more...); + return sz > rest ? sz : rest; +} +} // namespace detail + +// terminate the recursion +template +C4_ALWAYS_INLINE size_t format_dump(SinkFn &&sinkfn, substr, csubstr fmt) +{ + // we can dump without using buf, so no need to check it + std::forward(sinkfn)(fmt); + return 0u; +} +// terminate the recursion +/** take the function pointer as a template argument */ +template +C4_ALWAYS_INLINE size_t format_dump(substr, csubstr fmt) +{ + // we can dump without using buf, so no need to check it + sinkfn(fmt); + return 0u; +} +/// @endcond + + +/** take the function pointer as a function argument */ +template +C4_NO_INLINE size_t format_dump(SinkFn &&sinkfn, substr buf, csubstr fmt, Arg const& a, Args const& ...more) +{ + // we can dump without using buf + // but we'll only dump if the buffer is ok + size_t pos = fmt.find("{}"); // @todo use _find_fmt() + if(C4_UNLIKELY(pos == csubstr::npos)) + { + std::forward(sinkfn)(fmt); + return 0u; + } + std::forward(sinkfn)(fmt.first(pos)); // we can dump without using buf + fmt = fmt.sub(pos + 2); // skip {} do this before assigning to pos again + pos = dump(std::forward(sinkfn), buf, a); // reuse pos to get needed_size + // dump no more if the buffer was exhausted + size_t size_for_more; + if(C4_LIKELY(pos <= buf.len)) + size_for_more = format_dump(std::forward(sinkfn), buf, fmt, more...); + else + size_for_more = detail::_format_dump_compute_size(more...); + return size_for_more > pos ? size_for_more : pos; +} + +/** take the function pointer as a template argument */ +template +C4_NO_INLINE size_t format_dump(substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& ...more) +{ + // we can dump without using buf + // but we'll only dump if the buffer is ok + size_t pos = fmt.find("{}"); // @todo use _find_fmt() + if(C4_UNLIKELY(pos == csubstr::npos)) + { + sinkfn(fmt); + return 0u; + } + sinkfn(fmt.first(pos)); // we can dump without using buf + fmt = fmt.sub(pos + 2); // skip {} do this before assigning to pos again + pos = dump(buf, a); // reuse pos to get needed_size + // dump no more if the buffer was exhausted + size_t size_for_more; + if(C4_LIKELY(pos <= buf.len)) + size_for_more = format_dump(buf, fmt, more...); + else + size_for_more = detail::_format_dump_compute_size(more...); + return size_for_more > pos ? size_for_more : pos; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev +namespace detail { +// terminate the recursion +template +DumpResults format_dump_resume(size_t currarg, DumpResults results, substr, csubstr fmt) +{ + if(C4_LIKELY(results.write_arg(currarg))) + { + // we can dump without using buf + sinkfn(fmt); + results.lastok = currarg; + } + return results; +} + +// terminate the recursion +template +DumpResults format_dump_resume(size_t currarg, SinkFn &&sinkfn, DumpResults results, substr, csubstr fmt) +{ + if(C4_LIKELY(results.write_arg(currarg))) + { + // we can dump without using buf + std::forward(sinkfn)(fmt); + results.lastok = currarg; + } + return results; +} + +template +DumpResults format_dump_resume(size_t currarg, DumpResults results, substr buf, csubstr fmt, Arg const& a, Args const& ...more) +{ + // we need to process the format even if we're not + // going to print the first arguments because we're resuming + const size_t pos = fmt.find("{}"); // @todo use _find_fmt() + if(C4_LIKELY(pos != csubstr::npos)) + { + if(C4_LIKELY(results.write_arg(currarg))) + { + sinkfn(fmt.first(pos)); + results.lastok = currarg; + } + if(C4_LIKELY(results.write_arg(currarg + 1u))) + { + const size_t len = dump(buf, a); + results.bufsize = len > results.bufsize ? len : results.bufsize; + if(C4_LIKELY(len <= buf.len)) + { + results.lastok = currarg + 1u; + } + else + { + const size_t rest = _format_dump_compute_size(more...); + results.bufsize = rest > results.bufsize ? rest : results.bufsize; + return results; + } + } + } + else + { + if(C4_LIKELY(results.write_arg(currarg))) + { + sinkfn(fmt); + results.lastok = currarg; + } + return results; + } + // NOTE: sparc64 had trouble with reassignment to fmt, and + // was passing the original fmt to the recursion: + //fmt = fmt.sub(pos + 2); // DONT! + return detail::format_dump_resume(currarg + 2u, results, buf, fmt.sub(pos + 2), more...); +} + + +template +DumpResults format_dump_resume(size_t currarg, SinkFn &&sinkfn, DumpResults results, substr buf, csubstr fmt, Arg const& a, Args const& ...more) +{ + // we need to process the format even if we're not + // going to print the first arguments because we're resuming + const size_t pos = fmt.find("{}"); // @todo use _find_fmt() + if(C4_LIKELY(pos != csubstr::npos)) + { + if(C4_LIKELY(results.write_arg(currarg))) + { + std::forward(sinkfn)(fmt.first(pos)); + results.lastok = currarg; + } + if(C4_LIKELY(results.write_arg(currarg + 1u))) + { + const size_t len = dump(std::forward(sinkfn), buf, a); + results.bufsize = len > results.bufsize ? len : results.bufsize; + if(C4_LIKELY(len <= buf.len)) + { + results.lastok = currarg + 1u; + } + else + { + const size_t rest = _format_dump_compute_size(more...); + results.bufsize = rest > results.bufsize ? rest : results.bufsize; + return results; + } + } + } + else + { + if(C4_LIKELY(results.write_arg(currarg))) + { + std::forward(sinkfn)(fmt); + results.lastok = currarg; + } + return results; + } + // NOTE: sparc64 had trouble with reassignment to fmt, and + // was passing the original fmt to the recursion: + //fmt = fmt.sub(pos + 2); // DONT! + return detail::format_dump_resume(currarg + 2u, std::forward(sinkfn), results, buf, fmt.sub(pos + 2), more...); +} +} // namespace detail +/// @endcond + + +template +C4_ALWAYS_INLINE DumpResults format_dump_resume(substr buf, csubstr fmt, Args const& ...args) +{ + return detail::format_dump_resume(0u, DumpResults{}, buf, fmt, args...); +} + +template +C4_ALWAYS_INLINE DumpResults format_dump_resume(SinkFn &&sinkfn, substr buf, csubstr fmt, Args const& ...args) +{ + return detail::format_dump_resume(0u, std::forward(sinkfn), DumpResults{}, buf, fmt, args...); +} + + +template +C4_ALWAYS_INLINE DumpResults format_dump_resume(DumpResults results, substr buf, csubstr fmt, Args const& ...args) +{ + return detail::format_dump_resume(0u, results, buf, fmt, args...); +} + +template +C4_ALWAYS_INLINE DumpResults format_dump_resume(SinkFn &&sinkfn, DumpResults results, substr buf, csubstr fmt, Args const& ...args) +{ + return detail::format_dump_resume(0u, std::forward(sinkfn), results, buf, fmt, args...); +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace c4 + + +#endif /* C4_DUMP_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/error.hpp b/3rdparty/rapidyaml/include/c4/error.hpp new file mode 100644 index 0000000000..d64f96140f --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/error.hpp @@ -0,0 +1,440 @@ +#ifndef _C4_ERROR_HPP_ +#define _C4_ERROR_HPP_ + +/** @file error.hpp Facilities for error reporting and runtime assertions. */ + +/** @defgroup error_checking Error checking */ + +#include "c4/config.hpp" + +#ifdef _DOXYGEN_ + /** if this is defined and exceptions are enabled, then calls to C4_ERROR() + * will throw an exception + * @ingroup error_checking */ +# define C4_EXCEPTIONS_ENABLED + /** if this is defined and exceptions are enabled, then calls to C4_ERROR() + * will throw an exception + * @see C4_EXCEPTIONS_ENABLED + * @ingroup error_checking */ +# define C4_ERROR_THROWS_EXCEPTION + /** evaluates to noexcept when C4_ERROR might be called and + * exceptions are disabled. Otherwise, defaults to nothing. + * @ingroup error_checking */ +# define C4_NOEXCEPT +#endif // _DOXYGEN_ + +#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION) +# define C4_NOEXCEPT +#else +# define C4_NOEXCEPT noexcept +#endif + + +namespace c4 { +namespace detail { +struct fail_type__ {}; +} // detail +} // c4 +#define C4_STATIC_ERROR(dummy_type, errmsg) \ + static_assert(std::is_same::value, errmsg) + + +//----------------------------------------------------------------------------- + +#define C4_ASSERT_SAME_TYPE(ty1, ty2) \ + C4_STATIC_ASSERT(std::is_same::value) + +#define C4_ASSERT_DIFF_TYPE(ty1, ty2) \ + C4_STATIC_ASSERT( ! std::is_same::value) + + +//----------------------------------------------------------------------------- + +#ifdef _DOXYGEN_ +/** utility macro that triggers a breakpoint when + * the debugger is attached and NDEBUG is not defined. + * @ingroup error_checking */ +# define C4_DEBUG_BREAK() +#endif // _DOXYGEN_ + + +#if defined(NDEBUG) || defined(C4_NO_DEBUG_BREAK) +# define C4_DEBUG_BREAK() +#else +# ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wundef" +# if !defined(__APPLE_CC__) +# if __clang_major__ >= 10 +# pragma clang diagnostic ignored "-Wgnu-inline-cpp-without-extern" // debugbreak/debugbreak.h:50:16: error: 'gnu_inline' attribute without 'extern' in C++ treated as externally available, this changed in Clang 10 [-Werror,-Wgnu-inline-cpp-without-extern] +# endif +# else +# if __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wgnu-inline-cpp-without-extern" // debugbreak/debugbreak.h:50:16: error: 'gnu_inline' attribute without 'extern' in C++ treated as externally available, this changed in Clang 10 [-Werror,-Wgnu-inline-cpp-without-extern] +# endif +# endif +# elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wundef" +# endif +# include +# define C4_DEBUG_BREAK() if(c4::is_debugger_attached()) { ::debug_break(); } +# ifdef __clang__ +# pragma clang diagnostic pop +# elif defined(__GNUC__) +# pragma GCC diagnostic pop +# endif +#endif + +namespace c4 { +C4CORE_EXPORT bool is_debugger_attached(); +} // namespace c4 + + +//----------------------------------------------------------------------------- + +#ifdef __clang__ + /* NOTE: using , ## __VA_ARGS__ to deal with zero-args calls to + * variadic macros is not portable, but works in clang, gcc, msvc, icc. + * clang requires switching off compiler warnings for pedantic mode. + * @see http://stackoverflow.com/questions/32047685/variadic-macro-without-arguments */ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" // warning: token pasting of ',' and __VA_ARGS__ is a GNU extension +#elif defined(__GNUC__) + /* GCC also issues a warning for zero-args calls to variadic macros. + * This warning is switched on with -pedantic and apparently there is no + * easy way to turn it off as with clang. But marking this as a system + * header works. + * @see https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html + * @see http://stackoverflow.com/questions/35587137/ */ +# pragma GCC system_header +#endif + + +//----------------------------------------------------------------------------- + +namespace c4 { + +typedef enum : uint32_t { + /** when an error happens and the debugger is attached, call C4_DEBUG_BREAK(). + * Without effect otherwise. */ + ON_ERROR_DEBUGBREAK = 0x01u << 0u, + /** when an error happens log a message. */ + ON_ERROR_LOG = 0x01u << 1u, + /** when an error happens invoke a callback if it was set with + * set_error_callback(). */ + ON_ERROR_CALLBACK = 0x01u << 2u, + /** when an error happens call std::terminate(). */ + ON_ERROR_ABORT = 0x01u << 3u, + /** when an error happens and exceptions are enabled throw an exception. + * Without effect otherwise. */ + ON_ERROR_THROW = 0x01u << 4u, + /** the default flags. */ + ON_ERROR_DEFAULTS = ON_ERROR_DEBUGBREAK|ON_ERROR_LOG|ON_ERROR_CALLBACK|ON_ERROR_ABORT +} ErrorFlags_e; +using error_flags = uint32_t; +C4CORE_EXPORT void set_error_flags(error_flags f); +C4CORE_EXPORT error_flags get_error_flags(); + + +using error_callback_type = void (*)(const char* msg, size_t msg_size); +C4CORE_EXPORT void set_error_callback(error_callback_type cb); +C4CORE_EXPORT error_callback_type get_error_callback(); + + +//----------------------------------------------------------------------------- +/** RAII class controling the error settings inside a scope. */ +struct ScopedErrorSettings // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) +{ + error_flags m_flags; + error_callback_type m_callback; + + explicit ScopedErrorSettings(error_callback_type cb) + : m_flags(get_error_flags()), + m_callback(get_error_callback()) + { + set_error_callback(cb); + } + explicit ScopedErrorSettings(error_flags flags) + : m_flags(get_error_flags()), + m_callback(get_error_callback()) + { + set_error_flags(flags); + } + explicit ScopedErrorSettings(error_flags flags, error_callback_type cb) + : m_flags(get_error_flags()), + m_callback(get_error_callback()) + { + set_error_flags(flags); + set_error_callback(cb); + } + ~ScopedErrorSettings() + { + set_error_flags(m_flags); + set_error_callback(m_callback); + } +}; + + +//----------------------------------------------------------------------------- + +/** source location */ +struct srcloc; + +// watchout: for VS the [[noreturn]] needs to come before other annotations like C4CORE_EXPORT +[[noreturn]] C4CORE_EXPORT void handle_error(srcloc s, const char *fmt, ...); +C4CORE_EXPORT void handle_warning(srcloc s, const char *fmt, ...); + + +# define C4_ERROR(msg, ...) \ + do { \ + if(c4::get_error_flags() & c4::ON_ERROR_DEBUGBREAK) \ + { \ + C4_DEBUG_BREAK() \ + } \ + c4::handle_error(C4_SRCLOC(), msg, ## __VA_ARGS__); \ + } while(0) + + +# define C4_WARNING(msg, ...) \ + c4::handle_warning(C4_SRCLOC(), msg, ## __VA_ARGS__) + + +#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC) + +struct srcloc +{ + const char *file = ""; + const char *func = ""; + int line = 0; +}; +#define C4_SRCLOC() c4::srcloc{__FILE__, C4_PRETTY_FUNC, __LINE__} + +#elif defined(C4_ERROR_SHOWS_FILELINE) + +struct srcloc +{ + const char *file; + int line; +}; +#define C4_SRCLOC() c4::srcloc{__FILE__, __LINE__} + +#elif ! defined(C4_ERROR_SHOWS_FUNC) + +struct srcloc +{ +}; +#define C4_SRCLOC() c4::srcloc() + +#else +# error not implemented +#endif + + +//----------------------------------------------------------------------------- +// assertions + +// Doxygen needs this so that only one definition counts +#ifdef _DOXYGEN_ + /** Explicitly enables assertions, independently of NDEBUG status. + * This is meant to allow enabling assertions even when NDEBUG is defined. + * Defaults to undefined. + * @ingroup error_checking */ +# define C4_USE_ASSERT + /** assert that a condition is true; this is turned off when NDEBUG + * is defined and C4_USE_ASSERT is not true. + * @ingroup error_checking */ +# define C4_ASSERT + /** same as C4_ASSERT(), additionally prints a printf-formatted message + * @ingroup error_checking */ +# define C4_ASSERT_MSG + /** evaluates to C4_NOEXCEPT when C4_XASSERT is disabled; otherwise, defaults + * to noexcept + * @ingroup error_checking */ +# define C4_NOEXCEPT_A +#endif // _DOXYGEN_ + +#ifndef C4_USE_ASSERT +# ifdef NDEBUG +# define C4_USE_ASSERT 0 +# else +# define C4_USE_ASSERT 1 +# endif +#endif + +#if C4_USE_ASSERT +# define C4_ASSERT(cond) C4_CHECK(cond) +# define C4_ASSERT_MSG(cond, /*fmt, */...) C4_CHECK_MSG(cond, ## __VA_ARGS__) +# define C4_ASSERT_IF(predicate, cond) if(predicate) { C4_ASSERT(cond); } +# define C4_NOEXCEPT_A C4_NOEXCEPT +#else +# define C4_ASSERT(cond) +# define C4_ASSERT_MSG(cond, /*fmt, */...) +# define C4_ASSERT_IF(predicate, cond) +# define C4_NOEXCEPT_A noexcept +#endif + + +//----------------------------------------------------------------------------- +// extreme assertions + +// Doxygen needs this so that only one definition counts +#ifdef _DOXYGEN_ + /** Explicitly enables extreme assertions; this is meant to allow enabling + * assertions even when NDEBUG is defined. Defaults to undefined. + * @ingroup error_checking */ +# define C4_USE_XASSERT + /** extreme assertion: can be switched off independently of + * the regular assertion; use for example for bounds checking in hot code. + * Turned on only when C4_USE_XASSERT is defined + * @ingroup error_checking */ +# define C4_XASSERT + /** same as C4_XASSERT(), and additionally prints a printf-formatted message + * @ingroup error_checking */ +# define C4_XASSERT_MSG + /** evaluates to C4_NOEXCEPT when C4_XASSERT is disabled; otherwise, defaults to noexcept + * @ingroup error_checking */ +# define C4_NOEXCEPT_X +#endif // _DOXYGEN_ + +#ifndef C4_USE_XASSERT +# define C4_USE_XASSERT C4_USE_ASSERT +#endif + +#if C4_USE_XASSERT +# define C4_XASSERT(cond) C4_CHECK(cond) +# define C4_XASSERT_MSG(cond, /*fmt, */...) C4_CHECK_MSG(cond, ## __VA_ARGS__) +# define C4_XASSERT_IF(predicate, cond) if(predicate) { C4_XASSERT(cond); } +# define C4_NOEXCEPT_X C4_NOEXCEPT +#else +# define C4_XASSERT(cond) +# define C4_XASSERT_MSG(cond, /*fmt, */...) +# define C4_XASSERT_IF(predicate, cond) +# define C4_NOEXCEPT_X noexcept +#endif + + +//----------------------------------------------------------------------------- +// checks: never switched-off + +/** Check that a condition is true, or raise an error when not + * true. Unlike C4_ASSERT(), this check is not disabled in non-debug + * builds. + * @see C4_ASSERT + * @ingroup error_checking + * + * @todo add constexpr-compatible compile-time assert: + * https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/ + */ +#define C4_CHECK(cond) \ + do { \ + if(C4_UNLIKELY(!(cond))) \ + { \ + C4_ERROR("check failed: %s", #cond); \ + } \ + } while(0) + + +/** like C4_CHECK(), and additionally log a printf-style message. + * @see C4_CHECK + * @ingroup error_checking */ +#define C4_CHECK_MSG(cond, fmt, ...) \ + do { \ + if(C4_UNLIKELY(!(cond))) \ + { \ + C4_ERROR("check failed: " #cond "\n" fmt, ## __VA_ARGS__); \ + } \ + } while(0) + + +//----------------------------------------------------------------------------- +// Common error conditions + +#define C4_NOT_IMPLEMENTED() C4_ERROR("NOT IMPLEMENTED") +#define C4_NOT_IMPLEMENTED_MSG(/*msg, */...) C4_ERROR("NOT IMPLEMENTED: " __VA_ARGS__) +#define C4_NOT_IMPLEMENTED_IF(condition) do { if(C4_UNLIKELY(condition)) { C4_ERROR("NOT IMPLEMENTED"); } } while(0) +#define C4_NOT_IMPLEMENTED_IF_MSG(condition, /*msg, */...) do { if(C4_UNLIKELY(condition)) { C4_ERROR("NOT IMPLEMENTED: " __VA_ARGS__); } } while(0) + +#define C4_NEVER_REACH() do { C4_ERROR("never reach this point"); C4_UNREACHABLE(); } while(0) +#define C4_NEVER_REACH_MSG(/*msg, */...) do { C4_ERROR("never reach this point: " __VA_ARGS__); C4_UNREACHABLE(); } while(0) + + + +//----------------------------------------------------------------------------- +// helpers for warning suppression +// idea adapted from https://github.com/onqtam/doctest/ + +// TODO: add C4_MESSAGE() https://stackoverflow.com/questions/18252351/custom-preprocessor-macro-for-a-conditional-pragma-message-xxx?rq=1 + + +#ifdef C4_MSVC +#define C4_SUPPRESS_WARNING_MSVC_PUSH __pragma(warning(push)) +#define C4_SUPPRESS_WARNING_MSVC(w) __pragma(warning(disable : w)) +#define C4_SUPPRESS_WARNING_MSVC_POP __pragma(warning(pop)) +#else // C4_MSVC +#define C4_SUPPRESS_WARNING_MSVC_PUSH +#define C4_SUPPRESS_WARNING_MSVC(w) +#define C4_SUPPRESS_WARNING_MSVC_POP +#endif // C4_MSVC + + +#ifdef C4_CLANG +#define C4_PRAGMA_TO_STR(x) _Pragma(#x) +#define C4_SUPPRESS_WARNING_CLANG_PUSH _Pragma("clang diagnostic push") +#define C4_SUPPRESS_WARNING_CLANG(w) C4_PRAGMA_TO_STR(clang diagnostic ignored w) +#define C4_SUPPRESS_WARNING_CLANG_POP _Pragma("clang diagnostic pop") +#else // C4_CLANG +#define C4_SUPPRESS_WARNING_CLANG_PUSH +#define C4_SUPPRESS_WARNING_CLANG(w) +#define C4_SUPPRESS_WARNING_CLANG_POP +#endif // C4_CLANG + + +#ifdef C4_GCC +#define C4_PRAGMA_TO_STR(x) _Pragma(#x) +#define C4_SUPPRESS_WARNING_GCC_PUSH _Pragma("GCC diagnostic push") +#define C4_SUPPRESS_WARNING_GCC(w) C4_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define C4_SUPPRESS_WARNING_GCC_POP _Pragma("GCC diagnostic pop") +#else // C4_GCC +#define C4_SUPPRESS_WARNING_GCC_PUSH +#define C4_SUPPRESS_WARNING_GCC(w) +#define C4_SUPPRESS_WARNING_GCC_POP +#endif // C4_GCC + + +#define C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(w) \ + C4_SUPPRESS_WARNING_MSVC_PUSH \ + C4_SUPPRESS_WARNING_MSVC(w) + +#define C4_SUPPRESS_WARNING_CLANG_WITH_PUSH(w) \ + C4_SUPPRESS_WARNING_CLANG_PUSH \ + C4_SUPPRESS_WARNING_CLANG(w) + +#define C4_SUPPRESS_WARNING_GCC_WITH_PUSH(w) \ + C4_SUPPRESS_WARNING_GCC_PUSH \ + C4_SUPPRESS_WARNING_GCC(w) + + +#define C4_SUPPRESS_WARNING_GCC_CLANG_PUSH \ + C4_SUPPRESS_WARNING_GCC_PUSH \ + C4_SUPPRESS_WARNING_CLANG_PUSH + +#define C4_SUPPRESS_WARNING_GCC_CLANG(w) \ + C4_SUPPRESS_WARNING_GCC(w) \ + C4_SUPPRESS_WARNING_CLANG(w) + +#define C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH(w) \ + C4_SUPPRESS_WARNING_GCC_WITH_PUSH(w) \ + C4_SUPPRESS_WARNING_CLANG_WITH_PUSH(w) + +#define C4_SUPPRESS_WARNING_GCC_CLANG_POP \ + C4_SUPPRESS_WARNING_GCC_POP \ + C4_SUPPRESS_WARNING_CLANG_POP + +} // namespace c4 + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#endif /* _C4_ERROR_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/export.hpp b/3rdparty/rapidyaml/include/c4/export.hpp new file mode 100644 index 0000000000..ffd02482f9 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/export.hpp @@ -0,0 +1,18 @@ +#ifndef C4_EXPORT_HPP_ +#define C4_EXPORT_HPP_ + +#ifdef _WIN32 + #ifdef C4CORE_SHARED + #ifdef C4CORE_EXPORTS + #define C4CORE_EXPORT __declspec(dllexport) + #else + #define C4CORE_EXPORT __declspec(dllimport) + #endif + #else + #define C4CORE_EXPORT + #endif +#else + #define C4CORE_EXPORT +#endif + +#endif /* C4CORE_EXPORT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/format.hpp b/3rdparty/rapidyaml/include/c4/format.hpp new file mode 100644 index 0000000000..11da4992f6 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/format.hpp @@ -0,0 +1,1058 @@ +#ifndef _C4_FORMAT_HPP_ +#define _C4_FORMAT_HPP_ + +/** @file format.hpp provides type-safe facilities for formatting arguments + * to string buffers */ + +#include "c4/charconv.hpp" +#include "c4/blob.hpp" + + +#if defined(_MSC_VER) && !defined(__clang__) +# pragma warning(push) +# if C4_MSVC_VERSION != C4_MSVC_VERSION_2017 +# pragma warning(disable: 4800) // forcing value to bool 'true' or 'false' (performance warning) +# endif +# pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe +#elif defined(__clang__) +# pragma clang diagnostic push +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wuseless-cast" +#endif +// NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast,*avoid-goto*) + +/** @defgroup doc_format_utils Format utilities + * + * @brief Provides generic and type-safe formatting/scanning utilities + * built on top of @ref doc_to_chars() and @ref doc_from_chars, + * forwarding the arguments to these functions, which in turn use the + * @ref doc_charconv utilities. Like @ref doc_charconv, the formatting + * facilities are very efficient and many times faster than printf(). + * + * @see [a formatting sample in rapidyaml's docs](https://rapidyaml.readthedocs.io/latest/doxygen/group__doc__quickstart.html#gac2425b515eb552589708cfff70c52b14) + * */ + +/** @defgroup doc_format_specifiers Format specifiers + * + * @brief Format specifiers are tag types and functions that are used + * together with @ref doc_to_chars and @ref doc_from_chars + * + * @see [a formatting sample in rapidyaml's docs](https://rapidyaml.readthedocs.io/latest/doxygen/group__doc__quickstart.html#gac2425b515eb552589708cfff70c52b14) + * @ingroup doc_format_utils */ + +namespace c4 { + +/** @addtogroup doc_format_utils + * @{ */ + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// formatting truthy types as booleans + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_boolean_specifiers boolean specifiers + * @{ */ + +/** write a variable as an alphabetic boolean, ie as either true or false + * @param strict_read */ +template +struct boolalpha_ +{ + boolalpha_(T val_, bool strict_read_=false) : val(val_ ? true : false), strict_read(strict_read_) {} + bool val; + bool strict_read; +}; + +template +boolalpha_ boolalpha(T const& val, bool strict_read=false) +{ + return boolalpha_(val, strict_read); +} + +/** @} */ + +/** @} */ + +} // namespace fmt + +/** write a variable as an alphabetic boolean, ie as either true or + * false + * @ingroup doc_to_chars */ +template +inline size_t to_chars(substr buf, fmt::boolalpha_ fmt) +{ + return to_chars(buf, fmt.val ? "true" : "false"); +} + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// formatting integral types + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_integer_specifiers Integer specifiers + * @{ */ + +/** format an integral type with a custom radix */ +template +struct integral_ +{ + C4_STATIC_ASSERT(std::is_integral::value); + T val; + T radix; + C4_ALWAYS_INLINE integral_(T val_, T radix_) : val(val_), radix(radix_) {} +}; + +/** format an integral type with a custom radix, and pad with zeroes on the left */ +template +struct integral_padded_ +{ + C4_STATIC_ASSERT(std::is_integral::value); + T val; + T radix; + size_t num_digits; + C4_ALWAYS_INLINE integral_padded_(T val_, T radix_, size_t nd) : val(val_), radix(radix_), num_digits(nd) {} +}; + + +/** format an integral type with a custom radix */ +template +C4_ALWAYS_INLINE integral_ integral(T val, T radix=10) +{ + return integral_(val, radix); +} +/** format an integral type with a custom radix */ +template +C4_ALWAYS_INLINE integral_ integral(T const* val, T radix=10) +{ + return integral_(reinterpret_cast(val), static_cast(radix)); +} +/** format an integral type with a custom radix */ +template +C4_ALWAYS_INLINE integral_ integral(std::nullptr_t, T radix=10) +{ + return integral_(intptr_t(0), static_cast(radix)); +} + + +/** format the pointer as an hexadecimal value */ +template +inline integral_ hex(T * v) +{ + return integral_(reinterpret_cast(v), intptr_t(16)); +} +/** format the pointer as an hexadecimal value */ +template +inline integral_ hex(T const* v) +{ + return integral_(reinterpret_cast(v), intptr_t(16)); +} +/** format null as an hexadecimal value + * @overload hex */ +inline integral_ hex(std::nullptr_t) +{ + return integral_(0, intptr_t(16)); +} +/** format the integral_ argument as an hexadecimal value + * @overload hex */ +template +inline integral_ hex(T v) +{ + return integral_(v, T(16)); +} + +/** format the pointer as an octal value */ +template +inline integral_ oct(T const* v) +{ + return integral_(reinterpret_cast(v), intptr_t(8)); +} +/** format the pointer as an octal value */ +template +inline integral_ oct(T * v) +{ + return integral_(reinterpret_cast(v), intptr_t(8)); +} +/** format null as an octal value */ +inline integral_ oct(std::nullptr_t) +{ + return integral_(intptr_t(0), intptr_t(8)); +} +/** format the integral_ argument as an octal value */ +template +inline integral_ oct(T v) +{ + return integral_(v, T(8)); +} + +/** format the pointer as a binary 0-1 value + * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */ +template +inline integral_ bin(T const* v) +{ + return integral_(reinterpret_cast(v), intptr_t(2)); +} +/** format the pointer as a binary 0-1 value + * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */ +template +inline integral_ bin(T * v) +{ + return integral_(reinterpret_cast(v), intptr_t(2)); +} +/** format null as a binary 0-1 value + * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */ +inline integral_ bin(std::nullptr_t) +{ + return integral_(intptr_t(0), intptr_t(2)); +} +/** format the integral_ argument as a binary 0-1 value + * @see c4::raw() if you want to use a raw memcpy-based binary dump instead of 0-1 formatting */ +template +inline integral_ bin(T v) +{ + return integral_(v, T(2)); +} + +/** @} */ // integer_specifiers + + +/** @defgroup doc_zpad Pad the number with zeroes on the left + * @{ */ + +/** pad the argument with zeroes on the left, with decimal radix */ +template +C4_ALWAYS_INLINE integral_padded_ zpad(T val, size_t num_digits) +{ + return integral_padded_(val, T(10), num_digits); +} +/** pad the argument with zeroes on the left */ +template +C4_ALWAYS_INLINE integral_padded_ zpad(integral_ val, size_t num_digits) +{ + return integral_padded_(val.val, val.radix, num_digits); +} +/** pad the argument with zeroes on the left */ +C4_ALWAYS_INLINE integral_padded_ zpad(std::nullptr_t, size_t num_digits) +{ + return integral_padded_(0, 16, num_digits); +} +/** pad the argument with zeroes on the left */ +template +C4_ALWAYS_INLINE integral_padded_ zpad(T const* val, size_t num_digits) +{ + return integral_padded_(reinterpret_cast(val), 16, num_digits); +} +template +C4_ALWAYS_INLINE integral_padded_ zpad(T * val, size_t num_digits) +{ + return integral_padded_(reinterpret_cast(val), 16, num_digits); +} + +/** @} */ // zpad + + +/** @defgroup doc_overflow_checked Check read for overflow + * @{ */ + +template +struct overflow_checked_ +{ + static_assert(std::is_integral::value, "range checking only for integral types"); + C4_ALWAYS_INLINE overflow_checked_(T &val_) : val(&val_) {} + T *val; +}; +template +C4_ALWAYS_INLINE overflow_checked_ overflow_checked(T &val) +{ + return overflow_checked_(val); +} + +/** @} */ // overflow_checked + +/** @} */ // format_specifiers + + +} // namespace fmt + +/** format an integer signed type + * @ingroup doc_to_chars */ +template +C4_ALWAYS_INLINE +typename std::enable_if::value, size_t>::type +to_chars(substr buf, fmt::integral_ fmt) +{ + return itoa(buf, fmt.val, fmt.radix); +} +/** format an integer signed type, pad with zeroes + * @ingroup doc_to_chars */ +template +C4_ALWAYS_INLINE +typename std::enable_if::value, size_t>::type +to_chars(substr buf, fmt::integral_padded_ fmt) +{ + return itoa(buf, fmt.val, fmt.radix, fmt.num_digits); +} + +/** format an integer unsigned type + * @ingroup doc_to_chars */ +template +C4_ALWAYS_INLINE +typename std::enable_if::value, size_t>::type +to_chars(substr buf, fmt::integral_ fmt) +{ + return utoa(buf, fmt.val, fmt.radix); +} +/** format an integer unsigned type, pad with zeroes + * @ingroup doc_to_chars */ +template +C4_ALWAYS_INLINE +typename std::enable_if::value, size_t>::type +to_chars(substr buf, fmt::integral_padded_ fmt) +{ + return utoa(buf, fmt.val, fmt.radix, fmt.num_digits); +} + +/** read an integer type, detecting overflow (returns false on overflow) + * @ingroup doc_from_chars */ +template +C4_ALWAYS_INLINE bool from_chars(csubstr s, fmt::overflow_checked_ wrapper) +{ + if(C4_LIKELY(!overflows(s))) + return atox(s, wrapper.val); + return false; +} +/** read an integer type, detecting overflow (returns false on overflow) + * @ingroup doc_from_chars */ +template +C4_ALWAYS_INLINE bool from_chars(csubstr s, fmt::overflow_checked_ *wrapper) +{ + if(C4_LIKELY(!overflows(s))) + return atox(s, wrapper->val); + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// formatting real types + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_real_specifiers Real specifiers + * @{ */ + +template +struct real_ +{ + T val; + int precision; + RealFormat_e fmt; + real_(T v, int prec=-1, RealFormat_e f=FTOA_FLOAT) : val(v), precision(prec), fmt(f) {} +}; + +template +real_ real(T val, int precision, RealFormat_e fmt=FTOA_FLOAT) +{ + return real_(val, precision, fmt); +} + +/** @} */ // real_specifiers + +/** @} */ // format_specifiers + +} // namespace fmt + +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, fmt::real_< float> fmt) { return ftoa(buf, fmt.val, fmt.precision, fmt.fmt); } +/** @ingroup doc_to_chars */ +inline size_t to_chars(substr buf, fmt::real_ fmt) { return dtoa(buf, fmt.val, fmt.precision, fmt.fmt); } + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// writing raw binary data + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_raw_binary_specifiers Raw binary data + * @{ */ + +/** @see blob_ */ +template +struct raw_wrapper_ : public blob_ +{ + size_t alignment; + + C4_ALWAYS_INLINE raw_wrapper_(blob_ data, size_t alignment_) noexcept + : + blob_(data), + alignment(alignment_) + { + C4_ASSERT_MSG(alignment > 0 && (alignment & (alignment - 1)) == 0, "alignment must be a power of two"); + } +}; + +using const_raw_wrapper = raw_wrapper_; +using raw_wrapper = raw_wrapper_; + +/** mark a variable to be written in raw binary format, using memcpy + * @see blob_ */ +inline const_raw_wrapper craw(cblob data, size_t alignment=alignof(max_align_t)) +{ + return const_raw_wrapper(data, alignment); +} +/** mark a variable to be written in raw binary format, using memcpy + * @see blob_ */ +inline const_raw_wrapper raw(cblob data, size_t alignment=alignof(max_align_t)) +{ + return const_raw_wrapper(data, alignment); +} +/** mark a variable to be written in raw binary format, using memcpy + * @see blob_ */ +template +inline const_raw_wrapper craw(T const& C4_RESTRICT data, size_t alignment=alignof(T)) +{ + return const_raw_wrapper(cblob(data), alignment); +} +/** mark a variable to be written in raw binary format, using memcpy + * @see blob_ */ +template +inline const_raw_wrapper raw(T const& C4_RESTRICT data, size_t alignment=alignof(T)) +{ + return const_raw_wrapper(cblob(data), alignment); +} + +/** mark a variable to be read in raw binary format, using memcpy */ +inline raw_wrapper raw(blob data, size_t alignment=alignof(max_align_t)) +{ + return raw_wrapper(data, alignment); +} +/** mark a variable to be read in raw binary format, using memcpy */ +template +inline raw_wrapper raw(T & C4_RESTRICT data, size_t alignment=alignof(T)) +{ + return raw_wrapper(blob(data), alignment); +} + +/** @} */ // raw_binary_specifiers + +/** @} */ // format_specifiers + +} // namespace fmt + + +/** write a variable in raw binary format, using memcpy + * @ingroup doc_to_chars */ +C4CORE_EXPORT size_t to_chars(substr buf, fmt::const_raw_wrapper r); + +/** read a variable in raw binary format, using memcpy + * @ingroup doc_from_chars */ +C4CORE_EXPORT bool from_chars(csubstr buf, fmt::raw_wrapper *r); +/** read a variable in raw binary format, using memcpy + * @ingroup doc_from_chars */ +inline bool from_chars(csubstr buf, fmt::raw_wrapper r) +{ + return from_chars(buf, &r); +} + +/** read a variable in raw binary format, using memcpy + * @ingroup doc_from_chars_first */ +inline size_t from_chars_first(csubstr buf, fmt::raw_wrapper *r) +{ + return from_chars(buf, r); +} +/** read a variable in raw binary format, using memcpy + * @ingroup doc_from_chars_first */ +inline size_t from_chars_first(csubstr buf, fmt::raw_wrapper r) +{ + return from_chars(buf, &r); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// formatting aligned to left/right + +namespace fmt { + +/** @addtogroup doc_format_specifiers + * @{ */ + +/** @defgroup doc_alignment_specifiers Alignment specifiers + * @{ */ + +template +struct left_ +{ + T val; + size_t width; + char pad; + left_(T v, size_t w, char p) : val(v), width(w), pad(p) {} +}; + +template +struct right_ +{ + T val; + size_t width; + char pad; + right_(T v, size_t w, char p) : val(v), width(w), pad(p) {} +}; + +/** mark an argument to be aligned left */ +template +left_ left(T val, size_t width, char padchar=' ') +{ + return left_(val, width, padchar); +} + +/** mark an argument to be aligned right */ +template +right_ right(T val, size_t width, char padchar=' ') +{ + return right_(val, width, padchar); +} + +/** @} */ // alignment_specifiers + +/** @} */ // format_specifiers + +} // namespace fmt + + +/** @ingroup doc_to_chars */ +template +size_t to_chars(substr buf, fmt::left_ const& C4_RESTRICT align) +{ + size_t ret = to_chars(buf, align.val); + if(ret >= buf.len || ret >= align.width) + return ret > align.width ? ret : align.width; + buf.first(align.width).sub(ret).fill(align.pad); + to_chars(buf, align.val); + return align.width; +} + +/** @ingroup doc_to_chars */ +template +size_t to_chars(substr buf, fmt::right_ const& C4_RESTRICT align) +{ + size_t ret = to_chars(buf, align.val); + if(ret >= buf.len || ret >= align.width) + return ret > align.width ? ret : align.width; + size_t rem = static_cast(align.width - ret); + buf.first(rem).fill(align.pad); + to_chars(buf.sub(rem), align.val); + return align.width; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_cat cat: concatenate arguments to string + * @{ */ + +/** @cond dev */ +// terminates the variadic recursion +inline size_t cat(substr /*buf*/) +{ + return 0; +} +/** @endcond */ + + +/** serialize the arguments, concatenating them to the given fixed-size buffer. + * The buffer size is strictly respected: no writes will occur beyond its end. + * @return the number of characters needed to write all the arguments into the buffer. + * @see c4::catrs() if instead of a fixed-size buffer, a resizeable container is desired + * @see c4::uncat() for the inverse function + * @see c4::catsep() if a separator between each argument is to be used + * @see c4::format() if a format string is desired */ +template +size_t cat(substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more) +{ + size_t num = to_chars(buf, a); + buf = buf.len >= num ? buf.sub(num) : substr{}; + num += cat(buf, more...); + return num; +} + +/** like c4::cat() but return a substr instead of a size */ +template +substr cat_sub(substr buf, Args && ...args) +{ + size_t sz = cat(buf, std::forward(args)...); + C4_CHECK(sz <= buf.len); + return {buf.str, sz <= buf.len ? sz : buf.len}; +} + +/** @} */ + + +//----------------------------------------------------------------------------- + + +/** @defgroup doc_uncat uncat: read concatenated arguments from string + * @{ */ + +/** @cond dev */ +// terminates the variadic recursion +inline size_t uncat(csubstr /*buf*/) +{ + return 0; +} +/** @endcond */ + + +/** deserialize the arguments from the given buffer. + * + * @return the number of characters read from the buffer, or csubstr::npos + * if a conversion was not successful. + * @see c4::cat(). c4::uncat() is the inverse of c4::cat(). */ +template +size_t uncat(csubstr buf, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more) +{ + size_t out = from_chars_first(buf, &a); + if(C4_UNLIKELY(out == csubstr::npos)) + return csubstr::npos; + buf = buf.len >= out ? buf.sub(out) : substr{}; + size_t num = uncat(buf, more...); + if(C4_UNLIKELY(num == csubstr::npos)) + return csubstr::npos; + return out + num; +} + +/** @} */ + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** @defgroup doc_catsep catsep: cat arguments to string with separator + * @{ */ + +/** @cond dev */ +namespace detail { +template +C4_ALWAYS_INLINE size_t catsep_more(substr /*buf*/, Sep const& C4_RESTRICT /*sep*/) +{ + return 0; +} + +template +size_t catsep_more(substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more) +{ + size_t ret = to_chars(buf, sep); + size_t num = ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = to_chars(buf, a); + num += ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = catsep_more(buf, sep, more...); + num += ret; + return num; +} + + +template +inline size_t uncatsep_more(csubstr /*buf*/, Sep & /*sep*/) +{ + return 0; +} + +template +size_t uncatsep_more(csubstr buf, Sep & C4_RESTRICT sep, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more) +{ + size_t ret = from_chars_first(buf, &sep); + size_t num = ret; + if(C4_UNLIKELY(ret == csubstr::npos)) + return csubstr::npos; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = from_chars_first(buf, &a); + if(C4_UNLIKELY(ret == csubstr::npos)) + return csubstr::npos; + num += ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = uncatsep_more(buf, sep, more...); + if(C4_UNLIKELY(ret == csubstr::npos)) + return csubstr::npos; + num += ret; + return num; +} + +} // namespace detail + +template +size_t catsep(substr /*buf*/, Sep const& C4_RESTRICT /*sep*/) +{ + return 0; +} +/** @endcond */ + + +/** serialize the arguments, concatenating them to the given fixed-size + * buffer, using a separator between each argument. + * The buffer size is strictly respected: no writes will occur beyond its end. + * @return the number of characters needed to write all the arguments into the buffer. + * @see c4::catseprs() if instead of a fixed-size buffer, a resizeable container is desired + * @see c4::uncatsep() for the inverse function (ie, reading instead of writing) + * @see c4::cat() if no separator is needed + * @see c4::format() if a format string is desired */ +template +size_t catsep(substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more) +{ + size_t num = to_chars(buf, a); + buf = buf.len >= num ? buf.sub(num) : substr{}; + num += detail::catsep_more(buf, sep, more...); + return num; +} + +/** like c4::catsep() but return a substr instead of a size + * @see c4::catsep(). c4::uncatsep() is the inverse of c4::catsep(). */ +template +substr catsep_sub(substr buf, Args && ...args) +{ + size_t sz = catsep(buf, std::forward(args)...); + C4_CHECK(sz <= buf.len); + return {buf.str, sz <= buf.len ? sz : buf.len}; +} + +/** @} */ + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_uncatsep uncatsep: deserialize the separated arguments from a string + * @{ */ + +/** deserialize the arguments from the given buffer. + * + * @return the number of characters read from the buffer, or csubstr::npos + * if a conversion was not successful. + * @see c4::cat(). c4::uncat() is the inverse of c4::cat(). */ + +/** deserialize the arguments from the given buffer, using a separator. + * + * @return the number of characters read from the buffer, or csubstr::npos + * if a conversion was not successful + * @see c4::catsep(). c4::uncatsep() is the inverse of c4::catsep(). */ +template +size_t uncatsep(csubstr buf, Sep & C4_RESTRICT sep, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more) +{ + size_t ret = from_chars_first(buf, &a), num = ret; + if(C4_UNLIKELY(ret == csubstr::npos)) + return csubstr::npos; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = detail::uncatsep_more(buf, sep, more...); + if(C4_UNLIKELY(ret == csubstr::npos)) + return csubstr::npos; + num += ret; + return num; +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_format format: formatted string interpolation + * @{ */ + +/// @cond dev +// terminates the variadic recursion +inline size_t format(substr buf, csubstr fmt) +{ + return to_chars(buf, fmt); +} +/// @endcond + + +/** using a format string, serialize the arguments into the given + * fixed-size buffer. + * The buffer size is strictly respected: no writes will occur beyond its end. + * In the format string, each argument is marked with a compact + * curly-bracket pair: {}. Arguments beyond the last curly bracket pair + * are silently ignored. For example: + * @code{.cpp} + * c4::format(buf, "the {} drank {} {}", "partier", 5, "beers"); // the partier drank 5 beers + * c4::format(buf, "the {} drank {} {}", "programmer", 6, "coffees"); // the programmer drank 6 coffees + * @endcode + * @return the number of characters needed to write into the buffer. + * @see c4::formatrs() if instead of a fixed-size buffer, a resizeable container is desired + * @see c4::unformat() for the inverse function + * @see c4::cat() if no format or separator is needed + * @see c4::catsep() if no format is needed, but a separator must be used */ +template +size_t format(substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more) +{ + size_t pos = fmt.find("{}"); // @todo use _find_fmt() + if(C4_UNLIKELY(pos == csubstr::npos)) + return to_chars(buf, fmt); + size_t num = to_chars(buf, fmt.sub(0, pos)); + size_t out = num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = to_chars(buf, a); + out += num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = format(buf, fmt.sub(pos + 2), more...); + out += num; + return out; +} + +/** like c4::format() but return a substr instead of a size + * @see c4::format() + * @see c4::catsep(). uncatsep() is the inverse of catsep(). */ +template +substr format_sub(substr buf, csubstr fmt, Args const& C4_RESTRICT ...args) +{ + size_t sz = c4::format(buf, fmt, args...); + C4_CHECK(sz <= buf.len); + return {buf.str, sz <= buf.len ? sz : buf.len}; +} + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_unformat unformat: formatted read from string + * @{ */ + +/// @cond dev +// terminates the variadic recursion +inline size_t unformat(csubstr /*buf*/, csubstr fmt) +{ + return fmt.len; +} +/// @endcond + + +/** using a format string, deserialize the arguments from the given + * buffer. + * @return the number of characters read from the buffer, or npos if a conversion failed. + * @see c4::format(). c4::unformat() is the inverse function to format(). */ +template +size_t unformat(csubstr buf, csubstr fmt, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more) +{ + const size_t pos = fmt.find("{}"); + if(C4_UNLIKELY(pos == csubstr::npos)) + return unformat(buf, fmt); + size_t num = pos; + size_t out = num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = from_chars_first(buf, &a); + if(C4_UNLIKELY(num == csubstr::npos)) + return csubstr::npos; + out += num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = unformat(buf, fmt.sub(pos + 2), more...); + if(C4_UNLIKELY(num == csubstr::npos)) + return csubstr::npos; + out += num; + return out; +} + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** cat+resize: like c4::cat(), but receives a container, and resizes + * it as needed to contain the result. The container is + * overwritten. To append to it, use the append overload. + * @see c4::cat() + * @ingroup doc_cat */ +template +inline void catrs(CharOwningContainer * C4_RESTRICT cont, Args const& C4_RESTRICT ...args) +{ +retry: + substr buf = to_substr(*cont); + size_t ret = cat(buf, args...); + cont->resize(ret); + if(ret > buf.len) + goto retry; +} + +/** cat+resize: like c4::cat(), but creates and returns a new + * container sized as needed to contain the result. + * @see c4::cat() + * @ingroup doc_cat */ +template +inline CharOwningContainer catrs(Args const& C4_RESTRICT ...args) +{ + CharOwningContainer cont; + catrs(&cont, args...); + return cont; +} + +/** cat+resize+append: like c4::cat(), but receives a container, and + * appends to it instead of overwriting it. The container is resized + * as needed to contain the result. + * + * @return the region newly appended to the original container + * @see c4::cat() + * @see c4::catrs() + * @ingroup doc_cat */ +template +inline csubstr catrs_append(CharOwningContainer * C4_RESTRICT cont, Args const& C4_RESTRICT ...args) +{ + const size_t pos = cont->size(); +retry: + substr buf = to_substr(*cont).sub(pos); + size_t ret = cat(buf, args...); + cont->resize(pos + ret); + if(ret > buf.len) + goto retry; + return to_csubstr(*cont).range(pos, cont->size()); +} + + +//----------------------------------------------------------------------------- + +/** catsep+resize: like c4::catsep(), but receives a container, and + * resizes it as needed to contain the result. The container is + * overwritten. To append to the container use the append overload. + * + * @see c4::catsep() + * @ingroup doc_catsep */ +template +inline void catseprs(CharOwningContainer * C4_RESTRICT cont, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args) +{ +retry: + substr buf = to_substr(*cont); + size_t ret = catsep(buf, sep, args...); + cont->resize(ret); + if(ret > buf.len) + goto retry; +} + +/** catsep+resize: like c4::catsep(), but create a new container with + * the result. + * + * @return the requested container + * @ingroup doc_catsep */ +template +inline CharOwningContainer catseprs(Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args) +{ + CharOwningContainer cont; + catseprs(&cont, sep, args...); + return cont; +} + + +/** catsep+resize+append: like catsep(), but receives a container, and + * appends the arguments, resizing the container as needed to contain + * the result. The buffer is appended to. + * + * @return a csubstr of the appended part + * @ingroup doc_catsep */ +template +inline csubstr catseprs_append(CharOwningContainer * C4_RESTRICT cont, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args) +{ + const size_t pos = cont->size(); +retry: + substr buf = to_substr(*cont).sub(pos); + size_t ret = catsep(buf, sep, args...); + cont->resize(pos + ret); + if(ret > buf.len) + goto retry; + return to_csubstr(*cont).range(pos, cont->size()); +} + + +//----------------------------------------------------------------------------- + +/** format+resize: like c4::format(), but receives a container, and + * resizes it as needed to contain the result. The container is + * overwritten. To append to the container use the append overload. + * + * @see c4::format() + * @ingroup doc_format */ +template +inline void formatrs(CharOwningContainer * C4_RESTRICT cont, csubstr fmt, Args const& C4_RESTRICT ...args) +{ +retry: + substr buf = to_substr(*cont); + size_t ret = format(buf, fmt, args...); + cont->resize(ret); + if(ret > buf.len) + goto retry; +} + +/** format+resize: like c4::format(), but create a new container with + * the result. + * + * @return the requested container + * @ingroup doc_format */ +template +inline CharOwningContainer formatrs(csubstr fmt, Args const& C4_RESTRICT ...args) +{ + CharOwningContainer cont; + formatrs(&cont, fmt, args...); + return cont; +} + +/** format+resize+append: like format(), but receives a container, and appends the + * arguments, resizing the container as needed to contain the + * result. The buffer is appended to. + * @return the region newly appended to the original container + * @ingroup doc_format */ +template +inline csubstr formatrs_append(CharOwningContainer * C4_RESTRICT cont, csubstr fmt, Args const& C4_RESTRICT ...args) +{ + const size_t pos = cont->size(); +retry: + substr buf = to_substr(*cont).sub(pos); + size_t ret = format(buf, fmt, args...); + cont->resize(pos + ret); + if(ret > buf.len) + goto retry; + return to_csubstr(*cont).range(pos, cont->size()); +} + +/** @} */ + +} // namespace c4 + +// NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast,*avoid-goto*) +#ifdef _MSC_VER +# pragma warning(pop) +#elif defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif /* _C4_FORMAT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/language.hpp b/3rdparty/rapidyaml/include/c4/language.hpp new file mode 100644 index 0000000000..9bf6ccc465 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/language.hpp @@ -0,0 +1,358 @@ +#ifndef _C4_LANGUAGE_HPP_ +#define _C4_LANGUAGE_HPP_ + +/** @file language.hpp Provides language standard information macros and + * compiler agnostic utility macros: namespace facilities, function attributes, + * variable attributes, etc. + * @ingroup basic_headers */ + +#include "c4/preprocessor.hpp" +#include "c4/compiler.hpp" + +/* Detect C++ standard. + * @see http://stackoverflow.com/a/7132549/5875572 */ +#ifndef C4_CPP +# if defined(_MSC_VER) && !defined(__clang__) +# if _MSC_VER >= 1910 // >VS2015: VS2017, VS2019, VS2022 +# if (!defined(_MSVC_LANG)) +# error _MSVC not defined +# endif +# if _MSVC_LANG >= 201705L +# define C4_CPP 20 +# define C4_CPP20 +# elif _MSVC_LANG == 201703L +# define C4_CPP 17 +# define C4_CPP17 +# elif _MSVC_LANG >= 201402L +# define C4_CPP 14 +# define C4_CPP14 +# elif _MSVC_LANG >= 201103L +# define C4_CPP 11 +# define C4_CPP11 +# else +# error C++ lesser than C++11 not supported +# endif +# else +# if _MSC_VER == 1900 +# define C4_CPP 14 // VS2015 is c++14 https://devblogs.microsoft.com/cppblog/c111417-features-in-vs-2015-rtm/ +# define C4_CPP14 +# elif _MSC_VER == 1800 // VS2013 +# define C4_CPP 11 +# define C4_CPP11 +# else +# error C++ lesser than C++11 not supported +# endif +# endif +# elif defined(__INTEL_COMPILER) // https://software.intel.com/en-us/node/524490 +# ifdef __INTEL_CXX20_MODE__ // not sure about this +# define C4_CPP 20 +# define C4_CPP20 +# elif defined __INTEL_CXX17_MODE__ // not sure about this +# define C4_CPP 17 +# define C4_CPP17 +# elif defined __INTEL_CXX14_MODE__ // not sure about this +# define C4_CPP 14 +# define C4_CPP14 +# elif defined __INTEL_CXX11_MODE__ +# define C4_CPP 11 +# define C4_CPP11 +# else +# error C++ lesser than C++11 not supported +# endif +# else +# ifndef __cplusplus +# error __cplusplus is not defined? +# endif +# if __cplusplus == 1 +# error cannot handle __cplusplus==1 +# elif __cplusplus >= 201709L +# define C4_CPP 20 +# define C4_CPP20 +# elif __cplusplus >= 201703L +# define C4_CPP 17 +# define C4_CPP17 +# elif __cplusplus >= 201402L +# define C4_CPP 14 +# define C4_CPP14 +# elif __cplusplus >= 201103L +# define C4_CPP 11 +# define C4_CPP11 +# elif __cplusplus >= 199711L +# error C++ lesser than C++11 not supported +# endif +# endif +#else +# ifdef C4_CPP == 20 +# define C4_CPP20 +# elif C4_CPP == 17 +# define C4_CPP17 +# elif C4_CPP == 14 +# define C4_CPP14 +# elif C4_CPP == 11 +# define C4_CPP11 +# elif C4_CPP == 98 +# define C4_CPP98 +# error C++ lesser than C++11 not supported +# else +# error C4_CPP must be one of 20, 17, 14, 11, 98 +# endif +#endif + +#ifdef C4_CPP20 +# define C4_CPP17 +# define C4_CPP14 +# define C4_CPP11 +#elif defined(C4_CPP17) +# define C4_CPP14 +# define C4_CPP11 +#elif defined(C4_CPP14) +# define C4_CPP11 +#endif + +/** lifted from this answer: http://stackoverflow.com/a/20170989/5875572 */ +#if defined(_MSC_VER) && !defined(__clang__) +# if _MSC_VER < 1900 +# define C4_CONSTEXPR11 +# define C4_CONSTEXPR14 +# elif _MSC_VER < 2000 +# define C4_CONSTEXPR11 constexpr +# define C4_CONSTEXPR14 +# else +# define C4_CONSTEXPR11 constexpr +# define C4_CONSTEXPR14 constexpr +# endif +#else +# if __cplusplus < 201103 +# define C4_CONSTEXPR11 +# define C4_CONSTEXPR14 +# elif __cplusplus == 201103 +# define C4_CONSTEXPR11 constexpr +# define C4_CONSTEXPR14 +# else +# define C4_CONSTEXPR11 constexpr +# define C4_CONSTEXPR14 constexpr +# endif +#endif // _MSC_VER + + +#if C4_CPP < 17 +#define C4_IF_CONSTEXPR +#define C4_INLINE_CONSTEXPR constexpr +#else +#define C4_IF_CONSTEXPR constexpr +#define C4_INLINE_CONSTEXPR inline constexpr +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +# if (defined(_CPPUNWIND) && (_CPPUNWIND == 1)) +# define C4_EXCEPTIONS +# endif +#else +# if defined(__EXCEPTIONS) || defined(__cpp_exceptions) +# define C4_EXCEPTIONS +# endif +#endif + +#ifdef C4_EXCEPTIONS +# define C4_IF_EXCEPTIONS_(exc_code, setjmp_code) exc_code +# define C4_IF_EXCEPTIONS(exc_code, setjmp_code) do { exc_code } while(0) +#else +# define C4_IF_EXCEPTIONS_(exc_code, setjmp_code) setjmp_code +# define C4_IF_EXCEPTIONS(exc_code, setjmp_code) do { setjmp_code } while(0) +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +# if defined(_CPPRTTI) +# define C4_RTTI +# endif +#else +# if defined(__GXX_RTTI) +# define C4_RTTI +# endif +#endif + +#ifdef C4_RTTI +# define C4_IF_RTTI_(code_rtti, code_no_rtti) code_rtti +# define C4_IF_RTTI(code_rtti, code_no_rtti) do { code_rtti } while(0) +#else +# define C4_IF_RTTI_(code_rtti, code_no_rtti) code_no_rtti +# define C4_IF_RTTI(code_rtti, code_no_rtti) do { code_no_rtti } while(0) +#endif + + +//------------------------------------------------------------ + +#define _C4_BEGIN_NAMESPACE(ns) namespace ns { +#define _C4_END_NAMESPACE(ns) } + +// MSVC cant handle the C4_FOR_EACH macro... need to fix this +//#define C4_BEGIN_NAMESPACE(...) C4_FOR_EACH_SEP(_C4_BEGIN_NAMESPACE, , __VA_ARGS__) +//#define C4_END_NAMESPACE(...) C4_FOR_EACH_SEP(_C4_END_NAMESPACE, , __VA_ARGS__) +#define C4_BEGIN_NAMESPACE(ns) namespace ns { +#define C4_END_NAMESPACE(ns) } + +#define C4_BEGIN_HIDDEN_NAMESPACE namespace /*hidden*/ { +#define C4_END_HIDDEN_NAMESPACE } /* namespace hidden */ + +//------------------------------------------------------------ + +#ifndef C4_API +# if defined(_MSC_VER) && !defined(__clang__) +# if defined(C4_EXPORT) +# define C4_API __declspec(dllexport) +# elif defined(C4_IMPORT) +# define C4_API __declspec(dllimport) +# else +# define C4_API +# endif +# else +# define C4_API +# endif +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +# define C4_RESTRICT __restrict +# define C4_RESTRICT_FN __declspec(restrict) +# define C4_NO_INLINE __declspec(noinline) +# define C4_ALWAYS_INLINE inline __forceinline +/** these are not available in VS AFAIK */ +# define C4_CONST +# define C4_PURE +# define C4_FLATTEN +# define C4_HOT /** @todo */ +# define C4_COLD /** @todo */ +# define C4_ASSUME(...) __assume(__VA_ARGS__) +# define C4_EXPECT(x, y) x /** @todo */ +# define C4_LIKELY(x) x +# define C4_UNLIKELY(x) x +# define C4_UNREACHABLE() _c4_msvc_unreachable() +# define C4_ATTR_FORMAT(...) /** */ +# define C4_NORETURN [[noreturn]] +# if _MSC_VER >= 1700 // VS2012 +# define C4_NODISCARD _Check_return_ +# else +# define C4_NODISCARD +# endif +[[noreturn]] __forceinline void _c4_msvc_unreachable() { __assume(false); } ///< https://stackoverflow.com/questions/60802864/emulating-gccs-builtin-unreachable-in-visual-studio +# define C4_UNREACHABLE_AFTER_ERR() /* */ +#else + ///< @todo assuming gcc-like compiler. check it is actually so. +/** for function attributes in GCC, + * @see https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes */ +/** for __builtin functions in GCC, + * @see https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html */ +# define C4_RESTRICT __restrict__ +# define C4_RESTRICT_FN __attribute__((restrict)) +# define C4_NO_INLINE __attribute__((noinline)) +# define C4_ALWAYS_INLINE inline __attribute__((always_inline)) +# define C4_CONST __attribute__((const)) +# define C4_PURE __attribute__((pure)) +/** force inlining of every callee function */ +# define C4_FLATTEN __atribute__((flatten)) +/** mark a function as hot, ie as having a visible impact in CPU time + * thus making it more likely to inline, etc + * @see http://stackoverflow.com/questions/15028990/semantics-of-gcc-hot-attribute */ +# define C4_HOT __attribute__((hot)) +/** mark a function as cold, ie as NOT having a visible impact in CPU time + * @see http://stackoverflow.com/questions/15028990/semantics-of-gcc-hot-attribute */ +# define C4_COLD __attribute__((cold)) +# define C4_EXPECT(x, y) __builtin_expect(x, y) ///< @see https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html +# define C4_LIKELY(x) __builtin_expect(x, 1) +# define C4_UNLIKELY(x) __builtin_expect(x, 0) +# define C4_UNREACHABLE() __builtin_unreachable() +# define C4_ATTR_FORMAT(...) //__attribute__((format (__VA_ARGS__))) ///< @see https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes +# define C4_NORETURN __attribute__((noreturn)) +# define C4_NODISCARD __attribute__((warn_unused_result)) +# define C4_UNREACHABLE_AFTER_ERR() C4_UNREACHABLE() +// C4_ASSUME +// see https://stackoverflow.com/questions/63493968/reproducing-clangs-builtin-assume-for-gcc +// preferred option: C++ standard attribute +# ifdef __has_cpp_attribute +# if __has_cpp_attribute(assume) >= 202207L +# define C4_ASSUME(...) [[assume(__VA_ARGS__)]] +# endif +# endif +// first fallback: compiler intrinsics/attributes for assumptions +# ifndef C4_ASSUME +# if defined(__clang__) +# define C4_ASSUME(...) __builtin_assume(__VA_ARGS__) +# elif defined(__GNUC__) +# if __GNUC__ >= 13 +# define C4_ASSUME(...) __attribute__((__assume__(__VA_ARGS__))) +# endif +# endif +# endif +// second fallback: possibly evaluating uses of unreachable() +// Set this to 1 if you want to allow assumptions to possibly evaluate. +# ifndef C4_ASSUME_ALLOW_EVAL +# define C4_ASSUME_ALLOW_EVAL 0 +# endif +# if !defined(C4_ASSUME) && (C4_ASSUME_ALLOW_EVAL) +# define C4_ASSUME(...) do { if (!bool(__VA_ARGS__)) C4_UNREACHABLE(); ) while(0) +# endif +// last fallback: define macro as doing nothing +# ifndef C4_ASSUME +# define C4_ASSUME(...) +# endif +#endif + + +#if C4_CPP >= 14 +# define C4_DEPRECATED(msg) [[deprecated(msg)]] +#else +# if defined(_MSC_VER) +# define C4_DEPRECATED(msg) __declspec(deprecated(msg)) +# else // defined(__GNUC__) || defined(__clang__) +# define C4_DEPRECATED(msg) __attribute__((deprecated(msg))) +# endif +#endif + + +#ifdef _MSC_VER +# define C4_FUNC __FUNCTION__ +# define C4_PRETTY_FUNC __FUNCSIG__ +#else /// @todo assuming gcc-like compiler. check it is actually so. +# define C4_FUNC __FUNCTION__ +# define C4_PRETTY_FUNC __PRETTY_FUNCTION__ +#endif + +/** prevent compiler warnings about a specific var being unused */ +#define C4_UNUSED(var) (void)var + +#if C4_CPP >= 17 +#define C4_STATIC_ASSERT(cond) static_assert(cond) +#else +#define C4_STATIC_ASSERT(cond) static_assert((cond), #cond) +#endif +#define C4_STATIC_ASSERT_MSG(cond, msg) static_assert((cond), #cond ": " msg) + +/** @def C4_DONT_OPTIMIZE idea lifted from GoogleBenchmark. + * @see https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h */ +namespace c4 { +namespace detail { +#ifdef __GNUC__ +# define C4_DONT_OPTIMIZE(var) c4::detail::dont_optimize(var) +template< class T > +C4_ALWAYS_INLINE void dont_optimize(T const& value) { asm volatile("" : : "g"(value) : "memory"); } // NOLINT +#else +# define C4_DONT_OPTIMIZE(var) c4::detail::use_char_pointer(reinterpret_cast< const char* >(&var)) +void use_char_pointer(char const volatile*); +#endif +} // namespace detail +} // namespace c4 + +/** @def C4_KEEP_EMPTY_LOOP prevent an empty loop from being optimized out. + * @see http://stackoverflow.com/a/7084193/5875572 */ +#if defined(_MSC_VER) && !defined(__clang__) +# define C4_KEEP_EMPTY_LOOP { char c; C4_DONT_OPTIMIZE(c); } +#else +# define C4_KEEP_EMPTY_LOOP { asm(""); } +#endif + +/** @def C4_VA_LIST_REUSE_MUST_COPY + * @todo I strongly suspect that this is actually only in UNIX platforms. revisit this. */ +#ifdef __GNUC__ +# define C4_VA_LIST_REUSE_MUST_COPY +#endif + +#endif /* _C4_LANGUAGE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/memory_util.hpp b/3rdparty/rapidyaml/include/c4/memory_util.hpp new file mode 100644 index 0000000000..4907b2264f --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/memory_util.hpp @@ -0,0 +1,782 @@ +#ifndef _C4_MEMORY_UTIL_HPP_ +#define _C4_MEMORY_UTIL_HPP_ + +#include "c4/config.hpp" +#include "c4/error.hpp" +#include "c4/compiler.hpp" +#include "c4/cpu.hpp" +#ifdef C4_MSVC +#include +#endif +#include + +#if (defined(__GNUC__) && __GNUC__ >= 10) || defined(__has_builtin) +#define _C4_USE_LSB_INTRINSIC(which) __has_builtin(which) +#define _C4_USE_MSB_INTRINSIC(which) __has_builtin(which) +#elif defined(C4_MSVC) +#define _C4_USE_LSB_INTRINSIC(which) true +#define _C4_USE_MSB_INTRINSIC(which) true +#else +// let's try our luck +#define _C4_USE_LSB_INTRINSIC(which) true +#define _C4_USE_MSB_INTRINSIC(which) true +#endif + + +/** @file memory_util.hpp Some memory utilities. */ + +// NOLINTBEGIN(google-runtime-int) + +namespace c4 { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + +/** set the given memory to zero */ +C4_ALWAYS_INLINE void mem_zero(void* mem, size_t num_bytes) +{ + memset(mem, 0, num_bytes); +} +/** set the given memory to zero */ +template +C4_ALWAYS_INLINE void mem_zero(T* mem, size_t num_elms) +{ + memset(mem, 0, sizeof(T) * num_elms); +} +/** set the given memory to zero */ +template +C4_ALWAYS_INLINE void mem_zero(T* mem) +{ + memset(mem, 0, sizeof(T)); +} + +C4_ALWAYS_INLINE C4_CONST bool mem_overlaps(void const* a, void const* b, size_t sza, size_t szb) +{ + // thanks @timwynants + return (((const char*)b + szb) > a && b < ((const char*)a+sza)); +} + +void mem_repeat(void* dest, void const* pattern, size_t pattern_size, size_t num_times); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +C4_ALWAYS_INLINE C4_CONST bool is_aligned(T *ptr, uintptr_t alignment=alignof(T)) +{ + return (uintptr_t(ptr) & (alignment - uintptr_t(1))) == uintptr_t(0); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// least significant bit + +/** @name msb Compute the least significant bit + * @note the input value must be nonzero + * @note the input type must be unsigned + */ +/** @{ */ + +// https://graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightLinear +#define _c4_lsb_fallback \ + unsigned c = 0; \ + v = (v ^ (v - 1)) >> 1; /* Set v's trailing 0s to 1s and zero rest */ \ + for(; v; ++c) \ + v >>= 1; \ + return (unsigned) c + +// u8 +template +C4_CONSTEXPR14 +auto lsb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_LSB_INTRINSIC(__builtin_ctz) + // upcast to use the intrinsic, it's cheaper. + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanForward(&bit, (unsigned long)v); + return bit; + #else + _c4_lsb_fallback; + #endif + #else + return (unsigned)__builtin_ctz((unsigned)v); + #endif + #else + _c4_lsb_fallback; + #endif +} + +// u16 +template +C4_CONSTEXPR14 +auto lsb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_LSB_INTRINSIC(__builtin_ctz) + // upcast to use the intrinsic, it's cheaper. + // Then remember that the upcast makes it to 31bits + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanForward(&bit, (unsigned long)v); + return bit; + #else + _c4_lsb_fallback; + #endif + #else + return (unsigned)__builtin_ctz((unsigned)v); + #endif + #else + _c4_lsb_fallback; + #endif +} + +// u32 +template +C4_CONSTEXPR14 +auto lsb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_LSB_INTRINSIC(__builtin_ctz) + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanForward(&bit, v); + return bit; + #else + _c4_lsb_fallback; + #endif + #else + return (unsigned)__builtin_ctz((unsigned)v); + #endif + #else + _c4_lsb_fallback; + #endif +} + +// u64 in 64bits +template +C4_CONSTEXPR14 +auto lsb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_LSB_INTRINSIC(__builtin_ctzl) + #if defined(C4_MSVC) + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanForward64(&bit, v); + return bit; + #else + _c4_lsb_fallback; + #endif + #else + return (unsigned)__builtin_ctzl((unsigned long)v); + #endif + #else + _c4_lsb_fallback; + #endif +} + +// u64 in 32bits +template +C4_CONSTEXPR14 +auto lsb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_LSB_INTRINSIC(__builtin_ctzll) + #if defined(C4_MSVC) + #if !defined(C4_CPU_X86) && !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanForward64(&bit, v); + return bit; + #else + _c4_lsb_fallback; + #endif + #else + return (unsigned)__builtin_ctzll((unsigned long long)v); + #endif + #else + _c4_lsb_fallback; + #endif +} + +#undef _c4_lsb_fallback + +/** @} */ + + +namespace detail { +template struct _lsb11; +template +struct _lsb11 +{ + enum : unsigned { num = _lsb11>1), num_bits+I(1), (((val>>1)&I(1))!=I(0))>::num }; +}; +template +struct _lsb11 +{ + enum : unsigned { num = num_bits }; +}; +} // namespace detail + + +/** TMP version of lsb(); this needs to be implemented with template + * meta-programming because C++11 cannot use a constexpr function with + * local variables + * @see lsb */ +template +struct lsb11 +{ + static_assert(number != 0, "lsb: number must be nonzero"); + enum : unsigned { value = detail::_lsb11::num}; +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// most significant bit + + +/** @name msb Compute the most significant bit + * @note the input value must be nonzero + * @note the input type must be unsigned + */ +/** @{ */ + + +#define _c4_msb8_fallback \ + unsigned n = 0; \ + if(v & I(0xf0)) v >>= 4, n |= I(4); \ + if(v & I(0x0c)) v >>= 2, n |= I(2); \ + if(v & I(0x02)) v >>= 1, n |= I(1); \ + return n + +#define _c4_msb16_fallback \ + unsigned n = 0; \ + if(v & I(0xff00)) v >>= 8, n |= I(8); \ + if(v & I(0x00f0)) v >>= 4, n |= I(4); \ + if(v & I(0x000c)) v >>= 2, n |= I(2); \ + if(v & I(0x0002)) v >>= 1, n |= I(1); \ + return n + +#define _c4_msb32_fallback \ + unsigned n = 0; \ + if(v & I(0xffff0000)) v >>= 16, n |= 16; \ + if(v & I(0x0000ff00)) v >>= 8, n |= 8; \ + if(v & I(0x000000f0)) v >>= 4, n |= 4; \ + if(v & I(0x0000000c)) v >>= 2, n |= 2; \ + if(v & I(0x00000002)) v >>= 1, n |= 1; \ + return n + +#define _c4_msb64_fallback \ + unsigned n = 0; \ + if(v & I(0xffffffff00000000)) v >>= 32, n |= I(32); \ + if(v & I(0x00000000ffff0000)) v >>= 16, n |= I(16); \ + if(v & I(0x000000000000ff00)) v >>= 8, n |= I(8); \ + if(v & I(0x00000000000000f0)) v >>= 4, n |= I(4); \ + if(v & I(0x000000000000000c)) v >>= 2, n |= I(2); \ + if(v & I(0x0000000000000002)) v >>= 1, n |= I(1); \ + return n + + +// u8 +template +C4_CONSTEXPR14 +auto msb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_MSB_INTRINSIC(__builtin_clz) + // upcast to use the intrinsic, it's cheaper. + // Then remember that the upcast makes it to 31bits + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanReverse(&bit, (unsigned long)v); + return bit; + #else + _c4_msb8_fallback; + #endif + #else + return 31u - (unsigned)__builtin_clz((unsigned)v); + #endif + #else + _c4_msb8_fallback; + #endif +} + +// u16 +template +C4_CONSTEXPR14 +auto msb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_MSB_INTRINSIC(__builtin_clz) + // upcast to use the intrinsic, it's cheaper. + // Then remember that the upcast makes it to 31bits + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanReverse(&bit, (unsigned long)v); + return bit; + #else + _c4_msb16_fallback; + #endif + #else + return 31u - (unsigned)__builtin_clz((unsigned)v); + #endif + #else + _c4_msb16_fallback; + #endif +} + +// u32 +template +C4_CONSTEXPR14 +auto msb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_MSB_INTRINSIC(__builtin_clz) + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanReverse(&bit, v); + return bit; + #else + _c4_msb32_fallback; + #endif + #else + return 31u - (unsigned)__builtin_clz((unsigned)v); + #endif + #else + _c4_msb32_fallback; + #endif +} + +// u64 in 64bits +template +C4_CONSTEXPR14 +auto msb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_MSB_INTRINSIC(__builtin_clzl) + #ifdef C4_MSVC + #if !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanReverse64(&bit, v); + return bit; + #else + _c4_msb64_fallback; + #endif + #else + return 63u - (unsigned)__builtin_clzl((unsigned long)v); + #endif + #else + _c4_msb64_fallback; + #endif +} + +// u64 in 32bits +template +C4_CONSTEXPR14 +auto msb(I v) noexcept + -> typename std::enable_if::type +{ + C4_STATIC_ASSERT(std::is_unsigned::value); + C4_ASSERT(v != 0); + #if _C4_USE_MSB_INTRINSIC(__builtin_clzll) + #ifdef C4_MSVC + #if !defined(C4_CPU_X86) && !defined(C4_CPU_ARM64) && !defined(C4_CPU_ARM) + unsigned long bit; + _BitScanReverse64(&bit, v); + return bit; + #else + _c4_msb64_fallback; + #endif + #else + return 63u - (unsigned)__builtin_clzll((unsigned long long)v); + #endif + #else + _c4_msb64_fallback; + #endif +} + +#undef _c4_msb8_fallback +#undef _c4_msb16_fallback +#undef _c4_msb32_fallback +#undef _c4_msb64_fallback + +/** @} */ + + +namespace detail { +template struct _msb11; +template +struct _msb11< I, val, num_bits, false> +{ + enum : unsigned { num = _msb11>1), num_bits+I(1), ((val>>1)==I(0))>::num }; +}; +template +struct _msb11 +{ + static_assert(val == 0, "bad implementation"); + enum : unsigned { num = (unsigned)(num_bits-1) }; +}; +} // namespace detail + + +/** TMP version of msb(); this needs to be implemented with template + * meta-programming because C++11 cannot use a constexpr function with + * local variables + * @see msb */ +template +struct msb11 +{ + enum : unsigned { value = detail::_msb11::num }; +}; + + + +#undef _C4_USE_LSB_INTRINSIC +#undef _C4_USE_MSB_INTRINSIC + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +// there is an implicit conversion below; it happens when E or B are +// narrower than int, and thus any operation will upcast the result to +// int, and then downcast to assign +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wconversion") + +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(B base, E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + if(exponent >= 0) + { + for(E e = 0; e < exponent; ++e) + r *= base; + } + else + { + exponent *= E(-1); + for(E e = 0; e < exponent; ++e) + r /= base; + } + return r; +} + +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + if(exponent >= 0) + { + for(E e = 0; e < exponent; ++e) + r *= base; + } + else + { + exponent *= E(-1); + for(E e = 0; e < exponent; ++e) + r /= base; + } + return r; +} + +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + B bbase = B(base); + if(exponent >= 0) + { + for(E e = 0; e < exponent; ++e) + r *= bbase; + } + else + { + exponent *= E(-1); + for(E e = 0; e < exponent; ++e) + r /= bbase; + } + return r; +} + +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(B base, E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + for(E e = 0; e < exponent; ++e) + r *= base; + return r; +} + +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + for(E e = 0; e < exponent; ++e) + r *= base; + return r; +} +/** integer power; this function is constexpr-14 because of the local + * variables */ +template +C4_CONSTEXPR14 C4_CONST auto ipow(E exponent) noexcept -> typename std::enable_if::value, B>::type +{ + C4_STATIC_ASSERT(std::is_integral::value); + B r = B(1); + B bbase = B(base); + for(E e = 0; e < exponent; ++e) + r *= bbase; + return r; +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** return a mask with all bits set [first_bit,last_bit[; this function + * is constexpr-14 because of the local variables */ +template +C4_CONSTEXPR14 I contiguous_mask(I first_bit, I last_bit) +{ + I r = 0; + for(I i = first_bit; i < last_bit; ++i) + { + r |= (I(1) << i); + } + return r; +} + + +namespace detail { + +template +struct _ctgmsk11; + +template +struct _ctgmsk11< I, val, first, last, true> +{ + enum : I { value = _ctgmsk11::value }; +}; + +template +struct _ctgmsk11< I, val, first, last, false> +{ + enum : I { value = val }; +}; + +} // namespace detail + + +/** TMP version of contiguous_mask(); this needs to be implemented with template + * meta-programming because C++11 cannot use a constexpr function with + * local variables + * @see contiguous_mask */ +template +struct contiguous_mask11 +{ + enum : I { value = detail::_ctgmsk11::value }; +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** use Empty Base Class Optimization to reduce the size of a pair of + * potentially empty types*/ + +namespace detail { +typedef enum { + tpc_same, + tpc_same_empty, + tpc_both_empty, + tpc_first_empty, + tpc_second_empty, + tpc_general +} TightPairCase_e; + +template +constexpr TightPairCase_e tpc_which_case() +{ + return std::is_same::value ? + std::is_empty::value ? + tpc_same_empty + : + tpc_same + : + std::is_empty::value && std::is_empty::value ? + tpc_both_empty + : + std::is_empty::value ? + tpc_first_empty + : + std::is_empty::value ? + tpc_second_empty + : + tpc_general + ; +} + +template +struct tight_pair +{ +private: + + First m_first; + Second m_second; + +public: + + using first_type = First; + using second_type = Second; + + tight_pair() : m_first(), m_second() {} + tight_pair(First const& f, Second const& s) : m_first(f), m_second(s) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return m_first; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return m_first; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; } +}; + +template +struct tight_pair : public First +{ + static_assert(std::is_same::value, "bad implementation"); + + using first_type = First; + using second_type = Second; + + tight_pair() : First() {} + tight_pair(First const& f, Second const& /*s*/) : First(f) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return reinterpret_cast(*this); } // NOLINT + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return reinterpret_cast(*this); } // NOLINT +}; + +template +struct tight_pair : public First, public Second +{ + using first_type = First; + using second_type = Second; + + tight_pair() : First(), Second() {} + tight_pair(First const& f, Second const& s) : First(f), Second(s) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return static_cast(*this); } +}; + +template +struct tight_pair : public First +{ + Second m_second; + + using first_type = First; + using second_type = Second; + + tight_pair() : First() {} + tight_pair(First const& f, Second const& s) : First(f), m_second(s) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; } +}; + +template +struct tight_pair : public First +{ + Second m_second; + + using first_type = First; + using second_type = Second; + + tight_pair() : First(), m_second() {} + tight_pair(First const& f, Second const& s) : First(f), m_second(s) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; } +}; + +template +struct tight_pair : public Second +{ + First m_first; + + using first_type = First; + using second_type = Second; + + tight_pair() : Second(), m_first() {} + tight_pair(First const& f, Second const& s) : Second(s), m_first(f) {} + + C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return m_first; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return m_first; } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return static_cast(*this); } + C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return static_cast(*this); } +}; + +} // namespace detail + +template +using tight_pair = detail::tight_pair()>; + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace c4 + +// NOLINTEND(google-runtime-int) + +#endif /* _C4_MEMORY_UTIL_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/platform.hpp b/3rdparty/rapidyaml/include/c4/platform.hpp new file mode 100644 index 0000000000..d7c56d71ca --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/platform.hpp @@ -0,0 +1,46 @@ +#ifndef _C4_PLATFORM_HPP_ +#define _C4_PLATFORM_HPP_ + +/** @file platform.hpp Provides platform information macros + * @ingroup basic_headers */ + +// see also https://sourceforge.net/p/predef/wiki/OperatingSystems/ + +#if defined(_WIN64) +# define C4_WIN +# define C4_WIN64 +#elif defined(_WIN32) +# define C4_WIN +# define C4_WIN32 +#elif defined(__ANDROID__) +# define C4_ANDROID +#elif defined(__APPLE__) +# include "TargetConditionals.h" +# if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +# define C4_IOS +# elif TARGET_OS_MAC || TARGET_OS_OSX +# define C4_MACOS +# else +# error "Unknown Apple platform" +# endif +#elif defined(__linux__) || defined(__linux) +# define C4_UNIX +# define C4_LINUX +#elif defined(__unix__) || defined(__unix) +# define C4_UNIX +#elif defined(__arm__) || defined(__aarch64__) +# define C4_ARM +#elif defined(__xtensa__) || defined(__XTENSA__) +# define C4_XTENSA +#elif defined(SWIG) +# define C4_SWIG +#else +# error "unknown platform" +#endif + +#if defined(__posix) || defined(C4_UNIX) || defined(C4_LINUX) +# define C4_POSIX +#endif + + +#endif /* _C4_PLATFORM_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/preprocessor.hpp b/3rdparty/rapidyaml/include/c4/preprocessor.hpp new file mode 100644 index 0000000000..a554825402 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/preprocessor.hpp @@ -0,0 +1,123 @@ +#ifndef _C4_PREPROCESSOR_HPP_ +#define _C4_PREPROCESSOR_HPP_ + +/** @file preprocessor.hpp Contains basic macros and preprocessor utilities. + * @ingroup basic_headers */ + +#ifdef __clang__ + /* NOTE: using , ## __VA_ARGS__ to deal with zero-args calls to + * variadic macros is not portable, but works in clang, gcc, msvc, icc. + * clang requires switching off compiler warnings for pedantic mode. + * @see http://stackoverflow.com/questions/32047685/variadic-macro-without-arguments */ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" // warning: token pasting of ',' and __VA_ARGS__ is a GNU extension +#elif defined(__GNUC__) + /* GCC also issues a warning for zero-args calls to variadic macros. + * This warning is switched on with -pedantic and apparently there is no + * easy way to turn it off as with clang. But marking this as a system + * header works. + * @see https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html + * @see http://stackoverflow.com/questions/35587137/ */ +# pragma GCC system_header +#endif + +#define C4_WIDEN(str) L"" str + +#define C4_COUNTOF(arr) (sizeof(arr)/sizeof((arr)[0])) + +#define C4_EXPAND(arg) arg + +/** useful in some macro calls with template arguments */ +#define C4_COMMA , +/** useful in some macro calls with template arguments + * @see C4_COMMA */ +#define C4_COMMA_X C4_COMMA + +/** expand and quote */ +#define C4_XQUOTE(arg) _C4_XQUOTE(arg) +#define _C4_XQUOTE(arg) C4_QUOTE(arg) +#define C4_QUOTE(arg) #arg + +/** expand and concatenate */ +#define C4_XCAT(arg1, arg2) _C4_XCAT(arg1, arg2) +#define _C4_XCAT(arg1, arg2) C4_CAT(arg1, arg2) +#define C4_CAT(arg1, arg2) arg1##arg2 + +#define C4_VERSION_CAT(major, minor, patch) ((major)*10000 + (minor)*100 + (patch)) + +/** A preprocessor foreach. Spectacular trick taken from: + * http://stackoverflow.com/a/1872506/5875572 + * The first argument is for a macro receiving a single argument, + * which will be called with every subsequent argument. There is + * currently a limit of 32 arguments, and at least 1 must be provided. + * +Example: +@code{.cpp} +struct Example { + int a; + int b; + int c; +}; +// define a one-arg macro to be called +#define PRN_STRUCT_OFFSETS(field) PRN_STRUCT_OFFSETS_(Example, field) +#define PRN_STRUCT_OFFSETS_(structure, field) printf(C4_XQUOTE(structure) ":" C4_XQUOTE(field)" - offset=%zu\n", offsetof(structure, field)); + +// now call the macro for a, b and c +C4_FOR_EACH(PRN_STRUCT_OFFSETS, a, b, c); +@endcode */ +#define C4_FOR_EACH(what, ...) C4_FOR_EACH_SEP(what, ;, __VA_ARGS__) + +/** same as C4_FOR_EACH(), but use a custom separator between statements. + * If a comma is needed as the separator, use the C4_COMMA macro. + * @see C4_FOR_EACH + * @see C4_COMMA + */ +#define C4_FOR_EACH_SEP(what, sep, ...) _C4_FOR_EACH_(_C4_FOR_EACH_NARG(__VA_ARGS__), what, sep, __VA_ARGS__) + +/// @cond dev + +#define _C4_FOR_EACH_01(what, sep, x) what(x) sep +#define _C4_FOR_EACH_02(what, sep, x, ...) what(x) sep _C4_FOR_EACH_01(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_03(what, sep, x, ...) what(x) sep _C4_FOR_EACH_02(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_04(what, sep, x, ...) what(x) sep _C4_FOR_EACH_03(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_05(what, sep, x, ...) what(x) sep _C4_FOR_EACH_04(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_06(what, sep, x, ...) what(x) sep _C4_FOR_EACH_05(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_07(what, sep, x, ...) what(x) sep _C4_FOR_EACH_06(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_08(what, sep, x, ...) what(x) sep _C4_FOR_EACH_07(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_09(what, sep, x, ...) what(x) sep _C4_FOR_EACH_08(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_10(what, sep, x, ...) what(x) sep _C4_FOR_EACH_09(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_11(what, sep, x, ...) what(x) sep _C4_FOR_EACH_10(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_12(what, sep, x, ...) what(x) sep _C4_FOR_EACH_11(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_13(what, sep, x, ...) what(x) sep _C4_FOR_EACH_12(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_14(what, sep, x, ...) what(x) sep _C4_FOR_EACH_13(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_15(what, sep, x, ...) what(x) sep _C4_FOR_EACH_14(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_16(what, sep, x, ...) what(x) sep _C4_FOR_EACH_15(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_17(what, sep, x, ...) what(x) sep _C4_FOR_EACH_16(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_18(what, sep, x, ...) what(x) sep _C4_FOR_EACH_17(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_19(what, sep, x, ...) what(x) sep _C4_FOR_EACH_18(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_20(what, sep, x, ...) what(x) sep _C4_FOR_EACH_19(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_21(what, sep, x, ...) what(x) sep _C4_FOR_EACH_20(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_22(what, sep, x, ...) what(x) sep _C4_FOR_EACH_21(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_23(what, sep, x, ...) what(x) sep _C4_FOR_EACH_22(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_24(what, sep, x, ...) what(x) sep _C4_FOR_EACH_23(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_25(what, sep, x, ...) what(x) sep _C4_FOR_EACH_24(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_26(what, sep, x, ...) what(x) sep _C4_FOR_EACH_25(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_27(what, sep, x, ...) what(x) sep _C4_FOR_EACH_26(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_28(what, sep, x, ...) what(x) sep _C4_FOR_EACH_27(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_29(what, sep, x, ...) what(x) sep _C4_FOR_EACH_28(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_30(what, sep, x, ...) what(x) sep _C4_FOR_EACH_29(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_31(what, sep, x, ...) what(x) sep _C4_FOR_EACH_30(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_32(what, sep, x, ...) what(x) sep _C4_FOR_EACH_31(what, sep, __VA_ARGS__) +#define _C4_FOR_EACH_NARG(...) _C4_FOR_EACH_NARG_(__VA_ARGS__, _C4_FOR_EACH_RSEQ_N()) +#define _C4_FOR_EACH_NARG_(...) _C4_FOR_EACH_ARG_N(__VA_ARGS__) +#define _C4_FOR_EACH_ARG_N(_01, _02, _03, _04, _05, _06, _07, _08, _09, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, N, ...) N +#define _C4_FOR_EACH_RSEQ_N() 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01 +#define _C4_FOR_EACH_(N, what, sep, ...) C4_XCAT(_C4_FOR_EACH_, N)(what, sep, __VA_ARGS__) + +/// @endcond + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#endif /* _C4_PREPROCESSOR_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/std/std.hpp b/3rdparty/rapidyaml/include/c4/std/std.hpp new file mode 100644 index 0000000000..f500db0bde --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/std.hpp @@ -0,0 +1,11 @@ +#ifndef _C4_STD_STD_HPP_ +#define _C4_STD_STD_HPP_ + +/** @file std.hpp includes all c4-std interop files */ + +#include "c4/std/vector.hpp" +#include "c4/std/string.hpp" +#include "c4/std/string_view.hpp" +#include "c4/std/tuple.hpp" + +#endif // _C4_STD_STD_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/std_fwd.hpp b/3rdparty/rapidyaml/include/c4/std/std_fwd.hpp new file mode 100644 index 0000000000..8c42ce7118 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/std_fwd.hpp @@ -0,0 +1,10 @@ +#ifndef _C4_STD_STD_FWD_HPP_ +#define _C4_STD_STD_FWD_HPP_ + +/** @file std_fwd.hpp includes all c4-std interop fwd files */ + +#include "c4/std/vector_fwd.hpp" +#include "c4/std/string_fwd.hpp" +//#include "c4/std/tuple_fwd.hpp" + +#endif // _C4_STD_STD_FWD_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/string.hpp b/3rdparty/rapidyaml/include/c4/std/string.hpp new file mode 100644 index 0000000000..bc69347f17 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/string.hpp @@ -0,0 +1,97 @@ +#ifndef _C4_STD_STRING_HPP_ +#define _C4_STD_STRING_HPP_ + +/** @file string.hpp */ + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/substr.hpp" +#endif + +#include + +namespace c4 { + +//----------------------------------------------------------------------------- + +/** get a writeable view to an existing std::string. + * When the string is empty, the returned view will be pointing + * at the character with value '\0', but the size will be zero. + * @see https://en.cppreference.com/w/cpp/string/basic_string/operator_at + */ +C4_ALWAYS_INLINE c4::substr to_substr(std::string &s) noexcept +{ + #if C4_CPP < 11 + #error this function will have undefined behavior + #endif + // since c++11 it is legal to call s[s.size()]. + return c4::substr(&s[0], s.size()); // NOLINT(readability-container-data-pointer) +} + +/** get a readonly view to an existing std::string. + * When the string is empty, the returned view will be pointing + * at the character with value '\0', but the size will be zero. + * @see https://en.cppreference.com/w/cpp/string/basic_string/operator_at + */ +C4_ALWAYS_INLINE c4::csubstr to_csubstr(std::string const& s) noexcept +{ + #if C4_CPP < 11 + #error this function will have undefined behavior + #endif + // since c++11 it is legal to call s[s.size()]. + return c4::csubstr(&s[0], s.size()); // NOLINT(readability-container-data-pointer) +} + +//----------------------------------------------------------------------------- + +C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) == 0; } +C4_ALWAYS_INLINE bool operator!= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) != 0; } +C4_ALWAYS_INLINE bool operator>= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) >= 0; } +C4_ALWAYS_INLINE bool operator> (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) > 0; } +C4_ALWAYS_INLINE bool operator<= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) <= 0; } +C4_ALWAYS_INLINE bool operator< (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) < 0; } + +C4_ALWAYS_INLINE bool operator== (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) == 0; } +C4_ALWAYS_INLINE bool operator!= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) != 0; } +C4_ALWAYS_INLINE bool operator>= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) <= 0; } +C4_ALWAYS_INLINE bool operator> (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) < 0; } +C4_ALWAYS_INLINE bool operator<= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) >= 0; } +C4_ALWAYS_INLINE bool operator< (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) > 0; } + +//----------------------------------------------------------------------------- + +/** copy an std::string to a writeable string view */ +inline size_t to_chars(c4::substr buf, std::string const& s) +{ + C4_ASSERT(!buf.overlaps(to_csubstr(s))); + size_t len = buf.len < s.size() ? buf.len : s.size(); + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len) + { + C4_ASSERT(s.data() != nullptr); + C4_ASSERT(buf.str != nullptr); + memcpy(buf.str, s.data(), len); + } + return s.size(); // return the number of needed chars +} + +/** copy a string view to an existing std::string */ +inline bool from_chars(c4::csubstr buf, std::string * s) +{ + s->resize(buf.len); + C4_ASSERT(!buf.overlaps(to_csubstr(*s))); + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(buf.len) + { + C4_ASSERT(buf.str != nullptr); + memcpy(&(*s)[0], buf.str, buf.len); // NOLINT(readability-container-data-pointer) + } + return true; +} + +} // namespace c4 + +#endif // _C4_STD_STRING_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/string_fwd.hpp b/3rdparty/rapidyaml/include/c4/std/string_fwd.hpp new file mode 100644 index 0000000000..bf9459de89 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/string_fwd.hpp @@ -0,0 +1,59 @@ +#ifndef _C4_STD_STRING_FWD_HPP_ +#define _C4_STD_STRING_FWD_HPP_ + +/** @file string_fwd.hpp */ + +#ifndef DOXYGEN + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/substr_fwd.hpp" +#endif + +#include + +// forward declarations for std::string +#if defined(__GLIBCXX__) || defined(__GLIBCPP__) +#include // use the fwd header in glibcxx +#elif defined(_LIBCPP_VERSION) || defined(__APPLE_CC__) +#include // use the fwd header in stdlibc++ +#elif defined(_MSC_VER) +#include "c4/error.hpp" +//! @todo is there a fwd header in msvc? +namespace std { +C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4643) // Forward declaring 'char_traits' in namespace std is not permitted by the C++ Standard. +template struct char_traits; +template class allocator; +template class basic_string; +using string = basic_string, allocator>; +C4_SUPPRESS_WARNING_MSVC_POP +} /* namespace std */ +#else +#error "unknown standard library" +#endif + +namespace c4 { + +c4::substr to_substr(std::string &s) noexcept; +c4::csubstr to_csubstr(std::string const& s) noexcept; + +bool operator== (c4::csubstr ss, std::string const& s); +bool operator!= (c4::csubstr ss, std::string const& s); +bool operator>= (c4::csubstr ss, std::string const& s); +bool operator> (c4::csubstr ss, std::string const& s); +bool operator<= (c4::csubstr ss, std::string const& s); +bool operator< (c4::csubstr ss, std::string const& s); + +bool operator== (std::string const& s, c4::csubstr ss); +bool operator!= (std::string const& s, c4::csubstr ss); +bool operator>= (std::string const& s, c4::csubstr ss); +bool operator> (std::string const& s, c4::csubstr ss); +bool operator<= (std::string const& s, c4::csubstr ss); +bool operator< (std::string const& s, c4::csubstr ss); + +size_t to_chars(c4::substr buf, std::string const& s); +bool from_chars(c4::csubstr buf, std::string * s); + +} // namespace c4 + +#endif // DOXYGEN +#endif // _C4_STD_STRING_FWD_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/string_view.hpp b/3rdparty/rapidyaml/include/c4/std/string_view.hpp new file mode 100644 index 0000000000..61f8e561be --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/string_view.hpp @@ -0,0 +1,71 @@ +#ifndef _C4_STD_STRING_VIEW_HPP_ +#define _C4_STD_STRING_VIEW_HPP_ + +/** @file string_view.hpp */ + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/language.hpp" +#endif + +#if (C4_CPP >= 17 && defined(__cpp_lib_string_view)) || defined(__DOXYGEN__) + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/substr.hpp" +#endif + +#include + + +namespace c4 { + +//----------------------------------------------------------------------------- + +/** create a csubstr from an existing std::string_view. */ +C4_ALWAYS_INLINE c4::csubstr to_csubstr(std::string_view s) noexcept +{ + return c4::csubstr(s.data(), s.size()); +} + + +//----------------------------------------------------------------------------- + +C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) == 0; } +C4_ALWAYS_INLINE bool operator!= (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) != 0; } +C4_ALWAYS_INLINE bool operator>= (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) >= 0; } +C4_ALWAYS_INLINE bool operator> (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) > 0; } +C4_ALWAYS_INLINE bool operator<= (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) <= 0; } +C4_ALWAYS_INLINE bool operator< (c4::csubstr ss, std::string_view s) { return ss.compare(s.data(), s.size()) < 0; } + +C4_ALWAYS_INLINE bool operator== (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) == 0; } +C4_ALWAYS_INLINE bool operator!= (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) != 0; } +C4_ALWAYS_INLINE bool operator<= (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) >= 0; } +C4_ALWAYS_INLINE bool operator< (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) > 0; } +C4_ALWAYS_INLINE bool operator>= (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) <= 0; } +C4_ALWAYS_INLINE bool operator> (std::string_view s, c4::csubstr ss) { return ss.compare(s.data(), s.size()) < 0; } + + +//----------------------------------------------------------------------------- + +/** copy an std::string_view to a writeable substr */ +inline size_t to_chars(c4::substr buf, std::string_view s) +{ + C4_ASSERT(!buf.overlaps(to_csubstr(s))); + size_t sz = s.size(); + size_t len = buf.len < sz ? buf.len : sz; + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len) + { + C4_ASSERT(s.data() != nullptr); + C4_ASSERT(buf.str != nullptr); + memcpy(buf.str, s.data(), len); + } + return sz; // return the number of needed chars +} + +} // namespace c4 + +#endif // C4_STRING_VIEW_AVAILABLE + +#endif // _C4_STD_STRING_VIEW_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/tuple.hpp b/3rdparty/rapidyaml/include/c4/std/tuple.hpp new file mode 100644 index 0000000000..5edcd63da1 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/tuple.hpp @@ -0,0 +1,184 @@ +#ifndef _C4_STD_TUPLE_HPP_ +#define _C4_STD_TUPLE_HPP_ + +/** @file tuple.hpp */ + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/format.hpp" +#endif + +#include + +/** this is a work in progress */ +#undef C4_TUPLE_TO_CHARS + +namespace c4 { + +#ifdef C4_TUPLE_TO_CHARS +namespace detail { + +template< size_t Curr, class... Types > +struct tuple_helper +{ + static size_t do_cat(substr buf, std::tuple< Types... > const& tp) + { + size_t num = to_chars(buf, std::get(tp)); + buf = buf.len >= num ? buf.sub(num) : substr{}; + num += tuple_helper< Curr+1, Types... >::do_cat(buf, tp); + return num; + } + + static size_t do_uncat(csubstr buf, std::tuple< Types... > & tp) + { + size_t num = from_str_trim(buf, &std::get(tp)); + if(num == csubstr::npos) return csubstr::npos; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num += tuple_helper< Curr+1, Types... >::do_uncat(buf, tp); + return num; + } + + template< class Sep > + static size_t do_catsep_more(substr buf, Sep const& sep, std::tuple< Types... > const& tp) + { + size_t ret = to_chars(buf, sep), num = ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = to_chars(buf, std::get(tp)); + num += ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = tuple_helper< Curr+1, Types... >::do_catsep_more(buf, sep, tp); + num += ret; + return num; + } + + template< class Sep > + static size_t do_uncatsep_more(csubstr buf, Sep & sep, std::tuple< Types... > & tp) + { + size_t ret = from_str_trim(buf, &sep), num = ret; + if(ret == csubstr::npos) return csubstr::npos; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = from_str_trim(buf, &std::get(tp)); + if(ret == csubstr::npos) return csubstr::npos; + num += ret; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = tuple_helper< Curr+1, Types... >::do_uncatsep_more(buf, sep, tp); + if(ret == csubstr::npos) return csubstr::npos; + num += ret; + return num; + } + + static size_t do_format(substr buf, csubstr fmt, std::tuple< Types... > const& tp) + { + auto pos = fmt.find("{}"); + if(pos != csubstr::npos) + { + size_t num = to_chars(buf, fmt.sub(0, pos)); + size_t out = num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = to_chars(buf, std::get(tp)); + out += num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = tuple_helper< Curr+1, Types... >::do_format(buf, fmt.sub(pos + 2), tp); + out += num; + return out; + } + else + { + return format(buf, fmt); + } + } + + static size_t do_unformat(csubstr buf, csubstr fmt, std::tuple< Types... > & tp) + { + auto pos = fmt.find("{}"); + if(pos != csubstr::npos) + { + size_t num = pos; + size_t out = num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = from_str_trim(buf, &std::get(tp)); + out += num; + buf = buf.len >= num ? buf.sub(num) : substr{}; + num = tuple_helper< Curr+1, Types... >::do_unformat(buf, fmt.sub(pos + 2), tp); + out += num; + return out; + } + else + { + return tuple_helper< sizeof...(Types), Types... >::do_unformat(buf, fmt, tp); + } + } + +}; + +/** @todo VS compilation fails for this class */ +template< class... Types > +struct tuple_helper< sizeof...(Types), Types... > +{ + static size_t do_cat(substr /*buf*/, std::tuple const& /*tp*/) { return 0; } + static size_t do_uncat(csubstr /*buf*/, std::tuple & /*tp*/) { return 0; } + + template< class Sep > static size_t do_catsep_more(substr /*buf*/, Sep const& /*sep*/, std::tuple const& /*tp*/) { return 0; } + template< class Sep > static size_t do_uncatsep_more(csubstr /*buf*/, Sep & /*sep*/, std::tuple & /*tp*/) { return 0; } + + static size_t do_format(substr buf, csubstr fmt, std::tuple const& /*tp*/) + { + return to_chars(buf, fmt); + } + + static size_t do_unformat(csubstr buf, csubstr fmt, std::tuple const& /*tp*/) + { + return 0; + } +}; + +} // namespace detail + +template< class... Types > +inline size_t cat(substr buf, std::tuple< Types... > const& tp) +{ + return detail::tuple_helper< 0, Types... >::do_cat(buf, tp); +} + +template< class... Types > +inline size_t uncat(csubstr buf, std::tuple< Types... > & tp) +{ + return detail::tuple_helper< 0, Types... >::do_uncat(buf, tp); +} + +template< class Sep, class... Types > +inline size_t catsep(substr buf, Sep const& sep, std::tuple< Types... > const& tp) +{ + size_t num = to_chars(buf, std::cref(std::get<0>(tp))); + buf = buf.len >= num ? buf.sub(num) : substr{}; + num += detail::tuple_helper< 1, Types... >::do_catsep_more(buf, sep, tp); + return num; +} + +template< class Sep, class... Types > +inline size_t uncatsep(csubstr buf, Sep & sep, std::tuple< Types... > & tp) +{ + size_t ret = from_str_trim(buf, &std::get<0>(tp)), num = ret; + if(ret == csubstr::npos) return csubstr::npos; + buf = buf.len >= ret ? buf.sub(ret) : substr{}; + ret = detail::tuple_helper< 1, Types... >::do_uncatsep_more(buf, sep, tp); + if(ret == csubstr::npos) return csubstr::npos; + num += ret; + return num; +} + +template< class... Types > +inline size_t format(substr buf, csubstr fmt, std::tuple< Types... > const& tp) +{ + return detail::tuple_helper< 0, Types... >::do_format(buf, fmt, tp); +} + +template< class... Types > +inline size_t unformat(csubstr buf, csubstr fmt, std::tuple< Types... > & tp) +{ + return detail::tuple_helper< 0, Types... >::do_unformat(buf, fmt, tp); +} +#endif // C4_TUPLE_TO_CHARS + +} // namespace c4 + +#endif /* _C4_STD_TUPLE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/std/vector.hpp b/3rdparty/rapidyaml/include/c4/std/vector.hpp new file mode 100644 index 0000000000..43f4f82e47 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/vector.hpp @@ -0,0 +1,88 @@ +#ifndef _C4_STD_VECTOR_HPP_ +#define _C4_STD_VECTOR_HPP_ + +/** @file vector.hpp provides conversion and comparison facilities + * from/between std::vector to c4::substr and c4::csubstr. + * @todo add to_span() and friends + */ + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/substr.hpp" +#endif + +#include + +namespace c4 { + +//----------------------------------------------------------------------------- + +/** get a substr (writeable string view) of an existing std::vector */ +template +c4::substr to_substr(std::vector &vec) +{ + char *data = vec.empty() ? nullptr : vec.data(); // data() may or may not return a null pointer. + return c4::substr(data, vec.size()); +} + +/** get a csubstr (read-only string) view of an existing std::vector */ +template +c4::csubstr to_csubstr(std::vector const& vec) +{ + const char *data = vec.empty() ? nullptr : vec.data(); // data() may or may not return a null pointer. + return c4::csubstr(data, vec.size()); +} + +//----------------------------------------------------------------------------- +// comparisons between substrings and std::vector + +template C4_ALWAYS_INLINE bool operator!= (c4::csubstr ss, std::vector const& s) { return ss != to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::vector const& s) { return ss == to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator>= (c4::csubstr ss, std::vector const& s) { return ss >= to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator> (c4::csubstr ss, std::vector const& s) { return ss > to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator<= (c4::csubstr ss, std::vector const& s) { return ss <= to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator< (c4::csubstr ss, std::vector const& s) { return ss < to_csubstr(s); } + +template C4_ALWAYS_INLINE bool operator!= (std::vector const& s, c4::csubstr ss) { return ss != to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator== (std::vector const& s, c4::csubstr ss) { return ss == to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator>= (std::vector const& s, c4::csubstr ss) { return ss <= to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator> (std::vector const& s, c4::csubstr ss) { return ss < to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator<= (std::vector const& s, c4::csubstr ss) { return ss >= to_csubstr(s); } +template C4_ALWAYS_INLINE bool operator< (std::vector const& s, c4::csubstr ss) { return ss > to_csubstr(s); } + +//----------------------------------------------------------------------------- + +/** copy a std::vector to a writeable string view */ +template +inline size_t to_chars(c4::substr buf, std::vector const& s) +{ + C4_ASSERT(!buf.overlaps(to_csubstr(s))); + size_t len = buf.len < s.size() ? buf.len : s.size(); + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(len > 0) + { + memcpy(buf.str, s.data(), len); + } + return s.size(); // return the number of needed chars +} + +/** copy a string view to an existing std::vector */ +template +inline bool from_chars(c4::csubstr buf, std::vector * s) +{ + s->resize(buf.len); + C4_ASSERT(!buf.overlaps(to_csubstr(*s))); + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(buf.len > 0) + { + memcpy(&(*s)[0], buf.str, buf.len); // NOLINT(readability-container-data-pointer) + } + return true; +} + +} // namespace c4 + +#endif // _C4_STD_VECTOR_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/std/vector_fwd.hpp b/3rdparty/rapidyaml/include/c4/std/vector_fwd.hpp new file mode 100644 index 0000000000..791e0feeea --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/std/vector_fwd.hpp @@ -0,0 +1,70 @@ +#ifndef _C4_STD_VECTOR_FWD_HPP_ +#define _C4_STD_VECTOR_FWD_HPP_ + +/** @file vector_fwd.hpp */ + +#include + +// NOLINTBEGIN(cert-dcl58-cpp) + +// forward declarations for std::vector +#if defined(__GLIBCXX__) || defined(__GLIBCPP__) || defined(_MSC_VER) +#if defined(_MSC_VER) +__pragma(warning(push)) +__pragma(warning(disable : 4643)) +#endif +namespace std { +template class allocator; +#ifdef _GLIBCXX_DEBUG +inline namespace __debug { +template class vector; +} +#else +template class vector; +#endif +} // namespace std +#if defined(_MSC_VER) +__pragma(warning(pop)) +#endif +#elif defined(_LIBCPP_ABI_NAMESPACE) +namespace std { +inline namespace _LIBCPP_ABI_NAMESPACE { +template class allocator; +template class vector; +} // namespace _LIBCPP_ABI_NAMESPACE +} // namespace std +#else +#error "unknown standard library" +#endif + +#ifndef C4CORE_SINGLE_HEADER +#include "c4/substr_fwd.hpp" +#endif + +namespace c4 { + +template c4::substr to_substr(std::vector &vec); +template c4::csubstr to_csubstr(std::vector const& vec); + +template bool operator!= (c4::csubstr ss, std::vector const& s); +template bool operator== (c4::csubstr ss, std::vector const& s); +template bool operator>= (c4::csubstr ss, std::vector const& s); +template bool operator> (c4::csubstr ss, std::vector const& s); +template bool operator<= (c4::csubstr ss, std::vector const& s); +template bool operator< (c4::csubstr ss, std::vector const& s); + +template bool operator!= (std::vector const& s, c4::csubstr ss); +template bool operator== (std::vector const& s, c4::csubstr ss); +template bool operator>= (std::vector const& s, c4::csubstr ss); +template bool operator> (std::vector const& s, c4::csubstr ss); +template bool operator<= (std::vector const& s, c4::csubstr ss); +template bool operator< (std::vector const& s, c4::csubstr ss); + +template size_t to_chars(c4::substr buf, std::vector const& s); +template bool from_chars(c4::csubstr buf, std::vector * s); + +} // namespace c4 + +// NOLINTEND(cert-dcl58-cpp) + +#endif // _C4_STD_VECTOR_FWD_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/substr.hpp b/3rdparty/rapidyaml/include/c4/substr.hpp new file mode 100644 index 0000000000..4d28bdf0c3 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/substr.hpp @@ -0,0 +1,2290 @@ +#ifndef _C4_SUBSTR_HPP_ +#define _C4_SUBSTR_HPP_ + +/** @file substr.hpp read+write string views */ + +#include +#include +#include + +#include "c4/config.hpp" +#include "c4/error.hpp" +#include "c4/substr_fwd.hpp" + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtype-limits" // disable warnings on size_t>=0, used heavily in assertions below. These assertions are a preparation step for providing the index type as a template parameter. +# pragma GCC diagnostic ignored "-Wuseless-cast" +# pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + + +namespace c4 { + +/** @defgroup doc_substr Substring: read/write string views + * @{ */ + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +namespace detail { +template +static inline void _do_reverse(C *C4_RESTRICT first, C *C4_RESTRICT last) +{ + while(last > first) + { + C tmp = *last; + *last-- = *first; + *first++ = tmp; + } +} +} // namespace detail +/** @endcond */ + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +// utility macros to deuglify SFINAE code; undefined after the class. +// https://stackoverflow.com/questions/43051882/how-to-disable-a-class-member-funrtion-for-certain-template-types +#define C4_REQUIRE_RW(ret_type) \ + template \ + typename std::enable_if< ! std::is_const::value, ret_type>::type +/** @endcond */ + + +/** a non-owning string-view, consisting of a character pointer + * and a length. + * + * @note The pointer is explicitly restricted. + * + * @see a [quickstart + * sample](https://rapidyaml.readthedocs.io/latest/doxygen/group__doc__quickstart.html#ga43e253da0692c13967019446809c1113) + * in rapidyaml's documentation. + */ +template +struct C4CORE_EXPORT basic_substring // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) +{ +public: + + /** a restricted pointer to the first character of the substring */ + C * C4_RESTRICT str; + /** the length of the substring */ + size_t len; + +public: + + /** @name Types */ + /** @{ */ + + using CC = typename std::add_const::type; //!< CC=const char + using NCC_ = typename std::remove_const::type; //!< NCC_=non const char + + using ro_substr = basic_substring; + using rw_substr = basic_substring; + + using char_type = C; + using size_type = size_t; + + using iterator = C*; + using const_iterator = CC*; + + enum : size_t { npos = (size_t)-1, NONE = (size_t)-1 }; + + /// convert automatically to substring of const C + template + C4_ALWAYS_INLINE operator typename std::enable_if::value, ro_substr const&>::type () const noexcept + { + return *(ro_substr const*)this; // don't call the str+len ctor because it does a check + } + + /** @} */ + +public: + + /** @name Default construction and assignment */ + /** @{ */ + + C4_ALWAYS_INLINE constexpr basic_substring() noexcept : str(), len() {} + + C4_ALWAYS_INLINE basic_substring(basic_substring const&) noexcept = default; + C4_ALWAYS_INLINE basic_substring(basic_substring &&) noexcept = default; + C4_ALWAYS_INLINE basic_substring(std::nullptr_t) noexcept : str(nullptr), len(0) {} + + C4_ALWAYS_INLINE basic_substring& operator= (basic_substring const&) noexcept = default; + C4_ALWAYS_INLINE basic_substring& operator= (basic_substring &&) noexcept = default; + C4_ALWAYS_INLINE basic_substring& operator= (std::nullptr_t) noexcept { str = nullptr; len = 0; return *this; } + + C4_ALWAYS_INLINE void clear() noexcept { str = nullptr; len = 0; } + + /** @} */ + +public: + + /** @name Construction and assignment from characters with the same type */ + /** @{ */ + + /** Construct from an array. + * @warning the input string need not be zero terminated, but the + * length is taken as if the string was zero terminated */ + template + C4_ALWAYS_INLINE constexpr basic_substring(C (&s_)[N]) noexcept : str(s_), len(N-1) {} + /** Construct from a pointer and length. + * @warning the input string need not be zero terminated. */ + C4_ALWAYS_INLINE basic_substring(C *s_, size_t len_) noexcept : str(s_), len(len_) { C4_ASSERT(str || !len_); } + /** Construct from two pointers. + * @warning the end pointer MUST BE larger than or equal to the begin pointer + * @warning the input string need not be zero terminated */ + C4_ALWAYS_INLINE basic_substring(C *beg_, C *end_) noexcept : str(beg_), len(static_cast(end_ - beg_)) { C4_ASSERT(end_ >= beg_); } + /** Construct from a C-string (zero-terminated string) + * @warning the input string MUST BE zero terminated. + * @warning will call strlen() + * @note this overload uses SFINAE to prevent it from overriding the array ctor + * @see For a more detailed explanation on why the plain overloads cannot + * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */ + template::value || std::is_same::value, int>::type=0> + C4_ALWAYS_INLINE basic_substring(U s_) noexcept : str(s_), len(s_ ? strlen(s_) : 0) {} + + /** Assign from an array. + * @warning the input string need not be zero terminated, but the + * length is taken as if the string was zero terminated */ + template + C4_ALWAYS_INLINE void assign(C (&s_)[N]) noexcept { str = (s_); len = (N-1); } + /** Assign from a pointer and length. + * @warning the input string need not be zero terminated. */ + C4_ALWAYS_INLINE void assign(C *s_, size_t len_) noexcept { str = s_; len = len_; C4_ASSERT(str || !len_); } + /** Assign from two pointers. + * @warning the end pointer MUST BE larger than or equal to the begin pointer + * @warning the input string need not be zero terminated. */ + C4_ALWAYS_INLINE void assign(C *beg_, C *end_) noexcept { C4_ASSERT(end_ >= beg_); str = (beg_); len = static_cast(end_ - beg_); } + /** Assign from a C-string (zero-terminated string) + * @warning the input string must be zero terminated. + * @warning will call strlen() + * @note this overload uses SFINAE to prevent it from overriding the array ctor + * @see For a more detailed explanation on why the plain overloads cannot + * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */ + template::value || std::is_same::value, int>::type=0> + C4_ALWAYS_INLINE void assign(U s_) noexcept { str = (s_); len = (s_ ? strlen(s_) : 0); } + + /** Assign from an array. + * @warning the input string need not be zero terminated. */ + template + C4_ALWAYS_INLINE basic_substring& operator= (C (&s_)[N]) noexcept { str = (s_); len = (N-1); return *this; } + /** Assign from a C-string (zero-terminated string) + * @warning the input string MUST BE zero terminated. + * @warning will call strlen() + * @note this overload uses SFINAE to prevent it from overriding the array ctor + * @see For a more detailed explanation on why the plain overloads cannot + * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */ + template::value || std::is_same::value, int>::type=0> + C4_ALWAYS_INLINE basic_substring& operator= (U s_) noexcept { str = s_; len = s_ ? strlen(s_) : 0; return *this; } + + /** @} */ + +public: + + /** @name Standard accessor methods */ + /** @{ */ + + C4_ALWAYS_INLINE C4_PURE bool has_str() const noexcept { return ! empty() && str[0] != C(0); } + C4_ALWAYS_INLINE C4_PURE bool empty() const noexcept { return (len == 0 || str == nullptr); } + C4_ALWAYS_INLINE C4_PURE bool not_empty() const noexcept { return (len != 0 && str != nullptr); } + C4_ALWAYS_INLINE C4_PURE size_t size() const noexcept { return len; } + + C4_ALWAYS_INLINE C4_PURE iterator begin() noexcept { return str; } + C4_ALWAYS_INLINE C4_PURE iterator end () noexcept { return str + len; } + + C4_ALWAYS_INLINE C4_PURE const_iterator begin() const noexcept { return str; } + C4_ALWAYS_INLINE C4_PURE const_iterator end () const noexcept { return str + len; } + + C4_ALWAYS_INLINE C4_PURE C * data() noexcept { return str; } + C4_ALWAYS_INLINE C4_PURE C const* data() const noexcept { return str; } + + C4_ALWAYS_INLINE C4_PURE C & operator[] (size_t i) noexcept { C4_ASSERT(i >= 0 && i < len); return str[i]; } + C4_ALWAYS_INLINE C4_PURE C const& operator[] (size_t i) const noexcept { C4_ASSERT(i >= 0 && i < len); return str[i]; } + + C4_ALWAYS_INLINE C4_PURE C & front() noexcept { C4_ASSERT(len > 0 && str != nullptr); return *str; } + C4_ALWAYS_INLINE C4_PURE C const& front() const noexcept { C4_ASSERT(len > 0 && str != nullptr); return *str; } + + C4_ALWAYS_INLINE C4_PURE C & back() noexcept { C4_ASSERT(len > 0 && str != nullptr); return *(str + len - 1); } + C4_ALWAYS_INLINE C4_PURE C const& back() const noexcept { C4_ASSERT(len > 0 && str != nullptr); return *(str + len - 1); } + + /** @} */ + +public: + + /** @name Comparison methods */ + /** @{ */ + + C4_PURE int compare(C const c) const noexcept + { + C4_XASSERT((str != nullptr) || len == 0); + if(C4_LIKELY(str != nullptr && len > 0)) + return (*str != c) ? *str - c : (static_cast(len) - 1); + else + return -1; + } + + C4_PURE int compare(const char *C4_RESTRICT that, size_t sz) const noexcept + { + C4_XASSERT(that || sz == 0); + C4_XASSERT(str || len == 0); + if(C4_LIKELY(str && that)) + { + { + const size_t min = len < sz ? len : sz; + for(size_t i = 0; i < min; ++i) + if(str[i] != that[i]) + return str[i] < that[i] ? -1 : 1; + } + if(len < sz) + return -1; + else if(len == sz) + return 0; + else + return 1; + } + else if(len == sz) + { + C4_XASSERT(len == 0 && sz == 0); + return 0; + } + return len < sz ? -1 : 1; + } + + C4_ALWAYS_INLINE C4_PURE int compare(ro_substr const that) const noexcept { return this->compare(that.str, that.len); } + + C4_ALWAYS_INLINE C4_PURE bool operator== (std::nullptr_t) const noexcept { return str == nullptr; } + C4_ALWAYS_INLINE C4_PURE bool operator!= (std::nullptr_t) const noexcept { return str != nullptr; } + + C4_ALWAYS_INLINE C4_PURE bool operator== (C const c) const noexcept { return this->compare(c) == 0; } + C4_ALWAYS_INLINE C4_PURE bool operator!= (C const c) const noexcept { return this->compare(c) != 0; } + C4_ALWAYS_INLINE C4_PURE bool operator< (C const c) const noexcept { return this->compare(c) < 0; } + C4_ALWAYS_INLINE C4_PURE bool operator> (C const c) const noexcept { return this->compare(c) > 0; } + C4_ALWAYS_INLINE C4_PURE bool operator<= (C const c) const noexcept { return this->compare(c) <= 0; } + C4_ALWAYS_INLINE C4_PURE bool operator>= (C const c) const noexcept { return this->compare(c) >= 0; } + + template C4_ALWAYS_INLINE C4_PURE bool operator== (basic_substring const that) const noexcept { return this->compare(that) == 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator!= (basic_substring const that) const noexcept { return this->compare(that) != 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator< (basic_substring const that) const noexcept { return this->compare(that) < 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator> (basic_substring const that) const noexcept { return this->compare(that) > 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator<= (basic_substring const that) const noexcept { return this->compare(that) <= 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator>= (basic_substring const that) const noexcept { return this->compare(that) >= 0; } + + template C4_ALWAYS_INLINE C4_PURE bool operator== (const char (&that)[N]) const noexcept { return this->compare(that, N-1) == 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator!= (const char (&that)[N]) const noexcept { return this->compare(that, N-1) != 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator< (const char (&that)[N]) const noexcept { return this->compare(that, N-1) < 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator> (const char (&that)[N]) const noexcept { return this->compare(that, N-1) > 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator<= (const char (&that)[N]) const noexcept { return this->compare(that, N-1) <= 0; } + template C4_ALWAYS_INLINE C4_PURE bool operator>= (const char (&that)[N]) const noexcept { return this->compare(that, N-1) >= 0; } + + /** @} */ + +public: + + /** @name Sub-selection methods */ + /** @{ */ + + /** true if *this is a substring of that (ie, from the same buffer) */ + C4_ALWAYS_INLINE C4_PURE bool is_sub(ro_substr const that) const noexcept + { + return that.is_super(*this); + } + + /** true if that is a substring of *this (ie, from the same buffer) */ + C4_ALWAYS_INLINE C4_PURE bool is_super(ro_substr const that) const noexcept + { + if(C4_LIKELY(len > 0)) + return that.str >= str && that.str+that.len <= str+len; + else + return that.len == 0 && that.str == str && str != nullptr; + } + + /** true if there is overlap of at least one element between that and *this */ + C4_ALWAYS_INLINE C4_PURE bool overlaps(ro_substr const that) const noexcept + { + // thanks @timwynants + return that.str+that.len > str && that.str < str+len; + } + +public: + + /** return [first,len[ */ + C4_ALWAYS_INLINE C4_PURE basic_substring sub(size_t first) const noexcept + { + C4_ASSERT(first >= 0 && first <= len); + return basic_substring(str + first, len - first); + } + + /** return [first,first+num[. If num==npos, return [first,len[ */ + C4_ALWAYS_INLINE C4_PURE basic_substring sub(size_t first, size_t num) const noexcept + { + C4_ASSERT(first >= 0 && first <= len); + C4_ASSERT((num >= 0 && num <= len) || (num == npos)); + size_t rnum = num != npos ? num : len - first; + C4_ASSERT((first >= 0 && first + rnum <= len) || (num == 0)); + return basic_substring(str + first, rnum); + } + + /** return [first,last[. If last==npos, return [first,len[ */ + C4_ALWAYS_INLINE C4_PURE basic_substring range(size_t first, size_t last=npos) const noexcept + { + C4_ASSERT(first >= 0 && first <= len); + last = last != npos ? last : len; + C4_ASSERT(first <= last); + C4_ASSERT(last >= 0 && last <= len); + return basic_substring(str + first, last - first); + } + + /** return the first @p num elements: [0,num[*/ + C4_ALWAYS_INLINE C4_PURE basic_substring first(size_t num) const noexcept + { + C4_ASSERT(num <= len || num == npos); + return basic_substring(str, num != npos ? num : len); + } + + /** return the last @p num elements: [len-num,len[*/ + C4_ALWAYS_INLINE C4_PURE basic_substring last(size_t num) const noexcept + { + C4_ASSERT(num <= len || num == npos); + return num != npos ? + basic_substring(str + len - num, num) : + *this; + } + + /** offset from the ends: return [left,len-right[ ; ie, trim a + number of characters from the left and right. This is + equivalent to python's negative list indices. */ + C4_ALWAYS_INLINE C4_PURE basic_substring offs(size_t left, size_t right) const noexcept + { + C4_ASSERT(left >= 0 && left <= len); + C4_ASSERT(right >= 0 && right <= len); + C4_ASSERT(left <= len - right + 1); + return basic_substring(str + left, len - right - left); + } + + /** return [0, pos[ . Same as .first(pos), but provided for compatibility with .right_of() */ + C4_ALWAYS_INLINE C4_PURE basic_substring left_of(size_t pos) const noexcept + { + C4_ASSERT(pos <= len || pos == npos); + return (pos != npos) ? + basic_substring(str, pos) : + *this; + } + + /** return [0, pos+include_pos[ . Same as .first(pos+1), but provided for compatibility with .right_of() */ + C4_ALWAYS_INLINE C4_PURE basic_substring left_of(size_t pos, bool include_pos) const noexcept + { + C4_ASSERT(pos <= len || pos == npos); + return (pos != npos) ? + basic_substring(str, pos+include_pos) : + *this; + } + + /** return [pos+1, len[ */ + C4_ALWAYS_INLINE C4_PURE basic_substring right_of(size_t pos) const noexcept + { + C4_ASSERT(pos <= len || pos == npos); + return (pos != npos) ? + basic_substring(str + (pos + 1), len - (pos + 1)) : + basic_substring(str + len, size_t(0)); + } + + /** return [pos+!include_pos, len[ */ + C4_ALWAYS_INLINE C4_PURE basic_substring right_of(size_t pos, bool include_pos) const noexcept + { + C4_ASSERT(pos <= len || pos == npos); + return (pos != npos) ? + basic_substring(str + (pos + !include_pos), len - (pos + !include_pos)) : + basic_substring(str + len, size_t(0)); + } + +public: + + /** given @p subs a substring of the current string, get the + * portion of the current string to the left of it */ + C4_ALWAYS_INLINE C4_PURE basic_substring left_of(ro_substr const subs) const noexcept + { + C4_ASSERT(is_super(subs) || subs.empty()); + auto ssb = subs.begin(); + auto b = begin(); + auto e = end(); + if(ssb >= b && ssb <= e) + return sub(0, static_cast(ssb - b)); + else + return sub(0, 0); + } + + /** given @p subs a substring of the current string, get the + * portion of the current string to the right of it */ + C4_ALWAYS_INLINE C4_PURE basic_substring right_of(ro_substr const subs) const noexcept + { + C4_ASSERT(is_super(subs) || subs.empty()); + auto sse = subs.end(); + auto b = begin(); + auto e = end(); + if(sse >= b && sse <= e) + return sub(static_cast(sse - b), static_cast(e - sse)); + else + return sub(0, 0); + } + + /** @} */ + +public: + + /** @name Removing characters (trim()) / patterns (strip()) from the tips of the string */ + /** @{ */ + + /** trim left */ + basic_substring triml(const C c) const + { + if( ! empty()) + { + size_t pos = first_not_of(c); + if(pos != npos) + return sub(pos); + } + return sub(0, 0); + } + /** trim left ANY of the characters. + * @see stripl() to remove a pattern from the left */ + basic_substring triml(ro_substr chars) const + { + if( ! empty()) + { + size_t pos = first_not_of(chars); + if(pos != npos) + return sub(pos); + } + return sub(0, 0); + } + + /** trim the character c from the right */ + basic_substring trimr(const C c) const + { + if( ! empty()) + { + size_t pos = last_not_of(c, npos); + if(pos != npos) + return sub(0, pos+1); + } + return sub(0, 0); + } + /** trim right ANY of the characters + * @see stripr() to remove a pattern from the right */ + basic_substring trimr(ro_substr chars) const + { + if( ! empty()) + { + size_t pos = last_not_of(chars, npos); + if(pos != npos) + return sub(0, pos+1); + } + return sub(0, 0); + } + + /** trim the character c left and right */ + basic_substring trim(const C c) const + { + return triml(c).trimr(c); + } + /** trim left and right ANY of the characters + * @see strip() to remove a pattern from the left and right */ + basic_substring trim(ro_substr const chars) const + { + return triml(chars).trimr(chars); + } + + /** remove a pattern from the left + * @see triml() to remove characters*/ + basic_substring stripl(ro_substr pattern) const + { + if( ! begins_with(pattern)) + return *this; + return sub(pattern.len < len ? pattern.len : len); + } + + /** remove a pattern from the right + * @see trimr() to remove characters*/ + basic_substring stripr(ro_substr pattern) const + { + if( ! ends_with(pattern)) + return *this; + return left_of(len - (pattern.len < len ? pattern.len : len)); + } + + /** @} */ + +public: + + /** @name Lookup methods */ + /** @{ */ + + size_t find(const C c, size_t start_pos=0) const + { + return first_of(c, start_pos); + } + size_t find(ro_substr pattern, size_t start_pos=0) const + { + C4_ASSERT(start_pos == npos || (start_pos >= 0 && start_pos <= len)); + if(len < pattern.len) return npos; + for(size_t i = start_pos, e = len - pattern.len + 1; i < e; ++i) + { + bool gotit = true; + for(size_t j = 0; j < pattern.len; ++j) + { + C4_ASSERT(i + j < len); + if(str[i + j] != pattern.str[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return i; + } + } + return npos; + } + +public: + + /** count the number of occurrences of c */ + size_t count(const C c, size_t pos=0) const + { + C4_ASSERT(pos >= 0 && pos <= len); + size_t num = 0; + pos = find(c, pos); + while(pos != npos) + { + ++num; + pos = find(c, pos + 1); + } + return num; + } + + /** count the number of occurrences of s */ + size_t count(ro_substr c, size_t pos=0) const + { + C4_ASSERT(pos >= 0 && pos <= len); + size_t num = 0; + pos = find(c, pos); + while(pos != npos) + { + ++num; + pos = find(c, pos + c.len); + } + return num; + } + + /** get the substr consisting of the first occurrence of @p c after @p pos, or an empty substr if none occurs */ + basic_substring select(const C c, size_t pos=0) const + { + pos = find(c, pos); + return pos != npos ? sub(pos, 1) : basic_substring(); + } + + /** get the substr consisting of the first occurrence of @p pattern after @p pos, or an empty substr if none occurs */ + basic_substring select(ro_substr pattern, size_t pos=0) const + { + pos = find(pattern, pos); + return pos != npos ? sub(pos, pattern.len) : basic_substring(); + } + +public: + + struct first_of_any_result + { + size_t which; + size_t pos; + operator bool() const { return which != NONE && pos != npos; } + }; + + first_of_any_result first_of_any(ro_substr s0, ro_substr s1) const + { + ro_substr s[2] = {s0, s1}; + return first_of_any_iter(&s[0], &s[0] + 2); + } + + first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2) const + { + ro_substr s[3] = {s0, s1, s2}; + return first_of_any_iter(&s[0], &s[0] + 3); + } + + first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2, ro_substr s3) const + { + ro_substr s[4] = {s0, s1, s2, s3}; + return first_of_any_iter(&s[0], &s[0] + 4); + } + + first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2, ro_substr s3, ro_substr s4) const + { + ro_substr s[5] = {s0, s1, s2, s3, s4}; + return first_of_any_iter(&s[0], &s[0] + 5); + } + + template + first_of_any_result first_of_any_iter(It first_span, It last_span) const + { + for(size_t i = 0; i < len; ++i) + { + size_t curr = 0; + for(It it = first_span; it != last_span; ++curr, ++it) + { + auto const& chars = *it; + if((i + chars.len) > len) continue; + bool gotit = true; + for(size_t j = 0; j < chars.len; ++j) + { + C4_ASSERT(i + j < len); + if(str[i + j] != chars[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return {curr, i}; + } + } + } + return {NONE, npos}; + } + +public: + + /** true if the first character of the string is @p c */ + bool begins_with(const C c) const + { + return len > 0 ? str[0] == c : false; + } + + /** true if the first @p num characters of the string are @p c */ + bool begins_with(const C c, size_t num) const + { + if(len < num) + { + return false; + } + for(size_t i = 0; i < num; ++i) + { + if(str[i] != c) + { + return false; + } + } + return true; + } + + /** true if the string begins with the given @p pattern */ + bool begins_with(ro_substr pattern) const + { + if(len < pattern.len) + { + return false; + } + for(size_t i = 0; i < pattern.len; ++i) + { + if(str[i] != pattern[i]) + { + return false; + } + } + return true; + } + + /** true if the first character of the string is any of the given @p chars */ + bool begins_with_any(ro_substr chars) const + { + if(len == 0) + { + return false; + } + for(size_t i = 0; i < chars.len; ++i) + { + if(str[0] == chars.str[i]) + { + return true; + } + } + return false; + } + + /** true if the last character of the string is @p c */ + bool ends_with(const C c) const + { + return len > 0 ? str[len-1] == c : false; + } + + /** true if the last @p num characters of the string are @p c */ + bool ends_with(const C c, size_t num) const + { + if(len < num) + { + return false; + } + for(size_t i = len - num; i < len; ++i) + { + if(str[i] != c) + { + return false; + } + } + return true; + } + + /** true if the string ends with the given @p pattern */ + bool ends_with(ro_substr pattern) const + { + if(len < pattern.len) + { + return false; + } + for(size_t i = 0, s = len-pattern.len; i < pattern.len; ++i) + { + if(str[s+i] != pattern[i]) + { + return false; + } + } + return true; + } + + /** true if the last character of the string is any of the given @p chars */ + bool ends_with_any(ro_substr chars) const + { + if(len == 0) + { + return false; + } + for(size_t i = 0; i < chars.len; ++i) + { + if(str[len - 1] == chars[i]) + { + return true; + } + } + return false; + } + +public: + + /** @return the first position where c is found in the string, or npos if none is found */ + size_t first_of(const C c, size_t start=0) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + for(size_t i = start; i < len; ++i) + { + if(str[i] == c) + return i; + } + return npos; + } + + /** @return the last position where c is found in the string, or npos if none is found */ + size_t last_of(const C c, size_t start=npos) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + if(start == npos) + start = len; + for(size_t i = start-1; i != size_t(-1); --i) + { + if(str[i] == c) + return i; + } + return npos; + } + + /** @return the first position where ANY of the chars is found in the string, or npos if none is found */ + size_t first_of(ro_substr chars, size_t start=0) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + for(size_t i = start; i < len; ++i) + { + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars[j]) + return i; + } + } + return npos; + } + + /** @return the last position where ANY of the chars is found in the string, or npos if none is found */ + size_t last_of(ro_substr chars, size_t start=npos) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + if(start == npos) + start = len; + for(size_t i = start-1; i != size_t(-1); --i) + { + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars[j]) + return i; + } + } + return npos; + } + +public: + + size_t first_not_of(const C c) const + { + for(size_t i = 0; i < len; ++i) + { + if(str[i] != c) + return i; + } + return npos; + } + + size_t first_not_of(const C c, size_t start) const + { + C4_ASSERT((start >= 0 && start <= len) || (start == len && len == 0)); + for(size_t i = start; i < len; ++i) + { + if(str[i] != c) + return i; + } + return npos; + } + + size_t last_not_of(const C c) const + { + for(size_t i = len-1; i != size_t(-1); --i) + { + if(str[i] != c) + return i; + } + return npos; + } + + size_t last_not_of(const C c, size_t start) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + if(start == npos) + start = len; + for(size_t i = start-1; i != size_t(-1); --i) + { + if(str[i] != c) + return i; + } + return npos; + } + + size_t first_not_of(ro_substr chars) const + { + for(size_t i = 0; i < len; ++i) + { + bool gotit = true; + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars.str[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return i; + } + } + return npos; + } + + size_t first_not_of(ro_substr chars, size_t start) const + { + C4_ASSERT((start >= 0 && start <= len) || (start == len && len == 0)); + for(size_t i = start; i < len; ++i) + { + bool gotit = true; + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars.str[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return i; + } + } + return npos; + } + + size_t last_not_of(ro_substr chars) const + { + for(size_t i = len-1; i != size_t(-1); --i) + { + bool gotit = true; + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars.str[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return i; + } + } + return npos; + } + + size_t last_not_of(ro_substr chars, size_t start) const + { + C4_ASSERT(start == npos || (start >= 0 && start <= len)); + if(start == npos) + start = len; + for(size_t i = start-1; i != size_t(-1); --i) + { + bool gotit = true; + for(size_t j = 0; j < chars.len; ++j) + { + if(str[i] == chars.str[j]) + { + gotit = false; + break; + } + } + if(gotit) + { + return i; + } + } + return npos; + } + + /** @} */ + +public: + + /** @name Range lookup methods */ + /** @{ */ + + /** get the range delimited by an open-close pair of characters. + * @note There must be no nested pairs. + * @note No checks for escapes are performed. */ + basic_substring pair_range(CC open, CC close) const + { + size_t b = find(open); + if(b == npos) + return basic_substring(); + size_t e = find(close, b+1); + if(e == npos) + return basic_substring(); + basic_substring ret = range(b, e+1); + C4_ASSERT(ret.sub(1).find(open) == npos); + return ret; + } + + /** get the range delimited by a single open-close character (eg, quotes). + * @note The open-close character can be escaped. */ + basic_substring pair_range_esc(CC open_close, CC escape=CC('\\')) + { + size_t b = find(open_close); + if(b == npos) return basic_substring(); + for(size_t i = b+1; i < len; ++i) + { + CC c = str[i]; + if(c == open_close) + { + if(str[i-1] != escape) + { + return range(b, i+1); + } + } + } + return basic_substring(); + } + + /** get the range delimited by an open-close pair of characters, + * with possibly nested occurrences. No checks for escapes are + * performed. */ + basic_substring pair_range_nested(CC open, CC close) const + { + size_t b = find(open); + if(b == npos) return basic_substring(); + size_t e, curr = b+1, count = 0; + const char both[] = {open, close, '\0'}; + while((e = first_of(both, curr)) != npos) + { + if(str[e] == open) + { + ++count; + curr = e+1; + } + else if(str[e] == close) + { + if(count == 0) return range(b, e+1); + --count; + curr = e+1; + } + } + return basic_substring(); + } + + basic_substring unquoted() const + { + constexpr const C dq('"'), sq('\''); + if(len >= 2 && (str[len - 2] != C('\\')) && + ((begins_with(sq) && ends_with(sq)) + || + (begins_with(dq) && ends_with(dq)))) + { + return range(1, len -1); + } + return *this; + } + + /** @} */ + +public: + + /** @name Number-matching query methods */ + /** @{ */ + + /** @return true if the substring contents are a floating-point or integer number. + * @note any leading or trailing whitespace will return false. */ + bool is_number() const + { + if(empty() || (first_non_empty_span().empty())) + return false; + if(first_uint_span() == *this) + return true; + if(first_int_span() == *this) + return true; + if(first_real_span() == *this) + return true; + return false; + } + + /** @return true if the substring contents are a real number. + * @note any leading or trailing whitespace will return false. */ + bool is_real() const + { + if(empty() || (first_non_empty_span().empty())) + return false; + if(first_real_span() == *this) + return true; + return false; + } + + /** @return true if the substring contents are an integer number. + * @note any leading or trailing whitespace will return false. */ + bool is_integer() const + { + if(empty() || (first_non_empty_span().empty())) + return false; + if(first_uint_span() == *this) + return true; + if(first_int_span() == *this) + return true; + return false; + } + + /** @return true if the substring contents are an unsigned integer number. + * @note any leading or trailing whitespace will return false. */ + bool is_unsigned_integer() const + { + if(empty() || (first_non_empty_span().empty())) + return false; + if(first_uint_span() == *this) + return true; + return false; + } + + /** get the first span consisting exclusively of non-empty characters */ + basic_substring first_non_empty_span() const + { + constexpr const ro_substr empty_chars(" \n\r\t"); + size_t pos = first_not_of(empty_chars); + if(pos == npos) + return first(0); + auto ret = sub(pos); + pos = ret.first_of(empty_chars); + return ret.first(pos); + } + + /** get the first span which can be interpreted as an unsigned integer */ + basic_substring first_uint_span() const + { + basic_substring ne = first_non_empty_span(); + if(ne.empty()) + return ne; + if(ne.str[0] == '-') + return first(0); + size_t skip_start = size_t(ne.str[0] == '+'); + return ne._first_integral_span(skip_start); + } + + /** get the first span which can be interpreted as a signed integer */ + basic_substring first_int_span() const + { + basic_substring ne = first_non_empty_span(); + if(ne.empty()) + return ne; + size_t skip_start = size_t(ne.str[0] == '+' || ne.str[0] == '-'); + return ne._first_integral_span(skip_start); + } + + basic_substring _first_integral_span(size_t skip_start) const + { + C4_ASSERT(!empty()); + if(skip_start == len) + return first(0); + C4_ASSERT(skip_start < len); + if(len >= skip_start + 3) + { + if(str[skip_start] != '0') + { + for(size_t i = skip_start; i < len; ++i) + { + char c = str[i]; + if(c < '0' || c > '9') + return i > skip_start && _is_delim_char(c) ? first(i) : first(0); + } + } + else + { + char next = str[skip_start + 1]; + if(next == 'x' || next == 'X') + { + skip_start += 2; + for(size_t i = skip_start; i < len; ++i) + { + const char c = str[i]; + if( ! _is_hex_char(c)) + return i > skip_start && _is_delim_char(c) ? first(i) : first(0); + } + return *this; + } + else if(next == 'b' || next == 'B') + { + skip_start += 2; + for(size_t i = skip_start; i < len; ++i) + { + const char c = str[i]; + if(c != '0' && c != '1') + return i > skip_start && _is_delim_char(c) ? first(i) : first(0); + } + return *this; + } + else if(next == 'o' || next == 'O') + { + skip_start += 2; + for(size_t i = skip_start; i < len; ++i) + { + const char c = str[i]; + if(c < '0' || c > '7') + return i > skip_start && _is_delim_char(c) ? first(i) : first(0); + } + return *this; + } + } + } + // must be a decimal, or it is not a an number + for(size_t i = skip_start; i < len; ++i) + { + const char c = str[i]; + if(c < '0' || c > '9') + return i > skip_start && _is_delim_char(c) ? first(i) : first(0); + } + return *this; + } + + /** get the first span which can be interpreted as a real (floating-point) number */ + basic_substring first_real_span() const + { + basic_substring ne = first_non_empty_span(); + if(ne.empty()) + return ne; + const size_t skip_start = (ne.str[0] == '+' || ne.str[0] == '-'); + C4_ASSERT(skip_start == 0 || skip_start == 1); + // if we have at least three digits after the leading sign, it + // can be decimal, or hex, or bin or oct. Ex: + // non-decimal: 0x0, 0b0, 0o0 + // decimal: 1.0, 10., 1e1, 100, inf, nan, infinity + if(ne.len >= skip_start+3) + { + // if it does not have leading 0, it must be decimal, or it is not a real + if(ne.str[skip_start] != '0') + { + if(ne.str[skip_start] == 'i') // is it infinity or inf? + { + basic_substring word = ne._word_follows(skip_start + 1, "nfinity"); + if(word.len) + return word; + return ne._word_follows(skip_start + 1, "nf"); + } + else if(ne.str[skip_start] == 'n') // is it nan? + { + return ne._word_follows(skip_start + 1, "an"); + } + else // must be a decimal, or it is not a real + { + return ne._first_real_span_dec(skip_start); + } + } + else // starts with 0. is it 0x, 0b or 0o? + { + const char next = ne.str[skip_start + 1]; + // hexadecimal + if(next == 'x' || next == 'X') + return ne._first_real_span_hex(skip_start + 2); + // binary + else if(next == 'b' || next == 'B') + return ne._first_real_span_bin(skip_start + 2); + // octal + else if(next == 'o' || next == 'O') + return ne._first_real_span_oct(skip_start + 2); + // none of the above. may still be a decimal. + else + return ne._first_real_span_dec(skip_start); // do not skip the 0. + } + } + // less than 3 chars after the leading sign. It is either a + // decimal or it is not a real. (cannot be any of 0x0, etc). + return ne._first_real_span_dec(skip_start); + } + + /** true if the character is a delimiter character *at the end* */ + static constexpr C4_ALWAYS_INLINE C4_CONST bool _is_delim_char(char c) noexcept + { + return c == ' ' || c == '\n' + || c == ']' || c == ')' || c == '}' + || c == ',' || c == ';' || c == '\r' || c == '\t' || c == '\0'; + } + + /** true if the character is in [0-9a-fA-F] */ + static constexpr C4_ALWAYS_INLINE C4_CONST bool _is_hex_char(char c) noexcept + { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + C4_NO_INLINE C4_PURE basic_substring _word_follows(size_t pos, csubstr word) const noexcept + { + size_t posend = pos + word.len; + if(len >= posend && sub(pos, word.len) == word) + if(len == posend || _is_delim_char(str[posend])) + return first(posend); + return first(0); + } + + // this function is declared inside the class to avoid a VS error with __declspec(dllimport) + C4_NO_INLINE C4_PURE basic_substring _first_real_span_dec(size_t pos) const noexcept + { + bool intchars = false; + bool fracchars = false; + bool powchars; + // integral part + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + { + intchars = true; + } + else if(c == '.') + { + ++pos; + goto fractional_part_dec; // NOLINT + } + else if(c == 'e' || c == 'E') + { + ++pos; + goto power_part_dec; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + // no . or p were found; this is either an integral number + // or not a number at all + return intchars ? + *this : + first(0); + fractional_part_dec: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == '.'); + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + { + fracchars = true; + } + else if(c == 'e' || c == 'E') + { + ++pos; + goto power_part_dec; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars || fracchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + return intchars || fracchars ? + *this : + first(0); + power_part_dec: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == 'e' || str[pos - 1] == 'E'); + // either digits, or +, or - are expected here, followed by more digits. + if((len == pos) || ((!intchars) && (!fracchars))) + return first(0); + if(str[pos] == '-' || str[pos] == '+') + ++pos; // skip the sign + powchars = false; + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + powchars = true; + else if(powchars && _is_delim_char(c)) + return first(pos); + else + return first(0); + } + return powchars ? *this : first(0); + } + + // this function is declared inside the class to avoid a VS error with __declspec(dllimport) + C4_NO_INLINE C4_PURE basic_substring _first_real_span_hex(size_t pos) const noexcept + { + bool intchars = false; + bool fracchars = false; + bool powchars; + // integral part + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(_is_hex_char(c)) + { + intchars = true; + } + else if(c == '.') + { + ++pos; + goto fractional_part_hex; // NOLINT + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_hex; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + // no . or p were found; this is either an integral number + // or not a number at all + return intchars ? + *this : + first(0); + fractional_part_hex: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == '.'); + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(_is_hex_char(c)) + { + fracchars = true; + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_hex; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars || fracchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + return intchars || fracchars ? + *this : + first(0); + power_part_hex: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == 'p' || str[pos - 1] == 'P'); + // either a + or a - is expected here, followed by more chars. + // also, using (pos+1) in this check will cause an early + // return when no more chars follow the sign. + if(len <= (pos+1) || (str[pos] != '+' && str[pos] != '-') || ((!intchars) && (!fracchars))) + return first(0); + ++pos; // this was the sign. + // ... so the (pos+1) ensures that we enter the loop and + // hence that there exist chars in the power part + powchars = false; + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + powchars = true; + else if(powchars && _is_delim_char(c)) + return first(pos); + else + return first(0); + } + return *this; + } + + // this function is declared inside the class to avoid a VS error with __declspec(dllimport) + C4_NO_INLINE C4_PURE basic_substring _first_real_span_bin(size_t pos) const noexcept + { + bool intchars = false; + bool fracchars = false; + bool powchars; + // integral part + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c == '0' || c == '1') + { + intchars = true; + } + else if(c == '.') + { + ++pos; + goto fractional_part_bin; // NOLINT + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_bin; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + // no . or p were found; this is either an integral number + // or not a number at all + return intchars ? + *this : + first(0); + fractional_part_bin: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == '.'); + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c == '0' || c == '1') + { + fracchars = true; + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_bin; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars || fracchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + return intchars || fracchars ? + *this : + first(0); + power_part_bin: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == 'p' || str[pos - 1] == 'P'); + // either a + or a - is expected here, followed by more chars. + // also, using (pos+1) in this check will cause an early + // return when no more chars follow the sign. + if(len <= (pos+1) || (str[pos] != '+' && str[pos] != '-') || ((!intchars) && (!fracchars))) + return first(0); + ++pos; // this was the sign. + // ... so the (pos+1) ensures that we enter the loop and + // hence that there exist chars in the power part + powchars = false; + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + powchars = true; + else if(powchars && _is_delim_char(c)) + return first(pos); + else + return first(0); + } + return *this; + } + + // this function is declared inside the class to avoid a VS error with __declspec(dllimport) + C4_NO_INLINE C4_PURE basic_substring _first_real_span_oct(size_t pos) const noexcept + { + bool intchars = false; + bool fracchars = false; + bool powchars; + // integral part + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '7') + { + intchars = true; + } + else if(c == '.') + { + ++pos; + goto fractional_part_oct; // NOLINT + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_oct; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + // no . or p were found; this is either an integral number + // or not a number at all + return intchars ? + *this : + first(0); + fractional_part_oct: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == '.'); + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '7') + { + fracchars = true; + } + else if(c == 'p' || c == 'P') + { + ++pos; + goto power_part_oct; // NOLINT + } + else if(_is_delim_char(c)) + { + return intchars || fracchars ? first(pos) : first(0); + } + else + { + return first(0); + } + } + return intchars || fracchars ? + *this : + first(0); + power_part_oct: + C4_ASSERT(pos > 0); + C4_ASSERT(str[pos - 1] == 'p' || str[pos - 1] == 'P'); + // either a + or a - is expected here, followed by more chars. + // also, using (pos+1) in this check will cause an early + // return when no more chars follow the sign. + if(len <= (pos+1) || (str[pos] != '+' && str[pos] != '-') || ((!intchars) && (!fracchars))) + return first(0); + ++pos; // this was the sign. + // ... so the (pos+1) ensures that we enter the loop and + // hence that there exist chars in the power part + powchars = false; + for( ; pos < len; ++pos) + { + const char c = str[pos]; + if(c >= '0' && c <= '9') + powchars = true; + else if(powchars && _is_delim_char(c)) + return first(pos); + else + return first(0); + } + return *this; + } + + /** @} */ + +public: + + /** @name Splitting methods */ + /** @{ */ + + /** returns true if the string has not been exhausted yet, meaning + * it's ok to call next_split() again. When no instance of sep + * exists in the string, returns the full string. When the input + * is an empty string, the output string is the empty string. */ + bool next_split(C sep, size_t *C4_RESTRICT start_pos, basic_substring *C4_RESTRICT out) const + { + if(C4_LIKELY(*start_pos < len)) + { + for(size_t i = *start_pos; i < len; i++) + { + if(str[i] == sep) + { + out->assign(str + *start_pos, i - *start_pos); + *start_pos = i+1; + return true; + } + } + out->assign(str + *start_pos, len - *start_pos); + *start_pos = len + 1; + return true; + } + else + { + bool valid = len > 0 && (*start_pos == len); + if(valid && str && str[len-1] == sep) + { + out->assign(str + len, size_t(0)); // the cast is needed to prevent overload ambiguity + } + else + { + out->assign(str + len + 1, size_t(0)); // the cast is needed to prevent overload ambiguity + } + *start_pos = len + 1; + return valid; + } + } + +private: + + struct split_proxy_impl + { + struct split_iterator_impl + { + split_proxy_impl const* m_proxy; + basic_substring m_str; + size_t m_pos; + NCC_ m_sep; + + split_iterator_impl(split_proxy_impl const* proxy, size_t pos, C sep) + : m_proxy(proxy), m_pos(pos), m_sep(sep) + { + _tick(); + } + + void _tick() + { + m_proxy->m_str.next_split(m_sep, &m_pos, &m_str); + } + + split_iterator_impl& operator++ () { _tick(); return *this; } + split_iterator_impl operator++ (int) { split_iterator_impl it = *this; _tick(); return it; } // NOLINT + + basic_substring& operator* () { return m_str; } + basic_substring* operator-> () { return &m_str; } + + bool operator!= (split_iterator_impl const& that) const + { + return !(this->operator==(that)); + } + bool operator== (split_iterator_impl const& that) const + { + C4_XASSERT((m_sep == that.m_sep) && "cannot compare split iterators with different separators"); + if(m_str.size() != that.m_str.size()) + return false; + if(m_str.data() != that.m_str.data()) + return false; + return m_pos == that.m_pos; + } + }; + + basic_substring m_str; + size_t m_start_pos; + C m_sep; + + split_proxy_impl(basic_substring str_, size_t start_pos, C sep) + : m_str(str_), m_start_pos(start_pos), m_sep(sep) + { + } + + split_iterator_impl begin() const + { + auto it = split_iterator_impl(this, m_start_pos, m_sep); + return it; + } + split_iterator_impl end() const + { + size_t pos = m_str.size() + 1; + auto it = split_iterator_impl(this, pos, m_sep); + return it; + } + }; + +public: + + using split_proxy = split_proxy_impl; + + /** a view into the splits */ + split_proxy split(C sep, size_t start_pos=0) const + { + C4_XASSERT((start_pos >= 0 && start_pos < len) || empty()); + auto ss = sub(0, len); + auto it = split_proxy(ss, start_pos, sep); + return it; + } + +public: + + /** pop right: return the first split from the right. Use + * gpop_left() to get the reciprocal part. + */ + basic_substring pop_right(C sep=C('/'), bool skip_empty=false) const + { + if(C4_LIKELY(len > 1)) + { + auto pos = last_of(sep); + if(pos != npos) + { + if(pos + 1 < len) // does not end with sep + { + return sub(pos + 1); // return from sep to end + } + else // the string ends with sep + { + if( ! skip_empty) + { + return sub(pos + 1, 0); + } + auto ppos = last_not_of(sep); // skip repeated seps + if(ppos == npos) // the string is all made of seps + { + return sub(0, 0); + } + // find the previous sep + auto pos0 = last_of(sep, ppos); + if(pos0 == npos) // only the last sep exists + { + return sub(0); // return the full string (because skip_empty is true) + } + ++pos0; + return sub(pos0); + } + } + else // no sep was found, return the full string + { + return *this; + } + } + else if(len == 1) + { + if(begins_with(sep)) + { + return sub(0, 0); + } + return *this; + } + else // an empty string + { + return basic_substring(); + } + } + + /** return the first split from the left. Use gpop_right() to get + * the reciprocal part. */ + basic_substring pop_left(C sep = C('/'), bool skip_empty=false) const + { + if(C4_LIKELY(len > 1)) + { + auto pos = first_of(sep); + if(pos != npos) + { + if(pos > 0) // does not start with sep + { + return sub(0, pos); // return everything up to it + } + else // the string starts with sep + { + if( ! skip_empty) + { + return sub(0, 0); + } + auto ppos = first_not_of(sep); // skip repeated seps + if(ppos == npos) // the string is all made of seps + { + return sub(0, 0); + } + // find the next sep + auto pos0 = first_of(sep, ppos); + if(pos0 == npos) // only the first sep exists + { + return sub(0); // return the full string (because skip_empty is true) + } + C4_XASSERT(pos0 > 0); + // return everything up to the second sep + return sub(0, pos0); + } + } + else // no sep was found, return the full string + { + return sub(0); + } + } + else if(len == 1) + { + if(begins_with(sep)) + { + return sub(0, 0); + } + return sub(0); + } + else // an empty string + { + return basic_substring(); + } + } + +public: + + /** greedy pop left. eg, csubstr("a/b/c").gpop_left('/')="c" */ + basic_substring gpop_left(C sep = C('/'), bool skip_empty=false) const + { + auto ss = pop_right(sep, skip_empty); + ss = left_of(ss); + if(ss.find(sep) != npos) + { + if(ss.ends_with(sep)) + { + if(skip_empty) + { + ss = ss.trimr(sep); + } + else + { + ss = ss.sub(0, ss.len-1); // safe to subtract because ends_with(sep) is true + } + } + } + return ss; + } + + /** greedy pop right. eg, csubstr("a/b/c").gpop_right('/')="a" */ + basic_substring gpop_right(C sep = C('/'), bool skip_empty=false) const + { + auto ss = pop_left(sep, skip_empty); + ss = right_of(ss); + if(ss.find(sep) != npos) + { + if(ss.begins_with(sep)) + { + if(skip_empty) + { + ss = ss.triml(sep); + } + else + { + ss = ss.sub(1); + } + } + } + return ss; + } + + /** @} */ + +public: + + /** @name Path-like manipulation methods */ + /** @{ */ + + basic_substring basename(C sep=C('/')) const + { + auto ss = pop_right(sep, /*skip_empty*/true); + ss = ss.trimr(sep); + return ss; + } + + basic_substring dirname(C sep=C('/')) const + { + auto ss = basename(sep); + ss = ss.empty() ? *this : left_of(ss); + return ss; + } + + C4_ALWAYS_INLINE basic_substring name_wo_extshort() const + { + return gpop_left('.'); + } + + C4_ALWAYS_INLINE basic_substring name_wo_extlong() const + { + return pop_left('.'); + } + + C4_ALWAYS_INLINE basic_substring extshort() const + { + return pop_right('.'); + } + + C4_ALWAYS_INLINE basic_substring extlong() const + { + return gpop_right('.'); + } + + /** @} */ + +public: + + /** @name Content-modification methods (only for non-const C) */ + /** @{ */ + + /** convert the string to upper-case + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) toupper() + { + for(size_t i = 0; i < len; ++i) + { + str[i] = static_cast(::toupper(str[i])); + } + } + + /** convert the string to lower-case + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) tolower() + { + for(size_t i = 0; i < len; ++i) + { + str[i] = static_cast(::tolower(str[i])); + } + } + +public: + + /** fill the entire contents with the given @p val + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) fill(C val) + { + for(size_t i = 0; i < len; ++i) + { + str[i] = val; + } + } + +public: + + /** set the current substring to a copy of the given csubstr + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) copy_from(ro_substr that, size_t ifirst=0, size_t num=npos) + { + C4_ASSERT(ifirst >= 0 && ifirst <= len); + num = num != npos ? num : len - ifirst; + num = num < that.len ? num : that.len; + C4_ASSERT(ifirst + num >= 0 && ifirst + num <= len); + // calling memcpy with null strings is undefined behavior + // and will wreak havoc in calling code's branches. + // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637 + if(num) + memcpy(str + sizeof(C) * ifirst, that.str, sizeof(C) * num); + } + +public: + + /** reverse in place + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) reverse() + { + if(len == 0) return; + detail::_do_reverse(str, str + len - 1); + } + + /** revert a subpart in place + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) reverse_sub(size_t ifirst, size_t num) + { + C4_ASSERT(ifirst >= 0 && ifirst <= len); + C4_ASSERT(ifirst + num >= 0 && ifirst + num <= len); + if(num == 0) return; + detail::_do_reverse(str + ifirst, str + ifirst + num - 1); + } + + /** revert a range in place + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(void) reverse_range(size_t ifirst, size_t ilast) + { + C4_ASSERT(ifirst >= 0 && ifirst <= len); + C4_ASSERT(ilast >= 0 && ilast <= len); + if(ifirst == ilast) return; + detail::_do_reverse(str + ifirst, str + ilast - 1); + } + +public: + + /** erase part of the string. eg, with char s[] = "0123456789", + * substr(s).erase(3, 2) = "01256789", and s is now "01245678989" + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(basic_substring) erase(size_t pos, size_t num) + { + C4_ASSERT(pos >= 0 && pos+num <= len); + size_t num_to_move = len - pos - num; + memmove(str + pos, str + pos + num, sizeof(C) * num_to_move); + return basic_substring{str, len - num}; + } + + /** @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(basic_substring) erase_range(size_t first, size_t last) + { + C4_ASSERT(first <= last); + return erase(first, static_cast(last-first)); // NOLINT + } + + /** erase a part of the string. + * @note @p sub must be a substring of this string + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(basic_substring) erase(ro_substr sub) + { + C4_ASSERT(is_super(sub)); + C4_ASSERT(sub.str >= str); + return erase(static_cast(sub.str - str), sub.len); + } + +public: + + /** replace every occurrence of character @p value with the character @p repl + * @return the number of characters that were replaced + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(size_t) replace(C value, C repl, size_t pos=0) + { + C4_ASSERT((pos >= 0 && pos <= len) || pos == npos); + size_t did_it = 0; + while((pos = find(value, pos)) != npos) + { + str[pos++] = repl; + ++did_it; + } + return did_it; + } + + /** replace every occurrence of each character in @p value with + * the character @p repl. + * @return the number of characters that were replaced + * @note this method requires that the string memory is writeable and is SFINAEd out for const C */ + C4_REQUIRE_RW(size_t) replace(ro_substr chars, C repl, size_t pos=0) + { + C4_ASSERT((pos >= 0 && pos <= len) || pos == npos); + size_t did_it = 0; + while((pos = first_of(chars, pos)) != npos) + { + str[pos++] = repl; + ++did_it; + } + return did_it; + } + + /** replace @p pattern with @p repl, and write the result into + * @p dst. pattern and repl don't need equal sizes. + * + * @return the required size for dst. No overflow occurs if + * dst.len is smaller than the required size; this can be used to + * determine the required size for an existing container. */ + size_t replace_all(rw_substr dst, ro_substr pattern, ro_substr repl, size_t pos=0) const + { + C4_ASSERT( ! pattern.empty()); //!< @todo relax this precondition + C4_ASSERT( ! this ->overlaps(dst)); //!< @todo relax this precondition + C4_ASSERT( ! pattern.overlaps(dst)); + C4_ASSERT( ! repl .overlaps(dst)); + C4_ASSERT((pos >= 0 && pos <= len) || pos == npos); + C4_SUPPRESS_WARNING_GCC_PUSH + C4_SUPPRESS_WARNING_GCC("-Warray-bounds") // gcc11 has a false positive here + #if (!defined(__clang__)) && (defined(__GNUC__) && (__GNUC__ >= 7)) + C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow") // gcc11 has a false positive here + #endif + #define _c4append(first, last) \ + { \ + C4_ASSERT((last) >= (first)); \ + size_t num = static_cast((last) - (first)); \ + if(num > 0 && sz + num <= dst.len) \ + { \ + memcpy(dst.str + sz, first, num * sizeof(C)); \ + } \ + sz += num; \ + } + size_t sz = 0; + size_t b = pos; + _c4append(str, str + pos); + do { + size_t e = find(pattern, b); + if(e == npos) + { + _c4append(str + b, str + len); + break; + } + _c4append(str + b, str + e); + _c4append(repl.begin(), repl.end()); + b = e + pattern.size(); + } while(b < len && b != npos); + return sz; + #undef _c4append + C4_SUPPRESS_WARNING_GCC_POP + } + + /** @} */ + +}; // template class basic_substring + + +#undef C4_REQUIRE_RW + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** @defgroup doc_substr_adapters substr adapters + * + * to_substr() and to_csubstr() is used in generic code like + * format(), and allow adding construction of substrings from new + * types like containers. + * @{ */ + + +/** neutral version for use in generic code */ +C4_ALWAYS_INLINE substr to_substr(substr s) noexcept { return s; } +/** neutral version for use in generic code */ +C4_ALWAYS_INLINE csubstr to_csubstr(substr s) noexcept { return s; } +/** neutral version for use in generic code */ +C4_ALWAYS_INLINE csubstr to_csubstr(csubstr s) noexcept { return s; } + + +template +C4_ALWAYS_INLINE substr +to_substr(char (&s)[N]) noexcept { substr ss(s, N-1); return ss; } +template +C4_ALWAYS_INLINE csubstr +to_csubstr(const char (&s)[N]) noexcept { csubstr ss(s, N-1); return ss; } + + +/** @note this overload uses SFINAE to prevent it from overriding the array overload + * @see For a more detailed explanation on why the plain overloads cannot + * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */ +template +C4_ALWAYS_INLINE typename std::enable_if::value, substr>::type +to_substr(U s) noexcept { substr ss(s); return ss; } +/** @note this overload uses SFINAE to prevent it from overriding the array overload + * @see For a more detailed explanation on why the plain overloads cannot + * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */ +template +C4_ALWAYS_INLINE typename std::enable_if::value || std::is_same::value, csubstr>::type +to_csubstr(U s) noexcept { csubstr ss(s); return ss; } + + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_substr_cmp substr comparison operators + * @{ */ + +template inline bool operator== (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) == 0; } +template inline bool operator!= (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) != 0; } +template inline bool operator< (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) > 0; } +template inline bool operator> (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) < 0; } +template inline bool operator<= (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) >= 0; } +template inline bool operator>= (const char (&s)[N], basic_substring const that) noexcept { return that.compare(s, N-1) <= 0; } + +template inline bool operator== (const char c, basic_substring const that) noexcept { return that.compare(c) == 0; } +template inline bool operator!= (const char c, basic_substring const that) noexcept { return that.compare(c) != 0; } +template inline bool operator< (const char c, basic_substring const that) noexcept { return that.compare(c) > 0; } +template inline bool operator> (const char c, basic_substring const that) noexcept { return that.compare(c) < 0; } +template inline bool operator<= (const char c, basic_substring const that) noexcept { return that.compare(c) >= 0; } +template inline bool operator>= (const char c, basic_substring const that) noexcept { return that.compare(c) <= 0; } + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/* C4_SUBSTR_NO_OSTREAM_LSHIFT doctest does not deal well with + * template operator<< + * @see https://github.com/onqtam/doctest/pull/431 */ +#ifndef C4_SUBSTR_NO_OSTREAM_LSHIFT +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wsign-conversion" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +/** output the string to a stream */ +template +inline OStream& operator<< (OStream& os, basic_substring s) +{ + os.write(s.str, s.len); + return os; +} + +// this causes ambiguity +///** this is used by google test */ +//template +//inline void PrintTo(basic_substring s, OStream* os) +//{ +// os->write(s.str, s.len); +//} + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif +#endif // !C4_SUBSTR_NO_OSTREAM_LSHIFT + +/** @} */ + +} // namespace c4 + + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif /* _C4_SUBSTR_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/substr_fwd.hpp b/3rdparty/rapidyaml/include/c4/substr_fwd.hpp new file mode 100644 index 0000000000..63d01b5950 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/substr_fwd.hpp @@ -0,0 +1,16 @@ +#ifndef _C4_SUBSTR_FWD_HPP_ +#define _C4_SUBSTR_FWD_HPP_ + +#include "c4/export.hpp" + +namespace c4 { + +#ifndef DOXYGEN +template struct basic_substring; +using csubstr = C4CORE_EXPORT basic_substring; +using substr = C4CORE_EXPORT basic_substring; +#endif // !DOXYGEN + +} // namespace c4 + +#endif /* _C4_SUBSTR_FWD_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/szconv.hpp b/3rdparty/rapidyaml/include/c4/szconv.hpp new file mode 100644 index 0000000000..e571f9f3d0 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/szconv.hpp @@ -0,0 +1,68 @@ +#ifndef _C4_SZCONV_HPP_ +#define _C4_SZCONV_HPP_ + +/** @file szconv.hpp utilities to deal safely with narrowing conversions */ + +#include "c4/config.hpp" +#include "c4/error.hpp" + +#include + +namespace c4 { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + +/** @todo this would be so much easier with calls to numeric_limits::max()... */ +template +struct is_narrower_size : std::conditional +< + (std::is_signed::value == std::is_signed::value) + ? + (sizeof(SizeOut) < sizeof(SizeIn)) + : + ( + (sizeof(SizeOut) < sizeof(SizeIn)) + || + ( + (sizeof(SizeOut) == sizeof(SizeIn)) + && + (std::is_signed::value && std::is_unsigned::value) + ) + ), + std::true_type, + std::false_type +>::type +{ + static_assert(std::is_integral::value, "must be integral type"); + static_assert(std::is_integral::value, "must be integral type"); +}; + + +/** when SizeOut is wider than SizeIn, assignment can occur without reservations */ +template +C4_ALWAYS_INLINE +typename std::enable_if< ! is_narrower_size::value, SizeOut>::type +szconv(SizeIn sz) noexcept +{ + return static_cast(sz); +} + +/** when SizeOut is narrower than SizeIn, narrowing will occur, so we check + * for overflow. Note that this check is done only if C4_XASSERT is enabled. + * @see C4_XASSERT */ +template +C4_ALWAYS_INLINE +typename std::enable_if::value, SizeOut>::type +szconv(SizeIn sz) +{ + C4_XASSERT(sz >= 0); + C4_XASSERT_MSG((SizeIn)sz <= (SizeIn)std::numeric_limits::max(), "size conversion overflow: in=%zu", (size_t)sz); + SizeOut szo = static_cast(sz); + return szo; +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace c4 + +#endif /* _C4_SZCONV_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/types.hpp b/3rdparty/rapidyaml/include/c4/types.hpp new file mode 100644 index 0000000000..77527996cf --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/types.hpp @@ -0,0 +1,507 @@ +#ifndef _C4_TYPES_HPP_ +#define _C4_TYPES_HPP_ + +#include +#include +#include + +#if __cplusplus >= 201103L +#include // for integer_sequence and friends +#endif + +#include "c4/preprocessor.hpp" +#include "c4/language.hpp" + +/** @file types.hpp basic types, and utility macros and traits for types. + * @ingroup basic_headers */ + +/** @defgroup types Type utilities */ + +// NOLINTBEGIN(bugprone-macro-parentheses) + +namespace c4 { + +/** @defgroup intrinsic_types Intrinsic types + * @ingroup types + * @{ */ + +using cbyte = const char; /**< a constant byte */ +using byte = char; /**< a mutable byte */ + +using i8 = int8_t; +using i16 = int16_t; +using i32 = int32_t; +using i64 = int64_t; +using u8 = uint8_t; +using u16 = uint16_t; +using u32 = uint32_t; +using u64 = uint64_t; + +using f32 = float; +using f64 = double; + +using ssize_t = typename std::make_signed::type; + +/** @} */ + +//-------------------------------------------------- + +/** @defgroup utility_types Utility types + * @ingroup types + * @{ */ + +// some tag types + +#if !defined(__clang__) && defined(__GNUC__) +#pragma GCC diagnostic push +#if __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wunused-const-variable" +#endif +#endif + +/** a tag type for initializing the containers with variadic arguments a la + * initializer_list, minus the initializer_list overload problems. + */ +struct aggregate_t {}; +/** @see aggregate_t */ +constexpr const aggregate_t aggregate{}; + +/** a tag type for specifying the initial capacity of allocatable contiguous storage */ +struct with_capacity_t {}; +/** @see with_capacity_t */ +constexpr const with_capacity_t with_capacity{}; + +/** a tag type for disambiguating template parameter packs in variadic template overloads */ +struct varargs_t {}; +/** @see with_capacity_t */ +constexpr const varargs_t varargs{}; + +#if !defined(__clang__) && defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + + +//-------------------------------------------------- + +/** whether a value should be used in place of a const-reference in argument passing. */ +template +struct cref_uses_val +{ + enum { value = ( + std::is_scalar::value + || + ( +#if C4_CPP >= 20 + (std::is_trivially_copyable::value && std::is_standard_layout::value) +#else + std::is_pod::value +#endif + && + sizeof(T) <= sizeof(size_t))) }; +}; +/** utility macro to override the default behaviour for c4::fastcref + @see fastcref */ +#define C4_CREF_USES_VAL(T) \ +template<> \ +struct cref_uses_val \ +{ \ + enum { value = true }; \ +}; + +/** Whether to use pass-by-value or pass-by-const-reference in a function argument + * or return type. */ +template +using fastcref = typename std::conditional::value, T, T const&>::type; + +//-------------------------------------------------- + +/** Just what its name says. Useful sometimes as a default empty policy class. */ +struct EmptyStruct // NOLINT +{ + template EmptyStruct(T && ...){} // NOLINT +}; + +/** Just what its name says. Useful sometimes as a default policy class to + * be inherited from. */ +struct EmptyStructVirtual // NOLINT +{ + virtual ~EmptyStructVirtual() = default; + template EmptyStructVirtual(T && ...){} // NOLINT +}; + + +/** */ +template +struct inheritfrom : public T {}; + +//-------------------------------------------------- +// Utilities to make a class obey size restrictions (eg, min size or size multiple of). +// DirectX usually makes this restriction with uniform buffers. +// This is also useful for padding to prevent false-sharing. + +/** how many bytes must be added to size such that the result is at least minsize? */ +C4_ALWAYS_INLINE constexpr size_t min_remainder(size_t size, size_t minsize) noexcept +{ + return size < minsize ? minsize-size : 0; +} + +/** how many bytes must be added to size such that the result is a multiple of multipleof? */ +C4_ALWAYS_INLINE constexpr size_t mult_remainder(size_t size, size_t multipleof) noexcept +{ + return (((size % multipleof) != 0) ? (multipleof-(size % multipleof)) : 0); +} + +/* force the following class to be tightly packed. */ +#pragma pack(push, 1) +/** pad a class with more bytes at the end. + * @see http://stackoverflow.com/questions/21092415/force-c-structure-to-pack-tightly */ +template +struct Padded : public T +{ + using T::T; + using T::operator=; + Padded(T const& val) : T(val) {} + Padded(T && val) : T(std::forward(val)) {} // NOLINT + char ___c4padspace___[BytesToPadAtEnd]; +}; +#pragma pack(pop) +/** When the padding argument is 0, we cannot declare the char[] array. */ +template +struct Padded : public T +{ + using T::T; + using T::operator=; + Padded(T const& val) : T(val) {} + Padded(T && val) : T(std::forward(val)) {} // NOLINT +}; + +/** make T have a size which is at least Min bytes */ +template +using MinSized = Padded; + +/** make T have a size which is a multiple of Mult bytes */ +template +using MultSized = Padded; + +/** make T have a size which is simultaneously: + * -bigger or equal than Min + * -a multiple of Mult */ +template +using MinMultSized = MultSized, Mult>; + +/** make T be suitable for use as a uniform buffer. (at least with DirectX). */ +template +using UbufSized = MinMultSized; + + +//----------------------------------------------------------------------------- + +#define C4_NO_COPY_CTOR(ty) ty(ty const&) = delete +#define C4_NO_MOVE_CTOR(ty) ty(ty &&) = delete +#define C4_NO_COPY_ASSIGN(ty) ty& operator=(ty const&) = delete +#define C4_NO_MOVE_ASSIGN(ty) ty& operator=(ty &&) = delete +#define C4_DEFAULT_COPY_CTOR(ty) ty(ty const&) noexcept = default +#define C4_DEFAULT_MOVE_CTOR(ty) ty(ty &&) noexcept = default +#define C4_DEFAULT_COPY_ASSIGN(ty) ty& operator=(ty const&) noexcept = default +#define C4_DEFAULT_MOVE_ASSIGN(ty) ty& operator=(ty &&) noexcept = default + +#define C4_NO_COPY_OR_MOVE_CTOR(ty) \ + C4_NO_COPY_CTOR(ty); \ + C4_NO_MOVE_CTOR(ty) + +#define C4_NO_COPY_OR_MOVE_ASSIGN(ty) \ + C4_NO_COPY_ASSIGN(ty); \ + C4_NO_MOVE_ASSIGN(ty) + +#define C4_NO_COPY_OR_MOVE(ty) \ + C4_NO_COPY_OR_MOVE_CTOR(ty); \ + C4_NO_COPY_OR_MOVE_ASSIGN(ty) + +#define C4_DEFAULT_COPY_AND_MOVE_CTOR(ty) \ + C4_DEFAULT_COPY_CTOR(ty); \ + C4_DEFAULT_MOVE_CTOR(ty) + +#define C4_DEFAULT_COPY_AND_MOVE_ASSIGN(ty) \ + C4_DEFAULT_COPY_ASSIGN(ty); \ + C4_DEFAULT_MOVE_ASSIGN(ty) + +#define C4_DEFAULT_COPY_AND_MOVE(ty) \ + C4_DEFAULT_COPY_AND_MOVE_CTOR(ty); \ + C4_DEFAULT_COPY_AND_MOVE_ASSIGN(ty) + +/** @see https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable */ +#define C4_MUST_BE_TRIVIAL_COPY(ty) \ + static_assert(std::is_trivially_copyable::value, #ty " must be trivially copyable") + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @defgroup traits_types Type traits utilities + * @ingroup types + * @{ */ + +// http://stackoverflow.com/questions/10821380/is-t-an-instance-of-a-template-in-c +template class X, typename T> struct is_instance_of_tpl : std::false_type {}; +template class X, typename... Y> struct is_instance_of_tpl> : std::true_type {}; + +//----------------------------------------------------------------------------- + +/** SFINAE. use this macro to enable a template function overload +based on a compile-time condition. +@code +// define an overload for a non-pod type +template::value)> +void foo() { std::cout << "pod type\n"; } + +// define an overload for a non-pod type +template::value)> +void foo() { std::cout << "nonpod type\n"; } + +struct non_pod +{ + non_pod() : name("asdfkjhasdkjh") {} + const char *name; +}; + +int main() +{ + foo(); // prints "pod type" + foo(); // prints "nonpod type" +} +@endcode */ +#define C4_REQUIRE_T(cond) typename std::enable_if::type* = nullptr + +/** enable_if for a return type + * @see C4_REQUIRE_T */ +#define C4_REQUIRE_R(cond, type_) typename std::enable_if::type + +//----------------------------------------------------------------------------- +/** define a traits class reporting whether a type provides a member typedef */ +#define C4_DEFINE_HAS_TYPEDEF(member_typedef) \ +template \ +struct has_##stype \ +{ \ +private: \ + \ + typedef char yes; \ + typedef struct { char array[2]; } no; \ + \ + template \ + static yes _test(typename C::member_typedef*); \ + \ + template \ + static no _test(...); \ + \ +public: \ + \ + enum { value = (sizeof(_test(0)) == sizeof(yes)) }; \ + \ +} + + +/** @} */ + + +//----------------------------------------------------------------------------- + + +/** @defgroup type_declarations Type declaration utilities + * @ingroup types + * @{ */ + +#define _c4_DEFINE_ARRAY_TYPES_WITHOUT_ITERATOR(T, I) \ + \ + using size_type = I; \ + using ssize_type = typename std::make_signed::type; \ + using difference_type = typename std::make_signed::type; \ + \ + using value_type = T; \ + using pointer = T*; \ + using const_pointer = T const*; \ + using reference = T&; \ + using const_reference = T const& + +#define _c4_DEFINE_TUPLE_ARRAY_TYPES_WITHOUT_ITERATOR(interior_types, I) \ + \ + using size_type = I; \ + using ssize_type = typename std::make_signed::type; \ + using difference_type = typename std::make_signed::type; \ + \ + template using value_type = typename std::tuple_element< n, std::tuple>::type; \ + template using pointer = value_type*; \ + template using const_pointer = value_type const*; \ + template using reference = value_type&; \ + template using const_reference = value_type const& + + +#define _c4_DEFINE_ARRAY_TYPES(T, I) \ + \ + _c4_DEFINE_ARRAY_TYPES_WITHOUT_ITERATOR(T, I); \ + \ + using iterator = T*; \ + using const_iterator = T const*; \ + using reverse_iterator = std::reverse_iterator; \ + using const_reverse_iterator = std::reverse_iterator + + +#define _c4_DEFINE_TUPLE_ARRAY_TYPES(interior_types, I) \ + \ + _c4_DEFINE_TUPLE_ARRAY_TYPES_WITHOUT_ITERATOR(interior_types, I); \ + \ + template using iterator = value_type*; \ + template using const_iterator = value_type const*; \ + template using reverse_iterator = std::reverse_iterator< value_type*>; \ + template using const_reverse_iterator = std::reverse_iterator< value_type const*> + + + +/** @} */ + + +//----------------------------------------------------------------------------- + + +/** @defgroup compatility_utilities Backport implementation of some Modern C++ utilities + * @ingroup types + * @{ */ + +//----------------------------------------------------------------------------- +// index_sequence and friends are available only for C++14 and later. +// A C++11 implementation is provided here. +// This implementation was copied over from clang. +// see http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 + +#if __cplusplus > 201103L + +using std::integer_sequence; +using std::index_sequence; +using std::make_integer_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +/** C++11 implementation of integer sequence + * @see https://en.cppreference.com/w/cpp/utility/integer_sequence + * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */ +template +struct integer_sequence +{ + static_assert(std::is_integral<_Tp>::value, + "std::integer_sequence can only be instantiated with an integral type" ); + using value_type = _Tp; + static constexpr size_t size() noexcept { return sizeof...(_Ip); } +}; + +/** C++11 implementation of index sequence + * @see https://en.cppreference.com/w/cpp/utility/integer_sequence + * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */ +template +using index_sequence = integer_sequence; + +/** @cond DONT_DOCUMENT_THIS */ +namespace __detail { + +template +struct __repeat; + +template +struct __repeat, _Extra...> +{ + using type = integer_sequence<_Tp, + _Np..., + sizeof...(_Np) + _Np..., + 2 * sizeof...(_Np) + _Np..., + 3 * sizeof...(_Np) + _Np..., + 4 * sizeof...(_Np) + _Np..., + 5 * sizeof...(_Np) + _Np..., + 6 * sizeof...(_Np) + _Np..., + 7 * sizeof...(_Np) + _Np..., + _Extra...>; +}; + +template struct __parity; +template struct __make : __parity<_Np % 8>::template __pmake<_Np> {}; + +template<> struct __make<0> { using type = integer_sequence; }; +template<> struct __make<1> { using type = integer_sequence; }; +template<> struct __make<2> { using type = integer_sequence; }; +template<> struct __make<3> { using type = integer_sequence; }; +template<> struct __make<4> { using type = integer_sequence; }; +template<> struct __make<5> { using type = integer_sequence; }; +template<> struct __make<6> { using type = integer_sequence; }; +template<> struct __make<7> { using type = integer_sequence; }; + +template<> struct __parity<0> { template struct __pmake : __repeat::type> {}; }; +template<> struct __parity<1> { template struct __pmake : __repeat::type, _Np - 1> {}; }; +template<> struct __parity<2> { template struct __pmake : __repeat::type, _Np - 2, _Np - 1> {}; }; +template<> struct __parity<3> { template struct __pmake : __repeat::type, _Np - 3, _Np - 2, _Np - 1> {}; }; +template<> struct __parity<4> { template struct __pmake : __repeat::type, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; }; +template<> struct __parity<5> { template struct __pmake : __repeat::type, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; }; +template<> struct __parity<6> { template struct __pmake : __repeat::type, _Np - 6, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; }; +template<> struct __parity<7> { template struct __pmake : __repeat::type, _Np - 7, _Np - 6, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; }; + +template +struct __convert +{ + template struct __result; + template<_Tp ..._Np> struct __result> + { + using type = integer_sequence<_Up, _Np...>; + }; +}; + +template +struct __convert<_Tp, _Tp> +{ + template struct __result + { + using type = _Up; + }; +}; + +template +using __make_integer_sequence_unchecked = typename __detail::__convert::template __result::type>::type; + +template +struct __make_integer_sequence +{ + static_assert(std::is_integral<_Tp>::value, + "std::make_integer_sequence can only be instantiated with an integral type" ); + static_assert(0 <= _Ep, "std::make_integer_sequence input shall not be negative"); + typedef __make_integer_sequence_unchecked<_Tp, _Ep> type; +}; + +} // namespace __detail +/** @endcond */ + + +/** C++11 implementation of index sequence + * @see https://en.cppreference.com/w/cpp/utility/integer_sequence + * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */ +template +using make_integer_sequence = typename __detail::__make_integer_sequence<_Tp, _Np>::type; + +/** C++11 implementation of index sequence + * @see https://en.cppreference.com/w/cpp/utility/integer_sequence + * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */ +template +using make_index_sequence = make_integer_sequence; + +/** C++11 implementation of index sequence + * @see https://en.cppreference.com/w/cpp/utility/integer_sequence + * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */ +template +using index_sequence_for = make_index_sequence; +#endif + +/** @} */ + + +} // namespace c4 + +// NOLINTEND(bugprone-macro-parentheses) + +#endif /* _C4_TYPES_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/utf.hpp b/3rdparty/rapidyaml/include/c4/utf.hpp new file mode 100644 index 0000000000..362e3dc85b --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/utf.hpp @@ -0,0 +1,73 @@ +#ifndef C4_UTF_HPP_ +#define C4_UTF_HPP_ + +#include "c4/language.hpp" +#include "c4/substr_fwd.hpp" +#include +#include + +/** @file utf.hpp utilities for UTF and Byte Order Mark */ + +namespace c4 { + +/** @defgroup doc_utf UTF utilities + * @{ */ + + +/** skip the Byte Order Mark, or get the full string if there is Byte Order Mark. + * @see Implements the Byte Order Marks as described in https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding */ +C4CORE_EXPORT substr skip_bom(substr s); +/** skip the Byte Order Mark, or get the full string if there is Byte Order Mark + * @see Implements the Byte Order Marks as described in https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding */ +C4CORE_EXPORT csubstr skip_bom(csubstr s); + + +/** get the Byte Order Mark, or an empty string if there is no Byte Order Mark + * @see Implements the Byte Order Marks as described in https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding */ +C4CORE_EXPORT substr get_bom(substr s); +/** get the Byte Order Mark, or an empty string if there is no Byte Order Mark + * @see Implements the Byte Order Marks as described in https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding */ +C4CORE_EXPORT csubstr get_bom(csubstr s); + + +/** return the position of the first character not belonging to the + * Byte Order Mark, or 0 if there is no Byte Order Mark. + * @see Implements the Byte Order Marks as described in https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding */ +C4CORE_EXPORT size_t first_non_bom(csubstr s); + + +/** decode the given @p code_point, writing into the output string in + * @p out. + * + * @param out the output string. must have at least 4 bytes (this is + * asserted), and must not have a null string. + * + * @param code_point: must have length in ]0,8], and must not begin + * with any of `U+`,`\\x`,`\\u,`\\U`,`0` (asserted) + * + * @return the part of @p out that was written, which will always be + * at most 4 bytes. + */ +C4CORE_EXPORT substr decode_code_point(substr out, csubstr code_point); + +/** decode the given @p code point, writing into the output string @p + * buf, of size @p buflen + * + * @param buf the output string. must have at least 4 bytes (this is + * asserted), and must not be null + * + * @param buflen the length of the output string. must be at least 4 + * + * @param code: the code point must have length in ]0,8], and must not begin + * with any of `U+`,`\\x`,`\\u,`\\U`,`0` (asserted) + * + * @return the number of written characters, which will always be + * at most 4 bytes. + */ +C4CORE_EXPORT size_t decode_code_point(uint8_t *C4_RESTRICT buf, size_t buflen, uint32_t code); + +/** @} */ + +} // namespace c4 + +#endif // C4_UTF_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/windows.hpp b/3rdparty/rapidyaml/include/c4/windows.hpp new file mode 100644 index 0000000000..d94c66c55c --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/windows.hpp @@ -0,0 +1,10 @@ +#ifndef _C4_WINDOWS_HPP_ +#define _C4_WINDOWS_HPP_ + +#if defined(_WIN64) || defined(_WIN32) +#include "c4/windows_push.hpp" +#include +#include "c4/windows_pop.hpp" +#endif + +#endif /* _C4_WINDOWS_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/windows_pop.hpp b/3rdparty/rapidyaml/include/c4/windows_pop.hpp new file mode 100644 index 0000000000..e055af6fad --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/windows_pop.hpp @@ -0,0 +1,41 @@ +#ifndef _C4_WINDOWS_POP_HPP_ +#define _C4_WINDOWS_POP_HPP_ + +#if defined(_WIN64) || defined(_WIN32) + +#ifdef _c4_AMD64_ +# undef _c4_AMD64_ +# undef _AMD64_ +#endif +#ifdef _c4_X86_ +# undef _c4_X86_ +# undef _X86_ +#endif +#ifdef _c4_ARM_ +# undef _c4_ARM_ +# undef _ARM_ +#endif + +#ifdef _c4_NOMINMAX +# undef _c4_NOMINMAX +# undef NOMINMAX +#endif + +#ifdef NOGDI +# undef _c4_NOGDI +# undef NOGDI +#endif + +#ifdef VC_EXTRALEAN +# undef _c4_VC_EXTRALEAN +# undef VC_EXTRALEAN +#endif + +#ifdef WIN32_LEAN_AND_MEAN +# undef _c4_WIN32_LEAN_AND_MEAN +# undef WIN32_LEAN_AND_MEAN +#endif + +#endif /* defined(_WIN64) || defined(_WIN32) */ + +#endif /* _C4_WINDOWS_POP_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/windows_push.hpp b/3rdparty/rapidyaml/include/c4/windows_push.hpp new file mode 100644 index 0000000000..156fe2fb40 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/windows_push.hpp @@ -0,0 +1,102 @@ +#ifndef _C4_WINDOWS_PUSH_HPP_ +#define _C4_WINDOWS_PUSH_HPP_ + +/** @file windows_push.hpp sets up macros to include windows header files + * without pulling in all of + * + * @see #include windows_pop.hpp to undefine these macros + * + * @see https://aras-p.info/blog/2018/01/12/Minimizing-windows.h/ */ + + +#if defined(_WIN64) || defined(_WIN32) + +#if defined(_M_AMD64) +# ifndef _AMD64_ +# define _c4_AMD64_ +# define _AMD64_ +# endif +#elif defined(_M_IX86) +# ifndef _X86_ +# define _c4_X86_ +# define _X86_ +# endif +#elif defined(_M_ARM64) +# ifndef _ARM64_ +# define _c4_ARM64_ +# define _ARM64_ +# endif +#elif defined(_M_ARM) +# ifndef _ARM_ +# define _c4_ARM_ +# define _ARM_ +# endif +#endif + +#ifndef NOMINMAX +# define _c4_NOMINMAX +# define NOMINMAX +#endif + +#ifndef NOGDI +# define _c4_NOGDI +# define NOGDI +#endif + +#ifndef VC_EXTRALEAN +# define _c4_VC_EXTRALEAN +# define VC_EXTRALEAN +#endif + +#ifndef WIN32_LEAN_AND_MEAN +# define _c4_WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif + +/* If defined, the following flags inhibit definition + * of the indicated items. + * + * NOGDICAPMASKS - CC_*, LC_*, PC_*, CP_*, TC_*, RC_ + * NOVIRTUALKEYCODES - VK_* + * NOWINMESSAGES - WM_*, EM_*, LB_*, CB_* + * NOWINSTYLES - WS_*, CS_*, ES_*, LBS_*, SBS_*, CBS_* + * NOSYSMETRICS - SM_* + * NOMENUS - MF_* + * NOICONS - IDI_* + * NOKEYSTATES - MK_* + * NOSYSCOMMANDS - SC_* + * NORASTEROPS - Binary and Tertiary raster ops + * NOSHOWWINDOW - SW_* + * OEMRESOURCE - OEM Resource values + * NOATOM - Atom Manager routines + * NOCLIPBOARD - Clipboard routines + * NOCOLOR - Screen colors + * NOCTLMGR - Control and Dialog routines + * NODRAWTEXT - DrawText() and DT_* + * NOGDI - All GDI defines and routines + * NOKERNEL - All KERNEL defines and routines + * NOUSER - All USER defines and routines + * NONLS - All NLS defines and routines + * NOMB - MB_* and MessageBox() + * NOMEMMGR - GMEM_*, LMEM_*, GHND, LHND, associated routines + * NOMETAFILE - typedef METAFILEPICT + * NOMINMAX - Macros min(a,b) and max(a,b) + * NOMSG - typedef MSG and associated routines + * NOOPENFILE - OpenFile(), OemToAnsi, AnsiToOem, and OF_* + * NOSCROLL - SB_* and scrolling routines + * NOSERVICE - All Service Controller routines, SERVICE_ equates, etc. + * NOSOUND - Sound driver routines + * NOTEXTMETRIC - typedef TEXTMETRIC and associated routines + * NOWH - SetWindowsHook and WH_* + * NOWINOFFSETS - GWL_*, GCL_*, associated routines + * NOCOMM - COMM driver routines + * NOKANJI - Kanji support stuff. + * NOHELP - Help engine interface. + * NOPROFILER - Profiler interface. + * NODEFERWINDOWPOS - DeferWindowPos routines + * NOMCX - Modem Configuration Extensions + */ + +#endif /* defined(_WIN64) || defined(_WIN32) */ + +#endif /* _C4_WINDOWS_PUSH_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/common.hpp b/3rdparty/rapidyaml/include/c4/yml/common.hpp new file mode 100644 index 0000000000..c4afa6361e --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/common.hpp @@ -0,0 +1,660 @@ +#ifndef _C4_YML_COMMON_HPP_ +#define _C4_YML_COMMON_HPP_ + +/** @file common.hpp Common utilities and infrastructure used by ryml. */ + +#include +#include +#include +#include +#include + +#if defined(C4_MSVC) || defined(C4_MINGW) || defined(_WIN32) || defined(C4_WIN) +#include +#else +#include +#endif + + + +//----------------------------------------------------------------------------- + +#ifndef RYML_ERRMSG_SIZE +/// size for the error message buffer +#define RYML_ERRMSG_SIZE (1024) +#endif + +#ifndef RYML_LOGBUF_SIZE +/// size for the buffer used to format individual values to string +/// while preparing an error message. This is only used for formatting +/// individual values in the message; final messages will be larger +/// than this value (see @ref RYML_ERRMSG_SIZE). This is also used for +/// the detailed debug log messages when RYML_DBG is defined. +#define RYML_LOGBUF_SIZE (256) +#endif + +#ifndef RYML_LOGBUF_SIZE_MAX +/// size for the fallback larger log buffer. When @ref +/// RYML_LOGBUF_SIZE is not large enough to convert a value to string, +/// then temporary stack memory is allocated up to +/// RYML_LOGBUF_SIZE_MAX. This limit is in place to prevent a stack +/// overflow. If the printed value requires more than +/// RYML_LOGBUF_SIZE_MAX, the value is silently skipped. +#define RYML_LOGBUF_SIZE_MAX (1024) +#endif + +#ifndef RYML_LOCATIONS_SMALL_THRESHOLD +/// threshold at which a location search will revert from linear to +/// binary search. +#define RYML_LOCATIONS_SMALL_THRESHOLD (30) +#endif + + +//----------------------------------------------------------------------------- +// Specify groups to have a predefined topic order in doxygen: + +/** @defgroup doc_quickstart Quickstart + * + * Example code for every feature. + */ + +/** @defgroup doc_parse Parse utilities + * @see sample::sample_parse_in_place + * @see sample::sample_parse_in_arena + * @see sample::sample_parse_file + * @see sample::sample_parse_reuse_tree + * @see sample::sample_parse_reuse_parser + * @see sample::sample_parse_reuse_tree_and_parser + * @see sample::sample_location_tracking + */ + +/** @defgroup doc_emit Emit utilities + * + * Utilities to emit YAML and JSON, either to a memory buffer or to a + * file or ostream-like class. + * + * @see sample::sample_emit_to_container + * @see sample::sample_emit_to_stream + * @see sample::sample_emit_to_file + * @see sample::sample_emit_nested_node + * @see sample::sample_emit_style + */ + +/** @defgroup doc_node_type Node types + */ + +/** @defgroup doc_tree Tree utilities + * @see sample::sample_quick_overview + * @see sample::sample_iterate_trees + * @see sample::sample_create_trees + * @see sample::sample_tree_arena + * + * @see sample::sample_static_trees + * @see sample::sample_location_tracking + * + * @see sample::sample_docs + * @see sample::sample_anchors_and_aliases + * @see sample::sample_tags + */ + +/** @defgroup doc_node_classes Node classes + * + * High-level node classes. + * + * @see sample::sample_quick_overview + * @see sample::sample_iterate_trees + * @see sample::sample_create_trees + * @see sample::sample_tree_arena + */ + +/** @defgroup doc_callbacks Callbacks for errors and allocation + * + * Functions called by ryml to allocate/free memory and to report + * errors. + * + * @see sample::sample_error_handler + * @see sample::sample_global_allocator + * @see sample::sample_per_tree_allocator + */ + +/** @defgroup doc_serialization Serialization/deserialization + * + * Contains information on how to serialize and deserialize + * fundamental types, user scalar types, user container types and + * interop with std scalar/container types. + * + */ + +/** @defgroup doc_ref_utils Anchor/Reference utilities + * + * @see sample::sample_anchors_and_aliases + * */ + +/** @defgroup doc_tag_utils Tag utilities + * @see sample::sample_tags + */ + +/** @defgroup doc_preprocessors Preprocessors + * + * Functions for preprocessing YAML prior to parsing. + */ + + +//----------------------------------------------------------------------------- + +// document macros for doxygen +#ifdef __DOXYGEN__ // defined in Doxyfile::PREDEFINED + +/** define this macro with a boolean value to enable/disable + * assertions to check preconditions and assumptions throughout the + * codebase; this causes a slowdown of the code, and larger code + * size. By default, this macro is defined unless NDEBUG is defined + * (see C4_USE_ASSERT); as a result, by default this macro is truthy + * only in debug builds. */ +# define RYML_USE_ASSERT + +/** (Undefined by default) Define this macro to disable ryml's default + * implementation of the callback functions; see @ref c4::yml::Callbacks */ +# define RYML_NO_DEFAULT_CALLBACKS + +/** (Undefined by default) When this macro is defined (and + * @ref RYML_NO_DEFAULT_CALLBACKS is not defined), the default error + * handler will throw C++ exceptions of type `std::runtime_error`. */ +# define RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS + +/** Conditionally expands to `noexcept` when @ref RYML_USE_ASSERT is 0 and + * is empty otherwise. The user is unable to override this macro. */ +# define RYML_NOEXCEPT + +#endif + + +//----------------------------------------------------------------------------- + + +/** @cond dev*/ +#ifndef RYML_USE_ASSERT +# define RYML_USE_ASSERT C4_USE_ASSERT +#endif + +#if RYML_USE_ASSERT +# define RYML_ASSERT(cond) RYML_CHECK(cond) +# define RYML_ASSERT_MSG(cond, msg) RYML_CHECK_MSG(cond, msg) +# define _RYML_CB_ASSERT(cb, cond) _RYML_CB_CHECK((cb), (cond)) +# define _RYML_CB_ASSERT_(cb, cond, loc) _RYML_CB_CHECK((cb), (cond), (loc)) +# define RYML_NOEXCEPT +#else +# define RYML_ASSERT(cond) +# define RYML_ASSERT_MSG(cond, msg) +# define _RYML_CB_ASSERT(cb, cond) +# define _RYML_CB_ASSERT_(cb, cond, loc) +# define RYML_NOEXCEPT noexcept +#endif + +#define RYML_DEPRECATED(msg) C4_DEPRECATED(msg) + +#define RYML_CHECK(cond) \ + do { \ + if(C4_UNLIKELY(!(cond))) \ + { \ + RYML_DEBUG_BREAK(); \ + c4::yml::error("check failed: " #cond, c4::yml::Location(__FILE__, __LINE__, 0)); \ + C4_UNREACHABLE_AFTER_ERR(); \ + } \ + } while(0) + +#define RYML_CHECK_MSG(cond, msg) \ + do \ + { \ + if(C4_UNLIKELY(!(cond))) \ + { \ + RYML_DEBUG_BREAK(); \ + c4::yml::error(msg ": check failed: " #cond, c4::yml::Location(__FILE__, __LINE__, 0)); \ + C4_UNREACHABLE_AFTER_ERR(); \ + } \ + } while(0) + +#if defined(RYML_DBG) && !defined(NDEBUG) && !defined(C4_NO_DEBUG_BREAK) +# define RYML_DEBUG_BREAK() \ + do { \ + if(c4::get_error_flags() & c4::ON_ERROR_DEBUGBREAK) \ + { \ + C4_DEBUG_BREAK(); \ + } \ + } while(0) +#else +# define RYML_DEBUG_BREAK() +#endif + +/** @endcond */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +namespace c4 { +namespace yml { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + + +#ifndef RYML_ID_TYPE +/** The type of a node id in the YAML tree. In the future, the default + * will likely change to int32_t, which was observed to be faster. + * @see id_type */ +#define RYML_ID_TYPE size_t +#endif + + +/** The type of a node id in the YAML tree; to override the default + * type, define the macro @ref RYML_ID_TYPE to a suitable integer + * type. */ +using id_type = RYML_ID_TYPE; +static_assert(std::is_integral::value, "id_type must be an integer type"); + + +C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wuseless-cast") +enum : id_type { + /** an index to none */ + NONE = id_type(-1), +}; +C4_SUPPRESS_WARNING_GCC_CLANG_POP + + +enum : size_t { + /** a null string position */ + npos = size_t(-1) +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +//! holds a position into a source buffer +struct RYML_EXPORT LineCol +{ + //! number of bytes from the beginning of the source buffer + size_t offset; + //! line + size_t line; + //! column + size_t col; + + LineCol() = default; + //! construct from line and column + LineCol(size_t l, size_t c) : offset(0), line(l), col(c) {} + //! construct from offset, line and column + LineCol(size_t o, size_t l, size_t c) : offset(o), line(l), col(c) {} +}; +static_assert(std::is_trivially_copyable::value, "LineCol not trivially copyable"); +static_assert(std::is_trivially_default_constructible::value, "LineCol not trivially default constructible"); +static_assert(std::is_standard_layout::value, "Location not trivial"); + + +//! a source file position +struct RYML_EXPORT Location +{ + //! number of bytes from the beginning of the source buffer + size_t offset; + //! line + size_t line; + //! column + size_t col; + //! file name + csubstr name; + + operator bool () const { return !name.empty() || line != 0 || offset != 0 || col != 0; } + operator LineCol const& () const { return reinterpret_cast(*this); } // NOLINT + + Location() = default; + Location( size_t l, size_t c) : offset( ), line(l), col(c), name( ) {} + Location( size_t b, size_t l, size_t c) : offset(b), line(l), col(c), name( ) {} + Location( csubstr n, size_t l, size_t c) : offset( ), line(l), col(c), name(n) {} + Location( csubstr n, size_t b, size_t l, size_t c) : offset(b), line(l), col(c), name(n) {} + Location(const char *n, size_t l, size_t c) : offset( ), line(l), col(c), name(to_csubstr(n)) {} + Location(const char *n, size_t b, size_t l, size_t c) : offset(b), line(l), col(c), name(to_csubstr(n)) {} +}; +static_assert(std::is_standard_layout::value, "Location not trivial"); + + +//----------------------------------------------------------------------------- + +/** @addtogroup doc_callbacks + * + * @{ */ + +struct Callbacks; + + +/** set the global callbacks for the library; after a call to this + * function, these callbacks will be used by newly created objects + * (unless they are copying older objects with different + * callbacks). If @ref RYML_NO_DEFAULT_CALLBACKS is defined, it is + * mandatory to call this function prior to using any other library + * facility. + * + * @warning This function is NOT thread-safe. + * + * @warning the error callback must never return: see @ref pfn_error + * for more details */ +RYML_EXPORT void set_callbacks(Callbacks const& c); + +/** get the global callbacks + * @warning This function is not thread-safe. */ +RYML_EXPORT Callbacks const& get_callbacks(); + +/** set the global callbacks back to their defaults () + * @warning This function is not thread-safe. */ +RYML_EXPORT void reset_callbacks(); + + +/** the type of the function used to report errors + * + * @warning When given by the user, this function MUST interrupt + * execution, typically by either throwing an exception, or using + * `std::longjmp()` ([see + * documentation](https://en.cppreference.com/w/cpp/utility/program/setjmp)) + * or by calling `std::abort()`. If the function returned, the parser + * would enter into an infinite loop, or the program may crash. */ +using pfn_error = void (*) (const char* msg, size_t msg_len, Location location, void *user_data); + + +/** the type of the function used to allocate memory; ryml will only + * allocate memory through this callback. */ +using pfn_allocate = void* (*)(size_t len, void* hint, void *user_data); + + +/** the type of the function used to free memory; ryml will only free + * memory through this callback. */ +using pfn_free = void (*)(void* mem, size_t size, void *user_data); + + +/** a c-style callbacks class. Can be used globally by the library + * and/or locally by @ref Tree and @ref Parser objects. */ +struct RYML_EXPORT Callbacks +{ + void * m_user_data; + pfn_allocate m_allocate; + pfn_free m_free; + pfn_error m_error; + + /** Construct an object with the default callbacks. If + * @ref RYML_NO_DEFAULT_CALLBACKS is defined, the object will have null + * members.*/ + Callbacks() noexcept; + + /** Construct an object with the given callbacks. + * + * @param user_data Data to be forwarded in every call to a callback. + * + * @param alloc A pointer to an allocate function. Unless + * @ref RYML_NO_DEFAULT_CALLBACKS is defined, when this + * parameter is null, will fall back to ryml's default + * alloc implementation. + * + * @param free A pointer to a free function. Unless + * @ref RYML_NO_DEFAULT_CALLBACKS is defined, when this + * parameter is null, will fall back to ryml's default free + * implementation. + * + * @param error A pointer to an error function, which must never + * return (see @ref pfn_error). Unless + * @ref RYML_NO_DEFAULT_CALLBACKS is defined, when this + * parameter is null, will fall back to ryml's default + * error implementation. + */ + Callbacks(void *user_data, pfn_allocate alloc, pfn_free free, pfn_error error); + + bool operator!= (Callbacks const& that) const { return !operator==(that); } + bool operator== (Callbacks const& that) const + { + return (m_user_data == that.m_user_data && + m_allocate == that.m_allocate && + m_free == that.m_free && + m_error == that.m_error); + } +}; + + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +typedef enum { + NOBOM, + UTF8, + UTF16LE, + UTF16BE, + UTF32LE, + UTF32BE, +} Encoding_e; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/// @cond dev + +// BEWARE! MSVC requires that [[noreturn]] appears before RYML_EXPORT +[[noreturn]] RYML_EXPORT void error(Callbacks const& cb, const char *msg, size_t msg_len, Location loc); +[[noreturn]] RYML_EXPORT void error(const char *msg, size_t msg_len, Location loc); + +[[noreturn]] inline void error(const char *msg, size_t msg_len) +{ + error(msg, msg_len, Location{}); +} +template +[[noreturn]] inline void error(const char (&msg)[N], Location loc) +{ + error(msg, N-1, loc); +} +template +[[noreturn]] inline void error(const char (&msg)[N]) +{ + error(msg, N-1, Location{}); +} + +#define _RYML_CB_ERR(cb, msg_literal) \ + _RYML_CB_ERR_(cb, msg_literal, c4::yml::Location(__FILE__, 0, __LINE__, 0)) +#define _RYML_CB_CHECK(cb, cond) \ + _RYML_CB_CHECK_(cb, cond, c4::yml::Location(__FILE__, 0, __LINE__, 0)) +#define _RYML_CB_ERR_(cb, msg_literal, loc) \ +do \ +{ \ + const char msg[] = msg_literal; \ + RYML_DEBUG_BREAK(); \ + c4::yml::error((cb), msg, sizeof(msg)-1, loc); \ + C4_UNREACHABLE_AFTER_ERR(); \ +} while(0) +#define _RYML_CB_CHECK_(cb, cond, loc) \ + do \ + { \ + if(C4_UNLIKELY(!(cond))) \ + { \ + const char msg[] = "check failed: " #cond; \ + RYML_DEBUG_BREAK(); \ + c4::yml::error((cb), msg, sizeof(msg)-1, loc); \ + C4_UNREACHABLE_AFTER_ERR(); \ + } \ + } while(0) +#define _RYML_CB_ALLOC_HINT(cb, T, num, hint) (T*) (cb).m_allocate((num) * sizeof(T), (hint), (cb).m_user_data) +#define _RYML_CB_ALLOC(cb, T, num) _RYML_CB_ALLOC_HINT((cb), T, (num), nullptr) +#define _RYML_CB_FREE(cb, buf, T, num) \ + do { \ + (cb).m_free((buf), (num) * sizeof(T), (cb).m_user_data); \ + (buf) = nullptr; \ + } while(0) + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +typedef enum { + BLOCK_LITERAL, //!< keep newlines (|) + BLOCK_FOLD //!< replace newline with single space (>) +} BlockStyle_e; + +typedef enum { + CHOMP_CLIP, //!< single newline at end (default) + CHOMP_STRIP, //!< no newline at end (-) + CHOMP_KEEP //!< all newlines from end (+) +} BlockChomp_e; + + +/** Abstracts the fact that a scalar filter result may not fit in the + * intended memory. */ +struct FilterResult +{ + C4_ALWAYS_INLINE bool valid() const noexcept { return str.str != nullptr; } + C4_ALWAYS_INLINE size_t required_len() const noexcept { return str.len; } + C4_ALWAYS_INLINE csubstr get() const { RYML_ASSERT(valid()); return str; } + csubstr str; +}; +/** Abstracts the fact that a scalar filter result may not fit in the + * intended memory. */ +struct FilterResultExtending +{ + C4_ALWAYS_INLINE bool valid() const noexcept { return str.str != nullptr; } + C4_ALWAYS_INLINE size_t required_len() const noexcept { return reqlen; } + C4_ALWAYS_INLINE csubstr get() const { RYML_ASSERT(valid()); return str; } + csubstr str; + size_t reqlen; +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +namespace detail { +// is there a better way to do this? +template +struct _charconstant_t + : public std::conditional::value, + std::integral_constant(unsignedval)>, + std::integral_constant>::type +{}; +#define _RYML_CHCONST(signedval, unsignedval) ::c4::yml::detail::_charconstant_t::value +} // namespace detail + + +namespace detail { +struct _SubstrWriter +{ + substr buf; + size_t pos; + _SubstrWriter(substr buf_, size_t pos_=0) : buf(buf_), pos(pos_) { C4_ASSERT(buf.str); } + void append(csubstr s) + { + C4_ASSERT(!s.overlaps(buf)); + C4_ASSERT(s.str || !s.len); + if(s.len && pos + s.len <= buf.len) + { + C4_ASSERT(s.str); + memcpy(buf.str + pos, s.str, s.len); + } + pos += s.len; + } + void append(char c) + { + C4_ASSERT(buf.str); + if(pos < buf.len) + buf.str[pos] = c; + ++pos; + } + void append_n(char c, size_t numtimes) + { + C4_ASSERT(buf.str); + if(numtimes && pos + numtimes < buf.len) + memset(buf.str + pos, c, numtimes); + pos += numtimes; + } + size_t slack() const { return pos <= buf.len ? buf.len - pos : 0; } + size_t excess() const { return pos > buf.len ? pos - buf.len : 0; } + //! get the part written so far + csubstr curr() const { return pos <= buf.len ? buf.first(pos) : buf; } + //! get the part that is still free to write to (the remainder) + substr rem() const { return pos < buf.len ? buf.sub(pos) : buf.last(0); } + + size_t advance(size_t more) { pos += more; return pos; } +}; +} // namespace detail + + +namespace detail { +// dumpfn is a function abstracting prints to terminal (or to string). +template +C4_NO_INLINE void _dump(DumpFn &&dumpfn, csubstr fmt, Args&& ...args) +{ + DumpResults results; + // try writing everything: + { + // buffer for converting individual arguments. it is defined + // in a child scope to free it in case the buffer is too small + // for any of the arguments. + char writebuf[RYML_LOGBUF_SIZE]; + results = format_dump_resume(std::forward(dumpfn), writebuf, fmt, std::forward(args)...); + } + // if any of the arguments failed to fit the buffer, allocate a + // larger buffer (up to a limit) and resume writing. + // + // results.bufsize is set to the size of the largest element + // serialized. Eg int(1) will require 1 byte. + if(C4_UNLIKELY(results.bufsize > RYML_LOGBUF_SIZE)) + { + const size_t bufsize = results.bufsize <= RYML_LOGBUF_SIZE_MAX ? results.bufsize : RYML_LOGBUF_SIZE_MAX; + #ifdef C4_MSVC + substr largerbuf = {static_cast(_alloca(bufsize)), bufsize}; + #else + substr largerbuf = {static_cast(alloca(bufsize)), bufsize}; + #endif + results = format_dump_resume(std::forward(dumpfn), results, largerbuf, fmt, std::forward(args)...); + } +} +template +C4_NORETURN C4_NO_INLINE void _report_err(Callbacks const& C4_RESTRICT callbacks, csubstr fmt, Args const& C4_RESTRICT ...args) +{ + char errmsg[RYML_ERRMSG_SIZE] = {0}; + detail::_SubstrWriter writer(errmsg); + auto dumpfn = [&writer](csubstr s){ writer.append(s); }; + _dump(dumpfn, fmt, args...); + writer.append('\n'); + const size_t len = writer.pos < RYML_ERRMSG_SIZE ? writer.pos : RYML_ERRMSG_SIZE; + callbacks.m_error(errmsg, len, {}, callbacks.m_user_data); + C4_UNREACHABLE_AFTER_ERR(); +} +} // namespace detail + + +inline csubstr _c4prc(const char &C4_RESTRICT c) // pass by reference! +{ + switch(c) + { + case '\n': return csubstr("\\n"); + case '\t': return csubstr("\\t"); + case '\0': return csubstr("\\0"); + case '\r': return csubstr("\\r"); + case '\f': return csubstr("\\f"); + case '\b': return csubstr("\\b"); + case '\v': return csubstr("\\v"); + case '\a': return csubstr("\\a"); + default: return csubstr(&c, 1); + } +} + +/// @endcond + +C4_SUPPRESS_WARNING_GCC_POP + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_COMMON_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/detail/checks.hpp b/3rdparty/rapidyaml/include/c4/yml/detail/checks.hpp new file mode 100644 index 0000000000..dfc66d8a8e --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/detail/checks.hpp @@ -0,0 +1,200 @@ +#ifndef C4_YML_DETAIL_CHECKS_HPP_ +#define C4_YML_DETAIL_CHECKS_HPP_ + +#include "c4/yml/tree.hpp" + +#ifdef __clang__ +# pragma clang diagnostic push +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtype-limits" // error: comparison of unsigned expression >= 0 is always true +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable: 4296/*expression is always 'boolean_value'*/) +#endif + +namespace c4 { +namespace yml { + + +void check_invariants(Tree const& t, id_type node=NONE); +void check_free_list(Tree const& t); +void check_arena(Tree const& t); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +inline void check_invariants(Tree const& t, id_type node) +{ + if(node == NONE) + { + if(t.empty()) return; + node = t.root_id(); + } + + NodeData const& n = *t._p(node); +#if defined(RYML_DBG) && 0 + if(n.m_first_child != NONE || n.m_last_child != NONE) + { + printf("check(%zu): fc=%zu lc=%zu\n", node, n.m_first_child, n.m_last_child); + } + else + { + printf("check(%zu)\n", node); + } +#endif + + C4_CHECK(n.m_parent != node); + if(n.m_parent == NONE) + { + C4_CHECK(t.is_root(node)); + } + else //if(n.m_parent != NONE) + { + C4_CHECK(t.has_child(n.m_parent, node)); + + auto const& p = *t._p(n.m_parent); + if(n.m_prev_sibling == NONE) + { + C4_CHECK(p.m_first_child == node); + C4_CHECK(t.first_sibling(node) == node); + } + else + { + C4_CHECK(p.m_first_child != node); + C4_CHECK(t.first_sibling(node) != node); + } + + if(n.m_next_sibling == NONE) + { + C4_CHECK(p.m_last_child == node); + C4_CHECK(t.last_sibling(node) == node); + } + else + { + C4_CHECK(p.m_last_child != node); + C4_CHECK(t.last_sibling(node) != node); + } + } + + C4_CHECK(n.m_first_child != node); + C4_CHECK(n.m_last_child != node); + if(n.m_first_child != NONE || n.m_last_child != NONE) + { + C4_CHECK(n.m_first_child != NONE); + C4_CHECK(n.m_last_child != NONE); + } + + C4_CHECK(n.m_prev_sibling != node); + C4_CHECK(n.m_next_sibling != node); + if(n.m_prev_sibling != NONE) + { + C4_CHECK(t._p(n.m_prev_sibling)->m_next_sibling == node); + C4_CHECK(t._p(n.m_prev_sibling)->m_prev_sibling != node); + } + if(n.m_next_sibling != NONE) + { + C4_CHECK(t._p(n.m_next_sibling)->m_prev_sibling == node); + C4_CHECK(t._p(n.m_next_sibling)->m_next_sibling != node); + } + + id_type count = 0; + for(id_type i = n.m_first_child; i != NONE; i = t.next_sibling(i)) + { +#if defined(RYML_DBG) && 0 + printf("check(%zu): descend to child[%zu]=%zu\n", node, count, i); +#endif + auto const& ch = *t._p(i); + C4_CHECK(ch.m_parent == node); + C4_CHECK(ch.m_next_sibling != i); + ++count; + } + C4_CHECK(count == t.num_children(node)); + + if(n.m_prev_sibling == NONE && n.m_next_sibling == NONE) + { + if(n.m_parent != NONE) + { + C4_CHECK(t.num_children(n.m_parent) == 1); + C4_CHECK(t.num_siblings(node) == 1); + } + } + + if(node == t.root_id()) + { + C4_CHECK(t.size() == t.m_size); + C4_CHECK(t.capacity() == t.m_cap); + C4_CHECK(t.m_cap == t.m_size + t.slack()); + check_free_list(t); + check_arena(t); + } + + for(id_type i = t.first_child(node); i != NONE; i = t.next_sibling(i)) + { + check_invariants(t, i); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +inline void check_free_list(Tree const& t) +{ + if(t.m_free_head == NONE) + { + C4_CHECK(t.m_free_tail == t.m_free_head); + return; + } + + C4_CHECK(t.m_free_head >= 0 && t.m_free_head < t.m_cap); + C4_CHECK(t.m_free_tail >= 0 && t.m_free_tail < t.m_cap); + + auto const& head = *t._p(t.m_free_head); + //auto const& tail = *t._p(t.m_free_tail); + + //C4_CHECK(head.m_prev_sibling == NONE); + //C4_CHECK(tail.m_next_sibling == NONE); + + id_type count = 0; + for(id_type i = t.m_free_head, prev = NONE; i != NONE; i = t._p(i)->m_next_sibling) + { + auto const& elm = *t._p(i); + if(&elm != &head) + { + C4_CHECK(elm.m_prev_sibling == prev); + } + prev = i; + ++count; + } + C4_CHECK(count == t.slack()); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +inline void check_arena(Tree const& t) +{ + C4_CHECK(t.m_arena.len == 0 || (t.m_arena_pos >= 0 && t.m_arena_pos <= t.m_arena.len)); + C4_CHECK(t.arena_size() == t.m_arena_pos); + C4_CHECK(t.arena_slack() + t.m_arena_pos == t.m_arena.len); +} + + +} /* namespace yml */ +} /* namespace c4 */ + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#endif /* C4_YML_DETAIL_CHECKS_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/detail/dbgprint.hpp b/3rdparty/rapidyaml/include/c4/yml/detail/dbgprint.hpp new file mode 100644 index 0000000000..3423787a2b --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/detail/dbgprint.hpp @@ -0,0 +1,129 @@ +#ifndef _C4_YML_DETAIL_DBGPRINT_HPP_ +#define _C4_YML_DETAIL_DBGPRINT_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "../common.hpp" +#endif + +#ifdef RYML_DBG +#include +#endif + + +//----------------------------------------------------------------------------- +// debug prints + +#ifndef RYML_DBG +# define _c4dbgt(fmt, ...) +# define _c4dbgpf(fmt, ...) +# define _c4dbgpf_(fmt, ...) +# define _c4dbgp(msg) +# define _c4dbgp_(msg) +# define _c4dbgq(msg) +# define _c4presc(...) +# define _c4prscalar(msg, scalar, keep_newlines) +#else +# define _c4dbgt(fmt, ...) do { if(_dbg_enabled()) { \ + this->_dbg ("{}:{}: " fmt , __FILE__, __LINE__, __VA_ARGS__); } } while(0) +# define _c4dbgpf(fmt, ...) _dbg_printf("{}:{}: " fmt "\n", __FILE__, __LINE__, __VA_ARGS__) +# define _c4dbgpf_(fmt, ...) _dbg_printf("{}:{}: " fmt , __FILE__, __LINE__, __VA_ARGS__) +# define _c4dbgp(msg) _dbg_printf("{}:{}: " msg "\n", __FILE__, __LINE__ ) +# define _c4dbgp_(msg) _dbg_printf("{}:{}: " msg , __FILE__, __LINE__ ) +# define _c4dbgq(msg) _dbg_printf(msg "\n") +# define _c4presc(...) do { if(_dbg_enabled()) __c4presc(__VA_ARGS__); } while(0) +# define _c4prscalar(msg, scalar, keep_newlines) \ + do { \ + _c4dbgpf_("{}: [{}]~~~", msg, scalar.len); \ + if(_dbg_enabled()) { \ + __c4presc((scalar).str, (scalar).len, (keep_newlines)); \ + } \ + _c4dbgq("~~~"); \ + } while(0) +#endif // RYML_DBG + + +//----------------------------------------------------------------------------- + +#ifdef RYML_DBG + +#include +namespace c4 { +inline bool& _dbg_enabled() { static bool enabled = true; return enabled; } +inline void _dbg_set_enabled(bool yes) { _dbg_enabled() = yes; } +inline void _dbg_dumper(csubstr s) +{ + if(s.str) + fwrite(s.str, 1, s.len, stdout); +} +inline substr _dbg_buf() noexcept +{ + static char writebuf[2048]; + return substr{writebuf, sizeof(writebuf)}; // g++-5 has trouble with return writebuf; +} +template +C4_NO_INLINE void _dbg_printf(c4::csubstr fmt, Args const& ...args) +{ + if(_dbg_enabled()) + { + substr buf = _dbg_buf(); + const size_t needed_size = c4::format_dump(&_dbg_dumper, buf, fmt, args...); + C4_CHECK(needed_size <= buf.len); + } +} +inline C4_NO_INLINE void __c4presc(const char *s, size_t len, bool keep_newlines=false) +{ + RYML_ASSERT(s || !len); + size_t prev = 0; + for(size_t i = 0; i < len; ++i) + { + switch(s[i]) + { + case '\n' : _dbg_printf("{}{}{}", csubstr(s+prev, i-prev), csubstr("\\n"), csubstr(keep_newlines ? "\n":"")); prev = i+1; break; + case '\t' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\t")); prev = i+1; break; + case '\0' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\0")); prev = i+1; break; + case '\r' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\r")); prev = i+1; break; + case '\f' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\f")); prev = i+1; break; + case '\b' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\b")); prev = i+1; break; + case '\v' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\v")); prev = i+1; break; + case '\a' : _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\a")); prev = i+1; break; + case '\x1b': _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\x1b")); prev = i+1; break; + case -0x3e/*0xc2u*/: + if(i+1 < len) + { + if(s[i+1] == -0x60/*0xa0u*/) + { + _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\_")); prev = i+1; + } + else if(s[i+1] == -0x7b/*0x85u*/) + { + _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\N")); prev = i+1; + } + } + break; + case -0x1e/*0xe2u*/: + if(i+2 < len && s[i+1] == -0x80/*0x80u*/) + { + if(s[i+2] == -0x58/*0xa8u*/) + { + _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\L")); prev = i+1; + } + else if(s[i+2] == -0x57/*0xa9u*/) + { + _dbg_printf("{}{}", csubstr(s+prev, i-prev), csubstr("\\P")); prev = i+1; + } + } + break; + } + } + if(len > prev) + _dbg_printf("{}", csubstr(s+prev, len-prev)); +} +inline void __c4presc(csubstr s, bool keep_newlines=false) +{ + __c4presc(s.str, s.len, keep_newlines); +} +} // namespace c4 + +#endif // RYML_DBG + +#endif /* _C4_YML_DETAIL_DBGPRINT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/detail/print.hpp b/3rdparty/rapidyaml/include/c4/yml/detail/print.hpp new file mode 100644 index 0000000000..dd10964ae2 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/detail/print.hpp @@ -0,0 +1,184 @@ +#ifndef C4_YML_DETAIL_PRINT_HPP_ +#define C4_YML_DETAIL_PRINT_HPP_ + +#include "c4/yml/tree.hpp" +#include "c4/yml/node.hpp" + +#ifdef RYML_DBG +#define _c4dbg_tree(...) print_tree(__VA_ARGS__) +#define _c4dbg_node(...) print_tree(__VA_ARGS__) +#else +#define _c4dbg_tree(...) +#define _c4dbg_node(...) +#endif + +namespace c4 { +namespace yml { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") +C4_SUPPRESS_WARNING_GCC("-Wuseless-cast") + +inline const char* _container_style_code(Tree const& p, id_type node) +{ + if(p.is_container(node)) + { + if(p._p(node)->m_type & (FLOW_SL|FLOW_ML)) + { + return "[FLOW]"; + } + if(p._p(node)->m_type & (BLOCK)) + { + return "[BLCK]"; + } + } + return ""; +} +inline char _scalar_code(NodeType masked) +{ + if(masked & (KEY_LITERAL|VAL_LITERAL)) + return '|'; + if(masked & (KEY_FOLDED|VAL_FOLDED)) + return '>'; + if(masked & (KEY_SQUO|VAL_SQUO)) + return '\''; + if(masked & (KEY_DQUO|VAL_DQUO)) + return '"'; + if(masked & (KEY_PLAIN|VAL_PLAIN)) + return '~'; + return '@'; +} +inline char _scalar_code_key(NodeType t) +{ + return _scalar_code(t & KEY_STYLE); +} +inline char _scalar_code_val(NodeType t) +{ + return _scalar_code(t & VAL_STYLE); +} +inline char _scalar_code_key(Tree const& p, id_type node) +{ + return _scalar_code_key(p._p(node)->m_type); +} +inline char _scalar_code_val(Tree const& p, id_type node) +{ + return _scalar_code_key(p._p(node)->m_type); +} +inline id_type print_node(Tree const& p, id_type node, int level, id_type count, bool print_children) +{ + printf("[%zu]%*s[%zu] %p", (size_t)count, (2*level), "", (size_t)node, (void const*)p.get(node)); + if(p.is_root(node)) + { + printf(" [ROOT]"); + } + char typebuf[128]; + csubstr typestr = p.type(node).type_str(typebuf); + RYML_CHECK(typestr.str); + printf(" %.*s", (int)typestr.len, typestr.str); + if(p.has_key(node)) + { + if(p.has_key_anchor(node)) + { + csubstr ka = p.key_anchor(node); + printf(" &%.*s", (int)ka.len, ka.str); + } + if(p.has_key_tag(node)) + { + csubstr kt = p.key_tag(node); + printf(" <%.*s>", (int)kt.len, kt.str); + } + const char code = _scalar_code_key(p, node); + csubstr k = p.key(node); + printf(" %c%.*s%c :", code, (int)k.len, k.str, code); + } + if(p.has_val_anchor(node)) + { + csubstr a = p.val_anchor(node); + printf(" &%.*s'", (int)a.len, a.str); + } + if(p.has_val_tag(node)) + { + csubstr vt = p.val_tag(node); + printf(" <%.*s>", (int)vt.len, vt.str); + } + if(p.has_val(node)) + { + const char code = _scalar_code_val(p, node); + csubstr v = p.val(node); + printf(" %c%.*s%c", code, (int)v.len, v.str, code); + } + printf(" (%zu sibs)", (size_t)p.num_siblings(node)); + + ++count; + + if(!p.is_container(node)) + { + printf("\n"); + } + else + { + printf(" (%zu children)\n", (size_t)p.num_children(node)); + if(print_children) + { + for(id_type i = p.first_child(node); i != NONE; i = p.next_sibling(i)) + { + count = print_node(p, i, level+1, count, print_children); + } + } + } + + return count; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +inline void print_node(ConstNodeRef const& p, int level=0) +{ + print_node(*p.tree(), p.id(), level, 0, true); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +inline id_type print_tree(const char *message, Tree const& p, id_type node=NONE) +{ + printf("--------------------------------------\n"); + if(message != nullptr) + printf("%s:\n", message); + id_type ret = 0; + if(!p.empty()) + { + if(node == NONE) + node = p.root_id(); + ret = print_node(p, node, 0, 0, true); + } + printf("#nodes=%zu vs #printed=%zu\n", (size_t)p.size(), (size_t)ret); + printf("--------------------------------------\n"); + return ret; +} + +inline id_type print_tree(Tree const& p, id_type node=NONE) +{ + return print_tree(nullptr, p, node); +} + +inline void print_tree(ConstNodeRef const& p, int level) +{ + print_node(p, level); + for(ConstNodeRef ch : p.children()) + { + print_tree(ch, level+1); + } +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} /* namespace yml */ +} /* namespace c4 */ + + +#endif /* C4_YML_DETAIL_PRINT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/detail/stack.hpp b/3rdparty/rapidyaml/include/c4/yml/detail/stack.hpp new file mode 100644 index 0000000000..1ae9f57bc4 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/detail/stack.hpp @@ -0,0 +1,291 @@ +#ifndef _C4_YML_DETAIL_STACK_HPP_ +#define _C4_YML_DETAIL_STACK_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "../common.hpp" +#endif + +#ifdef RYML_DBG +# include +#endif + +#include + +namespace c4 { +namespace yml { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + +namespace detail { + +/** A lightweight contiguous stack with Small Storage + * Optimization. This is required because std::vector can throw + * exceptions, and we don't want to enforce any particular error + * mechanism. */ +template +class stack +{ + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + static_assert(std::is_trivially_destructible::value, "T must be trivially destructible"); + +public: + + enum : id_type { sso_size = N }; + +public: + + T m_buf[size_t(N)]; + T *C4_RESTRICT m_stack; + id_type m_size; + id_type m_capacity; + Callbacks m_callbacks; + +public: + + constexpr static bool is_contiguous() { return true; } + + stack(Callbacks const& cb) + : m_buf() + , m_stack(m_buf) + , m_size(0) + , m_capacity(N) + , m_callbacks(cb) {} + stack() : stack(get_callbacks()) {} + ~stack() + { + _free(); + } + + stack(stack const& that) RYML_NOEXCEPT : stack(that.m_callbacks) + { + resize(that.m_size); + _cp(&that); + } + + stack(stack &&that) noexcept : stack(that.m_callbacks) + { + _mv(&that); + } + + stack& operator= (stack const& that) RYML_NOEXCEPT + { + if(&that != this) + { + _cb(that.m_callbacks); + resize(that.m_size); + _cp(&that); + } + return *this; + } + + stack& operator= (stack &&that) noexcept + { + _cb(that.m_callbacks); + _mv(&that); + return *this; + } + +public: + + id_type size() const { return m_size; } + id_type empty() const { return m_size == 0; } + id_type capacity() const { return m_capacity; } + + void clear() + { + m_size = 0; + } + + void resize(id_type sz) + { + reserve(sz); + m_size = sz; + } + + void reserve(id_type sz); + + void push(T const& C4_RESTRICT n) + { + _RYML_CB_ASSERT(m_callbacks, (const char*)&n + sizeof(T) < (const char*)m_stack || &n > m_stack + m_capacity); + if(m_size == m_capacity) + { + id_type cap = m_capacity == 0 ? N : 2 * m_capacity; + reserve(cap); + } + m_stack[m_size] = n; + ++m_size; + } + + void push_top() + { + _RYML_CB_ASSERT(m_callbacks, m_size > 0); + if(m_size == m_capacity) + { + id_type cap = m_capacity == 0 ? N : 2 * m_capacity; + reserve(cap); + } + m_stack[m_size] = m_stack[m_size - 1]; + ++m_size; + } + + T const& C4_RESTRICT pop() + { + _RYML_CB_ASSERT(m_callbacks, m_size > 0); + --m_size; + return m_stack[m_size]; + } + + C4_ALWAYS_INLINE T const& C4_RESTRICT top() const { _RYML_CB_ASSERT(m_callbacks, m_size > 0); return m_stack[m_size - 1]; } + C4_ALWAYS_INLINE T & C4_RESTRICT top() { _RYML_CB_ASSERT(m_callbacks, m_size > 0); return m_stack[m_size - 1]; } + + C4_ALWAYS_INLINE T const& C4_RESTRICT bottom() const { _RYML_CB_ASSERT(m_callbacks, m_size > 0); return m_stack[0]; } + C4_ALWAYS_INLINE T & C4_RESTRICT bottom() { _RYML_CB_ASSERT(m_callbacks, m_size > 0); return m_stack[0]; } + + C4_ALWAYS_INLINE T const& C4_RESTRICT top(id_type i) const { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[m_size - 1 - i]; } + C4_ALWAYS_INLINE T & C4_RESTRICT top(id_type i) { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[m_size - 1 - i]; } + + C4_ALWAYS_INLINE T const& C4_RESTRICT bottom(id_type i) const { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[i]; } + C4_ALWAYS_INLINE T & C4_RESTRICT bottom(id_type i) { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[i]; } + + C4_ALWAYS_INLINE T const& C4_RESTRICT operator[](id_type i) const { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[i]; } + C4_ALWAYS_INLINE T & C4_RESTRICT operator[](id_type i) { _RYML_CB_ASSERT(m_callbacks, i < m_size); return m_stack[i]; } + +public: + + using iterator = T *; + using const_iterator = T const *; + + iterator begin() { return m_stack; } + iterator end () { return m_stack + m_size; } + + const_iterator begin() const { return (const_iterator)m_stack; } + const_iterator end () const { return (const_iterator)m_stack + m_size; } + +public: + + void _free(); + void _cp(stack const* C4_RESTRICT that); + void _mv(stack * that); + void _cb(Callbacks const& cb); + +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +void stack::reserve(id_type sz) +{ + if(sz <= m_size) + return; + if(sz <= N) + { + m_stack = m_buf; + m_capacity = N; + return; + } + T *buf = (T*) m_callbacks.m_allocate((size_t)sz * sizeof(T), m_stack, m_callbacks.m_user_data); + _RYML_CB_ASSERT(m_callbacks, ((uintptr_t)buf % alignof(T)) == 0u); + memcpy(buf, m_stack, (size_t)m_size * sizeof(T)); + if(m_stack != m_buf) + { + m_callbacks.m_free(m_stack, (size_t)m_capacity * sizeof(T), m_callbacks.m_user_data); + } + m_stack = buf; + m_capacity = sz; +} + + +//----------------------------------------------------------------------------- + +template +void stack::_free() +{ + _RYML_CB_ASSERT(m_callbacks, m_stack != nullptr); // this structure cannot be memset() to zero + if(m_stack != m_buf) + { + m_callbacks.m_free(m_stack, (size_t)m_capacity * sizeof(T), m_callbacks.m_user_data); + m_stack = m_buf; + m_size = N; + m_capacity = N; + } + else + { + _RYML_CB_ASSERT(m_callbacks, m_capacity == N); + } +} + + +//----------------------------------------------------------------------------- + +template +void stack::_cp(stack const* C4_RESTRICT that) +{ + if(that->m_stack != that->m_buf) + { + _RYML_CB_ASSERT(m_callbacks, that->m_capacity > N); + _RYML_CB_ASSERT(m_callbacks, that->m_size <= that->m_capacity); + } + else + { + _RYML_CB_ASSERT(m_callbacks, that->m_capacity <= N); + _RYML_CB_ASSERT(m_callbacks, that->m_size <= that->m_capacity); + } + memcpy(m_stack, that->m_stack, that->m_size * sizeof(T)); + m_size = that->m_size; + m_capacity = that->m_size < N ? N : that->m_size; + m_callbacks = that->m_callbacks; +} + + +//----------------------------------------------------------------------------- + +template +void stack::_mv(stack * that) +{ + if(that->m_stack != that->m_buf) + { + _RYML_CB_ASSERT(m_callbacks, that->m_capacity > N); + _RYML_CB_ASSERT(m_callbacks, that->m_size <= that->m_capacity); + m_stack = that->m_stack; + } + else + { + _RYML_CB_ASSERT(m_callbacks, that->m_capacity <= N); + _RYML_CB_ASSERT(m_callbacks, that->m_size <= that->m_capacity); + memcpy(m_buf, that->m_buf, that->m_size * sizeof(T)); + m_stack = m_buf; + } + m_size = that->m_size; + m_capacity = that->m_capacity; + m_callbacks = that->m_callbacks; + // make sure no deallocation happens on destruction + _RYML_CB_ASSERT(m_callbacks, that->m_stack != m_buf); + that->m_stack = that->m_buf; + that->m_capacity = N; + that->m_size = 0; +} + + +//----------------------------------------------------------------------------- + +template +void stack::_cb(Callbacks const& cb) +{ + if(cb != m_callbacks) + { + _free(); + m_callbacks = cb; + } +} + +} // namespace detail + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_DETAIL_STACK_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/emit.def.hpp b/3rdparty/rapidyaml/include/c4/yml/emit.def.hpp new file mode 100644 index 0000000000..7257e82b34 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/emit.def.hpp @@ -0,0 +1,1084 @@ +#ifndef _C4_YML_EMIT_DEF_HPP_ +#define _C4_YML_EMIT_DEF_HPP_ + +#ifndef _C4_YML_EMIT_HPP_ +#include "c4/yml/emit.hpp" +#endif + +/** @file emit.def.hpp Definitions for emit functions. */ +#ifndef _C4_YML_DETAIL_DBGPRINT_HPP_ +#include "c4/yml/detail/dbgprint.hpp" +#endif + +namespace c4 { +namespace yml { + +template +substr Emitter::emit_as(EmitType_e type, Tree const& t, id_type id, bool error_on_excess) +{ + if(t.empty()) + { + _RYML_CB_ASSERT(t.callbacks(), id == NONE); + return {}; + } + if(id == NONE) + id = t.root_id(); + _RYML_CB_CHECK(t.callbacks(), id < t.capacity()); + m_tree = &t; + m_flow = false; + if(type == EMIT_YAML) + _emit_yaml(id); + else if(type == EMIT_JSON) + _do_visit_json(id, 0); + else + _RYML_CB_ERR(m_tree->callbacks(), "unknown emit type"); + m_tree = nullptr; + return this->Writer::_get(error_on_excess); +} + + +//----------------------------------------------------------------------------- + +template +void Emitter::_emit_yaml(id_type id) +{ + // save branches in the visitor by doing the initial stream/doc + // logic here, sparing the need to check stream/val/keyval inside + // the visitor functions + auto dispatch = [this](id_type node){ + NodeType ty = m_tree->type(node); + if(ty.is_flow_sl()) + _do_visit_flow_sl(node, 0); + else if(ty.is_flow_ml()) + _do_visit_flow_ml(node, 0); + else + { + _do_visit_block(node, 0); + } + }; + if(!m_tree->is_root(id)) + { + if(m_tree->is_container(id) && !m_tree->type(id).is_flow()) + { + id_type ilevel = 0; + if(m_tree->has_key(id)) + { + this->Writer::_do_write(m_tree->key(id)); + this->Writer::_do_write(":\n"); + ++ilevel; + } + _do_visit_block_container(id, 0, ilevel, ilevel); + return; + } + } + + TagDirectiveRange tagds = m_tree->tag_directives(); + auto write_tag_directives = [&tagds, this](const id_type next_node){ + TagDirective const* C4_RESTRICT end = tagds.b; + while(end < tagds.e) + { + if(end->next_node_id > next_node) + break; + ++end; + } + const id_type parent = m_tree->parent(next_node); + for( ; tagds.b != end; ++tagds.b) + { + if(next_node != m_tree->first_child(parent)) + this->Writer::_do_write("...\n"); + this->Writer::_do_write("%TAG "); + this->Writer::_do_write(tagds.b->handle); + this->Writer::_do_write(' '); + this->Writer::_do_write(tagds.b->prefix); + this->Writer::_do_write('\n'); + } + }; + if(m_tree->is_stream(id)) + { + const id_type first_child = m_tree->first_child(id); + if(first_child != NONE) + write_tag_directives(first_child); + for(id_type child = first_child; child != NONE; child = m_tree->next_sibling(child)) + { + dispatch(child); + if(m_tree->is_doc(child) && m_tree->type(child).is_flow_sl()) + this->Writer::_do_write('\n'); + if(m_tree->next_sibling(child) != NONE) + write_tag_directives(m_tree->next_sibling(child)); + } + } + else if(m_tree->is_container(id)) + { + dispatch(id); + } + else if(m_tree->is_doc(id)) + { + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->is_container(id)); // checked above + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_val(id)); // so it must be a val + _write_doc(id); + } + else if(m_tree->is_keyval(id)) + { + _writek(id, 0); + this->Writer::_do_write(": "); + _writev(id, 0); + if(!m_tree->type(id).is_flow()) + this->Writer::_do_write('\n'); + } + else if(m_tree->is_val(id)) + { + //this->Writer::_do_write("- "); + _writev(id, 0); + if(!m_tree->type(id).is_flow()) + this->Writer::_do_write('\n'); + } + else if(m_tree->type(id) == NOTYPE) + { + ; + } + else + { + _RYML_CB_ERR(m_tree->callbacks(), "unknown type"); + } +} + +#define _rymlindent_nextline() this->_indent(ilevel + 1); + +template +void Emitter::_write_doc(id_type id) +{ + const NodeType ty = m_tree->type(id); + RYML_ASSERT(ty.is_doc()); + RYML_ASSERT(!ty.has_key()); + if(!m_tree->is_root(id)) + { + RYML_ASSERT(m_tree->is_stream(m_tree->parent(id))); + this->Writer::_do_write("---"); + } + // + if(!ty.has_val()) // this is more frequent + { + const bool tag = ty.has_val_tag(); + const bool anchor = ty.has_val_anchor(); + if(!tag && !anchor) + { + ; + } + else if(!tag && anchor) + { + if(!m_tree->is_root(id)) + this->Writer::_do_write(' '); + this->Writer::_do_write('&'); + this->Writer::_do_write(m_tree->val_anchor(id)); + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + if(m_tree->has_children(id) && m_tree->is_root(id)) + this->Writer::_do_write('\n'); + #endif + } + else if(tag && !anchor) + { + if(!m_tree->is_root(id)) + this->Writer::_do_write(' '); + _write_tag(m_tree->val_tag(id)); + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + if(m_tree->has_children(id) && m_tree->is_root(id)) + this->Writer::_do_write('\n'); + #endif + } + else // tag && anchor + { + if(!m_tree->is_root(id)) + this->Writer::_do_write(' '); + _write_tag(m_tree->val_tag(id)); + this->Writer::_do_write(" &"); + this->Writer::_do_write(m_tree->val_anchor(id)); + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + if(m_tree->has_children(id) && m_tree->is_root(id)) + this->Writer::_do_write('\n'); + #endif + } + } + else // docval + { + _RYML_CB_ASSERT(m_tree->callbacks(), ty.has_val()); + // some plain scalars such as '...' and '---' must not + // appear at 0-indentation + const csubstr val = m_tree->val(id); + const bool preceded_by_3_dashes = !m_tree->is_root(id); + const type_bits style_marks = ty & VAL_STYLE; + const bool is_plain = ty.is_val_plain(); + const bool is_ambiguous = (is_plain || !style_marks) + && ((val.begins_with("...") || val.begins_with("---")) + || + (val.find('\n') != npos)); + if(preceded_by_3_dashes) + { + if(is_plain && val.len == 0 && !ty.has_val_anchor() && !ty.has_val_tag()) + { + this->Writer::_do_write('\n'); + return; + } + else if(val.len && is_ambiguous) + { + this->Writer::_do_write('\n'); + } + else + { + this->Writer::_do_write(' '); + } + } + id_type ilevel = 0u; + if(is_ambiguous) + { + _rymlindent_nextline(); + ++ilevel; + } + _writev(id, ilevel); + if(val.len && m_tree->is_root(id)) + this->Writer::_do_write('\n'); + } + if(!m_tree->is_root(id)) + this->Writer::_do_write('\n'); +} + +template +void Emitter::_do_visit_flow_sl(id_type node, id_type depth, id_type ilevel) +{ + const bool prev_flow = m_flow; + m_flow = true; + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->is_stream(node)); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(node) || m_tree->is_doc(node)); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_root(node) || (m_tree->parent_is_map(node) || m_tree->parent_is_seq(node))); + if(C4_UNLIKELY(depth > m_opts.max_depth())) + _RYML_CB_ERR(m_tree->callbacks(), "max depth exceeded"); + + if(m_tree->is_doc(node)) + { + _write_doc(node); + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + if(!m_tree->has_children(node)) + return; + else + #endif + { + if(m_tree->is_map(node)) + { + this->Writer::_do_write('{'); + } + else + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_seq(node)); + this->Writer::_do_write('['); + } + } + } + else if(m_tree->is_container(node)) + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_map(node) || m_tree->is_seq(node)); + + bool spc = false; // write a space + + if(m_tree->has_key(node)) + { + _writek(node, ilevel); + this->Writer::_do_write(':'); + spc = true; + } + + if(m_tree->has_val_tag(node)) + { + if(spc) + this->Writer::_do_write(' '); + _write_tag(m_tree->val_tag(node)); + spc = true; + } + + if(m_tree->has_val_anchor(node)) + { + if(spc) + this->Writer::_do_write(' '); + this->Writer::_do_write('&'); + this->Writer::_do_write(m_tree->val_anchor(node)); + spc = true; + } + + if(spc) + this->Writer::_do_write(' '); + + if(m_tree->is_map(node)) + { + this->Writer::_do_write('{'); + } + else + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_seq(node)); + this->Writer::_do_write('['); + } + } // container + + for(id_type child = m_tree->first_child(node), count = 0; child != NONE; child = m_tree->next_sibling(child)) + { + if(count++) + this->Writer::_do_write(','); + if(m_tree->is_keyval(child)) + { + _writek(child, ilevel); + this->Writer::_do_write(": "); + _writev(child, ilevel); + } + else if(m_tree->is_val(child)) + { + _writev(child, ilevel); + } + else + { + // with single-line flow, we can never go back to block + _do_visit_flow_sl(child, depth + 1, ilevel + 1); + } + } + + if(m_tree->is_map(node)) + { + this->Writer::_do_write('}'); + } + else if(m_tree->is_seq(node)) + { + this->Writer::_do_write(']'); + } + m_flow = prev_flow; +} + +C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4702) // unreachable error, triggered by flow_ml not implemented + +template +void Emitter::_do_visit_flow_ml(id_type id, id_type depth, id_type ilevel, id_type do_indent) +{ + C4_UNUSED(id); + C4_UNUSED(depth); + C4_UNUSED(ilevel); + C4_UNUSED(do_indent); + c4::yml::error("not implemented"); + #ifdef THIS_IS_A_WORK_IN_PROGRESS + if(C4_UNLIKELY(depth > m_opts.max_depth())) + _RYML_CB_ERR(m_tree->callbacks(), "max depth exceeded"); + const bool prev_flow = m_flow; + m_flow = true; + // do it... + m_flow = prev_flow; + #endif +} + +template +void Emitter::_do_visit_block_container(id_type node, id_type depth, id_type level, bool do_indent) +{ + if(m_tree->is_seq(node)) + { + for(id_type child = m_tree->first_child(node); child != NONE; child = m_tree->next_sibling(child)) + { + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->has_key(child)); + if(m_tree->is_val(child)) + { + _indent(level, do_indent); + this->Writer::_do_write("- "); + _writev(child, level); + this->Writer::_do_write('\n'); + } + else + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(child)); + NodeType ty = m_tree->type(child); + if(ty.is_flow_sl()) + { + _indent(level, do_indent); + this->Writer::_do_write("- "); + _do_visit_flow_sl(child, depth+1, 0u); + this->Writer::_do_write('\n'); + } + else if(ty.is_flow_ml()) + { + _indent(level, do_indent); + this->Writer::_do_write("- "); + _do_visit_flow_ml(child, depth+1, 0u, do_indent); + this->Writer::_do_write('\n'); + } + else + { + _do_visit_block(child, depth+1, level, do_indent); // same indentation level + } + } + do_indent = true; + } + } + else // map + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_map(node)); + for(id_type ich = m_tree->first_child(node); ich != NONE; ich = m_tree->next_sibling(ich)) + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->has_key(ich)); + if(m_tree->is_keyval(ich)) + { + _indent(level, do_indent); + _writek(ich, level); + this->Writer::_do_write(": "); + _writev(ich, level); + this->Writer::_do_write('\n'); + } + else + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(ich)); + NodeType ty = m_tree->type(ich); + if(ty.is_flow_sl()) + { + _indent(level, do_indent); + _do_visit_flow_sl(ich, depth+1, 0u); + this->Writer::_do_write('\n'); + } + else if(ty.is_flow_ml()) + { + _indent(level, do_indent); + _do_visit_flow_ml(ich, depth+1, 0u); + this->Writer::_do_write('\n'); + } + else + { + _do_visit_block(ich, depth+1, level, do_indent); // same level! + } + } // keyval vs container + do_indent = true; + } // for children + } // seq vs map +} + +template +void Emitter::_do_visit_block(id_type node, id_type depth, id_type ilevel, id_type do_indent) +{ + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->is_stream(node)); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(node) || m_tree->is_doc(node)); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_root(node) || (m_tree->parent_is_map(node) || m_tree->parent_is_seq(node))); + if(C4_UNLIKELY(depth > m_opts.max_depth())) + _RYML_CB_ERR(m_tree->callbacks(), "max depth exceeded"); + if(m_tree->is_doc(node)) + { + _write_doc(node); + if(!m_tree->has_children(node)) + return; + } + else if(m_tree->is_container(node)) + { + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_map(node) || m_tree->is_seq(node)); + bool spc = false; // write a space + bool nl = false; // write a newline + if(m_tree->has_key(node)) + { + _indent(ilevel, do_indent); + _writek(node, ilevel); + this->Writer::_do_write(':'); + spc = true; + } + else if(!m_tree->is_root(node)) + { + _indent(ilevel, do_indent); + this->Writer::_do_write('-'); + spc = true; + } + + if(m_tree->has_val_tag(node)) + { + if(spc) + this->Writer::_do_write(' '); + _write_tag(m_tree->val_tag(node)); + spc = true; + nl = true; + } + + if(m_tree->has_val_anchor(node)) + { + if(spc) + this->Writer::_do_write(' '); + this->Writer::_do_write('&'); + this->Writer::_do_write(m_tree->val_anchor(node)); + spc = true; + nl = true; + } + + if(m_tree->has_children(node)) + { + if(m_tree->has_key(node)) + nl = true; + else + if(!m_tree->is_root(node) && !nl) + spc = true; + } + else + { + if(m_tree->is_seq(node)) + this->Writer::_do_write(" []\n"); + else if(m_tree->is_map(node)) + this->Writer::_do_write(" {}\n"); + return; + } + + if(spc && !nl) + this->Writer::_do_write(' '); + + do_indent = 0; + if(nl) + { + this->Writer::_do_write('\n'); + do_indent = 1; + } + } // container + + id_type next_level = ilevel + 1; + if(m_tree->is_root(node) || m_tree->is_doc(node)) + next_level = ilevel; // do not indent at top level + + _do_visit_block_container(node, depth, next_level, do_indent); +} + +C4_SUPPRESS_WARNING_MSVC_POP + + +template +void Emitter::_do_visit_json(id_type id, id_type depth) +{ + _RYML_CB_CHECK(m_tree->callbacks(), !m_tree->is_stream(id)); // JSON does not have streams + if(C4_UNLIKELY(depth > m_opts.max_depth())) + _RYML_CB_ERR(m_tree->callbacks(), "max depth exceeded"); + if(m_tree->is_keyval(id)) + { + _writek_json(id); + this->Writer::_do_write(": "); + _writev_json(id); + } + else if(m_tree->is_val(id)) + { + _writev_json(id); + } + else if(m_tree->is_container(id)) + { + if(m_tree->has_key(id)) + { + _writek_json(id); + this->Writer::_do_write(": "); + } + if(m_tree->is_seq(id)) + this->Writer::_do_write('['); + else if(m_tree->is_map(id)) + this->Writer::_do_write('{'); + } // container + + for(id_type ich = m_tree->first_child(id); ich != NONE; ich = m_tree->next_sibling(ich)) + { + if(ich != m_tree->first_child(id)) + this->Writer::_do_write(','); + _do_visit_json(ich, depth+1); + } + + if(m_tree->is_seq(id)) + this->Writer::_do_write(']'); + else if(m_tree->is_map(id)) + this->Writer::_do_write('}'); +} + +template +void Emitter::_write(NodeScalar const& C4_RESTRICT sc, NodeType flags, id_type ilevel) +{ + if( ! sc.tag.empty()) + { + _write_tag(sc.tag); + this->Writer::_do_write(' '); + } + if(flags.has_anchor()) + { + RYML_ASSERT(flags.is_ref() != flags.has_anchor()); + RYML_ASSERT( ! sc.anchor.empty()); + this->Writer::_do_write('&'); + this->Writer::_do_write(sc.anchor); + this->Writer::_do_write(' '); + } + else if(flags.is_ref()) + { + if(sc.anchor != "<<") + this->Writer::_do_write('*'); + this->Writer::_do_write(sc.anchor); + if(flags.is_key_ref()) + this->Writer::_do_write(' '); + return; + } + + // ensure the style flags only have one of KEY or VAL + _RYML_CB_ASSERT(m_tree->callbacks(), ((flags & SCALAR_STYLE) == 0) || (((flags & KEY_STYLE) == 0) != ((flags & VAL_STYLE) == 0))); + type_bits style_marks = flags & SCALAR_STYLE; + if(!style_marks) + style_marks = scalar_style_choose(sc.scalar); + if(style_marks & (KEY_LITERAL|VAL_LITERAL)) + { + _write_scalar_literal(sc.scalar, ilevel, flags.has_key()); + } + else if(style_marks & (KEY_FOLDED|VAL_FOLDED)) + { + _write_scalar_folded(sc.scalar, ilevel, flags.has_key()); + } + else if(style_marks & (KEY_SQUO|VAL_SQUO)) + { + _write_scalar_squo(sc.scalar, ilevel); + } + else if(style_marks & (KEY_DQUO|VAL_DQUO)) + { + _write_scalar_dquo(sc.scalar, ilevel); + } + else if(style_marks & (KEY_PLAIN|VAL_PLAIN)) + { + if(C4_LIKELY(!(sc.scalar.begins_with(": ") || sc.scalar.begins_with(":\t")))) + _write_scalar_plain(sc.scalar, ilevel); + else + _write_scalar_squo(sc.scalar, ilevel); + } + else + { + _RYML_CB_ERR(m_tree->callbacks(), "not implemented"); + } +} + +template +void Emitter::_write_json(NodeScalar const& C4_RESTRICT sc, NodeType flags) +{ + if(flags & (KEYTAG|VALTAG)) + if(m_opts.json_error_flags() & EmitOptions::JSON_ERR_ON_TAG) + _RYML_CB_ERR(m_tree->callbacks(), "JSON does not have tags"); + if(C4_UNLIKELY(flags.has_anchor())) + if(m_opts.json_error_flags() & EmitOptions::JSON_ERR_ON_ANCHOR) + _RYML_CB_ERR(m_tree->callbacks(), "JSON does not have anchors"); + if(sc.scalar.len) + { + // use double quoted style... + // if it is a key (mandatory in JSON) + // if the style is marked quoted + bool dquoted = ((flags & (KEY|VALQUO)) + || (scalar_style_json_choose(sc.scalar) & SCALAR_DQUO)); // choose the style + if(dquoted) + _write_scalar_json_dquo(sc.scalar); + else + this->Writer::_do_write(sc.scalar); + } + else + { + if(sc.scalar.str || (flags & (KEY|VALQUO|KEYTAG|VALTAG))) + this->Writer::_do_write("\"\""); + else + this->Writer::_do_write("null"); + } +} + +template +size_t Emitter::_write_escaped_newlines(csubstr s, size_t i) +{ + RYML_ASSERT(s.len > i); + RYML_ASSERT(s.str[i] == '\n'); + //_c4dbgpf("nl@i={} rem=[{}]~~~{}~~~", i, s.sub(i).len, s.sub(i)); + // add an extra newline for each sequence of consecutive + // newline/whitespace + this->Writer::_do_write('\n'); + do + { + this->Writer::_do_write('\n'); // write the newline again + ++i; // increase the outer loop counter! + } while(i < s.len && s.str[i] == '\n'); + _RYML_CB_ASSERT(m_tree->callbacks(), i > 0); + --i; + _RYML_CB_ASSERT(m_tree->callbacks(), s.str[i] == '\n'); + return i; +} + +inline bool _is_indented_block(csubstr s, size_t prev, size_t i) noexcept +{ + if(prev == 0 && s.begins_with_any(" \t")) + return true; + const size_t pos = s.first_not_of('\n', i); + return (pos != npos) && (s.str[pos] == ' ' || s.str[pos] == '\t'); +} + +template +size_t Emitter::_write_indented_block(csubstr s, size_t i, id_type ilevel) +{ + //_c4dbgpf("indblock@i={} rem=[{}]~~~\n{}~~~", i, s.sub(i).len, s.sub(i)); + _RYML_CB_ASSERT(m_tree->callbacks(), i > 0); + _RYML_CB_ASSERT(m_tree->callbacks(), s.str[i-1] == '\n'); + _RYML_CB_ASSERT(m_tree->callbacks(), i < s.len); + _RYML_CB_ASSERT(m_tree->callbacks(), s.str[i] == ' ' || s.str[i] == '\t' || s.str[i] == '\n'); +again: + size_t pos = s.find("\n ", i); + if(pos == npos) + pos = s.find("\n\t", i); + if(pos != npos) + { + ++pos; + //_c4dbgpf("indblock line@i={} rem=[{}]~~~\n{}~~~", i, s.range(i, pos).len, s.range(i, pos)); + _rymlindent_nextline(); + this->Writer::_do_write(s.range(i, pos)); + i = pos; + goto again; // NOLINT + } + // consume the newlines after the indented block + // to prevent them from being escaped + pos = s.find('\n', i); + if(pos != npos) + { + const size_t pos2 = s.first_not_of('\n', pos); + pos = (pos2 != npos) ? pos2 : pos; + //_c4dbgpf("indblock line@i={} rem=[{}]~~~\n{}~~~", i, s.range(i, pos).len, s.range(i, pos)); + _rymlindent_nextline(); + this->Writer::_do_write(s.range(i, pos)); + i = pos; + } + return i; +} + +template +void Emitter::_write_scalar_literal(csubstr s, id_type ilevel, bool explicit_key) +{ + _RYML_CB_ASSERT(m_tree->callbacks(), s.find("\r") == csubstr::npos); + if(explicit_key) + this->Writer::_do_write("? "); + csubstr trimmed = s.trimr('\n'); + const size_t numnewlines_at_end = s.len - trimmed.len; + const bool is_newline_only = (trimmed.len == 0 && (s.len > 0)); + const bool explicit_indentation = s.triml("\n\r").begins_with_any(" \t"); + // + this->Writer::_do_write('|'); + if(explicit_indentation) + this->Writer::_do_write('2'); + // + if(numnewlines_at_end > 1 || is_newline_only) + this->Writer::_do_write('+'); + else if(numnewlines_at_end == 0) + this->Writer::_do_write('-'); + // + if(trimmed.len) + { + this->Writer::_do_write('\n'); + size_t pos = 0; // tracks the last character that was already written + for(size_t i = 0; i < trimmed.len; ++i) + { + if(trimmed[i] != '\n') + continue; + // write everything up to this point + csubstr since_pos = trimmed.range(pos, i+1); // include the newline + _rymlindent_nextline() + this->Writer::_do_write(since_pos); + pos = i+1; // already written + } + if(pos < trimmed.len) + { + _rymlindent_nextline() + this->Writer::_do_write(trimmed.sub(pos)); + } + } + for(size_t i = !is_newline_only; i < numnewlines_at_end; ++i) + this->Writer::_do_write('\n'); + if(explicit_key) + { + this->Writer::_do_write('\n'); + this->_indent(ilevel); + } +} + +template +void Emitter::_write_scalar_folded(csubstr s, id_type ilevel, bool explicit_key) +{ + if(explicit_key) + this->Writer::_do_write("? "); + _RYML_CB_ASSERT(m_tree->callbacks(), s.find("\r") == csubstr::npos); + csubstr trimmed = s.trimr('\n'); + const size_t numnewlines_at_end = s.len - trimmed.len; + const bool is_newline_only = (trimmed.len == 0 && (s.len > 0)); + const bool explicit_indentation = s.triml("\n\r").begins_with_any(" \t"); + // + this->Writer::_do_write('>'); + if(explicit_indentation) + this->Writer::_do_write('2'); + // + if(numnewlines_at_end == 0) + this->Writer::_do_write('-'); + else if(numnewlines_at_end > 1 || is_newline_only) + this->Writer::_do_write('+'); + // + if(trimmed.len) + { + this->Writer::_do_write('\n'); + size_t pos = 0; // tracks the last character that was already written + for(size_t i = 0; i < trimmed.len; ++i) + { + if(trimmed[i] != '\n') + continue; + // escape newline sequences + if( ! _is_indented_block(s, pos, i)) + { + if(pos < i) + { + _rymlindent_nextline() + this->Writer::_do_write(s.range(pos, i)); + i = _write_escaped_newlines(s, i); + pos = i+1; + } + else + { + if(i+1 < s.len) + { + if(s.str[i+1] == '\n') + { + ++i; + i = _write_escaped_newlines(s, i); + pos = i+1; + } + else + { + this->Writer::_do_write('\n'); + pos = i+1; + } + } + } + } + else // do not escape newlines in indented blocks + { + ++i; + _rymlindent_nextline() + this->Writer::_do_write(s.range(pos, i)); + if(pos > 0 || !s.begins_with_any(" \t")) + i = _write_indented_block(s, i, ilevel); + pos = i; + } + } + if(pos < trimmed.len) + { + _rymlindent_nextline() + this->Writer::_do_write(trimmed.sub(pos)); + } + } + for(size_t i = !is_newline_only; i < numnewlines_at_end; ++i) + this->Writer::_do_write('\n'); + if(explicit_key) + { + this->Writer::_do_write('\n'); + this->_indent(ilevel); + } +} + +template +void Emitter::_write_scalar_squo(csubstr s, id_type ilevel) +{ + size_t pos = 0; // tracks the last character that was already written + this->Writer::_do_write('\''); + for(size_t i = 0; i < s.len; ++i) + { + if(s[i] == '\n') + { + this->Writer::_do_write(s.range(pos, i)); // write everything up to (excluding) this char + //_c4dbgpf("newline at {}. writing ~~~{}~~~", i, s.range(pos, i)); + i = _write_escaped_newlines(s, i); + //_c4dbgpf("newline --> {}", i); + if(i < s.len) + _rymlindent_nextline() + pos = i+1; + } + else if(s[i] == '\'') + { + csubstr sub = s.range(pos, i+1); + //_c4dbgpf("squote at {}. writing ~~~{}~~~", i, sub); + this->Writer::_do_write(sub); // write everything up to (including) this squote + this->Writer::_do_write('\''); // write the squote again + pos = i+1; + } + } + // write missing characters at the end of the string + if(pos < s.len) + this->Writer::_do_write(s.sub(pos)); + this->Writer::_do_write('\''); +} + +template +void Emitter::_write_scalar_dquo(csubstr s, id_type ilevel) +{ + size_t pos = 0; // tracks the last character that was already written + this->Writer::_do_write('"'); + for(size_t i = 0; i < s.len; ++i) + { + const char curr = s.str[i]; + switch(curr) // NOLINT + { + case '"': + case '\\': + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (excluding) this char + this->Writer::_do_write('\\'); // write the escape + this->Writer::_do_write(curr); // write the char + pos = i+1; + break; + } +#ifndef prefer_writing_newlines_as_double_newlines + case '\n': + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (excluding) this char + this->Writer::_do_write("\\n"); // write the escape + pos = i+1; + (void)ilevel; + break; + } +#else + case '\n': + { + // write everything up to (excluding) this newline + //_c4dbgpf("nl@i={} rem=[{}]~~~{}~~~", i, s.sub(i).len, s.sub(i)); + this->Writer::_do_write(s.range(pos, i)); + i = _write_escaped_newlines(s, i); + ++i; + pos = i; + // as for the next line... + if(i < s.len) + { + _rymlindent_nextline() // indent the next line + // escape leading whitespace, and flush it + size_t first = s.first_not_of(" \t", i); + _c4dbgpf("@i={} first={} rem=[{}]~~~{}~~~", i, first, s.sub(i).len, s.sub(i)); + if(first > i) + { + if(first == npos) + first = s.len; + this->Writer::_do_write('\\'); + this->Writer::_do_write(s.range(i, first)); + this->Writer::_do_write('\\'); + i = first-1; + pos = first; + } + } + break; + } + // escape trailing whitespace before a newline + case ' ': + case '\t': + { + const size_t next = s.first_not_of(" \t\r", i); + if(next != npos && s.str[next] == '\n') + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (excluding) this char + this->Writer::_do_write('\\'); // escape the whitespace + pos = i; + } + break; + } +#endif + case '\r': + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (excluding) this char + this->Writer::_do_write("\\r"); // write the escaped char + pos = i+1; + break; + } + case '\b': + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (excluding) this char + this->Writer::_do_write("\\b"); // write the escaped char + pos = i+1; + break; + } + } + } + // write missing characters at the end of the string + if(pos < s.len) + this->Writer::_do_write(s.sub(pos)); + this->Writer::_do_write('"'); +} + +template +void Emitter::_write_scalar_plain(csubstr s, id_type ilevel) +{ + if(C4_UNLIKELY(ilevel == 0 && (s.begins_with("...") || s.begins_with("---")))) + { + _rymlindent_nextline() // indent the next line + ++ilevel; + } + size_t pos = 0; // tracks the last character that was already written + for(size_t i = 0; i < s.len; ++i) + { + const char curr = s.str[i]; + if(curr == '\n') + { + csubstr sub = s.range(pos, i); + this->Writer::_do_write(sub); // write everything up to (including) this newline + i = _write_escaped_newlines(s, i); + pos = i+1; + if(pos < s.len) + _rymlindent_nextline() // indent the next line + } + } + // write missing characters at the end of the string + if(pos < s.len) + this->Writer::_do_write(s.sub(pos)); +} + +#undef _rymlindent_nextline + +template +void Emitter::_write_scalar_json_dquo(csubstr s) +{ + size_t pos = 0; + this->Writer::_do_write('"'); + for(size_t i = 0; i < s.len; ++i) + { + switch(s.str[i]) + { + case '"': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\\""); + pos = i + 1; + break; + case '\n': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\n"); + pos = i + 1; + break; + case '\t': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\t"); + pos = i + 1; + break; + case '\\': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\\\"); + pos = i + 1; + break; + case '\r': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\r"); + pos = i + 1; + break; + case '\b': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\b"); + pos = i + 1; + break; + case '\f': + this->Writer ::_do_write(s.range(pos, i)); + this->Writer ::_do_write("\\f"); + pos = i + 1; + break; + } + } + if(pos < s.len) + { + csubstr sub = s.sub(pos); + this->Writer::_do_write(sub); + } + this->Writer::_do_write('"'); +} + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_EMIT_DEF_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/emit.hpp b/3rdparty/rapidyaml/include/c4/yml/emit.hpp new file mode 100644 index 0000000000..d77c32a4ed --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/emit.hpp @@ -0,0 +1,909 @@ +#ifndef _C4_YML_EMIT_HPP_ +#define _C4_YML_EMIT_HPP_ + +/** @file emit.hpp Utilities to emit YAML and JSON. */ + +#ifndef _C4_YML_WRITER_HPP_ +#include "./writer.hpp" +#endif + +#ifndef _C4_YML_TREE_HPP_ +#include "./tree.hpp" +#endif + +#ifndef _C4_YML_NODE_HPP_ +#include "./node.hpp" +#endif + + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +namespace c4 { +namespace yml { + +/** @addtogroup doc_emit + * + * @{ + */ + +// fwd declarations +template class Emitter; +template +using EmitterOStream = Emitter>; +using EmitterFile = Emitter; +using EmitterBuf = Emitter; + +namespace detail { +inline bool is_set_(ConstNodeRef n) { return n.tree() && (n.id() != NONE); } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** Specifies the type of content to emit */ +typedef enum { + EMIT_YAML = 0, ///< emit YAML + EMIT_JSON = 1 ///< emit JSON +} EmitType_e; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** A lightweight object containing options to be used when emitting. */ +struct EmitOptions +{ + typedef enum : uint32_t { + DEFAULT_FLAGS = 0u, + JSON_ERR_ON_TAG = 1u << 0u, + JSON_ERR_ON_ANCHOR = 1u << 1u, + _JSON_ERR_MASK = JSON_ERR_ON_TAG|JSON_ERR_ON_ANCHOR, + } EmitOptionFlags_e; + +public: + + /** @name option flags + * + * @{ */ + C4_ALWAYS_INLINE EmitOptionFlags_e json_error_flags() const noexcept { return m_option_flags; } + EmitOptions& json_error_flags(EmitOptionFlags_e d) noexcept { m_option_flags = (EmitOptionFlags_e)(d & _JSON_ERR_MASK); return *this; } + /** @} */ + +public: + + /** @name max depth for the emitted tree + * + * This makes the emitter fail when emitting trees exceeding the + * max_depth. + * + * @{ */ + C4_ALWAYS_INLINE id_type max_depth() const noexcept { return m_max_depth; } + EmitOptions& max_depth(id_type d) noexcept { m_max_depth = d; return *this; } + static constexpr const id_type max_depth_default = 64; + /** @} */ + +public: + + bool operator== (const EmitOptions& that) const noexcept + { + return m_max_depth == that.m_max_depth && + m_option_flags == that.m_option_flags; + } + +private: + + /** @cond dev */ + id_type m_max_depth{max_depth_default}; + EmitOptionFlags_e m_option_flags{DEFAULT_FLAGS}; + /** @endcond */ +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** A stateful emitter, for use with a writer such as @ref WriterBuf, + * @ref WriterFile, or @ref WriterOStream */ +template +class Emitter : public Writer +{ +public: + + /** Construct the emitter and its internal Writer state, using default emit options. + * @param args arguments to be forwarded to the constructor of the writer. + * */ + template + Emitter(Args &&...args) : Writer(std::forward(args)...), m_tree(), m_opts(), m_flow(false) {} + + /** Construct the emitter and its internal Writer state. + * + * @param opts EmitOptions + * @param args arguments to be forwarded to the constructor of the writer. + * */ + template + Emitter(EmitOptions const& opts, Args &&...args) : Writer(std::forward(args)...), m_tree(), m_opts(opts), m_flow(false) {} + + /** emit! + * + * When writing to a buffer, returns a substr of the emitted YAML. + * If the given buffer has insufficient space, the returned substr + * will be null and its size will be the needed space. Whatever + * the size of the buffer, it is guaranteed that no writes are + * done past its end. + * + * When writing to a file, the returned substr will be null, but its + * length will be set to the number of bytes written. + * + * @param type specify what to emit + * @param t the tree to emit + * @param id the id of the node to emit + * @param error_on_excess when true, an error is raised when the + * output buffer is too small for the emitted YAML/JSON + * */ + substr emit_as(EmitType_e type, Tree const& t, id_type id, bool error_on_excess); + /** emit starting at the root node */ + substr emit_as(EmitType_e type, Tree const& t, bool error_on_excess=true) + { + if(t.empty()) + return {}; + return this->emit_as(type, t, t.root_id(), error_on_excess); + } + /** emit starting at the given node */ + substr emit_as(EmitType_e type, ConstNodeRef const& n, bool error_on_excess=true) + { + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + return this->emit_as(type, *n.tree(), n.id(), error_on_excess); + } + +public: + + /** get the emit options for this object */ + EmitOptions const& options() const noexcept { return m_opts; } + + /** set the max depth for emitted trees (to prevent a stack overflow) */ + void max_depth(id_type max_depth) noexcept { m_opts.max_depth(max_depth); } + /** get the max depth for emitted trees (to prevent a stack overflow) */ + id_type max_depth() const noexcept { return m_opts.max_depth(); } + +private: + + Tree const* C4_RESTRICT m_tree; + EmitOptions m_opts; + bool m_flow; + +private: + + void _emit_yaml(id_type id); + void _do_visit_flow_sl(id_type id, id_type depth, id_type ilevel=0); + void _do_visit_flow_ml(id_type id, id_type depth, id_type ilevel=0, id_type do_indent=1); + void _do_visit_block(id_type id, id_type depth, id_type ilevel=0, id_type do_indent=1); + void _do_visit_block_container(id_type id, id_type depth, id_type next_level, bool do_indent); + void _do_visit_json(id_type id, id_type depth); + +private: + + void _write(NodeScalar const& C4_RESTRICT sc, NodeType flags, id_type level); + void _write_json(NodeScalar const& C4_RESTRICT sc, NodeType flags); + + void _write_doc(id_type id); + void _write_scalar_json_dquo(csubstr s); + void _write_scalar_literal(csubstr s, id_type level, bool as_key); + void _write_scalar_folded(csubstr s, id_type level, bool as_key); + void _write_scalar_squo(csubstr s, id_type level); + void _write_scalar_dquo(csubstr s, id_type level); + void _write_scalar_plain(csubstr s, id_type level); + + size_t _write_escaped_newlines(csubstr s, size_t i); + size_t _write_indented_block(csubstr s, size_t i, id_type level); + + void _write_tag(csubstr tag) + { + if(!tag.begins_with('!')) + this->Writer::_do_write('!'); + this->Writer::_do_write(tag); + } + + enum : type_bits { + _keysc = (KEY|KEYREF|KEYANCH|KEYQUO|KEY_STYLE) | ~(VAL|VALREF|VALANCH|VALQUO|VAL_STYLE) | CONTAINER_STYLE, + _valsc = ~(KEY|KEYREF|KEYANCH|KEYQUO|KEY_STYLE) | (VAL|VALREF|VALANCH|VALQUO|VAL_STYLE) | CONTAINER_STYLE, + _keysc_json = (KEY) | ~(VAL), + _valsc_json = ~(KEY) | (VAL), + }; + + C4_ALWAYS_INLINE void _writek(id_type id, id_type level) { _write(m_tree->keysc(id), (m_tree->_p(id)->m_type.type & ~_valsc), level); } + C4_ALWAYS_INLINE void _writev(id_type id, id_type level) { _write(m_tree->valsc(id), (m_tree->_p(id)->m_type.type & ~_keysc), level); } + + C4_ALWAYS_INLINE void _writek_json(id_type id) { _write_json(m_tree->keysc(id), m_tree->_p(id)->m_type.type & ~(VAL)); } + C4_ALWAYS_INLINE void _writev_json(id_type id) { _write_json(m_tree->valsc(id), m_tree->_p(id)->m_type.type & ~(KEY)); } + + void _indent(id_type level, bool enabled) + { + if(enabled) + this->Writer::_do_write(' ', 2u * (size_t)level); + } + void _indent(id_type level) + { + if(!m_flow) + this->Writer::_do_write(' ', 2u * (size_t)level); + } +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_emit_to_file Emit to file + * + * @{ + */ + + +// emit from tree and node id ----------------------- + +/** (1) emit YAML to the given file, starting at the given node. A null + * file defaults to stdout. Return the number of bytes written. */ +inline size_t emit_yaml(Tree const& t, id_type id, EmitOptions const& opts, FILE *f) +{ + EmitterFile em(opts, f); + return em.emit_as(EMIT_YAML, t, id, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_yaml(Tree const& t, id_type id, FILE *f) +{ + EmitterFile em(f); + return em.emit_as(EMIT_YAML, t, id, /*error_on_excess*/true).len; +} +/** (1) emit JSON to the given file, starting at the given node. A null + * file defaults to stdout. Return the number of bytes written. */ +inline size_t emit_json(Tree const& t, id_type id, EmitOptions const& opts, FILE *f) +{ + EmitterFile em(opts, f); + return em.emit_as(EMIT_JSON, t, id, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_json(Tree const& t, id_type id, FILE *f) +{ + EmitterFile em(f); + return em.emit_as(EMIT_JSON, t, id, /*error_on_excess*/true).len; +} + + +// emit from root ------------------------- + +/** (1) emit YAML to the given file, starting at the root node. A null file defaults to stdout. + * Return the number of bytes written. */ +inline size_t emit_yaml(Tree const& t, EmitOptions const& opts, FILE *f=nullptr) +{ + EmitterFile em(opts, f); + return em.emit_as(EMIT_YAML, t, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_yaml(Tree const& t, FILE *f=nullptr) +{ + EmitterFile em(f); + return em.emit_as(EMIT_YAML, t, /*error_on_excess*/true).len; +} +/** (1) emit JSON to the given file. A null file defaults to stdout. + * Return the number of bytes written. */ +inline size_t emit_json(Tree const& t, EmitOptions const& opts, FILE *f=nullptr) +{ + EmitterFile em(opts, f); + return em.emit_as(EMIT_JSON, t, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_json(Tree const& t, FILE *f=nullptr) +{ + EmitterFile em(f); + return em.emit_as(EMIT_JSON, t, /*error_on_excess*/true).len; +} + + +// emit from ConstNodeRef ------------------------ + +/** (1) emit YAML to the given file. A null file defaults to stdout. + * Return the number of bytes written. */ +inline size_t emit_yaml(ConstNodeRef const& r, EmitOptions const& opts, FILE *f=nullptr) +{ + if(!detail::is_set_(r)) + return {}; + EmitterFile em(opts, f); + return em.emit_as(EMIT_YAML, r, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_yaml(ConstNodeRef const& r, FILE *f=nullptr) +{ + if(!detail::is_set_(r)) + return {}; + EmitterFile em(f); + return em.emit_as(EMIT_YAML, r, /*error_on_excess*/true).len; +} +/** (1) emit JSON to the given file. A null file defaults to stdout. + * Return the number of bytes written. */ +inline size_t emit_json(ConstNodeRef const& r, EmitOptions const& opts, FILE *f=nullptr) +{ + if(!detail::is_set_(r)) + return {}; + EmitterFile em(opts, f); + return em.emit_as(EMIT_JSON, r, /*error_on_excess*/true).len; +} +/** (2) like (1), but use default emit options */ +inline size_t emit_json(ConstNodeRef const& r, FILE *f=nullptr) +{ + if(!detail::is_set_(r)) + return {}; + EmitterFile em(f); + return em.emit_as(EMIT_JSON, r, /*error_on_excess*/true).len; +} + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_emit_to_ostream Emit to an STL-like ostream + * + * @{ + */ + +/** emit YAML to an STL-like ostream */ +template +inline OStream& operator<< (OStream& s, Tree const& t) +{ + EmitterOStream em(s); + em.emit_as(EMIT_YAML, t); + return s; +} + +/** emit YAML to an STL-like ostream + * @overload */ +template +inline OStream& operator<< (OStream& s, ConstNodeRef const& n) +{ + if(!detail::is_set_(n)) + return s; + EmitterOStream em(s); + em.emit_as(EMIT_YAML, n); + return s; +} + +/** mark a tree or node to be emitted as yaml when using @ref + * operator<<, with options. For example: + * + * ```cpp + * Tree t = parse_in_arena("{foo: bar}"); + * std::cout << t; // emits YAML + * std::cout << as_yaml(t); // emits YAML, same as above + * std::cout << as_yaml(t, EmitOptions().max_depth(10)); // emits JSON with a max tree depth + * ``` + * + * @see @ref operator<< */ +struct as_json +{ + Tree const* tree; + size_t node; + EmitOptions options; + as_json(Tree const& t, EmitOptions const& opts={}) : tree(&t), node(t.empty() ? NONE : t.root_id()), options(opts) {} + as_json(Tree const& t, size_t id, EmitOptions const& opts={}) : tree(&t), node(id), options(opts) {} + as_json(ConstNodeRef const& n, EmitOptions const& opts={}) : tree(n.tree()), node(n.id()), options(opts) {} +}; + +/** mark a tree or node to be emitted as yaml when using @ref + * operator<< . For example: + * + * ```cpp + * Tree t = parse_in_arena("{foo: bar}"); + * std::cout << t; // emits YAML + * std::cout << as_json(t); // emits JSON + * std::cout << as_json(t, EmitOptions().max_depth(10)); // emits JSON with a max tree depth + * ``` + * + * @see @ref operator<< */ +struct as_yaml +{ + Tree const* tree; + size_t node; + EmitOptions options; + as_yaml(Tree const& t, EmitOptions const& opts={}) : tree(&t), node(t.empty() ? NONE : t.root_id()), options(opts) {} + as_yaml(Tree const& t, size_t id, EmitOptions const& opts={}) : tree(&t), node(id), options(opts) {} + as_yaml(ConstNodeRef const& n, EmitOptions const& opts={}) : tree(n.tree()), node(n.id()), options(opts) {} +}; + +/** emit json to an STL-like stream */ +template +inline OStream& operator<< (OStream& s, as_json const& j) +{ + if(!j.tree || j.node == NONE) + return s; + EmitterOStream em(j.options, s); + em.emit_as(EMIT_JSON, *j.tree, j.node, true); + return s; +} + +/** emit yaml to an STL-like stream */ +template +inline OStream& operator<< (OStream& s, as_yaml const& y) +{ + if(!y.tree || y.node == NONE) + return s; + EmitterOStream em(y.options, s); + em.emit_as(EMIT_YAML, *y.tree, y.node, true); + return s; +} + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_emit_to_buffer Emit to memory buffer + * + * @{ + */ + +// emit from tree and node id ----------------------- + +/** (1) emit YAML to the given buffer. Return a substr trimmed to the emitted YAML. + * @param t the tree to emit. + * @param id the node where to start emitting. + * @param opts emit options. + * @param buf the output buffer. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_yaml(Tree const& t, id_type id, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_YAML, t, id, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_yaml(Tree const& t, id_type id, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(buf); + return em.emit_as(EMIT_YAML, t, id, error_on_excess); +} +/** (1) emit JSON to the given buffer. Return a substr trimmed to the emitted JSON. + * @param t the tree to emit. + * @param id the node where to start emitting. + * @param opts emit options. + * @param buf the output buffer. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_json(Tree const& t, id_type id, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_JSON, t, id, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_json(Tree const& t, id_type id, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(buf); + return em.emit_as(EMIT_JSON, t, id, error_on_excess); +} + + +// emit from root ------------------------- + +/** (1) emit YAML to the given buffer. Return a substr trimmed to the emitted YAML. + * @param t the tree; will be emitted from the root node. + * @param opts emit options. + * @param buf the output buffer. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_yaml(Tree const& t, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_YAML, t, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_yaml(Tree const& t, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(buf); + return em.emit_as(EMIT_YAML, t, error_on_excess); +} +/** (1) emit JSON to the given buffer. Return a substr trimmed to the emitted JSON. + * @param t the tree; will be emitted from the root node. + * @param opts emit options. + * @param buf the output buffer. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_json(Tree const& t, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_JSON, t, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_json(Tree const& t, substr buf, bool error_on_excess=true) +{ + EmitterBuf em(buf); + return em.emit_as(EMIT_JSON, t, error_on_excess); +} + + +// emit from ConstNodeRef ------------------------ + +/** (1) emit YAML to the given buffer. Return a substr trimmed to the emitted YAML. + * @param r the starting node. + * @param buf the output buffer. + * @param opts emit options. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_yaml(ConstNodeRef const& r, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + if(!detail::is_set_(r)) + return {}; + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_YAML, r, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_yaml(ConstNodeRef const& r, substr buf, bool error_on_excess=true) +{ + if(!detail::is_set_(r)) + return {}; + EmitterBuf em(buf); + return em.emit_as(EMIT_YAML, r, error_on_excess); +} +/** (1) emit JSON to the given buffer. Return a substr trimmed to the emitted JSON. + * @param r the starting node. + * @param buf the output buffer. + * @param opts emit options. + * @param error_on_excess Raise an error if the space in the buffer is insufficient. + * @return a substr trimmed to the result in the output buffer. If the buffer is + * insufficient (when error_on_excess is false), the string pointer of the + * result will be set to null, and the length will report the required buffer size. */ +inline substr emit_json(ConstNodeRef const& r, EmitOptions const& opts, substr buf, bool error_on_excess=true) +{ + if(!detail::is_set_(r)) + return {}; + EmitterBuf em(opts, buf); + return em.emit_as(EMIT_JSON, r, error_on_excess); +} +/** (2) like (1), but use default emit options */ +inline substr emit_json(ConstNodeRef const& r, substr buf, bool error_on_excess=true) +{ + if(!detail::is_set_(r)) + return {}; + EmitterBuf em(buf); + return em.emit_as(EMIT_JSON, r, error_on_excess); +} + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_emit_to_container Emit to resizeable container + * + * @{ + */ + +// emit from tree and node id --------------------------- + +/** (1) emit+resize: emit YAML to the given `std::string`/`std::vector`-like + * container, resizing it as needed to fit the emitted YAML. If @p append is + * set to true, the emitted YAML is appended at the end of the container. + * + * @return a substr trimmed to the emitted YAML (excluding the initial contents, when appending) */ +template +substr emitrs_yaml(Tree const& t, id_type id, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + size_t startpos = append ? cont->size() : 0u; + cont->resize(cont->capacity()); // otherwise the first emit would be certain to fail + substr buf = to_substr(*cont).sub(startpos); + substr ret = emit_yaml(t, id, opts, buf, /*error_on_excess*/false); + if(ret.str == nullptr && ret.len > 0) + { + cont->resize(startpos + ret.len); + buf = to_substr(*cont).sub(startpos); + ret = emit_yaml(t, id, opts, buf, /*error_on_excess*/true); + } + else + { + cont->resize(startpos + ret.len); + } + return ret; +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_yaml(Tree const& t, id_type id, CharOwningContainer * cont, bool append=false) +{ + return emitrs_yaml(t, id, EmitOptions{}, cont, append); +} +/** (1) emit+resize: emit JSON to the given `std::string`/`std::vector`-like + * container, resizing it as needed to fit the emitted JSON. If @p append is + * set to true, the emitted YAML is appended at the end of the container. + * + * @return a substr trimmed to the emitted JSON (excluding the initial contents, when appending) */ +template +substr emitrs_json(Tree const& t, id_type id, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + const size_t startpos = append ? cont->size() : 0u; + cont->resize(cont->capacity()); // otherwise the first emit would be certain to fail + substr buf = to_substr(*cont).sub(startpos); + EmitterBuf em(opts, buf); + substr ret = emit_json(t, id, opts, buf, /*error_on_excess*/false); + if(ret.str == nullptr && ret.len > 0) + { + cont->resize(startpos + ret.len); + buf = to_substr(*cont).sub(startpos); + ret = emit_json(t, id, opts, buf, /*error_on_excess*/true); + } + else + { + cont->resize(startpos + ret.len); + } + return ret; +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_json(Tree const& t, id_type id, CharOwningContainer * cont, bool append=false) +{ + return emitrs_json(t, id, EmitOptions{}, cont, append); +} + + +/** (3) emit+resize: YAML to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_yaml(Tree const& t, id_type id, EmitOptions const& opts={}) +{ + CharOwningContainer c; + emitrs_yaml(t, id, opts, &c); + return c; +} +/** (3) emit+resize: JSON to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_json(Tree const& t, id_type id, EmitOptions const& opts={}) +{ + CharOwningContainer c; + emitrs_json(t, id, opts, &c); + return c; +} + + +// emit from root ------------------------- + +/** (1) emit+resize: YAML to the given `std::string`/`std::vector`-like + * container, resizing it as needed to fit the emitted YAML. + * @return a substr trimmed to the new emitted contents. */ +template +substr emitrs_yaml(Tree const& t, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + if(t.empty()) + return {}; + return emitrs_yaml(t, t.root_id(), opts, cont, append); +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_yaml(Tree const& t, CharOwningContainer * cont, bool append=false) +{ + if(t.empty()) + return {}; + return emitrs_yaml(t, t.root_id(), EmitOptions{}, cont, append); +} +/** (1) emit+resize: JSON to the given `std::string`/`std::vector`-like + * container, resizing it as needed to fit the emitted JSON. + * @return a substr trimmed to the new emitted contents. */ +template +substr emitrs_json(Tree const& t, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + if(t.empty()) + return {}; + return emitrs_json(t, t.root_id(), opts, cont, append); +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_json(Tree const& t, CharOwningContainer * cont, bool append=false) +{ + if(t.empty()) + return {}; + return emitrs_json(t, t.root_id(), EmitOptions{}, cont, append); +} + + +/** (3) emit+resize: YAML to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_yaml(Tree const& t, EmitOptions const& opts={}) +{ + CharOwningContainer c; + if(t.empty()) + return c; + emitrs_yaml(t, t.root_id(), opts, &c); + return c; +} +/** (3) emit+resize: JSON to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_json(Tree const& t, EmitOptions const& opts={}) +{ + CharOwningContainer c; + if(t.empty()) + return c; + emitrs_json(t, t.root_id(), opts, &c); + return c; +} + + +// emit from ConstNodeRef ------------------------ + + +/** (1) emit+resize: YAML to the given `std::string`/`std::vector`-like container, + * resizing it as needed to fit the emitted YAML. + * @return a substr trimmed to the new emitted contents */ +template +substr emitrs_yaml(ConstNodeRef const& n, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + return emitrs_yaml(*n.tree(), n.id(), opts, cont, append); +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_yaml(ConstNodeRef const& n, CharOwningContainer * cont, bool append=false) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + return emitrs_yaml(*n.tree(), n.id(), EmitOptions{}, cont, append); +} +/** (1) emit+resize: JSON to the given `std::string`/`std::vector`-like container, + * resizing it as needed to fit the emitted JSON. + * @return a substr trimmed to the new emitted contents */ +template +substr emitrs_json(ConstNodeRef const& n, EmitOptions const& opts, CharOwningContainer * cont, bool append=false) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + return emitrs_json(*n.tree(), n.id(), opts, cont, append); +} +/** (2) like (1), but use default emit options */ +template +substr emitrs_json(ConstNodeRef const& n, CharOwningContainer * cont, bool append=false) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + return emitrs_json(*n.tree(), n.id(), EmitOptions{}, cont, append); +} + + +/** (3) emit+resize: YAML to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_yaml(ConstNodeRef const& n, EmitOptions const& opts={}) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + CharOwningContainer c; + emitrs_yaml(*n.tree(), n.id(), opts, &c); + return c; +} +/** (3) emit+resize: JSON to a newly-created `std::string`/`std::vector`-like container. */ +template +CharOwningContainer emitrs_json(ConstNodeRef const& n, EmitOptions const& opts={}) +{ + if(!detail::is_set_(n)) + return {}; + _RYML_CB_CHECK(n.tree()->callbacks(), n.readable()); + CharOwningContainer c; + emitrs_json(*n.tree(), n.id(), opts, &c); + return c; +} + + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @cond dev */ + +#define RYML_DEPRECATE_EMIT \ + RYML_DEPRECATED("use emit_yaml() instead. " \ + "See https://github.com/biojppm/rapidyaml/issues/120") +#define RYML_DEPRECATE_EMITRS \ + RYML_DEPRECATED("use emitrs_yaml() instead. " \ + "See https://github.com/biojppm/rapidyaml/issues/120") + +// workaround for Qt emit which is a macro; +// see https://github.com/biojppm/rapidyaml/issues/120. +// emit is defined in qobjectdefs.h (as an empty define). +#ifdef emit +#define RYML_TMP_EMIT_ +#undef emit +#endif + +RYML_DEPRECATE_EMIT inline size_t emit(Tree const& t, id_type id, FILE *f) +{ + return emit_yaml(t, id, f); +} +RYML_DEPRECATE_EMIT inline size_t emit(Tree const& t, FILE *f=nullptr) +{ + return emit_yaml(t, f); +} +RYML_DEPRECATE_EMIT inline size_t emit(ConstNodeRef const& r, FILE *f=nullptr) +{ + return emit_yaml(r, f); +} + +RYML_DEPRECATE_EMIT inline substr emit(Tree const& t, id_type id, substr buf, bool error_on_excess=true) +{ + return emit_yaml(t, id, buf, error_on_excess); +} +RYML_DEPRECATE_EMIT inline substr emit(Tree const& t, substr buf, bool error_on_excess=true) +{ + return emit_yaml(t, buf, error_on_excess); +} +RYML_DEPRECATE_EMIT inline substr emit(ConstNodeRef const& r, substr buf, bool error_on_excess=true) +{ + return emit_yaml(r, buf, error_on_excess); +} + +#ifdef RYML_TMP_EMIT_ +#define emit +#undef RYML_TMP_EMIT_ +#endif + +template +RYML_DEPRECATE_EMITRS substr emitrs(Tree const& t, id_type id, CharOwningContainer * cont) +{ + return emitrs_yaml(t, id, cont); +} +template +RYML_DEPRECATE_EMITRS CharOwningContainer emitrs(Tree const& t, id_type id) +{ + return emitrs_yaml(t, id); +} +template +RYML_DEPRECATE_EMITRS substr emitrs(Tree const& t, CharOwningContainer * cont) +{ + return emitrs_yaml(t, cont); +} +template +RYML_DEPRECATE_EMITRS CharOwningContainer emitrs(Tree const& t) +{ + return emitrs_yaml(t); +} +template +RYML_DEPRECATE_EMITRS substr emitrs(ConstNodeRef const& n, CharOwningContainer * cont) +{ + return emitrs_yaml(n, cont); +} +template +RYML_DEPRECATE_EMITRS CharOwningContainer emitrs(ConstNodeRef const& n) +{ + return emitrs_yaml(n); +} + +/** @endcond */ + + +} // namespace yml +} // namespace c4 + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +#undef RYML_DEPRECATE_EMIT +#undef RYML_DEPRECATE_EMITRS + +#include "c4/yml/emit.def.hpp" // NOLINT + +#endif /* _C4_YML_EMIT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/event_handler_stack.hpp b/3rdparty/rapidyaml/include/c4/yml/event_handler_stack.hpp new file mode 100644 index 0000000000..65960d3009 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/event_handler_stack.hpp @@ -0,0 +1,194 @@ +#ifndef _C4_YML_EVENT_HANDLER_STACK_HPP_ +#define _C4_YML_EVENT_HANDLER_STACK_HPP_ + +#ifndef _C4_YML_DETAIL_STACK_HPP_ +#include "c4/yml/detail/stack.hpp" +#endif + +#ifndef _C4_YML_NODE_TYPE_HPP_ +#include "c4/yml/node_type.hpp" +#endif + +#ifndef _C4_YML_DETAIL_DBGPRINT_HPP_ +#include "c4/yml/detail/dbgprint.hpp" +#endif + +#ifndef _C4_YML_PARSER_STATE_HPP_ +#include "c4/yml/parser_state.hpp" +#endif + +#ifdef RYML_DBG +#ifndef _C4_YML_DETAIL_PRINT_HPP_ +#include "c4/yml/detail/print.hpp" +#endif +#endif + +// NOLINTBEGIN(hicpp-signed-bitwise) + +namespace c4 { +namespace yml { + +/** @addtogroup doc_event_handlers + * @{ */ + +namespace detail { +using pfn_relocate_arena = void (*)(void*, csubstr prev_arena, substr next_arena); +} // detail + +/** Use this class a base of implementations of event handler to + * simplify the stack logic. */ +template +struct EventHandlerStack +{ + static_assert(std::is_base_of::value, + "ParserState must be a base of HandlerState"); + + using state = HandlerState; + using pfn_relocate_arena = detail::pfn_relocate_arena; + +public: + + detail::stack m_stack; + state *C4_RESTRICT m_curr; ///< current stack level: top of the stack. cached here for easier access. + state *C4_RESTRICT m_parent; ///< parent of the current stack level. + pfn_relocate_arena m_relocate_arena; ///< callback when the arena gets relocated + void * m_relocate_arena_data; + +protected: + + EventHandlerStack() : m_stack(), m_curr(), m_parent(), m_relocate_arena(), m_relocate_arena_data() {} + EventHandlerStack(Callbacks const& cb) : m_stack(cb), m_curr(), m_parent(), m_relocate_arena(), m_relocate_arena_data() {} + +protected: + + void _stack_start_parse(const char *filename, pfn_relocate_arena relocate_arena, void *relocate_arena_data) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_curr != nullptr); + _RYML_CB_ASSERT(m_stack.m_callbacks, relocate_arena != nullptr); + _RYML_CB_ASSERT(m_stack.m_callbacks, relocate_arena_data != nullptr); + m_curr->start_parse(filename, m_curr->node_id); + m_relocate_arena = relocate_arena; + m_relocate_arena_data = relocate_arena_data; + } + + void _stack_finish_parse() + { + } + +protected: + + void _stack_reset_root() + { + m_stack.clear(); + m_stack.push({}); + m_parent = nullptr; + m_curr = &m_stack.top(); + } + + void _stack_reset_non_root() + { + m_stack.clear(); + m_stack.push({}); // parent + m_stack.push({}); // node + m_parent = &m_stack.top(1); + m_curr = &m_stack.top(); + } + + void _stack_push() + { + m_stack.push_top(); + m_parent = &m_stack.top(1); // don't use m_curr. watch out for relocations inside the prev push + m_curr = &m_stack.top(); + m_curr->reset_after_push(); + } + + void _stack_pop() + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_parent); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_stack.size() > 1); + m_parent->reset_before_pop(*m_curr); + m_stack.pop(); + m_parent = m_stack.size() > 1 ? &m_stack.top(1) : nullptr; + m_curr = &m_stack.top(); + #ifdef RYML_DBG + if(m_parent) + _c4dbgpf("popped! top is now node={} (parent={})", m_curr->node_id, m_parent->node_id); + else + _c4dbgpf("popped! top is now node={} @ ROOT", m_curr->node_id); + #endif + } + +protected: + + // undefined at the end + #define _has_any_(bits) (static_cast(this)->template _has_any__()) + + bool _stack_should_push_on_begin_doc() const + { + const bool is_root = (m_stack.size() == 1u); + return is_root && (_has_any_(DOC|VAL|MAP|SEQ) || m_curr->has_children); + } + + bool _stack_should_pop_on_end_doc() const + { + const bool is_root = (m_stack.size() == 1u); + return !is_root && _has_any_(DOC); + } + +protected: + + void _stack_relocate_to_new_arena(csubstr prev, substr curr) + { + for(state &st : m_stack) + { + if(st.line_contents.rem.is_sub(prev)) + st.line_contents.rem = _stack_relocate_to_new_arena(st.line_contents.rem, prev, curr); + if(st.line_contents.full.is_sub(prev)) + st.line_contents.full = _stack_relocate_to_new_arena(st.line_contents.full, prev, curr); + if(st.line_contents.stripped.is_sub(prev)) + st.line_contents.stripped = _stack_relocate_to_new_arena(st.line_contents.stripped, prev, curr); + } + _RYML_CB_ASSERT(m_stack.m_callbacks, m_relocate_arena != nullptr); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_relocate_arena_data != nullptr); + m_relocate_arena(m_relocate_arena_data, prev, curr); + } + + substr _stack_relocate_to_new_arena(csubstr s, csubstr prev, substr curr) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, prev.is_super(s)); + auto pos = s.str - prev.str; + substr out = {curr.str + pos, s.len}; + _RYML_CB_ASSERT(m_stack.m_callbacks, curr.is_super(out)); + return out; + } + +public: + + /** Check whether the current parse tokens are trailing on the + * previous doc, and raise an error if they are. This function is + * called by the parse engine (not the event handler) before a doc + * is started. */ + void check_trailing_doc_token() const + { + const bool is_root = (m_stack.size() == 1u); + const bool isndoc = (m_curr->flags & NDOC) != 0; + const bool suspicious = _has_any_(MAP|SEQ|VAL); + _c4dbgpf("target={} isroot={} suspicious={} ndoc={}", m_curr->node_id, is_root, suspicious, isndoc); + if((is_root || _has_any_(DOC)) && suspicious && !isndoc) + _RYML_CB_ERR_(m_stack.m_callbacks, "parse error", m_curr->pos); + } + +protected: + + #undef _has_any_ + +}; + +/** @} */ + +} // namespace yml +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise) + +#endif /* _C4_YML_EVENT_HANDLER_STACK_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/event_handler_tree.hpp b/3rdparty/rapidyaml/include/c4/yml/event_handler_tree.hpp new file mode 100644 index 0000000000..6b35adabb1 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/event_handler_tree.hpp @@ -0,0 +1,768 @@ +#ifndef _C4_YML_EVENT_HANDLER_TREE_HPP_ +#define _C4_YML_EVENT_HANDLER_TREE_HPP_ + +#ifndef _C4_YML_TREE_HPP_ +#include "c4/yml/tree.hpp" +#endif + +#ifndef _C4_YML_EVENT_HANDLER_STACK_HPP_ +#include "c4/yml/event_handler_stack.hpp" +#endif + +C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4702) // unreachable code +// NOLINTBEGIN(hicpp-signed-bitwise) + +namespace c4 { +namespace yml { + +/** @addtogroup doc_event_handlers + * @{ */ + + +/** @cond dev */ +struct EventHandlerTreeState : public ParserState +{ + NodeData *tr_data; +}; +/** @endcond */ + + +/** The event handler to create a ryml @ref Tree. See the + * documentation for @ref doc_event_handlers, which has important + * notes about the event model used by rapidyaml. */ +struct EventHandlerTree : public EventHandlerStack +{ + + /** @name types + * @{ */ + + using state = EventHandlerTreeState; + + /** @} */ + +public: + + /** @cond dev */ + Tree *C4_RESTRICT m_tree; + id_type m_id; + size_t m_num_directives; + bool m_yaml_directive; + + #ifdef RYML_DBG + #define _enable_(bits) _enable__(); _c4dbgpf("node[{}]: enable {}", m_curr->node_id, #bits) + #define _disable_(bits) _disable__(); _c4dbgpf("node[{}]: disable {}", m_curr->node_id, #bits) + #else + #define _enable_(bits) _enable__() + #define _disable_(bits) _disable__() + #endif + #define _has_any_(bits) _has_any__() + /** @endcond */ + +public: + + /** @name construction and resetting + * @{ */ + + EventHandlerTree() : EventHandlerStack(), m_tree(), m_id(NONE), m_num_directives(), m_yaml_directive() {} + EventHandlerTree(Callbacks const& cb) : EventHandlerStack(cb), m_tree(), m_id(NONE), m_num_directives(), m_yaml_directive() {} + EventHandlerTree(Tree *tree, id_type id) : EventHandlerStack(tree->callbacks()), m_tree(tree), m_id(id), m_num_directives(), m_yaml_directive() + { + reset(tree, id); + } + + void reset(Tree *tree, id_type id) + { + if(C4_UNLIKELY(!tree)) + _RYML_CB_ERR(m_stack.m_callbacks, "null tree"); + if(C4_UNLIKELY(id >= tree->capacity())) + _RYML_CB_ERR(tree->callbacks(), "invalid node"); + if(C4_UNLIKELY(!tree->is_root(id))) + if(C4_UNLIKELY(tree->is_map(tree->parent(id)))) + if(C4_UNLIKELY(!tree->has_key(id))) + _RYML_CB_ERR(tree->callbacks(), "destination node belongs to a map and has no key"); + m_tree = tree; + m_id = id; + if(m_tree->is_root(id)) + { + _stack_reset_root(); + _reset_parser_state(m_curr, id, m_tree->root_id()); + } + else + { + _stack_reset_non_root(); + _reset_parser_state(m_parent, id, m_tree->parent(id)); + _reset_parser_state(m_curr, id, id); + } + m_num_directives = 0; + m_yaml_directive = false; + } + + /** @} */ + +public: + + /** @name parse events + * @{ */ + + void start_parse(const char* filename, detail::pfn_relocate_arena relocate_arena, void *relocate_arena_data) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree != nullptr); + this->_stack_start_parse(filename, relocate_arena, relocate_arena_data); + } + + void finish_parse() + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree != nullptr); + if(m_num_directives && !m_tree->is_stream(m_tree->root_id())) + _RYML_CB_ERR_(m_stack.m_callbacks, "directives cannot be used without a document", {}); + this->_stack_finish_parse(); + /* This pointer is temporary. Remember that: + * + * - this handler object may be held by the user + * - it may be used with a temporary tree inside the parse function + * - when the parse function returns the temporary tree, its address + * will change + * + * As a result, the user could try to read the tree from m_tree, and + * end up reading the stale temporary object. + * + * So it is better to clear it here; then the user will get an obvious + * segfault if reading from m_tree. */ + m_tree = nullptr; + } + + void cancel_parse() + { + m_tree = nullptr; + } + + /** @} */ + +public: + + /** @name YAML stream events */ + /** @{ */ + + C4_ALWAYS_INLINE void begin_stream() const noexcept { /*nothing to do*/ } + + C4_ALWAYS_INLINE void end_stream() const noexcept { /*nothing to do*/ } + + /** @} */ + +public: + + /** @name YAML document events */ + /** @{ */ + + /** implicit doc start (without ---) */ + void begin_doc() + { + _c4dbgp("begin_doc"); + if(_stack_should_push_on_begin_doc()) + { + _c4dbgp("push!"); + _set_root_as_stream(); + _push(); + _enable_(DOC); + } + } + /** implicit doc end (without ...) */ + void end_doc() + { + _c4dbgp("end_doc"); + if(_stack_should_pop_on_end_doc()) + { + _remove_speculative(); + _c4dbgp("pop!"); + _pop(); + } + } + + /** explicit doc start, with --- */ + void begin_doc_expl() + { + _c4dbgp("begin_doc_expl"); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->root_id() == m_curr->node_id); + if(!m_tree->is_stream(m_tree->root_id())) //if(_should_push_on_begin_doc()) + { + _c4dbgp("ensure stream"); + _set_root_as_stream(); + id_type first = m_tree->first_child(m_tree->root_id()); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_stream(m_tree->root_id())); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->num_children(m_tree->root_id()) == 1u); + if(m_tree->has_children(first) || m_tree->is_val(first)) + { + _c4dbgp("push!"); + _push(); + } + else + { + _c4dbgp("tweak"); + _push(); + _remove_speculative(); + m_curr->node_id = m_tree->last_child(m_tree->root_id()); + m_curr->tr_data = m_tree->_p(m_curr->node_id); + } + } + else + { + _c4dbgp("push!"); + _push(); + } + _enable_(DOC); + } + /** explicit doc end, with ... */ + void end_doc_expl() + { + _c4dbgp("end_doc_expl"); + _remove_speculative(); + if(_stack_should_pop_on_end_doc()) + { + _c4dbgp("pop!"); + _pop(); + } + m_yaml_directive = false; + } + + /** @} */ + +public: + + /** @name YAML map events */ + /** @{ */ + + void begin_map_key_flow() + { + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + } + void begin_map_key_block() + { + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + } + + void begin_map_val_flow() + { + _c4dbgpf("node[{}]: begin_map_val_flow", m_curr->node_id); + _RYML_CB_CHECK(m_stack.m_callbacks, !_has_any_(VAL)); + _enable_(MAP|FLOW_SL); + _save_loc(); + _push(); + } + void begin_map_val_block() + { + _c4dbgpf("node[{}]: begin_map_val_block", m_curr->node_id); + _RYML_CB_CHECK(m_stack.m_callbacks, !_has_any_(VAL)); + _enable_(MAP|BLOCK); + _save_loc(); + _push(); + } + + void end_map() + { + _pop(); + _c4dbgpf("node[{}]: end_map_val", m_curr->node_id); + } + + /** @} */ + +public: + + /** @name YAML seq events */ + /** @{ */ + + void begin_seq_key_flow() + { + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + } + void begin_seq_key_block() + { + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + } + + void begin_seq_val_flow() + { + _c4dbgpf("node[{}]: begin_seq_val_flow", m_curr->node_id); + _RYML_CB_CHECK(m_stack.m_callbacks, !_has_any_(VAL)); + _enable_(SEQ|FLOW_SL); + _save_loc(); + _push(); + } + void begin_seq_val_block() + { + _c4dbgpf("node[{}]: begin_seq_val_block", m_curr->node_id); + _RYML_CB_CHECK(m_stack.m_callbacks, !_has_any_(VAL)); + _enable_(SEQ|BLOCK); + _save_loc(); + _push(); + } + + void end_seq() + { + _pop(); + _c4dbgpf("node[{}]: end_seq_val", m_curr->node_id); + } + + /** @} */ + +public: + + /** @name YAML structure events */ + /** @{ */ + + void add_sibling() + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_parent); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->has_children(m_parent->node_id)); + NodeData const* prev = m_tree->m_buf; // watchout against relocation of the tree nodes + _set_state_(m_curr, m_tree->_append_child__unprotected(m_parent->node_id)); + if(prev != m_tree->m_buf) + _refresh_after_relocation(); + _c4dbgpf("node[{}]: added sibling={} prev={}", m_parent->node_id, m_curr->node_id, m_tree->prev_sibling(m_curr->node_id)); + } + + /** set the previous val as the first key of a new map, with flow style. + * + * See the documentation for @ref doc_event_handlers, which has + * important notes about this event. + */ + void actually_val_is_first_key_of_new_map_flow() + { + if(C4_UNLIKELY(m_tree->is_container(m_curr->node_id))) + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_parent); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_seq(m_parent->node_id)); + _RYML_CB_ASSERT(m_stack.m_callbacks, !m_tree->is_container(m_curr->node_id)); + _RYML_CB_ASSERT(m_stack.m_callbacks, !m_tree->has_key(m_curr->node_id)); + const NodeData tmp = _val2key_(*m_curr->tr_data); + _disable_(_VALMASK|VAL_STYLE); + m_curr->tr_data->m_val = {}; + begin_map_val_flow(); + m_curr->tr_data->m_type = tmp.m_type; + m_curr->tr_data->m_key = tmp.m_key; + } + + /** like its flow counterpart, but this function can only be + * called after the end of a flow-val at root or doc level. + * + * See the documentation for @ref doc_event_handlers, which has + * important notes about this event. + */ + void actually_val_is_first_key_of_new_map_block() + { + _RYML_CB_ERR_(m_stack.m_callbacks, "ryml trees cannot handle containers as keys", m_curr->pos); + } + + /** @} */ + +public: + + /** @name YAML scalar events */ + /** @{ */ + + + C4_ALWAYS_INLINE void set_key_scalar_plain_empty() noexcept + { + _c4dbgpf("node[{}]: set key scalar plain as empty", m_curr->node_id); + m_curr->tr_data->m_key.scalar = {}; + _enable_(KEY|KEY_PLAIN|KEYNIL); + } + C4_ALWAYS_INLINE void set_val_scalar_plain_empty() noexcept + { + _c4dbgpf("node[{}]: set val scalar plain as empty", m_curr->node_id); + m_curr->tr_data->m_val.scalar = {}; + _enable_(VAL|VAL_PLAIN|VALNIL); + } + + C4_ALWAYS_INLINE void set_key_scalar_plain(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set key scalar plain: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_key.scalar = scalar; + _enable_(KEY|KEY_PLAIN); + } + C4_ALWAYS_INLINE void set_val_scalar_plain(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set val scalar plain: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_val.scalar = scalar; + _enable_(VAL|VAL_PLAIN); + } + + + C4_ALWAYS_INLINE void set_key_scalar_dquoted(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set key scalar dquot: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_key.scalar = scalar; + _enable_(KEY|KEY_DQUO); + } + C4_ALWAYS_INLINE void set_val_scalar_dquoted(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set val scalar dquot: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_val.scalar = scalar; + _enable_(VAL|VAL_DQUO); + } + + + C4_ALWAYS_INLINE void set_key_scalar_squoted(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set key scalar squot: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_key.scalar = scalar; + _enable_(KEY|KEY_SQUO); + } + C4_ALWAYS_INLINE void set_val_scalar_squoted(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set val scalar squot: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_val.scalar = scalar; + _enable_(VAL|VAL_SQUO); + } + + + C4_ALWAYS_INLINE void set_key_scalar_literal(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set key scalar literal: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_key.scalar = scalar; + _enable_(KEY|KEY_LITERAL); + } + C4_ALWAYS_INLINE void set_val_scalar_literal(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set val scalar literal: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_val.scalar = scalar; + _enable_(VAL|VAL_LITERAL); + } + + + C4_ALWAYS_INLINE void set_key_scalar_folded(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set key scalar folded: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_key.scalar = scalar; + _enable_(KEY|KEY_FOLDED); + } + C4_ALWAYS_INLINE void set_val_scalar_folded(csubstr scalar) noexcept + { + _c4dbgpf("node[{}]: set val scalar folded: [{}]~~~{}~~~", m_curr->node_id, scalar.len, scalar); + m_curr->tr_data->m_val.scalar = scalar; + _enable_(VAL|VAL_FOLDED); + } + + + C4_ALWAYS_INLINE void mark_key_scalar_unfiltered() noexcept + { + _enable_(KEY_UNFILT); + } + C4_ALWAYS_INLINE void mark_val_scalar_unfiltered() noexcept + { + _enable_(VAL_UNFILT); + } + + /** @} */ + +public: + + /** @name YAML anchor/reference events */ + /** @{ */ + + void set_key_anchor(csubstr anchor) + { + _c4dbgpf("node[{}]: set key anchor: [{}]~~~{}~~~", m_curr->node_id, anchor.len, anchor); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_stack.m_callbacks, !_has_any_(KEYREF)); + _RYML_CB_ASSERT(m_stack.m_callbacks, !anchor.begins_with('&')); + _enable_(KEYANCH); + m_curr->tr_data->m_key.anchor = anchor; + } + void set_val_anchor(csubstr anchor) + { + _c4dbgpf("node[{}]: set val anchor: [{}]~~~{}~~~", m_curr->node_id, anchor.len, anchor); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_stack.m_callbacks, !_has_any_(VALREF)); + _RYML_CB_ASSERT(m_stack.m_callbacks, !anchor.begins_with('&')); + _enable_(VALANCH); + m_curr->tr_data->m_val.anchor = anchor; + } + + void set_key_ref(csubstr ref) + { + _c4dbgpf("node[{}]: set key ref: [{}]~~~{}~~~", m_curr->node_id, ref.len, ref); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + if(C4_UNLIKELY(_has_any_(KEYANCH))) + _RYML_CB_ERR_(m_tree->callbacks(), "key cannot have both anchor and ref", m_curr->pos); + _RYML_CB_ASSERT(m_tree->callbacks(), ref.begins_with('*')); + _enable_(KEY|KEYREF); + m_curr->tr_data->m_key.anchor = ref.sub(1); + m_curr->tr_data->m_key.scalar = ref; + } + void set_val_ref(csubstr ref) + { + _c4dbgpf("node[{}]: set val ref: [{}]~~~{}~~~", m_curr->node_id, ref.len, ref); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + if(C4_UNLIKELY(_has_any_(VALANCH))) + _RYML_CB_ERR_(m_tree->callbacks(), "val cannot have both anchor and ref", m_curr->pos); + _RYML_CB_ASSERT(m_tree->callbacks(), ref.begins_with('*')); + _enable_(VAL|VALREF); + m_curr->tr_data->m_val.anchor = ref.sub(1); + m_curr->tr_data->m_val.scalar = ref; + } + + /** @} */ + +public: + + /** @name YAML tag events */ + /** @{ */ + + void set_key_tag(csubstr tag) noexcept + { + _c4dbgpf("node[{}]: set key tag: [{}]~~~{}~~~", m_curr->node_id, tag.len, tag); + _enable_(KEYTAG); + m_curr->tr_data->m_key.tag = tag; + } + void set_val_tag(csubstr tag) noexcept + { + _c4dbgpf("node[{}]: set val tag: [{}]~~~{}~~~", m_curr->node_id, tag.len, tag); + _enable_(VALTAG); + m_curr->tr_data->m_val.tag = tag; + } + + /** @} */ + +public: + + /** @name YAML directive events */ + /** @{ */ + + C4_NO_INLINE void add_directive(csubstr directive) + { + _c4dbgpf("% directive! {}", directive); + _RYML_CB_ASSERT(m_tree->callbacks(), directive.begins_with('%')); + if(directive.begins_with("%TAG")) + { + if(C4_UNLIKELY(!m_tree->add_tag_directive(directive))) + _RYML_CB_ERR_(m_stack.m_callbacks, "failed to add directive", m_curr->pos); + } + else if(directive.begins_with("%YAML")) + { + _c4dbgpf("%YAML directive! ignoring...: {}", directive); + if(C4_UNLIKELY(m_yaml_directive)) + _RYML_CB_ERR_(m_stack.m_callbacks, "multiple yaml directives", m_curr->pos); + m_yaml_directive = true; + } + else + { + _c4dbgpf("unknown directive! ignoring... {}", directive); + } + ++m_num_directives; + } + + /** @} */ + +public: + + /** @name arena functions */ + /** @{ */ + + substr alloc_arena(size_t len) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + csubstr prev = m_tree->arena(); + substr out = m_tree->alloc_arena(len); + substr curr = m_tree->arena(); + if(curr.str != prev.str) + _stack_relocate_to_new_arena(prev, curr); + return out; + } + + substr alloc_arena(size_t len, substr *relocated) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + csubstr prev = m_tree->arena(); + if(!prev.is_super(*relocated)) + return alloc_arena(len); + substr out = alloc_arena(len); + substr curr = m_tree->arena(); + if(curr.str != prev.str) + *relocated = _stack_relocate_to_new_arena(*relocated, prev, curr); + return out; + } + + /** @} */ + +public: + + /** @cond dev */ + void _reset_parser_state(state* st, id_type parse_root, id_type node) + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _set_state_(st, node); + const NodeType type = m_tree->type(node); + #ifdef RYML_DBG + char flagbuf[80]; + _c4dbgpf("resetting state: initial flags={}", detail::_parser_flags_to_str(flagbuf, st->flags)); + #endif + if(type == NOTYPE) + { + _c4dbgpf("node[{}] is notype", node); + if(m_tree->is_root(parse_root)) + { + _c4dbgpf("node[{}] is root", node); + st->flags |= RUNK|RTOP; + } + else + { + _c4dbgpf("node[{}] is not root. setting USTY", node); + st->flags |= USTY; + } + } + else if(type.is_map()) + { + _c4dbgpf("node[{}] is map", node); + st->flags |= RMAP|USTY; + } + else if(type.is_seq()) + { + _c4dbgpf("node[{}] is map", node); + st->flags |= RSEQ|USTY; + } + else if(type.has_key()) + { + _c4dbgpf("node[{}] has key. setting USTY", node); + st->flags |= USTY; + } + else + { + _RYML_CB_ERR(m_tree->callbacks(), "cannot append to node"); + } + if(type.is_doc()) + { + _c4dbgpf("node[{}] is doc", node); + st->flags |= RDOC; + } + #ifdef RYML_DBG + _c4dbgpf("resetting state: final flags={}", detail::_parser_flags_to_str(flagbuf, st->flags)); + #endif + } + + /** push a new parent, add a child to the new parent, and set the + * child as the current node */ + void _push() + { + _stack_push(); + NodeData const* prev = m_tree->m_buf; // watch out against relocation of the tree nodes + m_curr->node_id = m_tree->_append_child__unprotected(m_parent->node_id); + m_curr->tr_data = m_tree->_p(m_curr->node_id); + if(prev != m_tree->m_buf) + _refresh_after_relocation(); + _c4dbgpf("pushed! level={}. top is now node={} (parent={})", m_curr->level, m_curr->node_id, m_parent ? m_parent->node_id : NONE); + } + /** end the current scope */ + void _pop() + { + _remove_speculative_with_parent(); + _stack_pop(); + } + +public: + + template C4_HOT C4_ALWAYS_INLINE void _enable__() noexcept + { + m_curr->tr_data->m_type.type = static_cast(m_curr->tr_data->m_type.type | bits); + } + template C4_HOT C4_ALWAYS_INLINE void _disable__() noexcept + { + m_curr->tr_data->m_type.type = static_cast(m_curr->tr_data->m_type.type & (~bits)); + } + template C4_HOT C4_ALWAYS_INLINE bool _has_any__() const noexcept + { + return (m_curr->tr_data->m_type.type & bits) != 0; + } + +public: + + C4_ALWAYS_INLINE void _set_state_(state *C4_RESTRICT s, id_type id) const noexcept + { + s->node_id = id; + s->tr_data = m_tree->_p(id); + } + void _refresh_after_relocation() + { + _c4dbgp("tree: refreshing stack data after tree data relocation"); + for(auto &st : m_stack) + st.tr_data = m_tree->_p(st.node_id); + } + + void _set_root_as_stream() + { + _c4dbgp("set root as stream"); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->root_id() == 0u); + _RYML_CB_ASSERT(m_tree->callbacks(), m_curr->node_id == 0u); + const bool hack = !m_tree->has_children(m_curr->node_id) && !m_tree->is_val(m_curr->node_id); + if(hack) + m_tree->_p(m_tree->root_id())->m_type.add(VAL); + m_tree->set_root_as_stream(); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_stream(m_tree->root_id())); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->has_children(m_tree->root_id())); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_doc(m_tree->first_child(m_tree->root_id()))); + if(hack) + m_tree->_p(m_tree->first_child(m_tree->root_id()))->m_type.rem(VAL); + _set_state_(m_curr, m_tree->root_id()); + } + + static NodeData _val2key_(NodeData const& C4_RESTRICT d) noexcept + { + NodeData r = d; + r.m_key = d.m_val; + r.m_val = {}; + r.m_type = d.m_type; + static_assert((_VALMASK >> 1u) == _KEYMASK, "required for this function to work"); + static_assert((VAL_STYLE >> 1u) == KEY_STYLE, "required for this function to work"); + r.m_type.type = ((d.m_type.type & (_VALMASK|VAL_STYLE)) >> 1u); + r.m_type.type = (r.m_type.type & ~(_VALMASK|VAL_STYLE)); + r.m_type.type = (r.m_type.type | KEY); + return r; + } + + void _remove_speculative() + { + _c4dbgp("remove speculative node"); + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->empty()); + const id_type last_added = m_tree->size() - 1; + if(m_tree->has_parent(last_added)) + if(m_tree->_p(last_added)->m_type == NOTYPE) + m_tree->remove(last_added); + } + + void _remove_speculative_with_parent() + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->empty()); + const id_type last_added = m_tree->size() - 1; + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->has_parent(last_added)); + if(m_tree->_p(last_added)->m_type == NOTYPE) + { + _c4dbgpf("remove speculative node with parent. parent={} node={} parent(node)={}", m_parent->node_id, last_added, m_tree->parent(last_added)); + m_tree->remove(last_added); + } + } + + C4_ALWAYS_INLINE void _save_loc() + { + _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree); + _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->_p(m_curr->node_id)->m_val.scalar.len == 0); + m_tree->_p(m_curr->node_id)->m_val.scalar.str = m_curr->line_contents.rem.str; + } + +#undef _enable_ +#undef _disable_ +#undef _has_any_ + + /** @endcond */ +}; + +/** @} */ + +} // namespace yml +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise) +C4_SUPPRESS_WARNING_MSVC_POP + +#endif /* _C4_YML_EVENT_HANDLER_TREE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/export.hpp b/3rdparty/rapidyaml/include/c4/yml/export.hpp new file mode 100644 index 0000000000..6b77f3f8dd --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/export.hpp @@ -0,0 +1,18 @@ +#ifndef C4_YML_EXPORT_HPP_ +#define C4_YML_EXPORT_HPP_ + +#ifdef _WIN32 + #ifdef RYML_SHARED + #ifdef RYML_EXPORTS + #define RYML_EXPORT __declspec(dllexport) + #else + #define RYML_EXPORT __declspec(dllimport) + #endif + #else + #define RYML_EXPORT + #endif +#else + #define RYML_EXPORT +#endif + +#endif /* C4_YML_EXPORT_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/filter_processor.hpp b/3rdparty/rapidyaml/include/c4/yml/filter_processor.hpp new file mode 100644 index 0000000000..2dc3810d35 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/filter_processor.hpp @@ -0,0 +1,512 @@ +#ifndef _C4_YML_FILTER_PROCESSOR_HPP_ +#define _C4_YML_FILTER_PROCESSOR_HPP_ + +#include "c4/yml/common.hpp" + +#ifdef RYML_DBG +#include "c4/charconv.hpp" +#include "c4/yml/detail/dbgprint.hpp" +#endif + +namespace c4 { +namespace yml { + +/** @defgroup doc_filter_processors Scalar filter processors + * + * These are internal classes used by @ref ParseEngine to parse the + * scalars; normally there is no reason for a user to be manually + * using these classes. + * + * @ingroup doc_parse */ +/** @{ */ + +//----------------------------------------------------------------------------- + +/** Filters an input string into a different output string */ +struct FilterProcessorSrcDst +{ + csubstr src; + substr dst; + size_t rpos; ///< read position + size_t wpos; ///< write position + + C4_ALWAYS_INLINE FilterProcessorSrcDst(csubstr src_, substr dst_) noexcept + : src(src_) + , dst(dst_) + , rpos(0) + , wpos(0) + { + RYML_ASSERT(!dst.overlaps(src)); + } + + C4_ALWAYS_INLINE void setwpos(size_t wpos_) noexcept { wpos = wpos_; } + C4_ALWAYS_INLINE void setpos(size_t rpos_, size_t wpos_) noexcept { rpos = rpos_; wpos = wpos_; } + C4_ALWAYS_INLINE void set_at_end() noexcept { skip(src.len - rpos); } + + C4_ALWAYS_INLINE bool has_more_chars() const noexcept { return rpos < src.len; } + C4_ALWAYS_INLINE bool has_more_chars(size_t maxpos) const noexcept { RYML_ASSERT(maxpos <= src.len); return rpos < maxpos; } + + C4_ALWAYS_INLINE csubstr rem() const noexcept { return src.sub(rpos); } + C4_ALWAYS_INLINE csubstr sofar() const noexcept { return csubstr(dst.str, wpos <= dst.len ? wpos : dst.len); } + C4_ALWAYS_INLINE FilterResult result() const noexcept + { + FilterResult ret; + ret.str.str = wpos <= dst.len ? dst.str : nullptr; + ret.str.len = wpos; + return ret; + } + + C4_ALWAYS_INLINE char curr() const noexcept { RYML_ASSERT(rpos < src.len); return src[rpos]; } + C4_ALWAYS_INLINE char next() const noexcept { return rpos+1 < src.len ? src[rpos+1] : '\0'; } + C4_ALWAYS_INLINE bool skipped_chars() const noexcept { return wpos != rpos; } + + C4_ALWAYS_INLINE void skip() noexcept { ++rpos; } + C4_ALWAYS_INLINE void skip(size_t num) noexcept { rpos += num; } + + C4_ALWAYS_INLINE void set_at(size_t pos, char c) noexcept // NOLINT(readability-make-member-function-const) + { + RYML_ASSERT(pos < wpos); + dst.str[pos] = c; + } + C4_ALWAYS_INLINE void set(char c) noexcept + { + if(wpos < dst.len) + dst.str[wpos] = c; + ++wpos; + } + C4_ALWAYS_INLINE void set(char c, size_t num) noexcept + { + RYML_ASSERT(num > 0); + if(wpos + num <= dst.len) + memset(dst.str + wpos, c, num); + wpos += num; + } + + C4_ALWAYS_INLINE void copy() noexcept + { + RYML_ASSERT(rpos < src.len); + if(wpos < dst.len) + dst.str[wpos] = src.str[rpos]; + ++wpos; + ++rpos; + } + C4_ALWAYS_INLINE void copy(size_t num) noexcept + { + RYML_ASSERT(num); + RYML_ASSERT(rpos+num <= src.len); + if(wpos + num <= dst.len) + memcpy(dst.str + wpos, src.str + rpos, num); + wpos += num; + rpos += num; + } + + C4_ALWAYS_INLINE void translate_esc(char c) noexcept + { + if(wpos < dst.len) + dst.str[wpos] = c; + ++wpos; + rpos += 2; + } + C4_ALWAYS_INLINE void translate_esc_bulk(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + RYML_ASSERT(nw > 0); + RYML_ASSERT(nr > 0); + RYML_ASSERT(rpos+nr <= src.len); + if(wpos+nw <= dst.len) + memcpy(dst.str + wpos, s, nw); + wpos += nw; + rpos += 1 + nr; + } + C4_ALWAYS_INLINE void translate_esc_extending(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + translate_esc_bulk(s, nw, nr); + } +}; + + +//----------------------------------------------------------------------------- +// filter in place + +// debugging scaffold +/** @cond dev */ +#if defined(RYML_DBG) && 0 +#define _c4dbgip(...) _c4dbgpf(__VA_ARGS__) +#else +#define _c4dbgip(...) +#endif +/** @endcond */ + +/** Filters in place. While the result may be larger than the source, + * any extending happens only at the end of the string. Consequently, + * it's impossible for characters to be left unfiltered. + * + * @see FilterProcessorInplaceMidExtending */ +struct FilterProcessorInplaceEndExtending +{ + substr src; ///< the subject string + size_t wcap; ///< write capacity - the capacity of the subject string's buffer + size_t rpos; ///< read position + size_t wpos; ///< write position + + C4_ALWAYS_INLINE FilterProcessorInplaceEndExtending(substr src_, size_t wcap_) noexcept + : src(src_) + , wcap(wcap_) + , rpos(0) + , wpos(0) + { + RYML_ASSERT(wcap >= src.len); + } + + C4_ALWAYS_INLINE void setwpos(size_t wpos_) noexcept { wpos = wpos_; } + C4_ALWAYS_INLINE void setpos(size_t rpos_, size_t wpos_) noexcept { rpos = rpos_; wpos = wpos_; } + C4_ALWAYS_INLINE void set_at_end() noexcept { skip(src.len - rpos); } + + C4_ALWAYS_INLINE bool has_more_chars() const noexcept { return rpos < src.len; } + C4_ALWAYS_INLINE bool has_more_chars(size_t maxpos) const noexcept { RYML_ASSERT(maxpos <= src.len); return rpos < maxpos; } + + C4_ALWAYS_INLINE FilterResult result() const noexcept + { + _c4dbgip("inplace: wpos={} wcap={} small={}", wpos, wcap, wpos > rpos); + FilterResult ret; + ret.str.str = (wpos <= wcap) ? src.str : nullptr; + ret.str.len = wpos; + return ret; + } + C4_ALWAYS_INLINE csubstr sofar() const noexcept { return csubstr(src.str, wpos <= wcap ? wpos : wcap); } + C4_ALWAYS_INLINE csubstr rem() const noexcept { return src.sub(rpos); } + + C4_ALWAYS_INLINE char curr() const noexcept { RYML_ASSERT(rpos < src.len); return src[rpos]; } + C4_ALWAYS_INLINE char next() const noexcept { return rpos+1 < src.len ? src[rpos+1] : '\0'; } + + C4_ALWAYS_INLINE void skip() noexcept { ++rpos; } + C4_ALWAYS_INLINE void skip(size_t num) noexcept { rpos += num; } + + void set_at(size_t pos, char c) noexcept + { + RYML_ASSERT(pos < wpos); + const size_t save = wpos; + wpos = pos; + set(c); + wpos = save; + } + void set(char c) noexcept + { + if(wpos < wcap) // respect write-capacity + src.str[wpos] = c; + ++wpos; + } + void set(char c, size_t num) noexcept + { + RYML_ASSERT(num); + if(wpos + num <= wcap) // respect write-capacity + memset(src.str + wpos, c, num); + wpos += num; + } + + void copy() noexcept + { + RYML_ASSERT(wpos <= rpos); + RYML_ASSERT(rpos < src.len); + if(wpos < wcap) // respect write-capacity + src.str[wpos] = src.str[rpos]; + ++rpos; + ++wpos; + } + void copy(size_t num) noexcept + { + RYML_ASSERT(num); + RYML_ASSERT(rpos+num <= src.len); + RYML_ASSERT(wpos <= rpos); + if(wpos + num <= wcap) // respect write-capacity + { + if(wpos + num <= rpos) // there is no overlap + memcpy(src.str + wpos, src.str + rpos, num); + else // there is overlap + memmove(src.str + wpos, src.str + rpos, num); + } + rpos += num; + wpos += num; + } + + void translate_esc(char c) noexcept + { + RYML_ASSERT(rpos + 2 <= src.len); + RYML_ASSERT(wpos <= rpos); + if(wpos < wcap) // respect write-capacity + src.str[wpos] = c; + rpos += 2; // add 1u to account for the escape character + ++wpos; + } + + void translate_esc_bulk(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + RYML_ASSERT(nw > 0); + RYML_ASSERT(nr > 0); + RYML_ASSERT(nw <= nr + 1u); + RYML_ASSERT(rpos+nr <= src.len); + RYML_ASSERT(wpos <= rpos); + const size_t wpos_next = wpos + nw; + const size_t rpos_next = rpos + nr + 1u; // add 1u to account for the escape character + RYML_ASSERT(wpos_next <= rpos_next); + if(wpos_next <= wcap) + memcpy(src.str + wpos, s, nw); + rpos = rpos_next; + wpos = wpos_next; + } + + C4_ALWAYS_INLINE void translate_esc_extending(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + translate_esc_bulk(s, nw, nr); + } +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** Filters in place. The result may be larger than the source, and + * extending may happen anywhere. As a result some characters may be + * left unfiltered when there is no slack in the buffer and the + * write-position would overlap the read-position. Consequently, it's + * possible for characters to be left unfiltered. In YAML, this + * happens only with double-quoted strings, and only with a small + * number of escape sequences such as `\L` which is substituted by three + * bytes. These escape sequences cause a call to translate_esc_extending() + * which is the only entry point to this unfiltered situation. + * + * @see FilterProcessorInplaceMidExtending */ +struct FilterProcessorInplaceMidExtending +{ + substr src; ///< the subject string + size_t wcap; ///< write capacity - the capacity of the subject string's buffer + size_t rpos; ///< read position + size_t wpos; ///< write position + size_t maxcap; ///< the max capacity needed for filtering the string. This may be larger than the final string size. + bool unfiltered_chars; ///< number of characters that were not added to wpos from lack of capacity + + C4_ALWAYS_INLINE FilterProcessorInplaceMidExtending(substr src_, size_t wcap_) noexcept + : src(src_) + , wcap(wcap_) + , rpos(0) + , wpos(0) + , maxcap(src.len) + , unfiltered_chars(false) + { + RYML_ASSERT(wcap >= src.len); + } + + C4_ALWAYS_INLINE void setwpos(size_t wpos_) noexcept { wpos = wpos_; } + C4_ALWAYS_INLINE void setpos(size_t rpos_, size_t wpos_) noexcept { rpos = rpos_; wpos = wpos_; } + C4_ALWAYS_INLINE void set_at_end() noexcept { skip(src.len - rpos); } + + C4_ALWAYS_INLINE bool has_more_chars() const noexcept { return rpos < src.len; } + C4_ALWAYS_INLINE bool has_more_chars(size_t maxpos) const noexcept { RYML_ASSERT(maxpos <= src.len); return rpos < maxpos; } + + C4_ALWAYS_INLINE FilterResultExtending result() const noexcept + { + _c4dbgip("inplace: wpos={} wcap={} unfiltered={} maxcap={}", this->wpos, this->wcap, this->unfiltered_chars, this->maxcap); + FilterResultExtending ret; + ret.str.str = (wpos <= wcap && !unfiltered_chars) ? src.str : nullptr; + ret.str.len = wpos; + ret.reqlen = maxcap; + return ret; + } + C4_ALWAYS_INLINE csubstr sofar() const noexcept { return csubstr(src.str, wpos <= wcap ? wpos : wcap); } + C4_ALWAYS_INLINE csubstr rem() const noexcept { return src.sub(rpos); } + + C4_ALWAYS_INLINE char curr() const noexcept { RYML_ASSERT(rpos < src.len); return src[rpos]; } + C4_ALWAYS_INLINE char next() const noexcept { return rpos+1 < src.len ? src[rpos+1] : '\0'; } + + C4_ALWAYS_INLINE void skip() noexcept { ++rpos; } + C4_ALWAYS_INLINE void skip(size_t num) noexcept { rpos += num; } + + void set_at(size_t pos, char c) noexcept + { + RYML_ASSERT(pos < wpos); + const size_t save = wpos; + wpos = pos; + set(c); + wpos = save; + } + void set(char c) noexcept + { + if(wpos < wcap) // respect write-capacity + { + if((wpos <= rpos) && !unfiltered_chars) + src.str[wpos] = c; + } + else + { + _c4dbgip("inplace: add unwritten {}->{} maxcap={}->{}!", unfiltered_chars, true, maxcap, (wpos+1u > maxcap ? wpos+1u : maxcap)); + unfiltered_chars = true; + } + ++wpos; + maxcap = wpos > maxcap ? wpos : maxcap; + } + void set(char c, size_t num) noexcept + { + RYML_ASSERT(num); + if(wpos + num <= wcap) // respect write-capacity + { + if((wpos <= rpos) && !unfiltered_chars) + memset(src.str + wpos, c, num); + } + else + { + _c4dbgip("inplace: add unwritten {}->{} maxcap={}->{}!", unfiltered_chars, true, maxcap, (wpos+num > maxcap ? wpos+num : maxcap)); + unfiltered_chars = true; + } + wpos += num; + maxcap = wpos > maxcap ? wpos : maxcap; + } + + void copy() noexcept + { + RYML_ASSERT(rpos < src.len); + if(wpos < wcap) // respect write-capacity + { + if((wpos < rpos) && !unfiltered_chars) // write only if wpos is behind rpos + src.str[wpos] = src.str[rpos]; + } + else + { + _c4dbgip("inplace: add unwritten {}->{} (wpos={}!=rpos={})={} (wpos={}{}!", unfiltered_chars, true, wpos, rpos, wpos!=rpos, wpos, wcap, wpos maxcap ? wpos+1u : maxcap)); + unfiltered_chars = true; + } + ++rpos; + ++wpos; + maxcap = wpos > maxcap ? wpos : maxcap; + } + void copy(size_t num) noexcept + { + RYML_ASSERT(num); + RYML_ASSERT(rpos+num <= src.len); + if(wpos + num <= wcap) // respect write-capacity + { + if((wpos < rpos) && !unfiltered_chars) // write only if wpos is behind rpos + { + if(wpos + num <= rpos) // there is no overlap + memcpy(src.str + wpos, src.str + rpos, num); + else // there is overlap + memmove(src.str + wpos, src.str + rpos, num); + } + } + else + { + _c4dbgip("inplace: add unwritten {}->{} (wpos={}!=rpos={})={} (wpos={}{}!", unfiltered_chars, true, wpos, rpos, wpos!=rpos, wpos, wcap, wpos maxcap ? wpos : maxcap; + } + + void translate_esc(char c) noexcept + { + RYML_ASSERT(rpos + 2 <= src.len); + if(wpos < wcap) // respect write-capacity + { + if((wpos <= rpos) && !unfiltered_chars) + src.str[wpos] = c; + } + else + { + _c4dbgip("inplace: add unfiltered {}->{} maxcap={}->{}!", unfiltered_chars, true, maxcap, (wpos+1u > maxcap ? wpos+1u : maxcap)); + unfiltered_chars = true; + } + rpos += 2; + ++wpos; + maxcap = wpos > maxcap ? wpos : maxcap; + } + + C4_NO_INLINE void translate_esc_bulk(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + RYML_ASSERT(nw > 0); + RYML_ASSERT(nr > 0); + RYML_ASSERT(nr+1u >= nw); + const size_t wpos_next = wpos + nw; + const size_t rpos_next = rpos + nr + 1u; // add 1u to account for the escape character + if(wpos_next <= wcap) // respect write-capacity + { + if((wpos <= rpos) && !unfiltered_chars) // write only if wpos is behind rpos + memcpy(src.str + wpos, s, nw); + } + else + { + _c4dbgip("inplace: add unwritten {}->{} (wpos={}!=rpos={})={} (wpos={}{}!", unfiltered_chars, true, wpos, rpos, wpos!=rpos, wpos, wcap, wpos maxcap ? wpos : maxcap; + } + + C4_NO_INLINE void translate_esc_extending(const char *C4_RESTRICT s, size_t nw, size_t nr) noexcept + { + RYML_ASSERT(nw > 0); + RYML_ASSERT(nr > 0); + RYML_ASSERT(rpos+nr <= src.len); + const size_t wpos_next = wpos + nw; + const size_t rpos_next = rpos + nr + 1u; // add 1u to account for the escape character + if(wpos_next <= rpos_next) // read and write do not overlap. just do a vanilla copy. + { + if((wpos_next <= wcap) && !unfiltered_chars) + memcpy(src.str + wpos, s, nw); + rpos = rpos_next; + wpos = wpos_next; + maxcap = wpos > maxcap ? wpos : maxcap; + } + else // there is overlap. move the (to-be-read) string to the right. + { + const size_t excess = wpos_next - rpos_next; + RYML_ASSERT(wpos_next > rpos_next); + if(src.len + excess <= wcap) // ensure we do not go past the end + { + RYML_ASSERT(rpos+nr+excess <= src.len); + if(wpos_next <= wcap) + { + if(!unfiltered_chars) + { + memmove(src.str + wpos_next, src.str + rpos_next, src.len - rpos_next); + memcpy(src.str + wpos, s, nw); + } + rpos = wpos_next; // wpos, not rpos + } + else + { + rpos = rpos_next; + //const size_t unw = nw > (nr + 1u) ? nw - (nr + 1u) : 0; + _c4dbgip("inplace: add unfiltered {}->{} maxcap={}->{}!", unfiltered_chars, true); + unfiltered_chars = true; + } + wpos = wpos_next; + // extend the string up to capacity + src.len += excess; + maxcap = wpos > maxcap ? wpos : maxcap; + } + else + { + //const size_t unw = nw > (nr + 1u) ? nw - (nr + 1u) : 0; + RYML_ASSERT(rpos_next <= src.len); + const size_t required_size = wpos_next + (src.len - rpos_next); + _c4dbgip("inplace: add unfiltered {}->{} maxcap={}->{}!", unfiltered_chars, true, maxcap, required_size > maxcap ? required_size : maxcap); + RYML_ASSERT(required_size > wcap); + unfiltered_chars = true; + maxcap = required_size > maxcap ? required_size : maxcap; + wpos = wpos_next; + rpos = rpos_next; + } + } + } +}; + +#undef _c4dbgip + + +/** @} */ + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_FILTER_PROCESSOR_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/fwd.hpp b/3rdparty/rapidyaml/include/c4/yml/fwd.hpp new file mode 100644 index 0000000000..7fa1f17699 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/fwd.hpp @@ -0,0 +1,24 @@ +#ifndef _C4_YML_FWD_HPP_ +#define _C4_YML_FWD_HPP_ + +/** @file fwd.hpp forward declarations */ + +namespace c4 { +namespace yml { + +struct NodeScalar; +struct NodeInit; +struct NodeData; +struct NodeType; +class NodeRef; +class ConstNodeRef; +class Tree; +struct ReferenceResolver; +template class ParseEngine; +struct EventHandlerTree; +using Parser = ParseEngine; + +} // namespace c4 +} // namespace yml + +#endif /* _C4_YML_FWD_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/node.hpp b/3rdparty/rapidyaml/include/c4/yml/node.hpp new file mode 100644 index 0000000000..4d74dd3d4f --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/node.hpp @@ -0,0 +1,1679 @@ +#ifndef _C4_YML_NODE_HPP_ +#define _C4_YML_NODE_HPP_ + +/** @file node.hpp Node classes */ + +#include + +#include "c4/yml/tree.hpp" +#include "c4/base64.hpp" + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wtype-limits" +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtype-limits" +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wuseless-cast" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable: 4251/*needs to have dll-interface to be used by clients of struct*/) +# pragma warning(disable: 4296/*expression is always 'boolean_value'*/) +#endif + +namespace c4 { +namespace yml { + +/** @addtogroup doc_node_classes + * + * @{ + */ + + +/** @defgroup doc_serialization_helpers Serialization helpers + * + * @{ + */ +template struct Key { K & k; }; // NOLINT +template<> struct Key { fmt::const_base64_wrapper wrapper; }; +template<> struct Key { fmt::base64_wrapper wrapper; }; + +template C4_ALWAYS_INLINE Key key(K & k) { return Key{k}; } +C4_ALWAYS_INLINE Key key(fmt::const_base64_wrapper w) { return {w}; } +C4_ALWAYS_INLINE Key key(fmt::base64_wrapper w) { return {w}; } + + +template void write(NodeRef *n, T const& v); + +template inline bool read(ConstNodeRef const& C4_RESTRICT n, T *v); +template inline bool read(NodeRef const& C4_RESTRICT n, T *v); +template inline bool readkey(ConstNodeRef const& C4_RESTRICT n, T *v); +template inline bool readkey(NodeRef const& C4_RESTRICT n, T *v); + +/** @} */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +// forward decls +class NodeRef; +class ConstNodeRef; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @cond dev */ +namespace detail { + +template +struct child_iterator +{ + using value_type = NodeRefType; + using tree_type = typename NodeRefType::tree_type; + + tree_type * C4_RESTRICT m_tree; + id_type m_child_id; + + child_iterator(tree_type * t, id_type id) : m_tree(t), m_child_id(id) {} + + child_iterator& operator++ () { RYML_ASSERT(m_child_id != NONE); m_child_id = m_tree->next_sibling(m_child_id); return *this; } + child_iterator& operator-- () { RYML_ASSERT(m_child_id != NONE); m_child_id = m_tree->prev_sibling(m_child_id); return *this; } + + NodeRefType operator* () const { return NodeRefType(m_tree, m_child_id); } + NodeRefType operator-> () const { return NodeRefType(m_tree, m_child_id); } + + bool operator!= (child_iterator that) const { RYML_ASSERT(m_tree == that.m_tree); return m_child_id != that.m_child_id; } + bool operator== (child_iterator that) const { RYML_ASSERT(m_tree == that.m_tree); return m_child_id == that.m_child_id; } +}; + +template +struct children_view_ +{ + using n_iterator = child_iterator; + + n_iterator b, e; + + children_view_(n_iterator const& C4_RESTRICT b_, + n_iterator const& C4_RESTRICT e_) : b(b_), e(e_) {} + + n_iterator begin() const { return b; } + n_iterator end () const { return e; } +}; + +template +bool _visit(NodeRefType &node, Visitor fn, id_type indentation_level, bool skip_root=false) +{ + id_type increment = 0; + if( ! (node.is_root() && skip_root)) + { + if(fn(node, indentation_level)) + return true; + ++increment; + } + if(node.has_children()) + { + for(auto ch : node.children()) + { + if(_visit(ch, fn, indentation_level + increment, false)) // no need to forward skip_root as it won't be root + { + return true; + } + } + } + return false; +} + +template +bool _visit_stacked(NodeRefType &node, Visitor fn, id_type indentation_level, bool skip_root=false) +{ + id_type increment = 0; + if( ! (node.is_root() && skip_root)) + { + if(fn(node, indentation_level)) + { + return true; + } + ++increment; + } + if(node.has_children()) + { + fn.push(node, indentation_level); + for(auto ch : node.children()) + { + if(_visit_stacked(ch, fn, indentation_level + increment, false)) // no need to forward skip_root as it won't be root + { + fn.pop(node, indentation_level); + return true; + } + } + fn.pop(node, indentation_level); + } + return false; +} + +template +struct RoNodeMethods; +} // detail +/** @endcond */ + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** a CRTP base providing read-only methods for @ref ConstNodeRef and @ref NodeRef */ +namespace detail { +template +struct RoNodeMethods +{ + C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wcast-align") + /** @cond dev */ + // helper CRTP macros, undefined at the end + #define tree_ ((ConstImpl const* C4_RESTRICT)this)->m_tree + #define id_ ((ConstImpl const* C4_RESTRICT)this)->m_id + #define tree__ ((Impl const* C4_RESTRICT)this)->m_tree + #define id__ ((Impl const* C4_RESTRICT)this)->m_id + // require readable: this is a precondition for reading from the + // tree using this object. + #define _C4RR() \ + RYML_ASSERT(tree_ != nullptr); \ + _RYML_CB_ASSERT(tree_->m_callbacks, id_ != NONE); \ + _RYML_CB_ASSERT(tree_->m_callbacks, (((Impl const* C4_RESTRICT)this)->readable())) + // a SFINAE beautifier to enable a function only if the + // implementation is mutable + #define _C4_IF_MUTABLE(ty) typename std::enable_if::value, ty>::type + /** @endcond */ + +public: + + /** @name node property getters */ + /** @{ */ + + /** returns the data or null when the id is NONE */ + C4_ALWAYS_INLINE NodeData const* get() const RYML_NOEXCEPT { return ((Impl const*)this)->readable() ? tree_->get(id_) : nullptr; } + /** returns the data or null when the id is NONE */ + template + C4_ALWAYS_INLINE auto get() RYML_NOEXCEPT -> _C4_IF_MUTABLE(NodeData*) { return ((Impl const*)this)->readable() ? tree__->get(id__) : nullptr; } + + C4_ALWAYS_INLINE NodeType type() const RYML_NOEXCEPT { _C4RR(); return tree_->type(id_); } /**< Forward to @ref Tree::type_str(). Node must be readable. */ + C4_ALWAYS_INLINE const char* type_str() const RYML_NOEXCEPT { _C4RR(); return tree_->type_str(id_); } /**< Forward to @ref Tree::type_str(). Node must be readable. */ + + C4_ALWAYS_INLINE csubstr key() const RYML_NOEXCEPT { _C4RR(); return tree_->key(id_); } /**< Forward to @ref Tree::key(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr key_tag() const RYML_NOEXCEPT { _C4RR(); return tree_->key_tag(id_); } /**< Forward to @ref Tree::key_tag(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr key_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->key_ref(id_); } /**< Forward to @ref Tree::key_ref(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr key_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->key_anchor(id_); } /**< Forward to @ref Tree::key_anchor(). Node must be readable. */ + + C4_ALWAYS_INLINE csubstr val() const RYML_NOEXCEPT { _C4RR(); return tree_->val(id_); } /**< Forward to @ref Tree::val(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr val_tag() const RYML_NOEXCEPT { _C4RR(); return tree_->val_tag(id_); } /**< Forward to @ref Tree::val_tag(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr val_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->val_ref(id_); } /**< Forward to @ref Tree::val_ref(). Node must be readable. */ + C4_ALWAYS_INLINE csubstr val_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->val_anchor(id_); } /**< Forward to @ref Tree::val_anchor(). Node must be readable. */ + + C4_ALWAYS_INLINE NodeScalar const& keysc() const RYML_NOEXCEPT { _C4RR(); return tree_->keysc(id_); } /**< Forward to @ref Tree::keysc(). Node must be readable. */ + C4_ALWAYS_INLINE NodeScalar const& valsc() const RYML_NOEXCEPT { _C4RR(); return tree_->valsc(id_); } /**< Forward to @ref Tree::valsc(). Node must be readable. */ + + C4_ALWAYS_INLINE bool key_is_null() const RYML_NOEXCEPT { _C4RR(); return tree_->key_is_null(id_); } /**< Forward to @ref Tree::key_is_null(). Node must be readable. */ + C4_ALWAYS_INLINE bool val_is_null() const RYML_NOEXCEPT { _C4RR(); return tree_->val_is_null(id_); } /**< Forward to @ref Tree::val_is_null(). Node must be readable. */ + + C4_ALWAYS_INLINE bool is_key_unfiltered() const noexcept { _C4RR(); return tree_->is_key_unfiltered(id_); } /**< Forward to @ref Tree::is_key_unfiltered(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_unfiltered() const noexcept { _C4RR(); return tree_->is_val_unfiltered(id_); } /**< Forward to @ref Tree::is_val_unfiltered(). Node must be readable. */ + + /** @} */ + +public: + + /** @name node type predicates */ + /** @{ */ + + C4_ALWAYS_INLINE bool empty() const RYML_NOEXCEPT { _C4RR(); return tree_->empty(id_); } /**< Forward to @ref Tree::empty(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_stream() const RYML_NOEXCEPT { _C4RR(); return tree_->is_stream(id_); } /**< Forward to @ref Tree::is_stream(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_doc() const RYML_NOEXCEPT { _C4RR(); return tree_->is_doc(id_); } /**< Forward to @ref Tree::is_doc(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_container() const RYML_NOEXCEPT { _C4RR(); return tree_->is_container(id_); } /**< Forward to @ref Tree::is_container(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_map() const RYML_NOEXCEPT { _C4RR(); return tree_->is_map(id_); } /**< Forward to @ref Tree::is_map(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_seq() const RYML_NOEXCEPT { _C4RR(); return tree_->is_seq(id_); } /**< Forward to @ref Tree::is_seq(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_val() const RYML_NOEXCEPT { _C4RR(); return tree_->has_val(id_); } /**< Forward to @ref Tree::has_val(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_key() const RYML_NOEXCEPT { _C4RR(); return tree_->has_key(id_); } /**< Forward to @ref Tree::has_key(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val(id_); } /**< Forward to @ref Tree::is_val(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_keyval() const RYML_NOEXCEPT { _C4RR(); return tree_->is_keyval(id_); } /**< Forward to @ref Tree::is_keyval(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_key_tag() const RYML_NOEXCEPT { _C4RR(); return tree_->has_key_tag(id_); } /**< Forward to @ref Tree::has_key_tag(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_val_tag() const RYML_NOEXCEPT { _C4RR(); return tree_->has_val_tag(id_); } /**< Forward to @ref Tree::has_val_tag(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_key_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->has_key_anchor(id_); } /**< Forward to @ref Tree::has_key_anchor(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_val_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->has_val_anchor(id_); } /**< Forward to @ref Tree::has_val_anchor(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->has_anchor(id_); } /**< Forward to @ref Tree::has_anchor(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_ref(id_); } /**< Forward to @ref Tree::is_key_ref(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_ref(id_); } /**< Forward to @ref Tree::is_val_ref(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->is_ref(id_); } /**< Forward to @ref Tree::is_ref(). Node must be readable. */ + C4_ALWAYS_INLINE bool parent_is_seq() const RYML_NOEXCEPT { _C4RR(); return tree_->parent_is_seq(id_); } /**< Forward to @ref Tree::parent_is_seq(). Node must be readable. */ + C4_ALWAYS_INLINE bool parent_is_map() const RYML_NOEXCEPT { _C4RR(); return tree_->parent_is_map(id_); } /**< Forward to @ref Tree::parent_is_map(). Node must be readable. */ + + RYML_DEPRECATED("use has_key_anchor()") bool is_key_anchor() const noexcept { _C4RR(); return tree_->has_key_anchor(id_); } + RYML_DEPRECATED("use has_val_anchor()") bool is_val_hanchor() const noexcept { _C4RR(); return tree_->has_val_anchor(id_); } + RYML_DEPRECATED("use has_anchor()") bool is_anchor() const noexcept { _C4RR(); return tree_->has_anchor(id_); } + RYML_DEPRECATED("use has_anchor() || is_ref()") bool is_anchor_or_ref() const noexcept { _C4RR(); return tree_->is_anchor_or_ref(id_); } + + /** @} */ + +public: + + /** @name style predicates */ + /** @{ */ + + // documentation to the right --> + + C4_ALWAYS_INLINE bool type_has_any(NodeType_e bits) const RYML_NOEXCEPT { _C4RR(); return tree_->type_has_any(id_, bits); } /**< Forward to @ref Tree::type_has_any(). Node must be readable. */ + C4_ALWAYS_INLINE bool type_has_all(NodeType_e bits) const RYML_NOEXCEPT { _C4RR(); return tree_->type_has_all(id_, bits); } /**< Forward to @ref Tree::type_has_all(). Node must be readable. */ + C4_ALWAYS_INLINE bool type_has_none(NodeType_e bits) const RYML_NOEXCEPT { _C4RR(); return tree_->type_has_none(id_, bits); } /**< Forward to @ref Tree::type_has_none(). Node must be readable. */ + + C4_ALWAYS_INLINE NodeType key_style() const RYML_NOEXCEPT { _C4RR(); return tree_->key_style(id_); } /**< Forward to @ref Tree::key_style(). Node must be readable. */ + C4_ALWAYS_INLINE NodeType val_style() const RYML_NOEXCEPT { _C4RR(); return tree_->val_style(id_); } /**< Forward to @ref Tree::val_style(). Node must be readable. */ + + C4_ALWAYS_INLINE bool is_container_styled() const RYML_NOEXCEPT { _C4RR(); return tree_->is_container_styled(id_); } /**< Forward to @ref Tree::is_container_styled(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_block() const RYML_NOEXCEPT { _C4RR(); return tree_->is_block(id_); } /**< Forward to @ref Tree::is_block(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_flow_sl() const RYML_NOEXCEPT { _C4RR(); return tree_->is_flow_sl(id_); } /**< Forward to @ref Tree::is_flow_sl(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_flow_ml() const RYML_NOEXCEPT { _C4RR(); return tree_->is_flow_ml(id_); } /**< Forward to @ref Tree::is_flow_ml(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_flow() const RYML_NOEXCEPT { _C4RR(); return tree_->is_flow(id_); } /**< Forward to @ref Tree::is_flow(). Node must be readable. */ + + C4_ALWAYS_INLINE bool is_key_styled() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_styled(id_); } /**< Forward to @ref Tree::is_key_styled(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_styled() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_styled(id_); } /**< Forward to @ref Tree::is_val_styled(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_literal() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_literal(id_); } /**< Forward to @ref Tree::is_key_literal(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_literal() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_literal(id_); } /**< Forward to @ref Tree::is_val_literal(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_folded() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_folded(id_); } /**< Forward to @ref Tree::is_key_folded(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_folded() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_folded(id_); } /**< Forward to @ref Tree::is_val_folded(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_squo() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_squo(id_); } /**< Forward to @ref Tree::is_key_squo(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_squo() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_squo(id_); } /**< Forward to @ref Tree::is_val_squo(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_dquo() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_dquo(id_); } /**< Forward to @ref Tree::is_key_dquo(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_dquo() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_dquo(id_); } /**< Forward to @ref Tree::is_val_dquo(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_plain() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_plain(id_); } /**< Forward to @ref Tree::is_key_plain(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_plain() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_plain(id_); } /**< Forward to @ref Tree::is_val_plain(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_key_quoted() const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_quoted(id_); } /**< Forward to @ref Tree::is_key_quoted(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_val_quoted() const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_quoted(id_); } /**< Forward to @ref Tree::is_val_quoted(). Node must be readable. */ + C4_ALWAYS_INLINE bool is_quoted() const RYML_NOEXCEPT { _C4RR(); return tree_->is_quoted(id_); } /**< Forward to @ref Tree::is_quoted(). Node must be readable. */ + + /** @} */ + +public: + + /** @name hierarchy predicates */ + /** @{ */ + + // documentation to the right --> + + C4_ALWAYS_INLINE bool is_root() const RYML_NOEXCEPT { _C4RR(); return tree_->is_root(id_); } /**< Forward to @ref Tree::is_root(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_parent() const RYML_NOEXCEPT { _C4RR(); return tree_->has_parent(id_); } /**< Forward to @ref Tree::has_parent() Node must be readable. */ + C4_ALWAYS_INLINE bool is_ancestor(ConstImpl const& ancestor) const RYML_NOEXCEPT { _C4RR(); return tree_->is_ancestor(id_, ancestor.m_id); } /**< Forward to @ref Tree::is_ancestor() Node must be readable. */ + + C4_ALWAYS_INLINE bool has_child(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); return n.readable() ? tree_->has_child(id_, n.m_id) : false; } /**< Forward to @ref Tree::has_child(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_child(id_type node) const RYML_NOEXCEPT { _C4RR(); return tree_->has_child(id_, node); } /**< Forward to @ref Tree::has_child(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_child(csubstr name) const RYML_NOEXCEPT { _C4RR(); return tree_->has_child(id_, name); } /**< Forward to @ref Tree::has_child(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_children() const RYML_NOEXCEPT { _C4RR(); return tree_->has_children(id_); } /**< Forward to @ref Tree::has_child(). Node must be readable. */ + + C4_ALWAYS_INLINE bool has_sibling(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); return n.readable() ? tree_->has_sibling(id_, n.m_id) : false; } /**< Forward to @ref Tree::has_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_sibling(id_type node) const RYML_NOEXCEPT { _C4RR(); return tree_->has_sibling(id_, node); } /**< Forward to @ref Tree::has_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_sibling(csubstr name) const RYML_NOEXCEPT { _C4RR(); return tree_->has_sibling(id_, name); } /**< Forward to @ref Tree::has_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE bool has_other_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->has_other_siblings(id_); } /**< Forward to @ref Tree::has_sibling(). Node must be readable. */ + + RYML_DEPRECATED("use has_other_siblings()") bool has_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->has_siblings(id_); } + + /** @} */ + +public: + + /** @name hierarchy getters */ + /** @{ */ + + // documentation to the right --> + + template + C4_ALWAYS_INLINE auto doc(id_type i) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { RYML_ASSERT(tree_); return {tree__, tree__->doc(i)}; } /**< Forward to @ref Tree::doc(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl doc(id_type i) const RYML_NOEXCEPT { RYML_ASSERT(tree_); return {tree_, tree_->doc(i)}; } /**< Forward to @ref Tree::doc(). Node must be readable. succeeds even when the node may have invalid or seed id */ + + template + C4_ALWAYS_INLINE auto parent() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->parent(id__)}; } /**< Forward to @ref Tree::parent(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl parent() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->parent(id_)}; } /**< Forward to @ref Tree::parent(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto first_child() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->first_child(id__)}; } /**< Forward to @ref Tree::first_child(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl first_child() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->first_child(id_)}; } /**< Forward to @ref Tree::first_child(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto last_child() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->last_child(id__)}; } /**< Forward to @ref Tree::last_child(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl last_child () const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->last_child (id_)}; } /**< Forward to @ref Tree::last_child(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto child(id_type pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->child(id__, pos)}; } /**< Forward to @ref Tree::child(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl child(id_type pos) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->child(id_, pos)}; } /**< Forward to @ref Tree::child(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto find_child(csubstr name) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->find_child(id__, name)}; } /**< Forward to @ref Tree::first_child(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl find_child(csubstr name) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->find_child(id_, name)}; } /**< Forward to @ref Tree::first_child(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto prev_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->prev_sibling(id__)}; } /**< Forward to @ref Tree::prev_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl prev_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->prev_sibling(id_)}; } /**< Forward to @ref Tree::prev_sibling(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto next_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->next_sibling(id__)}; } /**< Forward to @ref Tree::next_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl next_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->next_sibling(id_)}; } /**< Forward to @ref Tree::next_sibling(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto first_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->first_sibling(id__)}; } /**< Forward to @ref Tree::first_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl first_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->first_sibling(id_)}; } /**< Forward to @ref Tree::first_sibling(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto last_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->last_sibling(id__)}; } /**< Forward to @ref Tree::last_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl last_sibling () const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->last_sibling(id_)}; } /**< Forward to @ref Tree::last_sibling(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto sibling(id_type pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->sibling(id__, pos)}; } /**< Forward to @ref Tree::sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl sibling(id_type pos) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->sibling(id_, pos)}; } /**< Forward to @ref Tree::sibling(). Node must be readable. */ + + template + C4_ALWAYS_INLINE auto find_sibling(csubstr name) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->find_sibling(id__, name)}; } /**< Forward to @ref Tree::find_sibling(). Node must be readable. */ + C4_ALWAYS_INLINE ConstImpl find_sibling(csubstr name) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->find_sibling(id_, name)}; } /**< Forward to @ref Tree::find_sibling(). Node must be readable. */ + + C4_ALWAYS_INLINE id_type num_children() const RYML_NOEXCEPT { _C4RR(); return tree_->num_children(id_); } /**< O(num_children). Forward to @ref Tree::num_children(). */ + C4_ALWAYS_INLINE id_type num_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->num_siblings(id_); } /**< O(num_children). Forward to @ref Tree::num_siblings(). */ + C4_ALWAYS_INLINE id_type num_other_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->num_other_siblings(id_); } /**< O(num_siblings). Forward to @ref Tree::num_other_siblings(). */ + C4_ALWAYS_INLINE id_type child_pos(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); _RYML_CB_ASSERT(tree_->m_callbacks, n.readable()); return tree_->child_pos(id_, n.m_id); } /**< O(num_children). Forward to @ref Tree::child_pos(). */ + C4_ALWAYS_INLINE id_type sibling_pos(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); _RYML_CB_ASSERT(tree_->callbacks(), n.readable()); return tree_->child_pos(tree_->parent(id_), n.m_id); } /**< O(num_siblings). Forward to @ref Tree::sibling_pos(). */ + + C4_ALWAYS_INLINE id_type depth_asc() const RYML_NOEXCEPT { _C4RR(); return tree_->depth_asc(id_); } /** O(log(num_nodes)). Forward to Tree::depth_asc(). Node must be readable. */ + C4_ALWAYS_INLINE id_type depth_desc() const RYML_NOEXCEPT { _C4RR(); return tree_->depth_desc(id_); } /** O(num_nodes). Forward to Tree::depth_desc(). Node must be readable. */ + + /** @} */ + +public: + + /** @name square_brackets + * operator[] */ + /** @{ */ + + /** Find child by key; complexity is O(num_children). + * + * Returns the requested node, or an object in seed state if no + * such child is found (see @ref NodeRef for an explanation of + * what is seed state). When the object is in seed state, using it + * to read from the tree is UB. The seed node can be used to write + * to the tree provided that its create() method is called prior + * to writing, which happens in most modifying methods in + * NodeRef. It is the caller's responsibility to verify that the + * returned node is readable before subsequently using it to read + * from the tree. + * + * @warning the calling object must be readable. This precondition + * is asserted. The assertion is performed only if @ref + * RYML_USE_ASSERT is set to true. As with the non-const overload, + * it is UB to call this method if the node is not readable. + * + * @see https://github.com/biojppm/rapidyaml/issues/389 */ + template + C4_ALWAYS_INLINE auto operator[] (csubstr key) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) + { + _C4RR(); + id_type ch = tree__->find_child(id__, key); + return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, key); + } + + /** Find child by position; complexity is O(pos). + * + * Returns the requested node, or an object in seed state if no + * such child is found (see @ref NodeRef for an explanation of + * what is seed state). When the object is in seed state, using it + * to read from the tree is UB. The seed node can be used to write + * to the tree provided that its create() method is called prior + * to writing, which happens in most modifying methods in + * NodeRef. It is the caller's responsibility to verify that the + * returned node is readable before subsequently using it to read + * from the tree. + * + * @warning the calling object must be readable. This precondition + * is asserted. The assertion is performed only if @ref + * RYML_USE_ASSERT is set to true. As with the non-const overload, + * it is UB to call this method if the node is not readable. + * + * @see https://github.com/biojppm/rapidyaml/issues/389 */ + template + C4_ALWAYS_INLINE auto operator[] (id_type pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) + { + _C4RR(); + id_type ch = tree__->child(id__, pos); + return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, pos); + } + + /** Find a child by key; complexity is O(num_children). + * + * Behaves similar to the non-const overload, but further asserts + * that the returned node is readable (because it can never be in + * a seed state). The assertion is performed only if @ref + * RYML_USE_ASSERT is set to true. As with the non-const overload, + * it is UB to use the return value if it is not valid. + * + * @see https://github.com/biojppm/rapidyaml/issues/389 */ + C4_ALWAYS_INLINE ConstImpl operator[] (csubstr key) const RYML_NOEXCEPT + { + _C4RR(); + id_type ch = tree_->find_child(id_, key); + _RYML_CB_ASSERT(tree_->m_callbacks, ch != NONE); + return {tree_, ch}; + } + + /** Find a child by position; complexity is O(pos). + * + * Behaves similar to the non-const overload, but further asserts + * that the returned node is readable (because it can never be in + * a seed state). This assertion is performed only if @ref + * RYML_USE_ASSERT is set to true. As with the non-const overload, + * it is UB to use the return value if it is not valid. + * + * @see https://github.com/biojppm/rapidyaml/issues/389 */ + C4_ALWAYS_INLINE ConstImpl operator[] (id_type pos) const RYML_NOEXCEPT + { + _C4RR(); + id_type ch = tree_->child(id_, pos); + _RYML_CB_ASSERT(tree_->m_callbacks, ch != NONE); + return {tree_, ch}; + } + + /** @} */ + +public: + + /** @name at + * + * These functions are the analogue to operator[], with the + * difference that they emit an error instead of an + * assertion. That is, if any of the pre or post conditions is + * violated, an error is always emitted (resulting in a call to + * the error callback). + * + * @{ */ + + /** Find child by key; complexity is O(num_children). + * + * Returns the requested node, or an object in seed state if no + * such child is found (see @ref NodeRef for an explanation of + * what is seed state). When the object is in seed state, using it + * to read from the tree is UB. The seed node can be subsequently + * used to write to the tree provided that its create() method is + * called prior to writing, which happens inside most mutating + * methods in NodeRef. It is the caller's responsibility to verify + * that the returned node is readable before subsequently using it + * to read from the tree. + * + * @warning This method will call the error callback (regardless + * of build type or of the value of RYML_USE_ASSERT) whenever any + * of the following preconditions is violated: a) the object is + * valid (points at a tree and a node), b) the calling object must + * be readable (must not be in seed state), c) the calling object + * must be pointing at a MAP node. The preconditions are similar + * to the non-const operator[](csubstr), but instead of using + * assertions, this function directly checks those conditions and + * calls the error callback if any of the checks fail. + * + * @note since it is valid behavior for the returned node to be in + * seed state, the error callback is not invoked when this + * happens. */ + template + C4_ALWAYS_INLINE auto at(csubstr key) -> _C4_IF_MUTABLE(Impl) + { + RYML_CHECK(tree_ != nullptr); + _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < tree_->capacity())); + _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable()); + _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_map(id_)); + id_type ch = tree__->find_child(id__, key); + return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, key); + } + + /** Find child by position; complexity is O(pos). + * + * Returns the requested node, or an object in seed state if no + * such child is found (see @ref NodeRef for an explanation of + * what is seed state). When the object is in seed state, using it + * to read from the tree is UB. The seed node can be used to write + * to the tree provided that its create() method is called prior + * to writing, which happens in most modifying methods in + * NodeRef. It is the caller's responsibility to verify that the + * returned node is readable before subsequently using it to read + * from the tree. + * + * @warning This method will call the error callback (regardless + * of build type or of the value of RYML_USE_ASSERT) whenever any + * of the following preconditions is violated: a) the object is + * valid (points at a tree and a node), b) the calling object must + * be readable (must not be in seed state), c) the calling object + * must be pointing at a MAP node. The preconditions are similar + * to the non-const operator[](id_type), but instead of using + * assertions, this function directly checks those conditions and + * calls the error callback if any of the checks fail. + * + * @note since it is valid behavior for the returned node to be in + * seed state, the error callback is not invoked when this + * happens. */ + template + C4_ALWAYS_INLINE auto at(id_type pos) -> _C4_IF_MUTABLE(Impl) + { + RYML_CHECK(tree_ != nullptr); + const id_type cap = tree_->capacity(); + _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < cap)); + _RYML_CB_CHECK(tree_->m_callbacks, (pos >= 0 && pos < cap)); + _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable()); + _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_container(id_)); + id_type ch = tree__->child(id__, pos); + return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, pos); + } + + /** Get a child by name, with error checking; complexity is + * O(num_children). + * + * Behaves as operator[](csubstr) const, but always raises an + * error (even when RYML_USE_ASSERT is set to false) when the + * returned node does not exist, or when this node is not + * readable, or when it is not a map. This behaviour is similar to + * std::vector::at(), but the error consists in calling the error + * callback instead of directly raising an exception. */ + ConstImpl at(csubstr key) const + { + RYML_CHECK(tree_ != nullptr); + _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < tree_->capacity())); + _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable()); + _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_map(id_)); + id_type ch = tree_->find_child(id_, key); + _RYML_CB_CHECK(tree_->m_callbacks, ch != NONE); + return {tree_, ch}; + } + + /** Get a child by position, with error checking; complexity is + * O(pos). + * + * Behaves as operator[](id_type) const, but always raises an error + * (even when RYML_USE_ASSERT is set to false) when the returned + * node does not exist, or when this node is not readable, or when + * it is not a container. This behaviour is similar to + * std::vector::at(), but the error consists in calling the error + * callback instead of directly raising an exception. */ + ConstImpl at(id_type pos) const + { + RYML_CHECK(tree_ != nullptr); + const id_type cap = tree_->capacity(); + _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < cap)); + _RYML_CB_CHECK(tree_->m_callbacks, (pos >= 0 && pos < cap)); + _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable()); + _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_container(id_)); + const id_type ch = tree_->child(id_, pos); + _RYML_CB_CHECK(tree_->m_callbacks, ch != NONE); + return {tree_, ch}; + } + + /** @} */ + +public: + + /** @name locations */ + /** @{ */ + + Location location(Parser const& parser) const + { + _C4RR(); + return tree_->location(parser, id_); + } + + /** @} */ + +public: + + /** @name deserialization */ + /** @{ */ + + /** deserialize the node's val to the given variable, forwarding + * to the user-overrideable @ref read() function. */ + template + ConstImpl const& operator>> (T &v) const + { + _C4RR(); + if( ! read((ConstImpl const&)*this, &v)) + _RYML_CB_ERR(tree_->m_callbacks, "could not deserialize value"); + return *((ConstImpl const*)this); + } + + /** deserialize the node's key to the given variable, forwarding + * to the user-overrideable @ref read() function; use @ref key() + * to disambiguate; for example: `node >> ryml::key(var)` */ + template + ConstImpl const& operator>> (Key v) const + { + _C4RR(); + if( ! readkey((ConstImpl const&)*this, &v.k)) + _RYML_CB_ERR(tree_->m_callbacks, "could not deserialize key"); + return *((ConstImpl const*)this); + } + + /** look for a child by name, if it exists assign to var. return + * true if the child existed. */ + template + bool get_if(csubstr name, T *var) const + { + _C4RR(); + ConstImpl ch = find_child(name); + if(!ch.readable()) + return false; + ch >> *var; + return true; + } + + /** look for a child by name, if it exists assign to var, + * otherwise default to fallback. return true if the child + * existed. */ + template + bool get_if(csubstr name, T *var, T const& fallback) const + { + _C4RR(); + ConstImpl ch = find_child(name); + if(ch.readable()) + { + ch >> *var; + return true; + } + else + { + *var = fallback; + return false; + } + } + + /** @name deserialization_base64 */ + /** @{ */ + + /** deserialize the node's key as base64. lightweight wrapper over @ref deserialize_key() */ + ConstImpl const& operator>> (Key w) const + { + deserialize_key(w.wrapper); + return *((ConstImpl const*)this); + } + + /** deserialize the node's val as base64. lightweight wrapper over @ref deserialize_val() */ + ConstImpl const& operator>> (fmt::base64_wrapper w) const + { + deserialize_val(w); + return *((ConstImpl const*)this); + } + + /** decode the base64-encoded key and assign the + * decoded blob to the given buffer/ + * @return the size of base64-decoded blob */ + size_t deserialize_key(fmt::base64_wrapper v) const + { + _C4RR(); + return from_chars(key(), &v); + } + /** decode the base64-encoded key and assign the + * decoded blob to the given buffer/ + * @return the size of base64-decoded blob */ + size_t deserialize_val(fmt::base64_wrapper v) const + { + _C4RR(); + return from_chars(val(), &v); + }; + + /** @} */ + + /** @} */ + +public: + + #if defined(__clang__) + # pragma clang diagnostic push + # pragma clang diagnostic ignored "-Wnull-dereference" + #elif defined(__GNUC__) + # pragma GCC diagnostic push + # if __GNUC__ >= 6 + # pragma GCC diagnostic ignored "-Wnull-dereference" + # endif + #endif + + /** @name iteration */ + /** @{ */ + + using iterator = detail::child_iterator; + using const_iterator = detail::child_iterator; + using children_view = detail::children_view_; + using const_children_view = detail::children_view_; + + /** get an iterator to the first child */ + template + C4_ALWAYS_INLINE auto begin() RYML_NOEXCEPT -> _C4_IF_MUTABLE(iterator) { _C4RR(); return iterator(tree__, tree__->first_child(id__)); } + /** get an iterator to the first child */ + C4_ALWAYS_INLINE const_iterator begin() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); } + /** get an iterator to the first child */ + C4_ALWAYS_INLINE const_iterator cbegin() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); } + + /** get an iterator to after the last child */ + template + C4_ALWAYS_INLINE auto end() RYML_NOEXCEPT -> _C4_IF_MUTABLE(iterator) { _C4RR(); return iterator(tree__, NONE); } + /** get an iterator to after the last child */ + C4_ALWAYS_INLINE const_iterator end() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, NONE); } + /** get an iterator to after the last child */ + C4_ALWAYS_INLINE const_iterator cend() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); } + + /** get an iterable view over children */ + template + C4_ALWAYS_INLINE auto children() RYML_NOEXCEPT -> _C4_IF_MUTABLE(children_view) { _C4RR(); return children_view(begin(), end()); } + /** get an iterable view over children */ + C4_ALWAYS_INLINE const_children_view children() const RYML_NOEXCEPT { _C4RR(); return const_children_view(begin(), end()); } + /** get an iterable view over children */ + C4_ALWAYS_INLINE const_children_view cchildren() const RYML_NOEXCEPT { _C4RR(); return const_children_view(begin(), end()); } + + /** get an iterable view over all siblings (including the calling node) */ + template + C4_ALWAYS_INLINE auto siblings() RYML_NOEXCEPT -> _C4_IF_MUTABLE(children_view) + { + _C4RR(); + NodeData const *nd = tree__->get(id__); + return (nd->m_parent != NONE) ? // does it have a parent? + children_view(iterator(tree__, tree_->get(nd->m_parent)->m_first_child), iterator(tree__, NONE)) + : + children_view(end(), end()); + } + /** get an iterable view over all siblings (including the calling node) */ + C4_ALWAYS_INLINE const_children_view siblings() const RYML_NOEXCEPT + { + _C4RR(); + NodeData const *nd = tree_->get(id_); + return (nd->m_parent != NONE) ? // does it have a parent? + const_children_view(const_iterator(tree_, tree_->get(nd->m_parent)->m_first_child), const_iterator(tree_, NONE)) + : + const_children_view(end(), end()); + } + /** get an iterable view over all siblings (including the calling node) */ + C4_ALWAYS_INLINE const_children_view csiblings() const RYML_NOEXCEPT { return siblings(); } + + /** visit every child node calling fn(node) */ + template + bool visit(Visitor fn, id_type indentation_level=0, bool skip_root=true) const RYML_NOEXCEPT + { + _C4RR(); + return detail::_visit(*(ConstImpl const*)this, fn, indentation_level, skip_root); + } + /** visit every child node calling fn(node) */ + template + auto visit(Visitor fn, id_type indentation_level=0, bool skip_root=true) RYML_NOEXCEPT + -> _C4_IF_MUTABLE(bool) + { + _C4RR(); + return detail::_visit(*(Impl*)this, fn, indentation_level, skip_root); + } + + /** visit every child node calling fn(node, level) */ + template + bool visit_stacked(Visitor fn, id_type indentation_level=0, bool skip_root=true) const RYML_NOEXCEPT + { + _C4RR(); + return detail::_visit_stacked(*(ConstImpl const*)this, fn, indentation_level, skip_root); + } + /** visit every child node calling fn(node, level) */ + template + auto visit_stacked(Visitor fn, id_type indentation_level=0, bool skip_root=true) RYML_NOEXCEPT + -> _C4_IF_MUTABLE(bool) + { + _C4RR(); + return detail::_visit_stacked(*(Impl*)this, fn, indentation_level, skip_root); + } + + /** @} */ + + #if defined(__clang__) + # pragma clang diagnostic pop + #elif defined(__GNUC__) + # pragma GCC diagnostic pop + #endif + + #undef _C4_IF_MUTABLE + #undef _C4RR + #undef tree_ + #undef tree__ + #undef id_ + #undef id__ + + C4_SUPPRESS_WARNING_GCC_CLANG_POP +}; +} // detail + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** Holds a pointer to an existing tree, and a node id. It can be used + * only to read from the tree. + * + * @warning The lifetime of the tree must be larger than that of this + * object. It is up to the user to ensure that this happens. */ +class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods // NOLINT +{ +public: + + using tree_type = Tree const; + +public: + + Tree const* C4_RESTRICT m_tree; + id_type m_id; + + friend NodeRef; + friend struct detail::RoNodeMethods; + +public: + + /** @name construction */ + /** @{ */ + + ConstNodeRef() noexcept : m_tree(nullptr), m_id(NONE) {} + ConstNodeRef(Tree const &t) noexcept : m_tree(&t), m_id(t .root_id()) {} + ConstNodeRef(Tree const *t) noexcept : m_tree(t ), m_id(t->root_id()) {} + ConstNodeRef(Tree const *t, id_type id) noexcept : m_tree(t), m_id(id) {} + ConstNodeRef(std::nullptr_t) noexcept : m_tree(nullptr), m_id(NONE) {} + + ConstNodeRef(ConstNodeRef const&) noexcept = default; + ConstNodeRef(ConstNodeRef &&) noexcept = default; + + inline ConstNodeRef(NodeRef const&) noexcept; + inline ConstNodeRef(NodeRef &&) noexcept; + + /** @} */ + +public: + + /** @name assignment */ + /** @{ */ + + ConstNodeRef& operator= (std::nullptr_t) noexcept { m_tree = nullptr; m_id = NONE; return *this; } + + ConstNodeRef& operator= (ConstNodeRef const&) noexcept = default; + ConstNodeRef& operator= (ConstNodeRef &&) noexcept = default; + + ConstNodeRef& operator= (NodeRef const&) noexcept; + ConstNodeRef& operator= (NodeRef &&) noexcept; + + /** @} */ + +public: + + /** @name state queries + * + * see @ref NodeRef for an explanation on what these states mean */ + /** @{ */ + + C4_ALWAYS_INLINE bool invalid() const noexcept { return (!m_tree) || (m_id == NONE); } + /** because a ConstNodeRef cannot be used to write to the tree, + * readable() has the same meaning as !invalid() */ + C4_ALWAYS_INLINE bool readable() const noexcept { return m_tree != nullptr && m_id != NONE; } + /** because a ConstNodeRef cannot be used to write to the tree, it can never be a seed. + * This method is provided for API equivalence between ConstNodeRef and NodeRef. */ + constexpr static C4_ALWAYS_INLINE bool is_seed() noexcept { return false; } + + RYML_DEPRECATED("use one of readable(), is_seed() or !invalid()") bool valid() const noexcept { return m_tree != nullptr && m_id != NONE; } + + /** @} */ + +public: + + /** @name member getters */ + /** @{ */ + + C4_ALWAYS_INLINE Tree const* tree() const noexcept { return m_tree; } + C4_ALWAYS_INLINE id_type id() const noexcept { return m_id; } + + /** @} */ + +public: + + /** @name comparisons */ + /** @{ */ + + C4_ALWAYS_INLINE bool operator== (ConstNodeRef const& that) const RYML_NOEXCEPT { return that.m_tree == m_tree && m_id == that.m_id; } + C4_ALWAYS_INLINE bool operator!= (ConstNodeRef const& that) const RYML_NOEXCEPT { return ! this->operator== (that); } + + /** @cond dev */ + RYML_DEPRECATED("use invalid()") bool operator== (std::nullptr_t) const noexcept { return m_tree == nullptr || m_id == NONE; } + RYML_DEPRECATED("use !invalid()") bool operator!= (std::nullptr_t) const noexcept { return !(m_tree == nullptr || m_id == NONE); } + + RYML_DEPRECATED("use (this->val() == s)") bool operator== (csubstr s) const RYML_NOEXCEPT { RYML_ASSERT(m_tree); _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE); return m_tree->val(m_id) == s; } + RYML_DEPRECATED("use (this->val() != s)") bool operator!= (csubstr s) const RYML_NOEXCEPT { RYML_ASSERT(m_tree); _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE); return m_tree->val(m_id) != s; } + /** @endcond */ + + /** @} */ + +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +// NOLINTBEGIN(cppcoreguidelines-c-copy-assignment-signature,misc-unconventional-assign-operator) + +/** A reference to a node in an existing yaml tree, offering a more + * convenient API than the index-based API used in the tree. + * + * Unlike its imutable ConstNodeRef peer, a NodeRef can be used to + * mutate the tree, both by writing to existing nodes and by creating + * new nodes to subsequently write to. Semantically, a NodeRef + * object can be in one of three states: + * + * ```text + * invalid := not pointing at anything + * readable := points at an existing tree/node + * seed := points at an existing tree, and the node + * may come to exist, if we write to it. + * ``` + * + * So both `readable` and `seed` are states where the node is also `valid`. + * + * ```cpp + * Tree t = parse_in_arena("{a: b}"); + * NodeRef invalid; // not pointing at anything. + * NodeRef readable = t["a"]; // also valid, because "a" exists + * NodeRef seed = t["none"]; // also valid, but is seed because "none" is not in the map + * ``` + * + * When the object is in seed state, using it to read from the tree is + * UB. The seed node can be used to write to the tree, provided that + * its create() method is called prior to writing, which happens in + * most modifying methods in NodeRef. + * + * It is the owners's responsibility to verify that an existing + * node is readable before subsequently using it to read from the + * tree. + * + * @warning The lifetime of the tree must be larger than that of this + * object. It is up to the user to ensure that this happens. + */ +class RYML_EXPORT NodeRef : public detail::RoNodeMethods // NOLINT +{ +public: + + using tree_type = Tree; + using base_type = detail::RoNodeMethods; + +private: + + Tree *C4_RESTRICT m_tree; + id_type m_id; + + /** This member is used to enable lazy operator[] writing. When a child + * with a key or index is not found, m_id is set to the id of the parent + * and the asked-for key or index are stored in this member until a write + * does happen. Then it is given as key or index for creating the child. + * When a key is used, the csubstr stores it (so the csubstr's string is + * non-null and the csubstr's size is different from NONE). When an index is + * used instead, the csubstr's string is set to null, and only the csubstr's + * size is set to a value different from NONE. Otherwise, when operator[] + * does find the child then this member is empty: the string is null and + * the size is NONE. */ + csubstr m_seed; + + friend ConstNodeRef; + friend struct detail::RoNodeMethods; + + // require valid: a helper macro, undefined at the end + #define _C4RR() \ + RYML_ASSERT(m_tree != nullptr); \ + _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE && !is_seed()) + // require id: a helper macro, undefined at the end + #define _C4RID() \ + RYML_ASSERT(m_tree != nullptr); \ + _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE) + +public: + + /** @name construction */ + /** @{ */ + + NodeRef() noexcept : m_tree(nullptr), m_id(NONE), m_seed() { _clear_seed(); } + NodeRef(Tree &t) noexcept : m_tree(&t), m_id(t .root_id()), m_seed() { _clear_seed(); } + NodeRef(Tree *t) noexcept : m_tree(t ), m_id(t->root_id()), m_seed() { _clear_seed(); } + NodeRef(Tree *t, id_type id) noexcept : m_tree(t), m_id(id), m_seed() { _clear_seed(); } + NodeRef(Tree *t, id_type id, id_type seed_pos) noexcept : m_tree(t), m_id(id), m_seed() { m_seed.str = nullptr; m_seed.len = (size_t)seed_pos; } + NodeRef(Tree *t, id_type id, csubstr seed_key) noexcept : m_tree(t), m_id(id), m_seed(seed_key) {} + NodeRef(std::nullptr_t) noexcept : m_tree(nullptr), m_id(NONE), m_seed() {} + + void _clear_seed() noexcept { /*do the following manually or an assert is triggered: */ m_seed.str = nullptr; m_seed.len = npos; } + + /** @} */ + +public: + + /** @name assignment */ + /** @{ */ + + NodeRef(NodeRef const&) noexcept = default; + NodeRef(NodeRef &&) noexcept = default; + + NodeRef& operator= (NodeRef const&) noexcept = default; + NodeRef& operator= (NodeRef &&) noexcept = default; + + /** @} */ + +public: + + /** @name state_queries + * @{ */ + + /** true if the object is not referring to any existing or seed node. @see the doc for @ref NodeRef */ + bool invalid() const noexcept { return m_tree == nullptr || m_id == NONE; } + /** true if the object is not invalid and in seed state. @see the doc for @ref NodeRef */ + bool is_seed() const noexcept { return (m_tree != nullptr && m_id != NONE) && (m_seed.str != nullptr || m_seed.len != (size_t)NONE); } + /** true if the object is not invalid and not in seed state. @see the doc for @ref NodeRef */ + bool readable() const noexcept { return (m_tree != nullptr && m_id != NONE) && (m_seed.str == nullptr && m_seed.len == (size_t)NONE); } + + RYML_DEPRECATED("use one of readable(), is_seed() or !invalid()") inline bool valid() const { return m_tree != nullptr && m_id != NONE; } + + /** @} */ + +public: + + /** @name comparisons */ + /** @{ */ + + bool operator== (NodeRef const& that) const + { + if(m_tree == that.m_tree && m_id == that.m_id) + { + bool seed = is_seed(); + if(seed == that.is_seed()) + { + if(seed) + { + return (m_seed.len == that.m_seed.len) + && (m_seed.str == that.m_seed.str + || m_seed == that.m_seed); // do strcmp only in the last resort + } + return true; + } + } + return false; + } + bool operator!= (NodeRef const& that) const { return ! this->operator==(that); } + + bool operator== (ConstNodeRef const& that) const { return m_tree == that.m_tree && m_id == that.m_id && !is_seed(); } + bool operator!= (ConstNodeRef const& that) const { return ! this->operator==(that); } + + /** @cond dev */ + RYML_DEPRECATED("use !readable()") bool operator== (std::nullptr_t) const { return m_tree == nullptr || m_id == NONE || is_seed(); } + RYML_DEPRECATED("use readable()") bool operator!= (std::nullptr_t) const { return !(m_tree == nullptr || m_id == NONE || is_seed()); } + + RYML_DEPRECATED("use `this->val() == s`") bool operator== (csubstr s) const { _C4RR(); _RYML_CB_ASSERT(m_tree->m_callbacks, has_val()); return m_tree->val(m_id) == s; } + RYML_DEPRECATED("use `this->val() != s`") bool operator!= (csubstr s) const { _C4RR(); _RYML_CB_ASSERT(m_tree->m_callbacks, has_val()); return m_tree->val(m_id) != s; } + /** @endcond */ + +public: + + /** @name node_property_getters + * @{ */ + + C4_ALWAYS_INLINE Tree * tree() noexcept { return m_tree; } + C4_ALWAYS_INLINE Tree const* tree() const noexcept { return m_tree; } + + C4_ALWAYS_INLINE id_type id() const noexcept { return m_id; } + + /** @} */ + +public: + + /** @name node_modifiers */ + /** @{ */ + + void create() { _apply_seed(); } + + void change_type(NodeType t) { _C4RR(); m_tree->change_type(m_id, t); } + + void set_type(NodeType t) { _apply_seed(); m_tree->_set_flags(m_id, t); } + void set_key(csubstr key) { _apply_seed(); m_tree->_set_key(m_id, key); } + void set_val(csubstr val) { _apply_seed(); m_tree->_set_val(m_id, val); } + void set_key_tag(csubstr key_tag) { _apply_seed(); m_tree->set_key_tag(m_id, key_tag); } + void set_val_tag(csubstr val_tag) { _apply_seed(); m_tree->set_val_tag(m_id, val_tag); } + void set_key_anchor(csubstr key_anchor) { _apply_seed(); m_tree->set_key_anchor(m_id, key_anchor); } + void set_val_anchor(csubstr val_anchor) { _apply_seed(); m_tree->set_val_anchor(m_id, val_anchor); } + void set_key_ref(csubstr key_ref) { _apply_seed(); m_tree->set_key_ref(m_id, key_ref); } + void set_val_ref(csubstr val_ref) { _apply_seed(); m_tree->set_val_ref(m_id, val_ref); } + + void set_container_style(NodeType_e style) { _C4RR(); m_tree->set_container_style(m_id, style); } + void set_key_style(NodeType_e style) { _C4RR(); m_tree->set_key_style(m_id, style); } + void set_val_style(NodeType_e style) { _C4RR(); m_tree->set_val_style(m_id, style); } + void clear_style(bool recurse=false) { _C4RR(); m_tree->clear_style(m_id, recurse); } + void set_style_conditionally(NodeType type_mask, + NodeType rem_style_flags, + NodeType add_style_flags, + bool recurse=false) + { + _C4RR(); m_tree->set_style_conditionally(m_id, type_mask, rem_style_flags, add_style_flags, recurse); + } + +public: + + void clear() + { + if(is_seed()) + return; + m_tree->remove_children(m_id); + m_tree->_clear(m_id); + } + + void clear_key() + { + if(is_seed()) + return; + m_tree->_clear_key(m_id); + } + + void clear_val() + { + if(is_seed()) + return; + m_tree->_clear_val(m_id); + } + + void clear_children() + { + if(is_seed()) + return; + m_tree->remove_children(m_id); + } + + void operator= (NodeType_e t) + { + _apply_seed(); + m_tree->_add_flags(m_id, t); + } + + void operator|= (NodeType_e t) + { + _apply_seed(); + m_tree->_add_flags(m_id, t); + } + + void operator= (NodeInit const& v) + { + _apply_seed(); + _apply(v); + } + + void operator= (NodeScalar const& v) + { + _apply_seed(); + _apply(v); + } + + void operator= (std::nullptr_t) + { + _apply_seed(); + _apply(csubstr{}); + } + + void operator= (csubstr v) + { + _apply_seed(); + _apply(v); + } + + template + void operator= (const char (&v)[N]) + { + _apply_seed(); + csubstr sv; + sv.assign(v); + _apply(sv); + } + + /** @} */ + +public: + + /** @name serialization */ + /** @{ */ + + /** serialize a variable to the arena */ + template + csubstr to_arena(T const& C4_RESTRICT s) + { + RYML_ASSERT(m_tree); // no need for valid or readable + return m_tree->to_arena(s); + } + + template + size_t set_key_serialized(T const& C4_RESTRICT k) + { + _apply_seed(); + csubstr s = m_tree->to_arena(k); + m_tree->_set_key(m_id, s); + return s.len; + } + size_t set_key_serialized(std::nullptr_t) + { + _apply_seed(); + m_tree->_set_key(m_id, csubstr{}); + return 0; + } + + template + size_t set_val_serialized(T const& C4_RESTRICT v) + { + _apply_seed(); + csubstr s = m_tree->to_arena(v); + m_tree->_set_val(m_id, s); + return s.len; + } + size_t set_val_serialized(std::nullptr_t) + { + _apply_seed(); + m_tree->_set_val(m_id, csubstr{}); + return 0; + } + + /** encode a blob as base64 into the tree's arena, then assign the + * result to the node's key + * @return the size of base64-encoded blob */ + size_t set_key_serialized(fmt::const_base64_wrapper w); + /** encode a blob as base64 into the tree's arena, then assign the + * result to the node's val + * @return the size of base64-encoded blob */ + size_t set_val_serialized(fmt::const_base64_wrapper w); + + /** serialize a variable, then assign the result to the node's val */ + NodeRef& operator<< (csubstr s) + { + // this overload is needed to prevent ambiguity (there's also + // operator<< for writing a substr to a stream) + _apply_seed(); + write(this, s); + _RYML_CB_ASSERT(m_tree->m_callbacks, val() == s); + return *this; + } + + template + NodeRef& operator<< (T const& C4_RESTRICT v) + { + _apply_seed(); + write(this, v); + return *this; + } + + /** serialize a variable, then assign the result to the node's key */ + template + NodeRef& operator<< (Key const& C4_RESTRICT v) + { + _apply_seed(); + set_key_serialized(v.k); + return *this; + } + + /** serialize a variable, then assign the result to the node's key */ + template + NodeRef& operator<< (Key const& C4_RESTRICT v) + { + _apply_seed(); + set_key_serialized(v.k); + return *this; + } + + NodeRef& operator<< (Key w) + { + set_key_serialized(w.wrapper); + return *this; + } + + NodeRef& operator<< (fmt::const_base64_wrapper w) + { + set_val_serialized(w); + return *this; + } + + /** @} */ + +private: + + void _apply_seed() + { + _C4RID(); + if(m_seed.str) // we have a seed key: use it to create the new child + { + m_id = m_tree->append_child(m_id); + m_tree->_set_key(m_id, m_seed); + m_seed.str = nullptr; + m_seed.len = (size_t)NONE; + } + else if(m_seed.len != (size_t)NONE) // we have a seed index: create a child at that position + { + _RYML_CB_ASSERT(m_tree->m_callbacks, (size_t)m_tree->num_children(m_id) == m_seed.len); + m_id = m_tree->append_child(m_id); + m_seed.str = nullptr; + m_seed.len = (size_t)NONE; + } + else + { + _RYML_CB_ASSERT(m_tree->m_callbacks, readable()); + } + } + + void _apply(csubstr v) + { + m_tree->_set_val(m_id, v); + } + + void _apply(NodeScalar const& v) + { + m_tree->_set_val(m_id, v); + } + + void _apply(NodeInit const& i) + { + m_tree->_set(m_id, i); + } + +public: + + /** @name modification of hierarchy */ + /** @{ */ + + NodeRef insert_child(NodeRef after) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree); + NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id)); + return r; + } + + NodeRef insert_child(NodeInit const& i, NodeRef after) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree); + NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id)); + r._apply(i); + return r; + } + + NodeRef prepend_child() + { + _C4RR(); + NodeRef r(m_tree, m_tree->insert_child(m_id, NONE)); + return r; + } + + NodeRef prepend_child(NodeInit const& i) + { + _C4RR(); + NodeRef r(m_tree, m_tree->insert_child(m_id, NONE)); + r._apply(i); + return r; + } + + NodeRef append_child() + { + _C4RR(); + NodeRef r(m_tree, m_tree->append_child(m_id)); + return r; + } + + NodeRef append_child(NodeInit const& i) + { + _C4RR(); + NodeRef r(m_tree, m_tree->append_child(m_id)); + r._apply(i); + return r; + } + + NodeRef insert_sibling(ConstNodeRef const& after) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree); + NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id)); + return r; + } + + NodeRef insert_sibling(NodeInit const& i, ConstNodeRef const& after) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree); + NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id)); + r._apply(i); + return r; + } + + NodeRef prepend_sibling() + { + _C4RR(); + NodeRef r(m_tree, m_tree->prepend_sibling(m_id)); + return r; + } + + NodeRef prepend_sibling(NodeInit const& i) + { + _C4RR(); + NodeRef r(m_tree, m_tree->prepend_sibling(m_id)); + r._apply(i); + return r; + } + + NodeRef append_sibling() + { + _C4RR(); + NodeRef r(m_tree, m_tree->append_sibling(m_id)); + return r; + } + + NodeRef append_sibling(NodeInit const& i) + { + _C4RR(); + NodeRef r(m_tree, m_tree->append_sibling(m_id)); + r._apply(i); + return r; + } + +public: + + void remove_child(NodeRef & child) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, has_child(child)); + _RYML_CB_ASSERT(m_tree->m_callbacks, child.parent().id() == id()); + m_tree->remove(child.id()); + child.clear(); + } + + //! remove the nth child of this node + void remove_child(id_type pos) + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, pos >= 0 && pos < num_children()); + id_type child = m_tree->child(m_id, pos); + _RYML_CB_ASSERT(m_tree->m_callbacks, child != NONE); + m_tree->remove(child); + } + + //! remove a child by name + void remove_child(csubstr key) + { + _C4RR(); + id_type child = m_tree->find_child(m_id, key); + _RYML_CB_ASSERT(m_tree->m_callbacks, child != NONE); + m_tree->remove(child); + } + +public: + + /** change the node's position within its parent, placing it after + * @p after. To move to the first position in the parent, simply + * pass an empty or default-constructed reference like this: + * `n.move({})`. */ + void move(ConstNodeRef const& after) + { + _C4RR(); + m_tree->move(m_id, after.m_id); + } + + /** move the node to a different @p parent (which may belong to a + * different tree), placing it after @p after. When the + * destination parent is in a new tree, then this node's tree + * pointer is reset to the tree of the parent node. */ + void move(NodeRef const& parent, ConstNodeRef const& after) + { + _C4RR(); + if(parent.m_tree == m_tree) + { + m_tree->move(m_id, parent.m_id, after.m_id); + } + else + { + parent.m_tree->move(m_tree, m_id, parent.m_id, after.m_id); + m_tree = parent.m_tree; + } + } + + /** duplicate the current node somewhere within its parent, and + * place it after the node @p after. To place into the first + * position of the parent, simply pass an empty or + * default-constructed reference like this: `n.move({})`. */ + NodeRef duplicate(ConstNodeRef const& after) const + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree == after.m_tree || after.m_id == NONE); + id_type dup = m_tree->duplicate(m_id, m_tree->parent(m_id), after.m_id); + NodeRef r(m_tree, dup); + return r; + } + + /** duplicate the current node somewhere into a different @p parent + * (possibly from a different tree), and place it after the node + * @p after. To place into the first position of the parent, + * simply pass an empty or default-constructed reference like + * this: `n.move({})`. */ + NodeRef duplicate(NodeRef const& parent, ConstNodeRef const& after) const + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, parent.m_tree == after.m_tree || after.m_id == NONE); + if(parent.m_tree == m_tree) + { + id_type dup = m_tree->duplicate(m_id, parent.m_id, after.m_id); + NodeRef r(m_tree, dup); + return r; + } + else + { + id_type dup = parent.m_tree->duplicate(m_tree, m_id, parent.m_id, after.m_id); + NodeRef r(parent.m_tree, dup); + return r; + } + } + + void duplicate_children(NodeRef const& parent, ConstNodeRef const& after) const + { + _C4RR(); + _RYML_CB_ASSERT(m_tree->m_callbacks, parent.m_tree == after.m_tree); + if(parent.m_tree == m_tree) + { + m_tree->duplicate_children(m_id, parent.m_id, after.m_id); + } + else + { + parent.m_tree->duplicate_children(m_tree, m_id, parent.m_id, after.m_id); + } + } + + /** @} */ + +#undef _C4RR +#undef _C4RID +}; + +// NOLINTEND(cppcoreguidelines-c-copy-assignment-signature,misc-unconventional-assign-operator) + + +//----------------------------------------------------------------------------- + +inline ConstNodeRef::ConstNodeRef(NodeRef const& that) noexcept + : m_tree(that.m_tree) + , m_id(!that.is_seed() ? that.id() : (id_type)NONE) +{ +} + +inline ConstNodeRef::ConstNodeRef(NodeRef && that) noexcept // NOLINT + : m_tree(that.m_tree) + , m_id(!that.is_seed() ? that.id() : (id_type)NONE) +{ +} + + +inline ConstNodeRef& ConstNodeRef::operator= (NodeRef const& that) noexcept +{ + m_tree = (that.m_tree); + m_id = (!that.is_seed() ? that.id() : (id_type)NONE); + return *this; +} + +inline ConstNodeRef& ConstNodeRef::operator= (NodeRef && that) noexcept // NOLINT +{ + m_tree = (that.m_tree); + m_id = (!that.is_seed() ? that.id() : (id_type)NONE); + return *this; +} + + +//----------------------------------------------------------------------------- + +/** @addtogroup doc_serialization_helpers + * + * @{ + */ + +template +C4_ALWAYS_INLINE void write(NodeRef *n, T const& v) +{ + n->set_val_serialized(v); +} + +template +C4_ALWAYS_INLINE bool read(ConstNodeRef const& C4_RESTRICT n, T *v) +{ + return read(n.m_tree, n.m_id, v); +} + +template +C4_ALWAYS_INLINE bool read(NodeRef const& C4_RESTRICT n, T *v) +{ + return read(n.tree(), n.id(), v); +} + +template +C4_ALWAYS_INLINE bool readkey(ConstNodeRef const& C4_RESTRICT n, T *v) +{ + return readkey(n.m_tree, n.m_id, v); +} + +template +C4_ALWAYS_INLINE bool readkey(NodeRef const& C4_RESTRICT n, T *v) +{ + return readkey(n.tree(), n.id(), v); +} + +/** @} */ + +/** @} */ + + +} // namespace yml +} // namespace c4 + + + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#endif /* _C4_YML_NODE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/node_type.hpp b/3rdparty/rapidyaml/include/c4/yml/node_type.hpp new file mode 100644 index 0000000000..4ee79fdfaa --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/node_type.hpp @@ -0,0 +1,282 @@ +#ifndef C4_YML_NODE_TYPE_HPP_ +#define C4_YML_NODE_TYPE_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "c4/yml/common.hpp" +#endif + +C4_SUPPRESS_WARNING_MSVC_PUSH +C4_SUPPRESS_WARNING_GCC_CLANG_PUSH +C4_SUPPRESS_WARNING_GCC_CLANG("-Wold-style-cast") +#if __GNUC__ >= 6 +C4_SUPPRESS_WARNING_GCC("-Wnull-dereference") +#endif + +namespace c4 { +namespace yml { + +/** @addtogroup doc_node_type + * + * @{ + */ + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** the integral type necessary to cover all the bits for NodeType_e */ +using type_bits = uint32_t; + + +/** a bit mask for marking node types and styles */ +typedef enum : type_bits { + #define __(v) (type_bits(1) << v) // a convenience define, undefined below // NOLINT + NOTYPE = 0, ///< no node type or style is set + KEY = __(0), ///< is member of a map + VAL = __(1), ///< a scalar: has a scalar (ie string) value, possibly empty. must be a leaf node, and cannot be MAP or SEQ + MAP = __(2), ///< a map: a parent of KEYVAL/KEYSEQ/KEYMAP nodes + SEQ = __(3), ///< a seq: a parent of VAL/SEQ/MAP nodes + DOC = __(4), ///< a document + STREAM = __(5)|SEQ, ///< a stream: a seq of docs + KEYREF = __(6), ///< a *reference: the key references an &anchor + VALREF = __(7), ///< a *reference: the val references an &anchor + KEYANCH = __(8), ///< the key has an &anchor + VALANCH = __(9), ///< the val has an &anchor + KEYTAG = __(10), ///< the key has a tag + VALTAG = __(11), ///< the val has a tag + KEYNIL = __(12), ///< the key is null (eg `{ : b}` results in a null key) + VALNIL = __(13), ///< the val is null (eg `{a : }` results in a null val) + _TYMASK = __(14)-1, ///< all the bits up to here + // + // unfiltered flags: + // + KEY_UNFILT = __(14), ///< the key scalar was left unfiltered; the parser was set not to filter. @see ParserOptions + VAL_UNFILT = __(15), ///< the val scalar was left unfiltered; the parser was set not to filter. @see ParserOptions + // + // style flags: + // + FLOW_SL = __(16), ///< mark container with single-line flow style (seqs as '[val1,val2], maps as '{key: val,key2: val2}') + FLOW_ML = __(17), ///< (NOT IMPLEMENTED, work in progress) mark container with multi-line flow style (seqs as '[\n val1,\n val2\n], maps as '{\n key: val,\n key2: val2\n}') + BLOCK = __(18), ///< mark container with block style (seqs as '- val\n', maps as 'key: val') + KEY_LITERAL = __(19), ///< mark key scalar as multiline, block literal | + VAL_LITERAL = __(20), ///< mark val scalar as multiline, block literal | + KEY_FOLDED = __(21), ///< mark key scalar as multiline, block folded > + VAL_FOLDED = __(22), ///< mark val scalar as multiline, block folded > + KEY_SQUO = __(23), ///< mark key scalar as single quoted ' + VAL_SQUO = __(24), ///< mark val scalar as single quoted ' + KEY_DQUO = __(25), ///< mark key scalar as double quoted " + VAL_DQUO = __(26), ///< mark val scalar as double quoted " + KEY_PLAIN = __(27), ///< mark key scalar as plain scalar (unquoted, even when multiline) + VAL_PLAIN = __(28), ///< mark val scalar as plain scalar (unquoted, even when multiline) + // + // type combination masks: + // + KEYVAL = KEY|VAL, + KEYSEQ = KEY|SEQ, + KEYMAP = KEY|MAP, + DOCMAP = DOC|MAP, + DOCSEQ = DOC|SEQ, + DOCVAL = DOC|VAL, + // + // style combination masks: + // + SCALAR_LITERAL = KEY_LITERAL|VAL_LITERAL, + SCALAR_FOLDED = KEY_FOLDED|VAL_FOLDED, + SCALAR_SQUO = KEY_SQUO|VAL_SQUO, + SCALAR_DQUO = KEY_DQUO|VAL_DQUO, + SCALAR_PLAIN = KEY_PLAIN|VAL_PLAIN, + KEYQUO = KEY_SQUO|KEY_DQUO|KEY_FOLDED|KEY_LITERAL, ///< key style is one of ', ", > or | + VALQUO = VAL_SQUO|VAL_DQUO|VAL_FOLDED|VAL_LITERAL, ///< val style is one of ', ", > or | + KEY_STYLE = KEY_LITERAL|KEY_FOLDED|KEY_SQUO|KEY_DQUO|KEY_PLAIN, ///< mask of all the scalar styles for key (not container styles!) + VAL_STYLE = VAL_LITERAL|VAL_FOLDED|VAL_SQUO|VAL_DQUO|VAL_PLAIN, ///< mask of all the scalar styles for val (not container styles!) + SCALAR_STYLE = KEY_STYLE|VAL_STYLE, + CONTAINER_STYLE_FLOW = FLOW_SL|FLOW_ML, + CONTAINER_STYLE_BLOCK = BLOCK, + CONTAINER_STYLE = FLOW_SL|FLOW_ML|BLOCK, + STYLE = SCALAR_STYLE | CONTAINER_STYLE, + // + // mixed masks + _KEYMASK = KEY | KEYQUO | KEYANCH | KEYREF | KEYTAG, + _VALMASK = VAL | VALQUO | VALANCH | VALREF | VALTAG, + #undef __ +} NodeType_e; + +constexpr C4_ALWAYS_INLINE C4_CONST NodeType_e operator| (NodeType_e lhs, NodeType_e rhs) noexcept { return (NodeType_e)(((type_bits)lhs) | ((type_bits)rhs)); } +constexpr C4_ALWAYS_INLINE C4_CONST NodeType_e operator& (NodeType_e lhs, NodeType_e rhs) noexcept { return (NodeType_e)(((type_bits)lhs) & ((type_bits)rhs)); } +constexpr C4_ALWAYS_INLINE C4_CONST NodeType_e operator>> (NodeType_e bits, uint32_t n) noexcept { return (NodeType_e)(((type_bits)bits) >> n); } +constexpr C4_ALWAYS_INLINE C4_CONST NodeType_e operator<< (NodeType_e bits, uint32_t n) noexcept { return (NodeType_e)(((type_bits)bits) << n); } +constexpr C4_ALWAYS_INLINE C4_CONST NodeType_e operator~ (NodeType_e bits) noexcept { return (NodeType_e)(~(type_bits)bits); } +C4_ALWAYS_INLINE NodeType_e& operator&= (NodeType_e &subject, NodeType_e bits) noexcept { subject = (NodeType_e)((type_bits)subject & (type_bits)bits); return subject; } +C4_ALWAYS_INLINE NodeType_e& operator|= (NodeType_e &subject, NodeType_e bits) noexcept { subject = (NodeType_e)((type_bits)subject | (type_bits)bits); return subject; } + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** wraps a NodeType_e element with some syntactic sugar and predicates */ +struct RYML_EXPORT NodeType +{ +public: + + NodeType_e type; + +public: + + C4_ALWAYS_INLINE NodeType() noexcept : type(NOTYPE) {} + C4_ALWAYS_INLINE NodeType(NodeType_e t) noexcept : type(t) {} + C4_ALWAYS_INLINE NodeType(type_bits t) noexcept : type((NodeType_e)t) {} + + C4_ALWAYS_INLINE bool has_any(NodeType_e t) const noexcept { return (type & t) != 0u; } + C4_ALWAYS_INLINE bool has_all(NodeType_e t) const noexcept { return (type & t) == t; } + C4_ALWAYS_INLINE bool has_none(NodeType_e t) const noexcept { return (type & t) == 0; } + + C4_ALWAYS_INLINE void set(NodeType_e t) noexcept { type = t; } + C4_ALWAYS_INLINE void add(NodeType_e t) noexcept { type = (type|t); } + C4_ALWAYS_INLINE void rem(NodeType_e t) noexcept { type = (type & ~t); } + C4_ALWAYS_INLINE void addrem(NodeType_e bits_to_add, NodeType_e bits_to_remove) noexcept { type |= bits_to_add; type &= ~bits_to_remove; } + + C4_ALWAYS_INLINE void clear() noexcept { type = NOTYPE; } + +public: + + C4_ALWAYS_INLINE operator NodeType_e & C4_RESTRICT () noexcept { return type; } + C4_ALWAYS_INLINE operator NodeType_e const& C4_RESTRICT () const noexcept { return type; } + +public: + + /** @name node type queries + * @{ */ + + /** return a preset string based on the node type */ + C4_ALWAYS_INLINE const char *type_str() const noexcept { return type_str(type); } + /** return a preset string based on the node type */ + static const char* type_str(NodeType_e t) noexcept; + + /** fill a string with the node type flags. If the string is small, returns {null, len} */ + C4_ALWAYS_INLINE csubstr type_str(substr buf) const noexcept { return type_str(buf, type); } + /** fill a string with the node type flags. If the string is small, returns {null, len} */ + static csubstr type_str(substr buf, NodeType_e t) noexcept; + +public: + + /** @name node type queries + * @{ */ + + C4_ALWAYS_INLINE bool is_notype() const noexcept { return type == NOTYPE; } + C4_ALWAYS_INLINE bool is_stream() const noexcept { return ((type & STREAM) == STREAM) != 0; } + C4_ALWAYS_INLINE bool is_doc() const noexcept { return (type & DOC) != 0; } + C4_ALWAYS_INLINE bool is_container() const noexcept { return (type & (MAP|SEQ|STREAM)) != 0; } + C4_ALWAYS_INLINE bool is_map() const noexcept { return (type & MAP) != 0; } + C4_ALWAYS_INLINE bool is_seq() const noexcept { return (type & SEQ) != 0; } + C4_ALWAYS_INLINE bool has_key() const noexcept { return (type & KEY) != 0; } + C4_ALWAYS_INLINE bool has_val() const noexcept { return (type & VAL) != 0; } + C4_ALWAYS_INLINE bool is_val() const noexcept { return (type & KEYVAL) == VAL; } + C4_ALWAYS_INLINE bool is_keyval() const noexcept { return (type & KEYVAL) == KEYVAL; } + C4_ALWAYS_INLINE bool key_is_null() const noexcept { return (type & KEYNIL) != 0; } + C4_ALWAYS_INLINE bool val_is_null() const noexcept { return (type & VALNIL) != 0; } + C4_ALWAYS_INLINE bool has_key_tag() const noexcept { return (type & KEYTAG) != 0; } + C4_ALWAYS_INLINE bool has_val_tag() const noexcept { return (type & VALTAG) != 0; } + C4_ALWAYS_INLINE bool has_key_anchor() const noexcept { return (type & KEYANCH) != 0; } + C4_ALWAYS_INLINE bool has_val_anchor() const noexcept { return (type & VALANCH) != 0; } + C4_ALWAYS_INLINE bool has_anchor() const noexcept { return (type & (KEYANCH|VALANCH)) != 0; } + C4_ALWAYS_INLINE bool is_key_ref() const noexcept { return (type & KEYREF) != 0; } + C4_ALWAYS_INLINE bool is_val_ref() const noexcept { return (type & VALREF) != 0; } + C4_ALWAYS_INLINE bool is_ref() const noexcept { return (type & (KEYREF|VALREF)) != 0; } + + C4_ALWAYS_INLINE bool is_key_unfiltered() const noexcept { return (type & (KEY_UNFILT)) != 0; } + C4_ALWAYS_INLINE bool is_val_unfiltered() const noexcept { return (type & (VAL_UNFILT)) != 0; } + + RYML_DEPRECATED("use has_key_anchor()") bool is_key_anchor() const noexcept { return has_key_anchor(); } + RYML_DEPRECATED("use has_val_anchor()") bool is_val_anchor() const noexcept { return has_val_anchor(); } + RYML_DEPRECATED("use has_anchor()") bool is_anchor() const noexcept { return has_anchor(); } + RYML_DEPRECATED("use has_anchor() || is_ref()") bool is_anchor_or_ref() const noexcept { return has_anchor() || is_ref(); } + /** @} */ + +public: + + /** @name style functions + * @{ */ + + C4_ALWAYS_INLINE bool is_container_styled() const noexcept { return (type & (CONTAINER_STYLE)) != 0; } + C4_ALWAYS_INLINE bool is_block() const noexcept { return (type & (BLOCK)) != 0; } + C4_ALWAYS_INLINE bool is_flow_sl() const noexcept { return (type & (FLOW_SL)) != 0; } + C4_ALWAYS_INLINE bool is_flow_ml() const noexcept { return (type & (FLOW_ML)) != 0; } + C4_ALWAYS_INLINE bool is_flow() const noexcept { return (type & (FLOW_ML|FLOW_SL)) != 0; } + + C4_ALWAYS_INLINE bool is_key_styled() const noexcept { return (type & (KEY_STYLE)) != 0; } + C4_ALWAYS_INLINE bool is_val_styled() const noexcept { return (type & (VAL_STYLE)) != 0; } + C4_ALWAYS_INLINE bool is_key_literal() const noexcept { return (type & (KEY_LITERAL)) != 0; } + C4_ALWAYS_INLINE bool is_val_literal() const noexcept { return (type & (VAL_LITERAL)) != 0; } + C4_ALWAYS_INLINE bool is_key_folded() const noexcept { return (type & (KEY_FOLDED)) != 0; } + C4_ALWAYS_INLINE bool is_val_folded() const noexcept { return (type & (VAL_FOLDED)) != 0; } + C4_ALWAYS_INLINE bool is_key_squo() const noexcept { return (type & (KEY_SQUO)) != 0; } + C4_ALWAYS_INLINE bool is_val_squo() const noexcept { return (type & (VAL_SQUO)) != 0; } + C4_ALWAYS_INLINE bool is_key_dquo() const noexcept { return (type & (KEY_DQUO)) != 0; } + C4_ALWAYS_INLINE bool is_val_dquo() const noexcept { return (type & (VAL_DQUO)) != 0; } + C4_ALWAYS_INLINE bool is_key_plain() const noexcept { return (type & (KEY_PLAIN)) != 0; } + C4_ALWAYS_INLINE bool is_val_plain() const noexcept { return (type & (VAL_PLAIN)) != 0; } + C4_ALWAYS_INLINE bool is_key_quoted() const noexcept { return (type & KEYQUO) != 0; } + C4_ALWAYS_INLINE bool is_val_quoted() const noexcept { return (type & VALQUO) != 0; } + C4_ALWAYS_INLINE bool is_quoted() const noexcept { return (type & (KEYQUO|VALQUO)) != 0; } + + C4_ALWAYS_INLINE NodeType key_style() const noexcept { return (type & (KEY_STYLE)); } + C4_ALWAYS_INLINE NodeType val_style() const noexcept { return (type & (VAL_STYLE)); } + + C4_ALWAYS_INLINE void set_container_style(NodeType_e style) noexcept { type = ((style & CONTAINER_STYLE) | (type & ~CONTAINER_STYLE)); } + C4_ALWAYS_INLINE void set_key_style(NodeType_e style) noexcept { type = ((style & KEY_STYLE) | (type & ~KEY_STYLE)); } + C4_ALWAYS_INLINE void set_val_style(NodeType_e style) noexcept { type = ((style & VAL_STYLE) | (type & ~VAL_STYLE)); } + C4_ALWAYS_INLINE void clear_style() noexcept { type &= ~STYLE; } + + /** @} */ + +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @name scalar style helpers + * @{ */ + +/** choose a YAML emitting style based on the scalar's contents */ +RYML_EXPORT NodeType_e scalar_style_choose(csubstr scalar) noexcept; + +/** choose a json style based on the scalar's contents */ +RYML_EXPORT NodeType_e scalar_style_json_choose(csubstr scalar) noexcept; + +/** query whether a scalar can be encoded using single quotes. + * It may not be possible, notably when there is leading + * whitespace after a newline. */ +RYML_EXPORT bool scalar_style_query_squo(csubstr s) noexcept; + +/** query whether a scalar can be encoded using plain style (no + * quotes, not a literal/folded block scalar). */ +RYML_EXPORT bool scalar_style_query_plain(csubstr s) noexcept; + +/** YAML-sense query of nullity. returns true if the scalar points + * to `nullptr` or is otherwise equal to one of the strings + * `"~"`,`"null"`,`"Null"`,`"NULL"` */ +RYML_EXPORT inline C4_NO_INLINE bool scalar_is_null(csubstr s) noexcept +{ + return s.str == nullptr || + s == "~" || + s == "null" || + s == "Null" || + s == "NULL"; +} + +/** @} */ + + +/** @} */ + +} // namespace yml +} // namespace c4 + +C4_SUPPRESS_WARNING_MSVC_POP +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +#endif /* C4_YML_NODE_TYPE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/parse.hpp b/3rdparty/rapidyaml/include/c4/yml/parse.hpp new file mode 100644 index 0000000000..124defb3ec --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/parse.hpp @@ -0,0 +1,324 @@ +#ifndef _C4_YML_PARSE_HPP_ +#define _C4_YML_PARSE_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "c4/yml/common.hpp" +#endif + +namespace c4 { +namespace yml { + +class Tree; +class NodeRef; +template class ParseEngine; +struct EventHandlerTree; +RYML_EXPORT id_type estimate_tree_capacity(csubstr src); // NOLINT + + +/** @addtogroup doc_parse + * @{ */ + +/** This is the main ryml parser, where the parser events are handled + * to create a ryml tree. + * + * @warning This class cannot parse YAML where there are container + * keys. This is not a limitation of the @ref ParseEngine, but of the + * @ref EventHandlerTree, which is present because the @ref Tree does + * not accept containers as keys. However, the @ref ParseEngine *can* + * parse container keys; consult its documentation for more details. + * + * @see ParserOptions + * @see ParseEngine + * @see EventHandlerTree + * */ +using Parser = RYML_EXPORT ParseEngine; + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_parse_in_place__with_existing_parser Parse in place with existing parser + * + * @brief parse a mutable YAML source buffer (re)using an existing + * parser. Scalars requiring filtering are mutated in place (except in + * the rare cases where the filtered scalar is longer than the + * original scalar, or where filtering was disabled before the + * call). These overloads accept an existing parser object, and + * provide the opportunity to use special parser options. + * + * @see ParserOptions + * + * @{ + */ + +// this is vertically aligned to highlight the parameter differences. + +RYML_EXPORT void parse_in_place(Parser *parser, csubstr filename, substr yaml, Tree *t, id_type node_id); /**< (1) parse YAML into an existing tree node. + * + * The filename will be used in any error messages + * arising during the parse. The callbacks in the + * tree are kept, and used to allocate + * the tree members, if any allocation is required. */ +RYML_EXPORT void parse_in_place(Parser *parser, substr yaml, Tree *t, id_type node_id); /**< (2) like (1) but no filename will be reported */ +RYML_EXPORT void parse_in_place(Parser *parser, csubstr filename, substr yaml, Tree *t ); /**< (3) parse YAML into the root node of an existing tree. + * + * The filename will be used in any error messages + * arising during the parse. The callbacks in the + * tree are kept, and used to allocate + * the tree members, if any allocation is required. */ +RYML_EXPORT void parse_in_place(Parser *parser, substr yaml, Tree *t ); /**< (4) like (3) but no filename will be reported */ +RYML_EXPORT void parse_in_place(Parser *parser, csubstr filename, substr yaml, NodeRef node ); /**< (5) like (1) but the node is given as a NodeRef */ +RYML_EXPORT void parse_in_place(Parser *parser, substr yaml, NodeRef node ); /**< (6) like (5) but no filename will be reported */ +RYML_EXPORT Tree parse_in_place(Parser *parser, csubstr filename, substr yaml ); /**< (7) create a new tree, and parse YAML into its root node. + * + * The filename will be used in any error messages + * arising during the parse. The tree is created with + * the callbacks currently in the parser. + */ +RYML_EXPORT Tree parse_in_place(Parser *parser, substr yaml ); /**< (8) like (7) but no filename will be reported */ + + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_json_in_place(Parser *parser, csubstr filename, substr json, Tree *t, id_type node_id); ///< (1) parse JSON into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_json_in_place(Parser *parser, substr json, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_json_in_place(Parser *parser, csubstr filename, substr json, Tree *t ); ///< (3) parse JSON into an existing tree, into its root node. +RYML_EXPORT void parse_json_in_place(Parser *parser, substr json, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_json_in_place(Parser *parser, csubstr filename, substr json, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_json_in_place(Parser *parser, substr json, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_json_in_place(Parser *parser, csubstr filename, substr json ); ///< (7) create a new tree, and parse JSON into its root node. +RYML_EXPORT Tree parse_json_in_place(Parser *parser, substr json ); ///< (8) like (7) but no filename will be reported + +/** @} */ + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_parse_in_place___with_temporary_parser Parse in place with temporary parser + * + * @brief parse a mutable YAML source buffer. Scalars requiring + * filtering are mutated in place (except in the rare cases where the + * filtered scalar is longer than the original scalar). + * + * @note These freestanding functions use a temporary parser object, + * and are convenience functions to enable the user to easily parse + * YAML without the need to explicitly instantiate a parser and event + * handler. Note that some properties (notably node locations in the + * original source code) are only available through the parser + * class. If you need access to any of these properties, use + * the appropriate overload from @ref doc_parse_in_place__with_existing_parser + * + * @{ + */ + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_in_place(csubstr filename, substr yaml, Tree *t, id_type node_id); ///< (1) parse YAML into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_in_place( substr yaml, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_in_place(csubstr filename, substr yaml, Tree *t ); ///< (3) parse YAML into an existing tree, into its root node. +RYML_EXPORT void parse_in_place( substr yaml, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_in_place(csubstr filename, substr yaml, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_in_place( substr yaml, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_in_place(csubstr filename, substr yaml ); ///< (7) create a new tree, and parse YAML into its root node. +RYML_EXPORT Tree parse_in_place( substr yaml ); ///< (8) like (7) but no filename will be reported + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_json_in_place(csubstr filename, substr json, Tree *t, id_type node_id); ///< (1) parse JSON into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_json_in_place( substr json, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_json_in_place(csubstr filename, substr json, Tree *t ); ///< (3) parse JSON into an existing tree, into its root node. +RYML_EXPORT void parse_json_in_place( substr json, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_json_in_place(csubstr filename, substr json, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_json_in_place( substr json, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_json_in_place(csubstr filename, substr json ); ///< (7) create a new tree, and parse JSON into its root node. +RYML_EXPORT Tree parse_json_in_place( substr json ); ///< (8) like (7) but no filename will be reported + +/** @} */ + + +//----------------------------------------------------------------------------- + + +/** @defgroup doc_parse_in_arena__with_existing_parser Parse in arena with existing parser + * + * @brief parse a read-only (immutable) YAML source buffer. This is + * achieved by first copying the contents of the buffer to the tree's + * arena, and then calling @ref parse_in_arena() . All the resulting + * scalars will be filtered in the arena. These overloads accept an + * existing parser object, and provide the opportunity to use special + * parser options. + * + * @see ParserOptions + * + * + * @note These freestanding functions use a temporary parser object, + * and are convenience functions to easily parse YAML without the need + * to instantiate a separate parser. Note that some properties + * (notably node locations in the original source code) are only + * available through the parser class. If you need access to any of + * these properties, use the appropriate overload from @ref + * doc_parse_in_arena__with_existing_parser + * + * @warning overloads receiving a substr YAML buffer are intentionally + * left undefined, such that calling parse_in_arena() with a substr + * will cause a linker error. This is to prevent an accidental copy of + * the source buffer to the tree's arena, because substr (which is + * mutable) is implicitly convertible to csubstr (which is + * immutable). If you really intend to parse a mutable buffer in the + * tree's arena, convert it first to immutable by assigning the substr + * to a csubstr prior to calling parse_in_arena(). This is not needed + * for parse_in_place() because csubstr is not implicitly convertible + * to substr. To be clear: + * ```c++ + * substr mutable_buffer = ...; + * parser.parse_in_arena(mutable_buffer); // linker error + * + * csubstr immutable_buffer = ...; + * parser.parse_in_arena(immutable_buffer); // ok + * ``` + * + * @{ + */ + +#define RYML_DONT_PARSE_SUBSTR_IN_ARENA "" \ + "Do not pass a (mutable) substr to parse_in_arena(); " \ + "if you have a substr, it should be parsed in place. " \ + "Consider using parse_in_place() instead, or convert " \ + "the buffer to csubstr prior to calling. This function " \ + " is deliberately left undefined, so that calling it " \ + "will cause a linker error." + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, Tree *t, id_type node_id); ///< (1) parse YAML into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr yaml, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, Tree *t ); ///< (3) parse YAML into an existing tree, into its root node. +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr yaml, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_in_arena(Parser *parser, csubstr yaml, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_in_arena(Parser *parser, csubstr filename, csubstr yaml ); ///< (7) create a new tree, and parse YAML into its root node. +RYML_EXPORT Tree parse_in_arena(Parser *parser, csubstr yaml ); ///< (8) like (7) but no filename will be reported + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, Tree *t, id_type node_id); ///< (1) parse JSON into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr json, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, Tree *t ); ///< (3) parse JSON into an existing tree, into its root node. +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr json, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_json_in_arena(Parser *parser, csubstr json, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_json_in_arena(Parser *parser, csubstr filename, csubstr json ); ///< (7) create a new tree, and parse JSON into its root node. +RYML_EXPORT Tree parse_json_in_arena(Parser *parser, csubstr json ); ///< (8) like (7) but no filename will be reported + +/* READ THE DEPRECATION NOTE! + * + * All of the functions below are intentionally left undefined, to + * prevent them being used. + * + */ +/** @cond dev */ +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, substr yaml, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, csubstr filename, substr yaml, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, substr yaml, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, csubstr filename, substr yaml, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, substr yaml, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(Parser *parser, csubstr filename, substr yaml, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena(Parser *parser, substr yaml ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena(Parser *parser, csubstr filename, substr yaml ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, substr json, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, csubstr filename, substr json, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, substr json, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, csubstr filename, substr json, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, substr json, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(Parser *parser, csubstr filename, substr json, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_json_in_arena(Parser *parser, substr json ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_json_in_arena(Parser *parser, csubstr filename, substr json ); +/** @endcond */ + +/** @} */ + + +//----------------------------------------------------------------------------- + + +/** @defgroup doc_parse_in_arena__with_temporary_parser Parse in arena with temporary parser + * + * @brief parse a read-only (immutable) YAML source buffer. This is + * achieved by first copying the contents of the buffer to the tree's + * arena, and then calling @ref parse_in_arena() . + * + * @note These freestanding functions use a temporary parser object, + * and are convenience functions to easily one-off parse YAML without + * the need to instantiate a separate parser. Note that some + * properties (notably node locations in the original source code) are + * only available through the parser class. If you need access to any + * of these properties, use the appropriate overload from @ref + * doc_parse_in_arena__with_existing_parser + * + * @warning overloads receiving a substr YAML buffer are intentionally + * left undefined, such that calling parse_in_arena() with a substr + * will cause a linker error. This is to prevent an accidental copy of + * the source buffer to the tree's arena, because substr (which is + * mutable) is implicitly convertible to csubstr (which is + * immutable). If you really intend to parse a mutable buffer in the + * tree's arena, convert it first to immutable by assigning the substr + * to a csubstr prior to calling parse_in_arena(). This is not needed + * for parse_in_place() because csubstr is not implicitly convertible + * to substr. To be clear: + * ```c++ + * substr mutable_buffer = ...; + * parser.parse_in_arena(mutable_buffer); // linker error + * + * csubstr immutable_buffer = ...; + * parser.parse_in_arena(immutable_buffer); // ok + * ``` + * + * @{ + */ + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_in_arena(csubstr filename, csubstr yaml, Tree *t, id_type node_id); ///< (1) parse YAML into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_in_arena( csubstr yaml, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_in_arena(csubstr filename, csubstr yaml, Tree *t ); ///< (3) parse YAML into an existing tree, into its root node. +RYML_EXPORT void parse_in_arena( csubstr yaml, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_in_arena(csubstr filename, csubstr yaml, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_in_arena( csubstr yaml, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_in_arena(csubstr filename, csubstr yaml ); ///< (7) create a new tree, and parse YAML into its root node. +RYML_EXPORT Tree parse_in_arena( csubstr yaml ); ///< (8) like (7) but no filename will be reported + +// this is vertically aligned to highlight the parameter differences. +RYML_EXPORT void parse_json_in_arena(csubstr filename, csubstr json, Tree *t, id_type node_id); ///< (1) parse JSON into an existing tree node. The filename will be used in any error messages arising during the parse. +RYML_EXPORT void parse_json_in_arena( csubstr json, Tree *t, id_type node_id); ///< (2) like (1) but no filename will be reported +RYML_EXPORT void parse_json_in_arena(csubstr filename, csubstr json, Tree *t ); ///< (3) parse JSON into an existing tree, into its root node. +RYML_EXPORT void parse_json_in_arena( csubstr json, Tree *t ); ///< (4) like (3) but no filename will be reported +RYML_EXPORT void parse_json_in_arena(csubstr filename, csubstr json, NodeRef node ); ///< (5) like (1) but the node is given as a NodeRef +RYML_EXPORT void parse_json_in_arena( csubstr json, NodeRef node ); ///< (6) like (5) but no filename will be reported +RYML_EXPORT Tree parse_json_in_arena(csubstr filename, csubstr json ); ///< (7) create a new tree, and parse JSON into its root node. +RYML_EXPORT Tree parse_json_in_arena( csubstr json ); ///< (8) like (7) but no filename will be reported + + +/* READ THE DEPRECATION NOTE! + * + * All of the functions below are intentionally left undefined, to + * prevent them being used. + */ +/** @cond dev */ +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena( substr yaml ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena(csubstr filename, substr yaml ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena( substr json, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(csubstr filename, substr json, Tree *t, id_type node_id); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena( substr json, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(csubstr filename, substr json, Tree *t ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena( substr json, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_json_in_arena(csubstr filename, substr json, NodeRef node ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_json_in_arena( substr json ); +RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_json_in_arena(csubstr filename, substr json ); +/** @endcond */ + +/** @} */ +/** @} */ + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_PARSE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/parse_engine.def.hpp b/3rdparty/rapidyaml/include/c4/yml/parse_engine.def.hpp new file mode 100644 index 0000000000..2aebaa3c7f --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/parse_engine.def.hpp @@ -0,0 +1,8317 @@ +#ifndef _C4_YML_PARSE_ENGINE_DEF_HPP_ +#define _C4_YML_PARSE_ENGINE_DEF_HPP_ + +#include "c4/yml/parse_engine.hpp" +#include "c4/error.hpp" +#include "c4/charconv.hpp" +#include "c4/utf.hpp" + +#include + +#include "c4/yml/detail/dbgprint.hpp" +#include "c4/yml/filter_processor.hpp" +#ifdef RYML_DBG +#include +#include "c4/yml/detail/print.hpp" +#define _c4err_(fmt, ...) do { RYML_DEBUG_BREAK(); this->_err("ERROR:\n" "{}:{}: " fmt, __FILE__, __LINE__, __VA_ARGS__); } while(0) +#define _c4err(fmt) do { RYML_DEBUG_BREAK(); this->_err("ERROR:\n" "{}:{}: " fmt, __FILE__, __LINE__); } while(0) +#else +#define _c4err_(fmt, ...) this->_err("ERROR: " fmt, __VA_ARGS__) +#define _c4err(fmt) this->_err("ERROR: {}", fmt) +#endif + + +#if defined(RYML_WITH_TAB_TOKENS) +#define _RYML_WITH_TAB_TOKENS(...) __VA_ARGS__ +#define _RYML_WITHOUT_TAB_TOKENS(...) +#define _RYML_WITH_OR_WITHOUT_TAB_TOKENS(with, without) with +#else +#define _RYML_WITH_TAB_TOKENS(...) +#define _RYML_WITHOUT_TAB_TOKENS(...) __VA_ARGS__ +#define _RYML_WITH_OR_WITHOUT_TAB_TOKENS(with, without) without +#endif + + +// scaffold: +#define _c4dbgnextline() \ + do { \ + _c4dbgq("\n-----------"); \ + _c4dbgt("handling line={}, offset={}B", \ + m_evt_handler->m_curr->pos.line, \ + m_evt_handler->m_curr->pos.offset); \ + } while(0) + + +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable: 4296/*expression is always 'boolean_value'*/) +# pragma warning(disable: 4702/*unreachable code*/) +#elif defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wtype-limits" // to remove a warning on an assertion that a size_t >= 0. Later on, this size_t will turn into a template argument, and then it can become < 0. +# pragma clang diagnostic ignored "-Wformat-nonliteral" +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtype-limits" // to remove a warning on an assertion that a size_t >= 0. Later on, this size_t will turn into a template argument, and then it can become < 0. +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +# pragma GCC diagnostic ignored "-Wold-style-cast" +# if __GNUC__ >= 7 +# pragma GCC diagnostic ignored "-Wduplicated-branches" +# endif +#endif + +// NOLINTBEGIN(hicpp-signed-bitwise,cppcoreguidelines-avoid-goto,hicpp-avoid-goto,hicpp-multiway-paths-covered) + +namespace c4 { +namespace yml { + +namespace { // NOLINT + +C4_HOT C4_ALWAYS_INLINE bool _is_blck_token(csubstr s) noexcept +{ + RYML_ASSERT(s.len > 0); + RYML_ASSERT(s.str[0] == '-' || s.str[0] == ':' || s.str[0] == '?'); + return ((s.len == 1) || ((s.str[1] == ' ') _RYML_WITH_TAB_TOKENS( || (s.str[1] == '\t')))); +} + +inline bool _is_doc_begin_token(csubstr s) +{ + RYML_ASSERT(s.begins_with('-')); + RYML_ASSERT(!s.ends_with("\n")); + RYML_ASSERT(!s.ends_with("\r")); + return (s.len >= 3 && s.str[1] == '-' && s.str[2] == '-') + && (s.len == 3 || (s.str[3] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[3] == '\t'))); +} + +inline bool _is_doc_end_token(csubstr s) +{ + RYML_ASSERT(s.begins_with('.')); + RYML_ASSERT(!s.ends_with("\n")); + RYML_ASSERT(!s.ends_with("\r")); + return (s.len >= 3 && s.str[1] == '.' && s.str[2] == '.') + && (s.len == 3 || (s.str[3] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[3] == '\t'))); +} + +inline bool _is_doc_token(csubstr s) noexcept +{ + // + // NOTE: this function was failing under some scenarios when + // compiled with gcc -O2 (but not -O3 or -O1 or -O0), likely + // related to optimizer assumptions on the input string and + // possibly caused from UB around assignment to that string (the + // call site was in _scan_block()). For more details see: + // + // https://github.com/biojppm/rapidyaml/issues/440 + // + // The current version does not suffer this problem, but it may + // appear again. + // + // + // UPDATE. The problem appeared again in gcc12 and gcc13 with -Os + // (but not any other optimization level, nor any other compiler + // or version), because the assignment to s is being hoisted out + // of the loop which calls this function. Then the length doesn't + // enter the s.len >= 3 when it should. Adding a + // C4_DONT_OPTIMIZE(var) makes the problem go away. + // + if(s.len >= 3) + { + switch(s.str[0]) + { + case '-': + //return _is_doc_begin_token(s); // this was failing with gcc -O2 + return (s.str[1] == '-' && s.str[2] == '-') + && (s.len == 3 || (s.str[3] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[3] == '\t'))); + case '.': + //return _is_doc_end_token(s); // this was failing with gcc -O2 + return (s.str[1] == '.' && s.str[2] == '.') + && (s.len == 3 || (s.str[3] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[3] == '\t'))); + } + } + return false; +} + +inline size_t _is_special_json_scalar(csubstr s) +{ + RYML_ASSERT(s.len); + switch(s.str[0]) + { + case 'f': + if(s.len >= 5 && s.begins_with("false")) + return 5u; + break; + case 't': + if(s.len >= 4 && s.begins_with("true")) + return 4u; + break; + case 'n': + if(s.len >= 4 && s.begins_with("null")) + return 4u; + break; + } + return 0u; +} + + +//----------------------------------------------------------------------------- + +C4_ALWAYS_INLINE size_t _extend_from_combined_newline(char nl, char following) +{ + return (nl == '\n' && following == '\r') || (nl == '\r' && following == '\n'); +} + +//! look for the next newline chars, and jump to the right of those +inline substr from_next_line(substr rem) +{ + size_t nlpos = rem.first_of("\r\n"); + if(nlpos == csubstr::npos) + return {}; + const char nl = rem[nlpos]; + rem = rem.right_of(nlpos); + if(rem.empty()) + return {}; + if(_extend_from_combined_newline(nl, rem.front())) + rem = rem.sub(1); + return rem; +} + + +//----------------------------------------------------------------------------- + +inline size_t _count_following_newlines(csubstr r, size_t *C4_RESTRICT i) +{ + RYML_ASSERT(r[*i] == '\n'); + size_t numnl_following = 0; + ++(*i); + for( ; *i < r.len; ++(*i)) + { + if(r.str[*i] == '\n') + ++numnl_following; + // skip leading whitespace + else if(r.str[*i] == ' ' || r.str[*i] == '\t' || r.str[*i] == '\r') + ; + else + break; + } + return numnl_following; +} + +/** @p i is set to the first non whitespace character after the line + * @return the number of empty lines after the initial position */ +inline size_t _count_following_newlines(csubstr r, size_t *C4_RESTRICT i, size_t indentation) +{ + RYML_ASSERT(r[*i] == '\n'); + size_t numnl_following = 0; + ++(*i); + if(indentation == 0) + { + for( ; *i < r.len; ++(*i)) + { + if(r.str[*i] == '\n') + ++numnl_following; + // skip leading whitespace + else if(r.str[*i] == ' ' || r.str[*i] == '\t' || r.str[*i] == '\r') + ; + else + break; + } + } + else + { + for( ; *i < r.len; ++(*i)) + { + if(r.str[*i] == '\n') + { + ++numnl_following; + // skip the indentation after the newline + size_t stop = *i + indentation; + for( ; *i < r.len; ++(*i)) + { + if(r.str[*i] != ' ' && r.str[*i] != '\r') + break; + RYML_ASSERT(*i < stop); + } + C4_UNUSED(stop); + } + // skip leading whitespace + else if(r.str[*i] == ' ' || r.str[*i] == '\t' || r.str[*i] == '\r') + ; + else + break; + } + } + return numnl_following; +} + +} // anon namespace + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +ParseEngine::~ParseEngine() +{ + _free(); + _clr(); +} + +template +ParseEngine::ParseEngine(EventHandler *evt_handler, ParserOptions opts) + : m_options(opts) + , m_file() + , m_buf() + , m_evt_handler(evt_handler) + , m_pending_anchors() + , m_pending_tags() + , m_was_inside_qmrk(false) + , m_doc_empty(false) + , m_prev_colon(npos) + , m_encoding(NOBOM) + , m_newline_offsets() + , m_newline_offsets_size(0) + , m_newline_offsets_capacity(0) + , m_newline_offsets_buf() +{ + RYML_CHECK(evt_handler); +} + +template +ParseEngine::ParseEngine(ParseEngine &&that) noexcept + : m_options(that.m_options) + , m_file(that.m_file) + , m_buf(that.m_buf) + , m_evt_handler(that.m_evt_handler) + , m_pending_anchors(that.m_pending_anchors) + , m_pending_tags(that.m_pending_tags) + , m_was_inside_qmrk(false) + , m_doc_empty(false) + , m_prev_colon(npos) + , m_encoding(NOBOM) + , m_newline_offsets(that.m_newline_offsets) + , m_newline_offsets_size(that.m_newline_offsets_size) + , m_newline_offsets_capacity(that.m_newline_offsets_capacity) + , m_newline_offsets_buf(that.m_newline_offsets_buf) +{ + that._clr(); +} + +template +ParseEngine::ParseEngine(ParseEngine const& that) + : m_options(that.m_options) + , m_file(that.m_file) + , m_buf(that.m_buf) + , m_evt_handler(that.m_evt_handler) + , m_pending_anchors(that.m_pending_anchors) + , m_pending_tags(that.m_pending_tags) + , m_was_inside_qmrk(false) + , m_doc_empty(false) + , m_prev_colon(npos) + , m_encoding(NOBOM) + , m_newline_offsets() + , m_newline_offsets_size() + , m_newline_offsets_capacity() + , m_newline_offsets_buf() +{ + if(that.m_newline_offsets_capacity) + { + _resize_locations(that.m_newline_offsets_capacity); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_newline_offsets_capacity == that.m_newline_offsets_capacity); + memcpy(m_newline_offsets, that.m_newline_offsets, that.m_newline_offsets_size * sizeof(size_t)); + m_newline_offsets_size = that.m_newline_offsets_size; + } +} + +template +ParseEngine& ParseEngine::operator=(ParseEngine &&that) noexcept +{ + _free(); + m_options = (that.m_options); + m_file = (that.m_file); + m_buf = (that.m_buf); + m_evt_handler = that.m_evt_handler; + m_pending_anchors = that.m_pending_anchors; + m_pending_tags = that.m_pending_tags; + m_was_inside_qmrk = that.m_was_inside_qmrk; + m_doc_empty = that.m_doc_empty; + m_prev_colon = that.m_prev_colon; + m_encoding = that.m_encoding; + m_newline_offsets = (that.m_newline_offsets); + m_newline_offsets_size = (that.m_newline_offsets_size); + m_newline_offsets_capacity = (that.m_newline_offsets_capacity); + m_newline_offsets_buf = (that.m_newline_offsets_buf); + that._clr(); + return *this; +} + +template +ParseEngine& ParseEngine::operator=(ParseEngine const& that) +{ + if(&that != this) + { + _free(); + m_options = (that.m_options); + m_file = (that.m_file); + m_buf = (that.m_buf); + m_evt_handler = that.m_evt_handler; + m_pending_anchors = that.m_pending_anchors; + m_pending_tags = that.m_pending_tags; + m_was_inside_qmrk = that.m_was_inside_qmrk; + m_doc_empty = that.m_doc_empty; + m_prev_colon = that.m_prev_colon; + m_encoding = that.m_encoding; + if(that.m_newline_offsets_capacity > m_newline_offsets_capacity) + _resize_locations(that.m_newline_offsets_capacity); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_newline_offsets_capacity >= that.m_newline_offsets_capacity); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_newline_offsets_capacity >= that.m_newline_offsets_size); + memcpy(m_newline_offsets, that.m_newline_offsets, that.m_newline_offsets_size * sizeof(size_t)); + m_newline_offsets_size = that.m_newline_offsets_size; + m_newline_offsets_buf = that.m_newline_offsets_buf; + } + return *this; +} + +template +void ParseEngine::_clr() +{ + m_options = {}; + m_file = {}; + m_buf = {}; + m_evt_handler = {}; + m_pending_anchors = {}; + m_pending_tags = {}; + m_was_inside_qmrk = false; + m_doc_empty = true; + m_prev_colon = npos; + m_encoding = NOBOM; + m_newline_offsets = {}; + m_newline_offsets_size = {}; + m_newline_offsets_capacity = {}; + m_newline_offsets_buf = {}; +} + +template +void ParseEngine::_free() +{ + if(m_newline_offsets) + { + _RYML_CB_FREE(m_evt_handler->m_stack.m_callbacks, m_newline_offsets, size_t, m_newline_offsets_capacity); + m_newline_offsets = nullptr; + m_newline_offsets_size = 0u; + m_newline_offsets_capacity = 0u; + m_newline_offsets_buf = nullptr; + } +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_reset() +{ + m_pending_anchors = {}; + m_pending_tags = {}; + m_doc_empty = true; + m_was_inside_qmrk = false; + m_prev_colon = npos; + m_encoding = NOBOM; + if(m_options.locations()) + { + _prepare_locations(); + } +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_relocate_arena(csubstr prev_arena, substr next_arena) +{ + #define _ryml_relocate(s) \ + if((s).is_sub(prev_arena)) \ + { \ + (s).str = next_arena.str + ((s).str - prev_arena.str); \ + } + _ryml_relocate(m_buf); + _ryml_relocate(m_newline_offsets_buf); + for(size_t i = 0; i < m_pending_tags.num_entries; ++i) + _ryml_relocate(m_pending_tags.annotations[i].str); + for(size_t i = 0; i < m_pending_anchors.num_entries; ++i) + _ryml_relocate(m_pending_anchors.annotations[i].str); + #undef _ryml_relocate +} + +template +void ParseEngine::_s_relocate_arena(void* data, csubstr prev_arena, substr next_arena) +{ + ((ParseEngine*)data)->_relocate_arena(prev_arena, next_arena); +} + + +//----------------------------------------------------------------------------- + +template +template +void ParseEngine::_fmt_msg(DumpFn &&dumpfn) const +{ + auto const *const C4_RESTRICT st = m_evt_handler->m_curr; + auto const& lc = st->line_contents; + csubstr contents = lc.stripped; + if(contents.len) + { + // print the yaml src line + size_t offs = 3u + to_chars(substr{}, st->pos.line) + to_chars(substr{}, st->pos.col); + if(m_file.len) + { + detail::_dump(std::forward(dumpfn), "{}:", m_file); + offs += m_file.len + 1; + } + detail::_dump(std::forward(dumpfn), "{}:{}: ", st->pos.line, st->pos.col); + csubstr maybe_full_content = (contents.len < 80u ? contents : contents.first(80u)); + csubstr maybe_ellipsis = (contents.len < 80u ? csubstr{} : csubstr("...")); + detail::_dump(std::forward(dumpfn), "{}{} (size={})\n", maybe_full_content, maybe_ellipsis, contents.len); + // highlight the remaining portion of the previous line + size_t firstcol = (size_t)(lc.rem.begin() - lc.full.begin()); + size_t lastcol = firstcol + lc.rem.len; + for(size_t i = 0; i < offs + firstcol; ++i) + std::forward(dumpfn)(" "); + std::forward(dumpfn)("^"); + for(size_t i = 1, e = (lc.rem.len < 80u ? lc.rem.len : 80u); i < e; ++i) + std::forward(dumpfn)("~"); + detail::_dump(std::forward(dumpfn), "{} (cols {}-{})\n", maybe_ellipsis, firstcol+1, lastcol+1); + } + else + { + std::forward(dumpfn)("\n"); + } + +#ifdef RYML_DBG + // next line: print the state flags + { + char flagbuf_[128]; + detail::_dump(std::forward(dumpfn), "top state: {}\n", detail::_parser_flags_to_str(flagbuf_, m_evt_handler->m_curr->flags)); + } +#endif +} + + +//----------------------------------------------------------------------------- + +template +template +void ParseEngine::_err(csubstr fmt, Args const& C4_RESTRICT ...args) const +{ + char errmsg[RYML_ERRMSG_SIZE]; + detail::_SubstrWriter writer(errmsg); + auto dumpfn = [&writer](csubstr s){ writer.append(s); }; + detail::_dump(dumpfn, fmt, args...); + writer.append('\n'); + _fmt_msg(dumpfn); + size_t len = writer.pos < RYML_ERRMSG_SIZE ? writer.pos : RYML_ERRMSG_SIZE; + m_evt_handler->cancel_parse(); + m_evt_handler->m_stack.m_callbacks.m_error(errmsg, len, m_evt_handler->m_curr->pos, m_evt_handler->m_stack.m_callbacks.m_user_data); +} + + +//----------------------------------------------------------------------------- +#ifdef RYML_DBG +template +template +void ParseEngine::_dbg(csubstr fmt, Args const& C4_RESTRICT ...args) const +{ + if(_dbg_enabled()) + { + auto dumpfn = [](csubstr s){ if(s.str) fwrite(s.str, 1, s.len, stdout); }; + detail::_dump(dumpfn, fmt, args...); + dumpfn("\n"); + _fmt_msg(dumpfn); + } +} +#endif + + +//----------------------------------------------------------------------------- +template +bool ParseEngine::_finished_file() const +{ + bool ret = m_evt_handler->m_curr->pos.offset >= m_buf.len; + if(ret) + { + _c4dbgp("finished file!!!"); + } + return ret; +} + +template +C4_HOT C4_ALWAYS_INLINE bool ParseEngine::_finished_line() const +{ + return m_evt_handler->m_curr->line_contents.rem.empty(); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_maybe_skip_whitespace_tokens() +{ + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(rem.len && (rem.str[0] == ' ' _RYML_WITH_TAB_TOKENS(|| rem.str[0] == '\t'))) + { + size_t pos = rem.first_not_of(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' ')); + if(pos == npos) + pos = rem.len; // maybe the line is just all whitespace + _c4dbgpf("skip {} whitespace characters", pos); + _line_progressed(pos); + } +} + +template +void ParseEngine::_maybe_skipchars(char c) +{ + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(rem.len && rem.str[0] == c) + { + size_t pos = rem.first_not_of(c); + if(pos == npos) + pos = rem.len; // maybe the line is just all c + _c4dbgpf("skip {}x'{}'", pos, c); + _line_progressed(pos); + } +} + +#ifdef RYML_NO_COVERAGE__TO_BE_DELETED +template +void ParseEngine::_maybe_skipchars_up_to(char c, size_t max_to_skip) +{ + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(rem.len && rem.str[0] == c) + { + size_t pos = rem.first_not_of(c); + if(pos == npos) + pos = rem.len; // maybe the line is just all c + if(pos > max_to_skip) + pos = max_to_skip; + _c4dbgpf("skip {}x'{}'", pos, c); + _line_progressed(pos); + } +} +#endif + +template +template +void ParseEngine::_skipchars(const char (&chars)[N]) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->line_contents.rem.begins_with_any(chars)); + size_t pos = m_evt_handler->m_curr->line_contents.rem.first_not_of(chars); + if(pos == npos) + pos = m_evt_handler->m_curr->line_contents.rem.len; // maybe the line is just whitespace + _c4dbgpf("skip {} characters", pos); + _line_progressed(pos); +} + +template +void ParseEngine::_skip_comment() +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->line_contents.rem.begins_with('#')); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->line_contents.rem.is_sub(m_evt_handler->m_curr->line_contents.full)); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + csubstr full = m_evt_handler->m_curr->line_contents.full; + // raise an error if the comment is not preceded by whitespace + if(!full.begins_with('#')) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.str > full.str); + const char c = full[(size_t)(rem.str - full.str - 1)]; + if(C4_UNLIKELY(c != ' ' && c != '\t')) + _RYML_CB_ERR(m_evt_handler->m_stack.m_callbacks, "comment not preceded by whitespace"); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.str == full.str); + } + _c4dbgpf("comment was '{}'", rem); + _line_progressed(rem.len); +} + +template +void ParseEngine::_maybe_skip_comment() +{ + csubstr s = m_evt_handler->m_curr->line_contents.rem.triml(' '); + if(s.begins_with('#')) + { + _line_progressed((size_t)(s.str - m_evt_handler->m_curr->line_contents.rem.str)); + _skip_comment(); + } +} + +template +bool ParseEngine::_maybe_scan_following_colon() noexcept +{ + if(m_evt_handler->m_curr->line_contents.rem.len) + { + if(m_evt_handler->m_curr->line_contents.rem.str[0] == ' ' || m_evt_handler->m_curr->line_contents.rem.str[0] == '\t') + { + size_t pos = m_evt_handler->m_curr->line_contents.rem.first_not_of(" \t"); + if(pos == npos) + pos = m_evt_handler->m_curr->line_contents.rem.len; // maybe the line has only spaces + _c4dbgpf("skip {}x'{}'", pos, ' '); + _line_progressed(pos); + } + if(m_evt_handler->m_curr->line_contents.rem.len && (m_evt_handler->m_curr->line_contents.rem.str[0] == ':')) + { + _c4dbgp("found ':' colon next"); + _line_progressed(1); + return true; + } + } + return false; +} + +template +bool ParseEngine::_maybe_scan_following_comma() noexcept +{ + if(m_evt_handler->m_curr->line_contents.rem.len) + { + if(m_evt_handler->m_curr->line_contents.rem.str[0] == ' ' || m_evt_handler->m_curr->line_contents.rem.str[0] == '\t') + { + size_t pos = m_evt_handler->m_curr->line_contents.rem.first_not_of(" \t"); + if(pos == npos) + pos = m_evt_handler->m_curr->line_contents.rem.len; // maybe the line has only spaces + _c4dbgpf("skip {}x'{}'", pos, ' '); + _line_progressed(pos); + } + if(m_evt_handler->m_curr->line_contents.rem.len && (m_evt_handler->m_curr->line_contents.rem.str[0] == ',')) + { + _c4dbgp("found ',' comma next"); + _line_progressed(1); + return true; + } + } + return false; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_scan_anchor() +{ + csubstr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('&')); + csubstr anchor = s.range(1, s.first_of(' ')); + _line_progressed(1u + anchor.len); + _maybe_skipchars(' '); + return anchor; +} + +template +csubstr ParseEngine::_scan_ref_seq() +{ + csubstr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('*')); + csubstr ref = s.first(s.first_of(",] :")); + _line_progressed(ref.len); + return ref; +} + +template +csubstr ParseEngine::_scan_ref_map() +{ + csubstr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('*')); + csubstr ref = s.first(s.first_of(",} ")); + _line_progressed(ref.len); + return ref; +} + +template +csubstr ParseEngine::_scan_tag() +{ + csubstr rem = m_evt_handler->m_curr->line_contents.rem.triml(' '); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.begins_with('!')); + csubstr t; + if(rem.begins_with("!!")) + { + _c4dbgp("begins with '!!'"); + if(has_any(FLOW)) + t = rem.left_of(rem.first_of(" ,")); + else + t = rem.left_of(rem.first_of(' ')); + } + else if(rem.begins_with("!<")) + { + _c4dbgp("begins with '!<'"); + t = rem.left_of(rem.first_of('>'), true); + } + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + else if(rem.begins_with("!h!")) + { + _c4dbgp("begins with '!h!'"); + t = rem.left_of(rem.first_of(' ')); + } + #endif + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.begins_with('!')); + _c4dbgp("begins with '!'"); + if(has_any(FLOW)) + t = rem.left_of(rem.first_of(" ,")); + else + t = rem.left_of(rem.first_of(' ')); + } + _line_progressed(t.len); + _maybe_skip_whitespace_tokens(); + return t; +} + + +//----------------------------------------------------------------------------- + +template +bool ParseEngine::_is_valid_start_scalar_plain_flow(csubstr s) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.empty()); + + // it's not a scalar if it starts with any of these characters: + switch(s.str[0]) + { + // these are all legal tokens which mean no scalar is starting: + case '[': + case ']': + case '{': + case '}': + case '!': + case '&': + case '*': + case '|': + case '>': + case '#': + _c4dbgpf("not a scalar: found non-scalar token '{}'", _c4prc(s.str[0])); + return false; + // '-' and ':' are illegal at the beginning if not followed by a scalar character + case '-': + case ':': + if(s.len > 1) + { + switch(s.str[1]) + { + case '\n': + case '\r': + case '{': + case '[': + //_RYML_WITHOUT_TAB_TOKENS(case '\t'): + _c4err_("invalid token \":{}\"", _c4prc(s.str[1])); + break; + case ' ': + case '}': + case ']': + if(s.str[0] == ':') + { + _c4dbgpf("not a scalar: found non-scalar token '{}{}'", s.str[0], s.str[1]); + return false; + } + break; + default: + break; + } + } + else + { + return false; + } + break; + case '?': + if(s.len > 1) + { + switch(s.str[1]) + { + case ' ': + case '\n': + case '\r': + _RYML_WITHOUT_TAB_TOKENS(case '\t':) + _c4dbgpf("not a scalar: found non-scalar token '?{}'", _c4prc(s.str[1])); + return false; + case '{': + case '}': + case '[': + case ']': + _c4err_("invalid token \"?{}\"", _c4prc(s.str[1])); + break; + default: + break; + } + } + else + { + return false; + } + break; + // everything else is a legal starting character + default: + break; + } + + return true; +} + +template +bool ParseEngine::_scan_scalar_plain_seq_flow(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RSEQ|RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL)); + + substr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with(' ')); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with('\n')); + + if(!s.len) + return false; + + if(!_is_valid_start_scalar_plain_flow(s)) + return false; + + _c4dbgp("scanning seqflow scalar..."); + + const size_t start_offset = m_evt_handler->m_curr->pos.offset; + bool needs_filter = false; + while(true) + { + _c4dbgpf("scanning scalar: curr line=[{}]~~~{}~~~", s.len, s); + for(size_t i = 0; i < s.len; ++i) + { + const char c = s.str[i]; + switch(c) + { + case ',': + _c4dbgpf("found terminating character at {}: '{}'", i, c); + _line_progressed(i); + if(m_evt_handler->m_curr->pos.offset + i > start_offset) + { + goto ended_scalar; + } + else + { + _c4dbgp("at the beginning. no scalar here."); + return false; + } + break; + case ']': + _c4dbgpf("found terminating character at {}: '{}'", i, c); + _line_progressed(i); + goto ended_scalar; + break; + case '#': + _c4dbgp("found suspicious '#'"); + if(!i || (s.str[i-1] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[i-1] == '\t'))) + { + _c4dbgpf("found terminating character at {}: '{}'", i, c); + _line_progressed(i); + goto ended_scalar; + } + break; + case ':': + _c4dbgp("found suspicious ':'"); + if(s.len > i+1) + { + const char next = s.str[i+1]; + _c4dbgpf("next char is '{}'", _c4prc(next)); + if(next == ' ' || next == ',' _RYML_WITH_TAB_TOKENS(|| next == '\t')) + { + _c4dbgp("map starting!"); + if(m_evt_handler->m_curr->pos.offset + i > start_offset) + { + _c4dbgp("scalar finished!"); + _line_progressed(i); + goto ended_scalar; + } + else + { + _c4dbgp("at the beginning. no scalar here."); + return false; + } + } + else + { + _c4dbgp("it's a scalar indeed."); + ++i; // skip the next char + } + } + else if(s.len == i+1) + { + _c4dbgp("':' at line end. map starting!"); + return false; + } + break; + case '[': + case '{': + case '}': + _line_progressed(i); + _c4err_("invalid character: '{}'", c); // noreturn + default: + ; + } + } + _line_progressed(s.len); + if(!_finished_file()) + { + _c4dbgp("next line!"); + _line_ended(); + _scan_line(); + } + else + { + _c4dbgp("file finished!"); + goto ended_scalar; + } + s = m_evt_handler->m_curr->line_contents.rem; + needs_filter = true; + } + +ended_scalar: + + sc->scalar = m_buf.range(start_offset, m_evt_handler->m_curr->pos.offset).trimr(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' ')); + sc->needs_filter = needs_filter; + + _c4prscalar("scanned plain scalar", sc->scalar, /*keep_newlines*/true); + + return true; +} + +template +bool ParseEngine::_scan_scalar_plain_map_flow(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RSEQ) || has_any(RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RMAP|RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RVAL|QMRK)); + + substr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with(' ')); + + if(!s.len) + return false; + + if(!_is_valid_start_scalar_plain_flow(s)) + return false; + + _c4dbgp("scanning scalar..."); + + const size_t start_offset = m_evt_handler->m_curr->pos.offset; + bool needs_filter = false; + while(true) + { + for(size_t i = 0; i < s.len; ++i) + { + const char c = s.str[i]; + switch(c) + { + case ',': + case '}': + _line_progressed(i); + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + case ':': + if(s.len == i+1 || s.str[i+1] == ' ' || s.str[i+1] == ',' || s.str[i+1] == '}' _RYML_WITH_TAB_TOKENS(|| s.str[i+1] == '\t')) + { + _line_progressed(i); + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + } + break; + case '{': + case '[': + _line_progressed(i); + _c4err_("invalid character: '{}'", c); // noreturn + break; + case ']': + _line_progressed(i); + if(has_any(RSEQIMAP)) + goto ended_scalar; + else + _c4err_("invalid character: '{}'", c); // noreturn + break; + case '#': + if(!i || s.str[i-1] == ' ' _RYML_WITH_TAB_TOKENS(|| s.str[i-1] == '\t')) + { + _line_progressed(i); + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + } + break; + default: + ; + } + } + _c4dbgp("next line!"); + _line_progressed(s.len); + if(!_finished_file()) + { + _c4dbgp("next line!"); + _line_ended(); + _scan_line(); + } + else + { + _c4dbgp("file finished!"); + goto ended_scalar; + } + s = m_evt_handler->m_curr->line_contents.rem; + needs_filter = true; + } + +ended_scalar: + + sc->scalar = m_buf.range(start_offset, m_evt_handler->m_curr->pos.offset).trimr(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \n\t\r", " \n\r")); + sc->needs_filter = needs_filter; + + _c4dbgpf("scalar was [{}]~~~{}~~~", sc->scalar.len, sc->scalar); + + return sc->scalar.len > 0u; +} + +template +bool ParseEngine::_scan_scalar_seq_json(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(FLOW)); + + substr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with(' ')); + + if(!s.len) + return false; + + _c4dbgp("scanning scalar..."); + + switch(s.str[0]) + { + case ']': + case '{': + case ',': + _c4dbgp("not a scalar."); + return false; + } + + { + const size_t len = _is_special_json_scalar(s); + if(len) + { + sc->scalar = s.first(len); + sc->needs_filter = false; + _c4dbgpf("special json scalar: '{}'", sc->scalar); + _line_progressed(len); + return true; + } + } + + // must be a number + size_t i = 0; + for( ; i < s.len; ++i) + { + const char c = s.str[i]; + switch(c) + { + case ',': + case ']': + case ' ': + case '\t': + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + case '#': + if(!i || s.str[i-1] == ' ') + { + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + } + break; + default: + ; + } + } + +ended_scalar: + + if(C4_LIKELY(i > 0)) + { + _line_progressed(i); + sc->scalar = s.first(i); + sc->needs_filter = false; + _c4dbgpf("scalar was [{}]~~~{}~~~", sc->scalar.len, sc->scalar); + return true; + } + + return false; +} + +template +bool ParseEngine::_scan_scalar_map_json(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RVAL)); + + substr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with(' ')); + + if(!s.len) + return false; + + _c4dbgp("scanning scalar..."); + + { + const size_t len = _is_special_json_scalar(s); + if(len) + { + sc->scalar = s.first(len); + sc->needs_filter = false; + _c4dbgpf("special json scalar: '{}'", sc->scalar); + _line_progressed(len); + return true; + } + } + + // must be a number + size_t i = 0; + for( ; i < s.len; ++i) + { + const char c = s.str[i]; + switch(c) + { + case ',': + case '}': + case ' ': + case '\t': + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + case '#': + if(!i || s.str[i-1] == ' ') + { + _c4dbgpf("found terminating character: '{}'", c); + goto ended_scalar; + } + break; + default: + ; + } + } + +ended_scalar: + + if(C4_LIKELY(i > 0)) + { + _line_progressed(i); + sc->scalar = s.first(i); + sc->needs_filter = false; + _c4dbgpf("scalar was [{}]~~~{}~~~", sc->scalar.len, sc->scalar); + return true; + } + + return false; +} + +template +bool ParseEngine::_is_doc_begin(csubstr s) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s[0] == '-'); + return (m_evt_handler->m_curr->line_contents.indentation == 0u && _at_line_begin() && _is_doc_begin_token(s)); +} + +template +bool ParseEngine::_is_doc_end(csubstr s) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s[0] == '.'); + return (m_evt_handler->m_curr->line_contents.indentation == 0u && _at_line_begin() && _is_doc_end_token(s)); +} + +template +bool ParseEngine::_scan_scalar_plain_blck(ScannedScalar *C4_RESTRICT sc, size_t indentation) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(BLCK|RUNK|USTY)); + + substr s = m_evt_handler->m_curr->line_contents.rem; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !s.begins_with(' ')); + + if(!s.len) + return false; + + switch(s.str[0]) + { + case '-': + if(_is_blck_token(s)) + { + return false; + } + else if(_is_doc_begin(s)) + { + _c4dbgp("token is doc start"); + return false; + } + break; + case ':': + case '?': + if(_is_blck_token(s)) + return false; + break; + case '[': + case '{': + case '&': + case '*': + case '!': + _RYML_WITH_TAB_TOKENS(case '\t':) + return false; + case '.': + if(_is_doc_end(s)) + { + _c4dbgp("token is doc end"); + return false; + } + break; + } + + _c4dbgpf("plain scalar! indentation={}", indentation); + + const size_t start_offset = m_evt_handler->m_curr->pos.offset; + const size_t start_line = m_evt_handler->m_curr->pos.line; + + bool needs_filter = false; + while(true) + { + _c4dbgpf("plain scalar line: [{}]~~~{}~~~", s.len, s); + for(size_t i = 0; i < s.len; ++i) + { + const char curr = s.str[i]; + //_c4dbgpf("[{}]='{}'", i, _c4prc(curr)); + switch(curr) + { + case ':': + _c4dbgpf("[{}]: got suspicious ':'", i); + // are there more characters? + if((i + 1 == s.len) || ((s.str[i+1] == ' ') _RYML_WITH_TAB_TOKENS( || (s.str[i+1] == '\t')))) + { + _c4dbgpf("followed by '{}'", i+1 == s.len ? csubstr("\\n") : _c4prc(s.str[i+1])); + _line_progressed(i); + // ': ' is accepted only on the first line + if(C4_LIKELY(m_evt_handler->m_curr->pos.line == start_line)) + { + _c4dbgp("start line. scalar ends here"); + goto ended_scalar; + } + else + { + _c4err("parse error"); + } + } + else + { + size_t j = i; + while(j + 1 < s.len && s.str[j+1] == ':') + { + _c4dbgp("skip colon"); + ++j; + } + i = j > i ? j-1 : i; + _c4dbgp("nothing to see here"); + } + break; + case '#': + _c4dbgp("got suspicious '#'"); + if(!i || (s.str[i-1] == ' ' || s.str[i-1] == '\t')) + { + _c4dbgp("comment! scalar ends here"); + _line_progressed(i); + goto ended_scalar; + } + else + { + _c4dbgp("nothing to see here"); + } + break; + } + } + _line_progressed(s.len); + csubstr next_peeked = _peek_next_line(m_evt_handler->m_curr->pos.offset); + next_peeked = next_peeked.trimr("\n\r"); + const size_t next_indentation = next_peeked.first_not_of(' '); + _c4dbgpf("indentation curr={} next={}", indentation, next_indentation); + if(next_indentation < indentation) + { + _c4dbgp("smaller indentation! scalar ended"); + goto ended_scalar; + } + else if(next_indentation == 0 && next_peeked.len > 0) + { + const char first = next_peeked.str[0]; + switch(first) + { + case '-': + next_peeked = next_peeked.trimr("\n\r"); + _c4dbgpf("doc begin? peeked=[{}]~~~{}{}~~~", next_peeked.len, next_peeked.len >= 3 ? next_peeked.first(3) : next_peeked, next_peeked.len > 3 ? "..." : ""); + if(_is_doc_begin_token(next_peeked)) + { + _c4dbgp("doc begin! scalar ended"); + goto ended_scalar; + } + break; + case '.': + next_peeked = next_peeked.trimr("\n\r"); + _c4dbgpf("doc end? peeked=[{}]~~~{}{}~~~", next_peeked.len, next_peeked.len >= 3 ? next_peeked.first(3) : next_peeked, next_peeked.len > 3 ? "..." : ""); + if(_is_doc_end_token(next_peeked)) + { + _c4dbgp("doc end! scalar ended"); + goto ended_scalar; + } + break; + } + } + // load with next line + _c4dbgp("next line!"); + if(!_finished_file()) + { + _c4dbgp("next line!"); + _line_ended(); + _scan_line(); + } + else + { + _c4dbgp("file finished!"); + goto ended_scalar; + } + s = m_evt_handler->m_curr->line_contents.rem; + needs_filter = true; + } + +ended_scalar: + + sc->scalar = m_buf.range(start_offset, m_evt_handler->m_curr->pos.offset).trimr(" \n\r\t"); + sc->needs_filter = needs_filter; + + _c4dbgpf("scalar was [{}]~~~{}~~~", sc->scalar.len, sc->scalar); + + return true; +} + +template +bool ParseEngine::_scan_scalar_plain_seq_blck(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL)); + return _scan_scalar_plain_blck(sc, m_evt_handler->m_curr->indref + 1u); +} + +template +bool ParseEngine::_scan_scalar_plain_map_blck(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RVAL|QMRK)); + return _scan_scalar_plain_blck(sc, m_evt_handler->m_curr->indref + 1u); +} + +template +bool ParseEngine::_scan_scalar_plain_unk(ScannedScalar *C4_RESTRICT sc) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RUNK|USTY)); + return _scan_scalar_plain_blck(sc, m_evt_handler->m_curr->indref); +} + + +//----------------------------------------------------------------------------- + +template +substr ParseEngine::_peek_next_line(size_t pos) const +{ + substr rem{}; // declare here because of the goto + size_t nlpos{}; // declare here because of the goto + pos = pos == npos ? m_evt_handler->m_curr->pos.offset : pos; + if(pos >= m_buf.len) + goto next_is_empty; + + // look for the next newline chars, and jump to the right of those + rem = from_next_line(m_buf.sub(pos)); + if(rem.empty()) + goto next_is_empty; + + // now get everything up to and including the following newline chars + nlpos = rem.first_of("\r\n"); + if((nlpos != csubstr::npos) && (nlpos + 1 < rem.len)) + nlpos += _extend_from_combined_newline(rem[nlpos], rem[nlpos+1]); + rem = rem.left_of(nlpos, /*include_pos*/true); + + _c4dbgpf("peek next line @ {}: (len={})'{}'", pos, rem.len, rem.trimr("\r\n")); + return rem; + +next_is_empty: + _c4dbgpf("peek next line @ {}: (len=0)''", pos); + return {}; +} + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_scan_line() +{ + if(C4_LIKELY(m_evt_handler->m_curr->pos.offset < m_buf.len)) + m_evt_handler->m_curr->line_contents.reset_with_next_line(m_buf, m_evt_handler->m_curr->pos.offset); + else + m_evt_handler->m_curr->line_contents.reset(m_buf.last(0), m_buf.last(0)); +} + +template +void ParseEngine::_line_progressed(size_t ahead) +{ + _c4dbgpf("line[{}] ({} cols) progressed by {}: col {}-->{} offset {}-->{}", m_evt_handler->m_curr->pos.line, m_evt_handler->m_curr->line_contents.full.len, ahead, m_evt_handler->m_curr->pos.col, m_evt_handler->m_curr->pos.col+ahead, m_evt_handler->m_curr->pos.offset, m_evt_handler->m_curr->pos.offset+ahead); + m_evt_handler->m_curr->pos.offset += ahead; + m_evt_handler->m_curr->pos.col += ahead; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.col <= m_evt_handler->m_curr->line_contents.stripped.len+1); + m_evt_handler->m_curr->line_contents.rem = m_evt_handler->m_curr->line_contents.rem.sub(ahead); +} + +template +void ParseEngine::_line_ended() +{ + _c4dbgpf("line[{}] ({} cols) ended! offset {}-->{} / col {}-->{}", + m_evt_handler->m_curr->pos.line, + m_evt_handler->m_curr->line_contents.full.len, + m_evt_handler->m_curr->pos.offset, m_evt_handler->m_curr->pos.offset + m_evt_handler->m_curr->line_contents.full.len - m_evt_handler->m_curr->line_contents.stripped.len, + m_evt_handler->m_curr->pos.col, 1); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.col == m_evt_handler->m_curr->line_contents.stripped.len + 1); + m_evt_handler->m_curr->pos.offset += m_evt_handler->m_curr->line_contents.full.len - m_evt_handler->m_curr->line_contents.stripped.len; + ++m_evt_handler->m_curr->pos.line; + m_evt_handler->m_curr->pos.col = 1; +} + +template +void ParseEngine::_line_ended_undo() +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.col == 1u); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.line > 0u); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.offset >= m_evt_handler->m_curr->line_contents.full.len - m_evt_handler->m_curr->line_contents.stripped.len); + const size_t delta = m_evt_handler->m_curr->line_contents.full.len - m_evt_handler->m_curr->line_contents.stripped.len; + _c4dbgpf("line[{}] undo ended! line {}-->{}, offset {}-->{}", m_evt_handler->m_curr->pos.line, m_evt_handler->m_curr->pos.line, m_evt_handler->m_curr->pos.line - 1, m_evt_handler->m_curr->pos.offset, m_evt_handler->m_curr->pos.offset - delta); + m_evt_handler->m_curr->pos.offset -= delta; + --m_evt_handler->m_curr->pos.line; + m_evt_handler->m_curr->pos.col = m_evt_handler->m_curr->line_contents.stripped.len + 1u; + // don't forget to undo also the changes to the remainder of the line + //_RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.offset >= m_buf.len || m_buf[m_evt_handler->m_curr->pos.offset] == '\n' || m_buf[m_evt_handler->m_curr->pos.offset] == '\r'); + m_evt_handler->m_curr->line_contents.rem = m_buf.sub(m_evt_handler->m_curr->pos.offset, 0); +} + + +//----------------------------------------------------------------------------- +template +void ParseEngine::_set_indentation(size_t indentation) +{ + m_evt_handler->m_curr->indref = indentation; + _c4dbgpf("state[{}]: saving indentation: {}", m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); +} + +template +void ParseEngine::_save_indentation() +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->line_contents.rem.begin() >= m_evt_handler->m_curr->line_contents.full.begin()); + m_evt_handler->m_curr->indref = m_evt_handler->m_curr->line_contents.current_col(); + _c4dbgpf("state[{}]: saving indentation: {}", m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_end_map_blck() +{ + _c4dbgp("mapblck: end"); + if(has_any(RKCL|RVAL)) + { + _c4dbgp("mapblck: set missing val"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + } + else if(has_any(QMRK)) + { + _c4dbgp("mapblck: set missing keyval"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_plain_empty(); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + } + m_evt_handler->end_map(); +} + +template +void ParseEngine::_end_seq_blck() +{ + if(has_any(RVAL)) + { + _c4dbgp("seqblck: set missing val"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + } + m_evt_handler->end_seq(); +} + +template +void ParseEngine::_end2_map() +{ + _c4dbgp("map: end"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RMAP)); + if(has_any(BLCK)) + { + _end_map_blck(); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(USTY)); + m_evt_handler->_pop(); + } +} + +template +void ParseEngine::_end2_seq() +{ + _c4dbgp("seq: end"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RSEQ)); + if(has_any(BLCK)) + { + _end_seq_blck(); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(USTY)); + m_evt_handler->_pop(); + } +} + +template +void ParseEngine::_begin2_doc() +{ + m_doc_empty = true; + add_flags(RDOC); + m_evt_handler->begin_doc(); + m_evt_handler->m_curr->indref = 0; // ? +} + +template +void ParseEngine::_begin2_doc_expl() +{ + m_doc_empty = true; + add_flags(RDOC); + m_evt_handler->begin_doc_expl(); + m_evt_handler->m_curr->indref = 0; // ? +} + +template +void ParseEngine::_end2_doc() +{ + _c4dbgp("doc: end"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RDOC)); + if(m_doc_empty || (m_pending_tags.num_entries || m_pending_anchors.num_entries)) + { + _c4dbgp("doc was empty; add empty val"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + } + m_evt_handler->end_doc(); +} + +template +void ParseEngine::_end2_doc_expl() +{ + _c4dbgp("doc: end"); + if(m_doc_empty || (m_pending_tags.num_entries || m_pending_anchors.num_entries)) + { + _c4dbgp("doc: no children; add empty val"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + } + m_evt_handler->end_doc_expl(); +} + +template +void ParseEngine::_maybe_begin_doc() +{ + if(has_none(RDOC)) + { + _c4dbgp("doc must be started"); + _begin2_doc(); + } +} +template +void ParseEngine::_maybe_end_doc() +{ + if(has_any(RDOC)) + { + _c4dbgp("doc must be finished"); + _end2_doc(); + } + else if(m_doc_empty && (m_pending_tags.num_entries || m_pending_anchors.num_entries)) + { + _c4dbgp("no doc to finish, but pending annotations"); + m_evt_handler->begin_doc(); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_doc(); + } +} + +template +void ParseEngine::_end_doc_suddenly__pop() +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_stack.size() >= 1); + if(m_evt_handler->m_stack[0].flags & RDOC) + { + _c4dbgp("root is RDOC"); + if(m_evt_handler->m_curr->level != 0) + _handle_indentation_pop(&m_evt_handler->m_stack[0]); + } + else if((m_evt_handler->m_stack.size() > 1) && (m_evt_handler->m_stack[1].flags & RDOC)) + { + _c4dbgp("root is STREAM"); + if(m_evt_handler->m_curr->level != 1) + _handle_indentation_pop(&m_evt_handler->m_stack[1]); + } + else + { + _c4err("internal error"); + } + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RDOC)); +} + +template +void ParseEngine::_end_doc_suddenly() +{ + _c4dbgp("end doc suddenly"); + _end_doc_suddenly__pop(); + _end2_doc_expl(); + addrem_flags(RUNK|RTOP|NDOC, RMAP|RSEQ|RDOC); +} + +template +void ParseEngine::_start_doc_suddenly() +{ + _c4dbgp("start doc suddenly"); + _end_doc_suddenly__pop(); + _end2_doc(); + _begin2_doc_expl(); +} + +template +void ParseEngine::_end_stream() +{ + _c4dbgpf("end_stream, level={} node_id={}", m_evt_handler->m_curr->level, m_evt_handler->m_curr->node_id); + if(has_all(RSEQ|FLOW)) + _c4err("missing terminating ]"); + else if(has_all(RMAP|FLOW)) + _c4err("missing terminating }"); + if(m_evt_handler->m_stack.size() > 1) + _handle_indentation_pop(m_evt_handler->m_stack.begin()); + if(has_all(RDOC)) + { + _end2_doc(); + } + else if(has_all(RTOP|RUNK)) + { + if(m_pending_anchors.num_entries || m_pending_tags.num_entries) + { + if(m_doc_empty) + { + m_evt_handler->begin_doc(); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_doc(); + } + } + } + m_evt_handler->end_stream(); +} + + +template +void ParseEngine::_handle_indentation_pop(ParserState const* popto) +{ + _c4dbgpf("popping {} level{}: from level {}(@ind={}) to level {}(@ind={})", m_evt_handler->m_curr->level - popto->level, (((m_evt_handler->m_curr->level - popto->level) > 1) ? "s" : ""), m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref, popto->level, popto->indref); + while(m_evt_handler->m_curr != popto) + { + if(has_any(RSEQ)) + { + _c4dbgpf("popping seq at level {} (indentation={},addr={})", m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref, m_evt_handler->m_curr); + _end2_seq(); + } + else if(has_any(RMAP)) + { + _c4dbgpf("popping map at level {} (indentation={},addr={})", m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref, m_evt_handler->m_curr); + _end2_map(); + } + else + { + break; + } + } + _c4dbgpf("current level is {} (indentation={})", m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); +} + +template +void ParseEngine::_handle_indentation_pop_from_block_seq() +{ + // search the stack frame to jump to based on its indentation + using state_type = typename EventHandler::state; + state_type const* popto = nullptr; + auto &stack = m_evt_handler->m_stack; + _RYML_CB_ASSERT(stack.m_callbacks, stack.is_contiguous()); // this search relies on the stack being contiguous + _RYML_CB_ASSERT(stack.m_callbacks, m_evt_handler->m_curr >= stack.begin() && m_evt_handler->m_curr < stack.end()); + const size_t ind = m_evt_handler->m_curr->line_contents.indentation; + #ifdef RYML_DBG + if(_dbg_enabled()) + { + char flagbuf_[128]; + for(state_type const& s : stack) + _dbg_printf("state[{}]: ind={} node={} flags={}\n", s.level, s.indref, s.node_id, detail::_parser_flags_to_str(flagbuf_, s.flags)); + } + #endif + for(state_type const* s = m_evt_handler->m_curr-1; s >= stack.begin(); --s) + { + _c4dbgpf("searching for state with indentation {}. curr={} (level={},node={})", ind, s->indref, s->level, s->node_id); + if(s->indref == ind) + { + _c4dbgpf("gotit!!! level={} node={}", s->level, s->node_id); + popto = s; + break; + } + } + if(!popto || popto >= m_evt_handler->m_curr || popto->level >= m_evt_handler->m_curr->level) + { + _c4err("parse error: incorrect indentation?"); + } + _handle_indentation_pop(popto); +} + +template +void ParseEngine::_handle_indentation_pop_from_block_map() +{ + // search the stack frame to jump to based on its indentation + using state_type = typename EventHandler::state; + auto &stack = m_evt_handler->m_stack; + _RYML_CB_ASSERT(stack.m_callbacks, stack.is_contiguous()); // this search relies on the stack being contiguous + _RYML_CB_ASSERT(stack.m_callbacks, m_evt_handler->m_curr >= stack.begin() && m_evt_handler->m_curr < stack.end()); + const size_t ind = m_evt_handler->m_curr->line_contents.indentation; + state_type const* popto = nullptr; + #ifdef RYML_DBG + char flagbuf_[128]; + if(_dbg_enabled()) + { + for(state_type const& s : stack) + _dbg_printf("state[{}]: ind={} node={} flags={}\n", s.level, s.indref, s.node_id, detail::_parser_flags_to_str(flagbuf_, s.flags)); + } + #endif + for(state_type const* s = m_evt_handler->m_curr-1; s > stack.begin(); --s) // never go to the stack bottom. that's the root + { + _c4dbgpf("searching for state with indentation {}. current: ind={},level={},node={},flags={}", ind, s->indref, s->level, s->node_id, detail::_parser_flags_to_str(flagbuf_, s->flags)); + if(s->indref < ind) + { + break; + } + else if(s->indref == ind) + { + _c4dbgpf("same indentation!!! level={} node={}", s->level, s->node_id); + if(popto && has_any(RTOP, s) && has_none(RMAP|RSEQ, s)) + { + break; + } + popto = s; + if(has_all(RSEQ|BLCK, s)) + { + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + const size_t first = rem.first_not_of(' '); + _RYML_CB_ASSERT(stack.m_callbacks, first == ind || first == npos); + rem = rem.right_of(first, true); + _c4dbgpf("indentless? rem='{}' first={}", rem, first); + if(rem.begins_with('-') && _is_blck_token(rem)) + { + _c4dbgp("parent was indentless seq"); + break; + } + } + } + } + if(!popto || popto >= m_evt_handler->m_curr || popto->level >= m_evt_handler->m_curr->level) + { + _c4err("parse error: incorrect indentation?"); + } + _handle_indentation_pop(popto); +} + + +//----------------------------------------------------------------------------- +template +typename ParseEngine::ScannedScalar ParseEngine::_scan_scalar_squot() +{ + // quoted scalars can spread over multiple lines! + // nice explanation here: http://yaml-multiline.info/ + + // a span to the end of the file + size_t b = m_evt_handler->m_curr->pos.offset; + substr s = m_buf.sub(b); + if(s.begins_with(' ')) + { + s = s.triml(' '); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf.sub(b).is_super(s)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begin() >= m_buf.sub(b).begin()); + _line_progressed((size_t)(s.begin() - m_buf.sub(b).begin())); + } + b = m_evt_handler->m_curr->pos.offset; // take this into account + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('\'')); + + // skip the opening quote + _line_progressed(1); + s = s.sub(1); + + bool needs_filter = false; + + size_t numlines = 1; // we already have one line + size_t pos = npos; // find the pos of the matching quote + while( ! _finished_file()) + { + const csubstr line = m_evt_handler->m_curr->line_contents.rem; + bool line_is_blank = true; + _c4dbgpf("scanning single quoted scalar @ line[{}]: ~~~{}~~~", m_evt_handler->m_curr->pos.line, line); + for(size_t i = 0; i < line.len; ++i) + { + const char curr = line.str[i]; + if(curr == '\'') // single quotes are escaped with two single quotes + { + const char next = i+1 < line.len ? line.str[i+1] : '~'; + if(next != '\'') // so just look for the first quote + { // without another after it + pos = i; + break; + } + else + { + needs_filter = true; // needs filter to remove escaped quotes + ++i; // skip the escaped quote + } + } + else if(curr != ' ') + { + line_is_blank = false; + } + } + + // leading whitespace also needs filtering + needs_filter = needs_filter + || (numlines > 1) + || line_is_blank + || (_at_line_begin() && line.begins_with(' ')); + + if(pos == npos) + { + _line_progressed(line.len); + ++numlines; + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, pos >= 0 && pos < m_buf.len); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf[m_evt_handler->m_curr->pos.offset + pos] == '\''); + _line_progressed(pos + 1); // progress beyond the quote + pos = m_evt_handler->m_curr->pos.offset - b - 1; // but we stop before it + break; + } + + _line_ended(); + _scan_line(); + } + + if(pos == npos) + { + _c4err("reached end of file while looking for closing quote"); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, pos > 0); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.end() >= m_buf.begin() && s.end() <= m_buf.end()); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.end() == m_buf.end() || *s.end() == '\''); + s = s.sub(0, pos-1); + } + + _c4prscalar("scanned squoted scalar", s, /*keep_newlines*/true); + + return ScannedScalar { s, needs_filter }; +} + + +//----------------------------------------------------------------------------- +template +typename ParseEngine::ScannedScalar ParseEngine::_scan_scalar_dquot() +{ + // quoted scalars can spread over multiple lines! + // nice explanation here: http://yaml-multiline.info/ + + // a span to the end of the file + size_t b = m_evt_handler->m_curr->pos.offset; + substr s = m_buf.sub(b); + if(s.begins_with(' ')) + { + s = s.triml(' '); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf.sub(b).is_super(s)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begin() >= m_buf.sub(b).begin()); + _line_progressed((size_t)(s.begin() - m_buf.sub(b).begin())); + } + b = m_evt_handler->m_curr->pos.offset; // take this into account + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('"')); + + // skip the opening quote + _line_progressed(1); + s = s.sub(1); + + bool needs_filter = false; + + size_t numlines = 1; // we already have one line + size_t pos = npos; // find the pos of the matching quote + while( ! _finished_file()) + { + const csubstr line = m_evt_handler->m_curr->line_contents.rem; + #if defined(__GNUC__) && __GNUC__ == 11 + C4_DONT_OPTIMIZE(line); // prevent erroneous hoist of the assignment out of the loop + #endif + bool line_is_blank = true; + _c4dbgpf("scanning double quoted scalar @ line[{}]: line='{}'", m_evt_handler->m_curr->pos.line, line); + for(size_t i = 0; i < line.len; ++i) + { + const char curr = line.str[i]; + if(curr != ' ') + line_is_blank = false; + // every \ is an escape + if(curr == '\\') + { + const char next = i+1 < line.len ? line.str[i+1] : '~'; + needs_filter = true; + if(next == '"' || next == '\\') + ++i; + } + else if(curr == '"') + { + pos = i; + break; + } + } + + // leading whitespace also needs filtering + needs_filter = needs_filter + || (numlines > 1) + || line_is_blank + || (_at_line_begin() && line.begins_with(' ')); + + if(pos == npos) + { + _line_progressed(line.len); + ++numlines; + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, pos >= 0 && pos < m_buf.len); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf[m_evt_handler->m_curr->pos.offset + pos] == '"'); + _line_progressed(pos + 1); // progress beyond the quote + pos = m_evt_handler->m_curr->pos.offset - b - 1; // but we stop before it + break; + } + + _line_ended(); + _scan_line(); + } + + if(pos == npos) + { + _c4err("reached end of file looking for closing quote"); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, pos > 0); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.end() == m_buf.end() || *s.end() == '"'); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.end() >= m_buf.begin() && s.end() <= m_buf.end()); + s = s.sub(0, pos-1); + } + + _c4prscalar("scanned dquoted scalar", s, /*keep_newlines*/true); + + return ScannedScalar { s, needs_filter }; +} + + +//----------------------------------------------------------------------------- +template +void ParseEngine::_scan_block(ScannedBlock *C4_RESTRICT sb, size_t indref) +{ + _c4dbgpf("blck: indref={}", indref); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, indref != npos); + + // nice explanation here: http://yaml-multiline.info/ + csubstr s = m_evt_handler->m_curr->line_contents.rem; + csubstr trimmed = s.triml(' '); + if(trimmed.str > s.str) + { + _c4dbgp("skipping whitespace"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, trimmed.str >= s.str); + _line_progressed(static_cast(trimmed.str - s.str)); + s = trimmed; + } + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with('|') || s.begins_with('>')); + + _c4dbgpf("blck: specs=[{}]~~~{}~~~", s.len, s); + + // parse the spec + BlockChomp_e chomp = CHOMP_CLIP; // default to clip unless + or - are used + size_t indentation = npos; // have to find out if no spec is given + csubstr digits; + if(s.len > 1) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.begins_with_any("|>")); + csubstr t = s.sub(1); + _c4dbgpf("blck: spec is multichar: '{}'", t); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, t.len >= 1); + size_t pos = t.first_of("-+"); + _c4dbgpf("blck: spec chomp char at {}", pos); + if(pos != npos) + { + if(t[pos] == '-') + chomp = CHOMP_STRIP; + else if(t[pos] == '+') + chomp = CHOMP_KEEP; + if(pos == 0) + t = t.sub(1); + else + t = t.first(pos); + } + // from here to the end, only digits are considered + digits = t.left_of(t.first_not_of("0123456789")); + if( ! digits.empty()) + { + if(C4_UNLIKELY(digits.len > 1)) + _c4err("parse error: invalid indentation"); + _c4dbgpf("blck: parse indentation digits: [{}]~~~{}~~~", digits.len, digits); + if(C4_UNLIKELY( ! c4::atou(digits, &indentation))) + _c4err("parse error: could not read indentation as decimal"); + if(C4_UNLIKELY( ! indentation)) + _c4err("parse error: null indentation"); + _c4dbgpf("blck: indentation specified: {}. add {} from curr state -> {}", indentation, m_evt_handler->m_curr->indref, indentation+indref); + indentation += m_evt_handler->m_curr->indref; + } + } + + _c4dbgpf("blck: style={} chomp={} indentation={}", s.begins_with('>') ? "fold" : "literal", chomp==CHOMP_CLIP ? "clip" : (chomp==CHOMP_STRIP ? "strip" : "keep"), indentation); + + // finish the current line + _line_progressed(s.len); + _line_ended(); + _scan_line(); + + // start with a zero-length block, already pointing at the right place + substr raw_block(m_buf.data() + m_evt_handler->m_curr->pos.offset, size_t(0));// m_evt_handler->m_curr->line_contents.full.sub(0, 0); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, raw_block.begin() == m_evt_handler->m_curr->line_contents.full.begin()); + + // read every full line into a raw block, + // from which newlines are to be stripped as needed. + // + // If no explicit indentation was given, pick it from the first + // non-empty line. See + // https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator + size_t num_lines = 0; + size_t first = m_evt_handler->m_curr->pos.line; + size_t provisional_indentation = npos; + LineContents lc; + while(( ! _finished_file())) + { + // peek next line, but do not advance immediately + lc.reset_with_next_line(m_buf, m_evt_handler->m_curr->pos.offset); + #if defined(__GNUC__) && (__GNUC__ == 12 || __GNUC__ == 13) + C4_DONT_OPTIMIZE(lc.rem); + #endif + _c4dbgpf("blck: peeking at [{}]~~~{}~~~", lc.stripped.len, lc.stripped); + // evaluate termination conditions + if(indentation != npos) + { + _c4dbgpf("blck: indentation={}", indentation); + // stop when the line is deindented and not empty + if(lc.indentation < indentation && ( ! lc.rem.trim(" \t").empty())) + { + if(raw_block.len) + { + _c4dbgpf("blck: indentation decreased ref={} thisline={}", indentation, lc.indentation); + } + else + { + _c4err("indentation decreased without any scalar"); + } + break; + } + else if(indentation == 0) + { + _c4dbgpf("blck: noindent. lc.rem=[{}]~~~{}~~~", lc.rem.len, lc.rem); + if(_is_doc_token(lc.rem)) + { + _c4dbgp("blck: stop. indentation=0 and doc ended"); + break; + } + } + } + else + { + const size_t fns = lc.stripped.first_not_of(' '); + _c4dbgpf("blck: indentation ref not set. firstnonws={}", fns); + if(fns != npos) // non-empty line + { + _RYML_WITH_TAB_TOKENS( + if(C4_UNLIKELY(lc.stripped.begins_with('\t'))) + _c4err("parse error"); + ) + _c4dbgpf("blck: line not empty. indref={} indprov={} indentation={}", indref, provisional_indentation, lc.indentation); + if(provisional_indentation == npos) + { + if(lc.indentation < indref) + { + _c4dbgpf("blck: block terminated indentation={} < indref={}", lc.indentation, indref); + if(raw_block.len == 0) + { + _c4dbgp("blck: was empty, undo next line"); + _line_ended_undo(); + } + break; + } + else if(lc.indentation == m_evt_handler->m_curr->indref) + { + if(has_any(RSEQ|RMAP)) + { + _c4dbgpf("blck: block terminated. reading container and indentation={}==indref={}", lc.indentation, m_evt_handler->m_curr->indref); + break; + } + } + _c4dbgpf("blck: set indentation ref from this line: ref={}", lc.indentation); + indentation = lc.indentation; + } + else + { + if(lc.indentation >= provisional_indentation) + { + _c4dbgpf("blck: set indentation ref from provisional indentation: provisional_ref={}, thisline={}", provisional_indentation, lc.indentation); + //indentation = provisional_indentation ? provisional_indentation : lc.indentation; + indentation = lc.indentation; + } + else + { + break; + //_c4err("parse error: first non-empty block line should have at least the original indentation"); + } + } + } + else // empty line + { + _c4dbgpf("blck: line empty or {} spaces. line_indentation={} prov_indentation={}", lc.stripped.len, lc.indentation, provisional_indentation); + if(provisional_indentation != npos) + { + if(lc.stripped.len >= provisional_indentation) + { + _c4dbgpf("blck: increase provisional_ref {} -> {}", provisional_indentation, lc.stripped.len); + provisional_indentation = lc.stripped.len; + } + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + else if(lc.indentation >= provisional_indentation && lc.indentation != npos) + { + _c4dbgpf("blck: increase provisional_ref {} -> {}", provisional_indentation, lc.indentation); + provisional_indentation = lc.indentation; + } + #endif + } + else + { + provisional_indentation = lc.indentation ? lc.indentation : has_any(RSEQ|RVAL); + _c4dbgpf("blck: initialize provisional_ref={}", provisional_indentation); + if(provisional_indentation == npos) + { + provisional_indentation = lc.stripped.len ? lc.stripped.len : has_any(RSEQ|RVAL); + _c4dbgpf("blck: initialize provisional_ref={}", provisional_indentation); + } + if(provisional_indentation < indref) + { + provisional_indentation = indref; + _c4dbgpf("blck: initialize provisional_ref={}", provisional_indentation); + } + } + } + } + // advance now that we know the folded scalar continues + m_evt_handler->m_curr->line_contents = lc; + _c4dbgpf("blck: append '{}'", m_evt_handler->m_curr->line_contents.rem); + raw_block.len += m_evt_handler->m_curr->line_contents.full.len; + _line_progressed(m_evt_handler->m_curr->line_contents.rem.len); + _line_ended(); + ++num_lines; + } + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->pos.line == (first + num_lines) || (raw_block.len == 0)); + C4_UNUSED(num_lines); + C4_UNUSED(first); + + if(indentation == npos) + { + _c4dbgpf("blck: set indentation from provisional: {}", provisional_indentation); + indentation = provisional_indentation; + } + + if(num_lines) + _line_ended_undo(); + + _c4prscalar("scanned block", raw_block, /*keep_newlines*/true); + + sb->scalar = raw_block; + sb->indentation = indentation; + sb->chomp = chomp; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** @cond dev */ + +// a debugging scaffold: +#if 0 +#define _c4dbgfws(fmt, ...) _c4dbgpf("filt_ws[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfws(...) +#endif + +template +template +bool ParseEngine::_filter_ws_handle_to_first_non_space(FilterProcessor &proc) +{ + _c4dbgfws("found whitespace '{}'", _c4prc(proc.curr())); + _RYML_CB_ASSERT(this->callbacks(), proc.curr() == ' ' || proc.curr() == '\t'); + + const size_t first_pos = proc.rpos > 0 ? proc.src.first_not_of(" \t", proc.rpos) : proc.src.first_not_of(' ', proc.rpos); + if(first_pos != npos) + { + const char first_char = proc.src[first_pos]; + _c4dbgfws("firstnonws='{}'@{}", _c4prc(first_char), first_pos); + if(first_char == '\n' || first_char == '\r') // skip trailing whitespace + { + _c4dbgfws("whitespace is trailing on line", ""); + proc.skip(first_pos - proc.rpos); + } + else // a legit whitespace + { + proc.copy(); + _c4dbgfws("legit whitespace. sofar=[{}]~~~{}~~~", proc.wpos, proc.sofar()); + } + return true; + } + _c4dbgfws("whitespace is trailing on line", ""); + return false; +} + +template +template +void ParseEngine::_filter_ws_copy_trailing(FilterProcessor &proc) +{ + if(!_filter_ws_handle_to_first_non_space(proc)) + { + _c4dbgfws("... everything else is trailing whitespace - copy {} chars", proc.src.len - proc.rpos); + proc.copy(proc.src.len - proc.rpos); + } +} + +template +template +void ParseEngine::_filter_ws_skip_trailing(FilterProcessor &proc) +{ + if(!_filter_ws_handle_to_first_non_space(proc)) + { + _c4dbgfws("... everything else is trailing whitespace - skip {} chars", proc.src.len - proc.rpos); + proc.skip(proc.src.len - proc.rpos); + } +} + +#undef _c4dbgfws + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/* plain scalars */ + +// a debugging scaffold: +#if 0 +#define _c4dbgfps(fmt, ...) _c4dbgpf("filt_plain[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfps(fmt, ...) +#endif + +template +template +void ParseEngine::_filter_nl_plain(FilterProcessor &C4_RESTRICT proc, size_t indentation) +{ + _RYML_CB_ASSERT(this->callbacks(), proc.curr() == '\n'); + + _c4dbgfps("found newline. sofar=[{}]~~~{}~~~", proc.wpos, proc.sofar()); + size_t ii = proc.rpos; + const size_t numnl_following = _count_following_newlines(proc.src, &ii, indentation); + if(numnl_following) + { + proc.set('\n', numnl_following); + _c4dbgfps("{} consecutive (empty) lines {}. totalws={}", 1+numnl_following, ii < proc.src.len ? "in the middle" : "at the end", proc.rpos-ii); + } + else + { + const size_t ret = proc.src.first_not_of(" \t", proc.rpos+1); + if(ret != npos) + { + proc.set(' '); + _c4dbgfps("single newline. convert to space. ret={}/{}. sofar=[{}]~~~{}~~~", ii, proc.src.len, proc.wpos, proc.sofar()); + } + else + { + _c4dbgfps("last newline, everything else is whitespace. ii={}/{}", ii, proc.src.len); + ii = proc.src.len; + } + } + proc.rpos = ii; +} + +template +template +auto ParseEngine::_filter_plain(FilterProcessor &C4_RESTRICT proc, size_t indentation) -> decltype(proc.result()) +{ + _RYML_CB_ASSERT(this->callbacks(), indentation != npos); + _c4dbgfps("before=[{}]~~~{}~~~", proc.src.len, proc.src); + + while(proc.has_more_chars()) + { + const char curr = proc.curr(); + _c4dbgfps("'{}', sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case ' ': + _RYML_WITH_TAB_TOKENS(case '\t':) + _c4dbgfps("whitespace", curr); + _filter_ws_skip_trailing(proc); + break; + case '\n': + _c4dbgfps("newline", curr); + _filter_nl_plain(proc, /*indentation*/indentation); + break; + case '\r': // skip \r --- https://stackoverflow.com/questions/1885900 + _c4dbgfps("carriage return, ignore", curr); + proc.skip(); + break; + default: + proc.copy(); + break; + } + } + + _c4dbgfps("after[{}]=~~~{}~~~", proc.wpos, proc.sofar()); + + return proc.result(); +} + +#undef _c4dbgfps + + +template +FilterResult ParseEngine::filter_scalar_plain(csubstr scalar, substr dst, size_t indentation) +{ + FilterProcessorSrcDst proc(scalar, dst); + return _filter_plain(proc, indentation); +} + +template +FilterResult ParseEngine::filter_scalar_plain_in_place(substr dst, size_t cap, size_t indentation) +{ + FilterProcessorInplaceEndExtending proc(dst, cap); + return _filter_plain(proc, indentation); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/* single quoted */ + +// a debugging scaffold: +#if 0 +#define _c4dbgfsq(fmt, ...) _c4dbgpf("filt_squo[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfsq(fmt, ...) +#endif + +template +template +void ParseEngine::_filter_nl_squoted(FilterProcessor &C4_RESTRICT proc) +{ + _RYML_CB_ASSERT(this->callbacks(), proc.curr() == '\n'); + + _c4dbgfsq("found newline. sofar=[{}]~~~{}~~~", proc.wpos, proc.sofar()); + size_t ii = proc.rpos; + const size_t numnl_following = _count_following_newlines(proc.src, &ii); + if(numnl_following) + { + proc.set('\n', numnl_following); + _c4dbgfsq("{} consecutive (empty) lines {}. totalws={}", 1+numnl_following, ii < proc.src.len ? "in the middle" : "at the end", proc.rpos-ii); + } + else + { + const size_t ret = proc.src.first_not_of(" \t", proc.rpos+1); + if(ret != npos) + { + proc.set(' '); + _c4dbgfsq("single newline. convert to space. ret={}/{}. sofar=[{}]~~~{}~~~", ii, proc.src.len, proc.wpos, proc.sofar()); + } + else + { + proc.set(' '); + _c4dbgfsq("single newline. convert to space. ii={}/{}. sofar=[{}]~~~{}~~~", ii, proc.src.len, proc.wpos, proc.sofar()); + } + } + proc.rpos = ii; +} + +template +template +auto ParseEngine::_filter_squoted(FilterProcessor &C4_RESTRICT proc) -> decltype(proc.result()) +{ + _c4dbgfsq("before=[{}]~~~{}~~~", proc.src.len, proc.src); + + // from the YAML spec for double-quoted scalars: + // https://yaml.org/spec/1.2-old/spec.html#style/flow/single-quoted + while(proc.has_more_chars()) + { + const char curr = proc.curr(); + _c4dbgfsq("'{}', sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case ' ': + case '\t': + _c4dbgfsq("whitespace", curr); + _filter_ws_copy_trailing(proc); + break; + case '\n': + _c4dbgfsq("newline", curr); + _filter_nl_squoted(proc); + break; + case '\r': // skip \r --- https://stackoverflow.com/questions/1885900 + _c4dbgfsq("skip cr", curr); + proc.skip(); + break; + case '\'': + _c4dbgfsq("squote", curr); + if(proc.next() == '\'') + { + _c4dbgfsq("two consecutive squotes", curr); + proc.skip(); + proc.copy(); + } + else + { + _c4err("filter error"); + } + break; + default: + proc.copy(); + break; + } + } + + _c4dbgfsq(": #filteredchars={} after=~~~[{}]{}~~~", proc.src.len-proc.sofar().len, proc.sofar().len, proc.sofar()); + + return proc.result(); +} + +#undef _c4dbgfsq + +template +FilterResult ParseEngine::filter_scalar_squoted(csubstr scalar, substr dst) +{ + FilterProcessorSrcDst proc(scalar, dst); + return _filter_squoted(proc); +} + +template +FilterResult ParseEngine::filter_scalar_squoted_in_place(substr dst, size_t cap) +{ + FilterProcessorInplaceEndExtending proc(dst, cap); + return _filter_squoted(proc); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/* double quoted */ + +// a debugging scaffold: +#if 0 +#define _c4dbgfdq(fmt, ...) _c4dbgpf("filt_dquo[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfdq(...) +#endif + +template +template +void ParseEngine::_filter_nl_dquoted(FilterProcessor &C4_RESTRICT proc) +{ + _RYML_CB_ASSERT(this->callbacks(), proc.curr() == '\n'); + + _c4dbgfdq("found newline. sofar=[{}]~~~{}~~~", proc.wpos, proc.sofar()); + size_t ii = proc.rpos; + const size_t numnl_following = _count_following_newlines(proc.src, &ii); + if(numnl_following) + { + proc.set('\n', numnl_following); + _c4dbgfdq("{} consecutive (empty) lines {}. totalws={}", 1+numnl_following, ii < proc.src.len ? "in the middle" : "at the end", proc.rpos-ii); + } + else + { + const size_t ret = proc.src.first_not_of(" \t", proc.rpos+1); + if(ret != npos) + { + proc.set(' '); + _c4dbgfdq("single newline. convert to space. ret={}/{}. sofar=[{}]~~~{}~~~", ii, proc.src.len, proc.wpos, proc.sofar()); + } + else + { + proc.set(' '); + _c4dbgfdq("single newline. convert to space. ii={}/{}. sofar=[{}]~~~{}~~~", ii, proc.src.len, proc.wpos, proc.sofar()); + } + if(ii < proc.src.len && proc.src.str[ii] == '\\') + { + _c4dbgfdq("backslash at [{}]", ii); + const char next = ii+1 < proc.src.len ? proc.src.str[ii+1] : '\0'; + if(next == ' ' || next == '\t') + { + _c4dbgfdq("extend skip to backslash", ""); + ++ii; + } + } + } + proc.rpos = ii; +} + +template +template +void ParseEngine::_filter_dquoted_backslash(FilterProcessor &C4_RESTRICT proc) +{ + char next = proc.next(); + _c4dbgfdq("backslash, next='{}'", _c4prc(next)); + if(next == '\r') + { + if(proc.rpos+2 < proc.src.len && proc.src.str[proc.rpos+2] == '\n') + { + proc.skip(); // newline escaped with \ -- skip both (add only one as i is loop-incremented) + next = '\n'; + _c4dbgfdq("[{}]: was \\r\\n, now next='\\n'", proc.rpos); + } + } + + if(next == '\n') + { + size_t ii = proc.rpos + 2; + for( ; ii < proc.src.len; ++ii) + { + // skip leading whitespace + if(proc.src.str[ii] == ' ' || proc.src.str[ii] == '\t') + ; + else + break; + } + proc.skip(ii - proc.rpos); + } + else if(next == '"' || next == '/' || next == ' ' || next == '\t') + { + // escapes for json compatibility + proc.translate_esc(next); + _c4dbgfdq("here, used '{}'", _c4prc(next)); + } + else if(next == '\r') + { + proc.skip(); + } + else if(next == 'n') + { + proc.translate_esc('\n'); + } + else if(next == 'r') + { + proc.translate_esc('\r'); + } + else if(next == 't') + { + proc.translate_esc('\t'); + } + else if(next == '\\') + { + proc.translate_esc('\\'); + } + else if(next == 'x') // 2-digit Unicode escape (\xXX), code point 0x00–0xFF + { + if(C4_UNLIKELY(proc.rpos + 1u + 2u >= proc.src.len)) + _c4err_("\\x requires 2 hex digits. scalar pos={}", proc.rpos); + char readbuf[8]; + csubstr codepoint = proc.src.sub(proc.rpos + 2u, 2u); + _c4dbgfdq("utf8 ~~~{}~~~ rpos={} rem=~~~{}~~~", codepoint, proc.rpos, proc.src.sub(proc.rpos)); + uint32_t codepoint_val = {}; + if(C4_UNLIKELY(!read_hex(codepoint, &codepoint_val))) + _c4err_("failed to read \\x codepoint. scalar pos={}", proc.rpos); + const size_t numbytes = decode_code_point((uint8_t*)readbuf, sizeof(readbuf), codepoint_val); + if(C4_UNLIKELY(numbytes == 0)) + _c4err_("failed to decode code point={}", proc.rpos); + _RYML_CB_ASSERT(callbacks(), numbytes <= 4); + proc.translate_esc_bulk(readbuf, numbytes, /*nread*/3u); + _c4dbgfdq("utf8 after rpos={} rem=~~~{}~~~", proc.rpos, proc.src.sub(proc.rpos)); + } + else if(next == 'u') // 4-digit Unicode escape (\uXXXX), code point 0x0000–0xFFFF + { + if(C4_UNLIKELY(proc.rpos + 1u + 4u >= proc.src.len)) + _c4err_("\\u requires 4 hex digits. scalar pos={}", proc.rpos); + char readbuf[8]; + csubstr codepoint = proc.src.sub(proc.rpos + 2u, 4u); + uint32_t codepoint_val = {}; + if(C4_UNLIKELY(!read_hex(codepoint, &codepoint_val))) + _c4err_("failed to parse \\u codepoint. scalar pos={}", proc.rpos); + const size_t numbytes = decode_code_point((uint8_t*)readbuf, sizeof(readbuf), codepoint_val); + if(C4_UNLIKELY(numbytes == 0)) + _c4err_("failed to decode code point={}", proc.rpos); + _RYML_CB_ASSERT(callbacks(), numbytes <= 4); + proc.translate_esc_bulk(readbuf, numbytes, /*nread*/5u); + } + else if(next == 'U') // 8-digit Unicode escape (\UXXXXXXXX), full 32-bit code point + { + if(C4_UNLIKELY(proc.rpos + 1u + 8u >= proc.src.len)) + _c4err_("\\U requires 8 hex digits. scalar pos={}", proc.rpos); + char readbuf[8]; + csubstr codepoint = proc.src.sub(proc.rpos + 2u, 8u); + uint32_t codepoint_val = {}; + if(C4_UNLIKELY(!read_hex(codepoint, &codepoint_val))) + _c4err_("failed to parse \\U codepoint. scalar pos={}", proc.rpos); + const size_t numbytes = decode_code_point((uint8_t*)readbuf, sizeof(readbuf), codepoint_val); + if(C4_UNLIKELY(numbytes == 0)) + _c4err_("failed to decode code point={}", proc.rpos); + _RYML_CB_ASSERT(callbacks(), numbytes <= 4); + proc.translate_esc_bulk(readbuf, numbytes, /*nread*/9u); + } + // https://yaml.org/spec/1.2.2/#rule-c-ns-esc-char + else if(next == '0') + { + proc.translate_esc('\0'); + } + else if(next == 'b') // backspace + { + proc.translate_esc('\b'); + } + else if(next == 'f') // form feed + { + proc.translate_esc('\f'); + } + else if(next == 'a') // bell character + { + proc.translate_esc('\a'); + } + else if(next == 'v') // vertical tab + { + proc.translate_esc('\v'); + } + else if(next == 'e') // escape character + { + proc.translate_esc('\x1b'); + } + else if(next == '_') // unicode non breaking space \u00a0 + { + // https://www.compart.com/en/unicode/U+00a0 + const char payload[] = { + _RYML_CHCONST(-0x3e, 0xc2), + _RYML_CHCONST(-0x60, 0xa0), + }; + proc.translate_esc_bulk(payload, /*nwrite*/2, /*nread*/1); + } + else if(next == 'N') // unicode next line \u0085 + { + // https://www.compart.com/en/unicode/U+0085 + const char payload[] = { + _RYML_CHCONST(-0x3e, 0xc2), + _RYML_CHCONST(-0x7b, 0x85), + }; + proc.translate_esc_bulk(payload, /*nwrite*/2, /*nread*/1); + } + else if(next == 'L') // unicode line separator \u2028 + { + // https://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192&number=1024&names=-&utf8=0x&unicodeinhtml=hex + const char payload[] = { + _RYML_CHCONST(-0x1e, 0xe2), + _RYML_CHCONST(-0x80, 0x80), + _RYML_CHCONST(-0x58, 0xa8), + }; + proc.translate_esc_extending(payload, /*nwrite*/3, /*nread*/1); + } + else if(next == 'P') // unicode paragraph separator \u2029 + { + // https://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192&number=1024&names=-&utf8=0x&unicodeinhtml=hex + const char payload[] = { + _RYML_CHCONST(-0x1e, 0xe2), + _RYML_CHCONST(-0x80, 0x80), + _RYML_CHCONST(-0x57, 0xa9), + }; + proc.translate_esc_extending(payload, /*nwrite*/3, /*nread*/1); + } + else if(next == '\0') + { + proc.skip(); + } + else + { + _c4err_("unknown character '{}' after '\\' pos={}", _c4prc(next), proc.rpos); + } + _c4dbgfdq("backslash...sofar=[{}]~~~{}~~~", proc.wpos, proc.sofar()); +} + + +template +template +auto ParseEngine::_filter_dquoted(FilterProcessor &C4_RESTRICT proc) -> decltype(proc.result()) +{ + _c4dbgfdq("before=[{}]~~~{}~~~", proc.src.len, proc.src); + // from the YAML spec for double-quoted scalars: + // https://yaml.org/spec/1.2-old/spec.html#style/flow/double-quoted + while(proc.has_more_chars()) + { + const char curr = proc.curr(); + _c4dbgfdq("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case ' ': + case '\t': + { + _c4dbgfdq("whitespace", curr); + _filter_ws_copy_trailing(proc); + break; + } + case '\n': + { + _c4dbgfdq("newline", curr); + _filter_nl_dquoted(proc); + break; + } + case '\r': // skip \r --- https://stackoverflow.com/questions/1885900 + { + _c4dbgfdq("carriage return, ignore", curr); + proc.skip(); + break; + } + case '\\': + { + _filter_dquoted_backslash(proc); + break; + } + default: + { + proc.copy(); + break; + } + } + } + _c4dbgfdq("after[{}]=~~~{}~~~", proc.wpos, proc.sofar()); + return proc.result(); +} + +#undef _c4dbgfdq + + +template +FilterResult ParseEngine::filter_scalar_dquoted(csubstr scalar, substr dst) +{ + FilterProcessorSrcDst proc(scalar, dst); + return _filter_dquoted(proc); +} + +template +FilterResultExtending ParseEngine::filter_scalar_dquoted_in_place(substr dst, size_t cap) +{ + FilterProcessorInplaceMidExtending proc(dst, cap); + return _filter_dquoted(proc); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// block filtering helpers + +C4_NO_INLINE inline size_t _find_last_newline_and_larger_indentation(csubstr s, size_t indentation) noexcept +{ + if(indentation + 1 > s.len) + return npos; + for(size_t i = s.len-indentation-1; i != size_t(-1); --i) + { + if(s.str[i] == '\n') + { + csubstr rem = s.sub(i + 1); + size_t first = rem.first_not_of(' '); + first = (first != npos) ? first : rem.len; + if(first > indentation) + return i; + } + } + return npos; +} + +template +template +void ParseEngine::_filter_chomp(FilterProcessor &C4_RESTRICT proc, BlockChomp_e chomp, size_t indentation) +{ + _RYML_CB_ASSERT(this->callbacks(), chomp == CHOMP_CLIP || chomp == CHOMP_KEEP || chomp == CHOMP_STRIP); + _RYML_CB_ASSERT(this->callbacks(), proc.rem().first_not_of(" \n\r") == npos); + + // a debugging scaffold: + #if 0 + #define _c4dbgchomp(fmt, ...) _c4dbgpf("chomp[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) + #else + #define _c4dbgchomp(...) + #endif + + // advance to the last line having spaces beyond the indentation + { + size_t last = _find_last_newline_and_larger_indentation(proc.rem(), indentation); + if(last != npos) + { + _c4dbgchomp("found newline and larger indentation. last={}", last); + last = proc.rpos + last + size_t(1) + indentation; // last started at to-be-read. + _RYML_CB_ASSERT(this->callbacks(), last <= proc.src.len); + // remove indentation spaces, copy the rest + while((proc.rpos < last) && proc.has_more_chars()) + { + const char curr = proc.curr(); + _c4dbgchomp("curr='{}'", _c4prc(curr)); + switch(curr) + { + case '\n': + { + _c4dbgchomp("newline! remlen={}", proc.rem().len); + proc.copy(); + // are there spaces after the newline? + csubstr at_next_line = proc.rem(); + if(at_next_line.begins_with(' ')) + { + _c4dbgchomp("next line begins with spaces. indentation={}", indentation); + // there are spaces. + size_t first_non_space = at_next_line.first_not_of(' '); + _c4dbgchomp("first_non_space={}", first_non_space); + if(first_non_space == npos) + { + _c4dbgchomp("{} spaces, to the end", at_next_line.len); + first_non_space = at_next_line.len; + } + if(first_non_space <= indentation) + { + _c4dbgchomp("skip spaces={}<=indentation={}", first_non_space, indentation); + proc.skip(first_non_space); + } + else + { + _c4dbgchomp("skip indentation={}{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfb(...) +#endif + +template +template +void ParseEngine::_filter_block_indentation(FilterProcessor &C4_RESTRICT proc, size_t indentation) +{ + csubstr rem = proc.rem(); // remaining + if(rem.len) + { + size_t first = rem.first_not_of(' '); + if(first != npos) + { + _c4dbgfb("{} spaces follow before next nonws character", first); + if(first < indentation) + { + _c4dbgfb("skip {}<{} spaces from indentation", first, indentation); + proc.skip(first); + } + else + { + _c4dbgfb("skip {} spaces from indentation", indentation); + proc.skip(indentation); + } + } + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + else + { + _c4dbgfb("all spaces to the end: {} spaces", first); + first = rem.len; + if(first) + { + if(first < indentation) + { + _c4dbgfb("skip everything", first); + proc.skip(proc.src.len - proc.rpos); + } + else + { + _c4dbgfb("skip {} spaces from indentation", indentation); + proc.skip(indentation); + } + } + } + #endif + } +} + +template +template +size_t ParseEngine::_handle_all_whitespace(FilterProcessor &C4_RESTRICT proc, BlockChomp_e chomp) +{ + csubstr contents = proc.src.trimr(" \n\r"); + _c4dbgfb("ws: contents_len={} wslen={}", contents.len, proc.src.len-contents.len); + if(!contents.len) + { + _c4dbgfb("ws: all whitespace: len={}", proc.src.len); + if(chomp == CHOMP_KEEP && proc.src.len) + { + _c4dbgfb("ws: chomp=KEEP all {} newlines", proc.src.count('\n')); + while(proc.has_more_chars()) + { + const char curr = proc.curr(); + if(curr == '\n') + proc.copy(); + else + proc.skip(); + } + if(!proc.wpos) + { + proc.set('\n'); + } + } + } + return contents.len; +} + +template +template +size_t ParseEngine::_extend_to_chomp(FilterProcessor &C4_RESTRICT proc, size_t contents_len) +{ + _c4dbgfb("contents_len={}", contents_len); + + _RYML_CB_ASSERT(this->callbacks(), contents_len > 0u); + + // extend contents to just before the first newline at the end, + // in case it is preceded by spaces + size_t firstnewl = proc.src.first_of('\n', contents_len); + if(firstnewl != npos) + { + contents_len = firstnewl; + _c4dbgfb("contents_len={} <--- firstnewl={}", contents_len, firstnewl); + } + else + { + contents_len = proc.src.len; + _c4dbgfb("contents_len={} <--- src.len={}", contents_len, proc.src.len); + } + + return contents_len; +} + +#undef _c4dbgfb + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +// a debugging scaffold: +#if 0 +#define _c4dbgfbl(fmt, ...) _c4dbgpf("filt_block_lit[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfbl(...) +#endif + +template +template +auto ParseEngine::_filter_block_literal(FilterProcessor &C4_RESTRICT proc, size_t indentation, BlockChomp_e chomp) -> decltype(proc.result()) +{ + _c4dbgfbl("indentation={} before=[{}]~~~{}~~~", indentation, proc.src.len, proc.src); + + size_t contents_len = _handle_all_whitespace(proc, chomp); + if(!contents_len) + return proc.result(); + + contents_len = _extend_to_chomp(proc, contents_len); + + _c4dbgfbl("to filter=[{}]~~~{}~~~", contents_len, proc.src.first(contents_len)); + + _filter_block_indentation(proc, indentation); + + // now filter the bulk + while(proc.has_more_chars(/*maxpos*/contents_len)) + { + const char curr = proc.curr(); + _c4dbgfbl("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case '\n': + { + _c4dbgfbl("found newline. skip indentation on the next line", curr); + proc.copy(); // copy the newline + _filter_block_indentation(proc, indentation); + break; + } + case '\r': + proc.skip(); + break; + default: + proc.copy(); + break; + } + } + + _c4dbgfbl("before chomp: #tochomp={} sofar=[{}]~~~{}~~~", proc.rem().len, proc.sofar().len, proc.sofar()); + + _filter_chomp(proc, chomp, indentation); + + _c4dbgfbl("final=[{}]~~~{}~~~", proc.sofar().len, proc.sofar()); + + return proc.result(); +} + +#undef _c4dbgfbl + +template +FilterResult ParseEngine::filter_scalar_block_literal(csubstr scalar, substr dst, size_t indentation, BlockChomp_e chomp) +{ + FilterProcessorSrcDst proc(scalar, dst); + return _filter_block_literal(proc, indentation, chomp); +} + +template +FilterResult ParseEngine::filter_scalar_block_literal_in_place(substr scalar, size_t cap, size_t indentation, BlockChomp_e chomp) +{ + FilterProcessorInplaceEndExtending proc(scalar, cap); + return _filter_block_literal(proc, indentation, chomp); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +// a debugging scaffold: +#if 0 +#define _c4dbgfbf(fmt, ...) _c4dbgpf("filt_block_folded[{}->{}]: " fmt, proc.rpos, proc.wpos, __VA_ARGS__) +#else +#define _c4dbgfbf(...) +#endif + + +template +template +void ParseEngine::_filter_block_folded_newlines_leading(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len) +{ + _filter_block_indentation(proc, indentation); + while(proc.has_more_chars(len)) + { + const char curr = proc.curr(); + _c4dbgfbf("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case '\n': + _c4dbgfbf("newline.", curr); + proc.copy(); + _filter_block_indentation(proc, indentation); + break; + case '\r': + proc.skip(); + break; + case ' ': + case '\t': + { + size_t first = proc.rem().first_not_of(" \t"); + _c4dbgfbf("space. first={}", first); + if(first == npos) + first = proc.rem().len; + _c4dbgfbf("... indentation increased to {}", first); + _filter_block_folded_indented_block(proc, indentation, len, first); + break; + } + default: + _c4dbgfbf("newl leading: not space, not newline. stop.", 0); + return; + } + } +} + +template +template +size_t ParseEngine::_filter_block_folded_newlines_compress(FilterProcessor &C4_RESTRICT proc, size_t num_newl, size_t wpos_at_first_newl) +{ + switch(num_newl) + { + case 1u: + _c4dbgfbf("... this is the first newline. turn into space. wpos={}", proc.wpos); + wpos_at_first_newl = proc.wpos; + proc.skip(); + proc.set(' '); + break; + case 2u: + _c4dbgfbf("... this is the second newline. prev space (at wpos={}) must be newline", wpos_at_first_newl); + _RYML_CB_ASSERT(this->callbacks(), wpos_at_first_newl != npos); + _RYML_CB_ASSERT(this->callbacks(), proc.sofar()[wpos_at_first_newl] == ' '); + _RYML_CB_ASSERT(this->callbacks(), wpos_at_first_newl + 1u == proc.wpos); + proc.skip(); + proc.set_at(wpos_at_first_newl, '\n'); + _RYML_CB_ASSERT(this->callbacks(), proc.sofar()[wpos_at_first_newl] == '\n'); + break; + default: + _c4dbgfbf("... subsequent newline (num_newl={}). copy", num_newl); + proc.copy(); + break; + } + return wpos_at_first_newl; +} + +template +template +void ParseEngine::_filter_block_folded_newlines(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len) +{ + _RYML_CB_ASSERT(this->callbacks(), proc.curr() == '\n'); + size_t num_newl = 0; + size_t wpos_at_first_newl = npos; + while(proc.has_more_chars(len)) + { + const char curr = proc.curr(); + _c4dbgfbf("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case '\n': + { + _c4dbgfbf("newline. sofar={}", num_newl); + // NOTE: vs2022-32bit-release builds were giving wrong + // results in this block, if it was written as either + // as a switch(num_newl) or its equivalent if-form. + // + // For this reason, we're using a dedicated function + // (**_compress), which seems to work around the issue. + // + // The manifested problem was that somewhere between the + // assignment to curr and this point, proc.wpos (the + // write-position of the processor) jumped to npos, which + // made the write wrap-around! To make things worse, + // enabling prints via _c4dbgpf() and _c4dbgfbf() made the + // problem go away! + // + // The only way to make the problem appear with prints + // enabled was by disabling all prints in this function + // (including in the block which was moved to the compress + // function) and then selectively enabling only some of + // those prints. + // + // This may be due to some bug in the cl-x86 optimizer; or + // it may be triggered by some UB which may be + // inadvertedly present in this function or in the filter + // processor. This is despite our best efforts to weed out + // any such UB problem: neither clang-tidy nor none of the + // sanitizers, or gcc's -fanalyzer pointed to any problems + // in this code. + // + // In the end, moving this block to a separate function + // was the only way to bury the problem. But it may + // resurface again, as The Undead, rising to from the + // grave to haunt us with his terrible presence. + // + // We may have to revisit this. With a stake, and lots of + // garlic. + wpos_at_first_newl = _filter_block_folded_newlines_compress(proc, ++num_newl, wpos_at_first_newl); + _filter_block_indentation(proc, indentation); + break; + } + case ' ': + case '\t': + { + size_t first = proc.rem().first_not_of(" \t"); + _c4dbgfbf("space. first={}", first); + if(first == npos) + first = proc.rem().len; + _c4dbgfbf("... indentation increased to {}", first); + if(num_newl) + { + _c4dbgfbf("... prev space (at wpos={}) must be newline", wpos_at_first_newl); + proc.set_at(wpos_at_first_newl, '\n'); + } + if(num_newl > 1u) + { + _c4dbgfbf("... add missing newline", wpos_at_first_newl); + proc.set('\n'); + } + _filter_block_folded_indented_block(proc, indentation, len, first); + num_newl = 0; + wpos_at_first_newl = npos; + break; + } + case '\r': + proc.skip(); + break; + default: + _c4dbgfbf("not space, not newline. stop.", 0); + return; + } + } +} + + +template +template +void ParseEngine::_filter_block_folded_indented_block(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len, size_t curr_indentation) noexcept +{ + _RYML_CB_ASSERT(this->callbacks(), (proc.rem().first_not_of(" \t") == curr_indentation) || (proc.rem().first_not_of(" \t") == npos)); + if(curr_indentation) + proc.copy(curr_indentation); + while(proc.has_more_chars(len)) + { + const char curr = proc.curr(); + _c4dbgfbf("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case '\n': + { + proc.copy(); + _filter_block_indentation(proc, indentation); + csubstr rem = proc.rem(); + const size_t first = rem.first_not_of(' '); + _c4dbgfbf("newline. firstns={}", first); + if(first == 0) + { + const char c = rem[first]; + _c4dbgfbf("firstns={}='{}'", first, _c4prc(c)); + if(c == '\n' || c == '\r') + { + ; + } + else + { + _c4dbgfbf("done with indented block", first); + goto endloop; + } + } + else if(first != npos) + { + proc.copy(first); + _c4dbgfbf("copy all {} spaces", first); + } + break; + } + break; + case '\r': + proc.skip(); + break; + default: + proc.copy(); + break; + } + } + endloop: + return; +} + + +template +template +auto ParseEngine::_filter_block_folded(FilterProcessor &C4_RESTRICT proc, size_t indentation, BlockChomp_e chomp) -> decltype(proc.result()) +{ + _c4dbgfbf("indentation={} before=[{}]~~~{}~~~", indentation, proc.src.len, proc.src); + + size_t contents_len = _handle_all_whitespace(proc, chomp); + if(!contents_len) + return proc.result(); + + contents_len = _extend_to_chomp(proc, contents_len); + + _c4dbgfbf("to filter=[{}]~~~{}~~~", contents_len, proc.src.first(contents_len)); + + _filter_block_folded_newlines_leading(proc, indentation, contents_len); + + // now filter the bulk + while(proc.has_more_chars(/*maxpos*/contents_len)) + { + const char curr = proc.curr(); + _c4dbgfbf("'{}' sofar=[{}]~~~{}~~~", _c4prc(curr), proc.wpos, proc.sofar()); + switch(curr) + { + case '\n': + { + _c4dbgfbf("found newline", curr); + _filter_block_folded_newlines(proc, indentation, contents_len); + break; + } + case '\r': + proc.skip(); + break; + default: + proc.copy(); + break; + } + } + + _c4dbgfbf("before chomp: #tochomp={} sofar=[{}]~~~{}~~~", proc.rem().len, proc.sofar().len, proc.sofar()); + + _filter_chomp(proc, chomp, indentation); + + _c4dbgfbf("final=[{}]~~~{}~~~", proc.sofar().len, proc.sofar()); + + return proc.result(); +} + +#undef _c4dbgfbf + +template +FilterResult ParseEngine::filter_scalar_block_folded(csubstr scalar, substr dst, size_t indentation, BlockChomp_e chomp) +{ + FilterProcessorSrcDst proc(scalar, dst); + return _filter_block_folded(proc, indentation, chomp); +} + +template +FilterResult ParseEngine::filter_scalar_block_folded_in_place(substr scalar, size_t cap, size_t indentation, BlockChomp_e chomp) +{ + FilterProcessorInplaceEndExtending proc(scalar, cap); + return _filter_block_folded(proc, indentation, chomp); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_filter_scalar_plain(substr s, size_t indentation) +{ + _c4dbgpf("filtering plain scalar: s=[{}]~~~{}~~~", s.len, s); + FilterResult r = this->filter_scalar_plain_in_place(s, s.len, indentation); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, r.valid()); + _c4dbgpf("filtering plain scalar: success! s=[{}]~~~{}~~~", r.get().len, r.get()); + return r.get(); +} + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_filter_scalar_squot(substr s) +{ + _c4dbgpf("filtering squo scalar: s=[{}]~~~{}~~~", s.len, s); + FilterResult r = this->filter_scalar_squoted_in_place(s, s.len); + _RYML_CB_ASSERT(this->callbacks(), r.valid()); + _c4dbgpf("filtering squo scalar: success! s=[{}]~~~{}~~~", r.get().len, r.get()); + return r.get(); +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_filter_scalar_dquot(substr s) +{ + _c4dbgpf("filtering dquo scalar: s=[{}]~~~{}~~~", s.len, s); + FilterResultExtending r = this->filter_scalar_dquoted_in_place(s, s.len); + if(C4_LIKELY(r.valid())) + { + _c4dbgpf("filtering dquo scalar: success! s=[{}]~~~{}~~~", r.get().len, r.get()); + return r.get(); + } + else + { + const size_t len = r.required_len(); + _c4dbgpf("filtering dquo scalar: not enough space: needs {}, have {}", len, s.len); + substr dst = m_evt_handler->alloc_arena(len, &s); + _c4dbgpf("filtering dquo scalar: dst.len={}", dst.len); + if(dst.str) + { + _RYML_CB_ASSERT(this->callbacks(), dst.len == len); + FilterResult rsd = this->filter_scalar_dquoted(s, dst); + _c4dbgpf("filtering dquo scalar: ... result now needs {} was {}", rsd.required_len(), len); + _RYML_CB_ASSERT(this->callbacks(), rsd.required_len() <= len); // may be smaller! + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, rsd.valid()); + _c4dbgpf("filtering dquo scalar: success! s=[{}]~~~{}~~~", rsd.get().len, rsd.get()); + return rsd.get(); + } + return dst; + } +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_move_scalar_left_and_add_newline(substr s) +{ + if(s.is_sub(m_buf)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.str > m_buf.str); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, s.str-1 >= m_buf.str); + if(s.len) + memmove(s.str - 1, s.str, s.len); + --s.str; + s.str[s.len] = '\n'; + ++s.len; + return s; + } + else + { + substr dst = m_evt_handler->alloc_arena(s.len + 1); + if(s.len) + memcpy(dst.str, s.str, s.len); + dst[s.len] = '\n'; + return dst; + } +} + +template +csubstr ParseEngine::_filter_scalar_literal(substr s, size_t indentation, BlockChomp_e chomp) +{ + _c4dbgpf("filtering block literal scalar: s=[{}]~~~{}~~~", s.len, s); + FilterResult r = this->filter_scalar_block_literal_in_place(s, s.len, indentation, chomp); + csubstr result; + if(C4_LIKELY(r.valid())) + { + result = r.get(); + } + else + { + _c4dbgpf("filtering block literal scalar: not enough space: needs {}, have {}", r.required_len(), s.len); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, r.required_len() == s.len + 1); + // this can only happen when adding a single newline in clip mode. + // so we shift left the scalar by one place + result = _move_scalar_left_and_add_newline(s); + } + _c4dbgpf("filtering block literal scalar: success! s=[{}]~~~{}~~~", result.len, result); + return result; +} + + +//----------------------------------------------------------------------------- +template +csubstr ParseEngine::_filter_scalar_folded(substr s, size_t indentation, BlockChomp_e chomp) +{ + _c4dbgpf("filtering block folded scalar: s=[{}]~~~{}~~~", s.len, s); + FilterResult r = this->filter_scalar_block_folded_in_place(s, s.len, indentation, chomp); + csubstr result; + if(C4_LIKELY(r.valid())) + { + result = r.get(); + } + else + { + _c4dbgpf("filtering block folded scalar: not enough space: needs {}, have {}", r.required_len(), s.len); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, r.required_len() == s.len + 1); + // this can only happen when adding a single newline in clip mode. + // so we shift left the scalar by one place + result = _move_scalar_left_and_add_newline(s); + } + _c4dbgpf("filtering block folded scalar: success! s=[{}]~~~{}~~~", result.len, result); + return result; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_maybe_filter_key_scalar_plain(ScannedScalar const& C4_RESTRICT sc, size_t indentation) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_plain(sc.scalar, indentation); + } + else + { + _c4dbgp("plain scalar left unfiltered"); + m_evt_handler->mark_key_scalar_unfiltered(); + } + } + else + { + _c4dbgp("plain scalar doesn't need filtering"); + } + return sc.scalar; +} + +template +csubstr ParseEngine::_maybe_filter_val_scalar_plain(ScannedScalar const& C4_RESTRICT sc, size_t indentation) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_plain(sc.scalar, indentation); + } + else + { + _c4dbgp("plain scalar left unfiltered"); + m_evt_handler->mark_val_scalar_unfiltered(); + } + } + else + { + _c4dbgp("plain scalar doesn't need filtering"); + } + return sc.scalar; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_maybe_filter_key_scalar_squot(ScannedScalar const& C4_RESTRICT sc) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_squot(sc.scalar); + } + else + { + _c4dbgp("squo key scalar left unfiltered"); + m_evt_handler->mark_key_scalar_unfiltered(); + } + } + else + { + _c4dbgp("squo key scalar doesn't need filtering"); + } + return sc.scalar; +} + +template +csubstr ParseEngine::_maybe_filter_val_scalar_squot(ScannedScalar const& C4_RESTRICT sc) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_squot(sc.scalar); + } + else + { + _c4dbgp("squo val scalar left unfiltered"); + m_evt_handler->mark_val_scalar_unfiltered(); + } + } + else + { + _c4dbgp("squo val scalar doesn't need filtering"); + } + return sc.scalar; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_maybe_filter_key_scalar_dquot(ScannedScalar const& C4_RESTRICT sc) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_dquot(sc.scalar); + } + else + { + _c4dbgp("dquo scalar left unfiltered"); + m_evt_handler->mark_key_scalar_unfiltered(); + } + } + else + { + _c4dbgp("dquo scalar doesn't need filtering"); + } + return sc.scalar; +} + +template +csubstr ParseEngine::_maybe_filter_val_scalar_dquot(ScannedScalar const& C4_RESTRICT sc) +{ + if(sc.needs_filter) + { + if(m_options.scalar_filtering()) + { + return _filter_scalar_dquot(sc.scalar); + } + else + { + _c4dbgp("dquo scalar left unfiltered"); + m_evt_handler->mark_val_scalar_unfiltered(); + } + } + else + { + _c4dbgp("dquo scalar doesn't need filtering"); + } + return sc.scalar; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_maybe_filter_key_scalar_literal(ScannedBlock const& C4_RESTRICT sb) +{ + if(m_options.scalar_filtering()) + { + return _filter_scalar_literal(sb.scalar, sb.indentation, sb.chomp); + } + else + { + _c4dbgp("literal scalar left unfiltered"); + m_evt_handler->mark_key_scalar_unfiltered(); + } + return sb.scalar; +} + +template +csubstr ParseEngine::_maybe_filter_val_scalar_literal(ScannedBlock const& C4_RESTRICT sb) +{ + if(m_options.scalar_filtering()) + { + return _filter_scalar_literal(sb.scalar, sb.indentation, sb.chomp); + } + else + { + _c4dbgp("literal scalar left unfiltered"); + m_evt_handler->mark_val_scalar_unfiltered(); + } + return sb.scalar; +} + + +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::_maybe_filter_key_scalar_folded(ScannedBlock const& C4_RESTRICT sb) +{ + if(m_options.scalar_filtering()) + { + return _filter_scalar_folded(sb.scalar, sb.indentation, sb.chomp); + } + else + { + _c4dbgp("folded scalar left unfiltered"); + m_evt_handler->mark_key_scalar_unfiltered(); + } + return sb.scalar; +} + +template +csubstr ParseEngine::_maybe_filter_val_scalar_folded(ScannedBlock const& C4_RESTRICT sb) +{ + if(m_options.scalar_filtering()) + { + return _filter_scalar_folded(sb.scalar, sb.indentation, sb.chomp); + } + else + { + _c4dbgp("folded scalar left unfiltered"); + m_evt_handler->mark_val_scalar_unfiltered(); + } + return sb.scalar; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#ifdef RYML_DBG // !!! <---------------------------------- + +template +void ParseEngine::add_flags(ParserFlag_t on, ParserState * s) +{ + char buf1_[64], buf2_[64], buf3_[64]; + csubstr buf1 = detail::_parser_flags_to_str(buf1_, on); + csubstr buf2 = detail::_parser_flags_to_str(buf2_, s->flags); + csubstr buf3 = detail::_parser_flags_to_str(buf3_, s->flags|on); + _c4dbgpf("state[{}]: add {}: before={} after={}", s->level, buf1, buf2, buf3); + s->flags |= on; +} + +template +void ParseEngine::addrem_flags(ParserFlag_t on, ParserFlag_t off, ParserState * s) +{ + char buf1_[64], buf2_[64], buf3_[64], buf4_[64]; + csubstr buf1 = detail::_parser_flags_to_str(buf1_, on); + csubstr buf2 = detail::_parser_flags_to_str(buf2_, off); + csubstr buf3 = detail::_parser_flags_to_str(buf3_, s->flags); + csubstr buf4 = detail::_parser_flags_to_str(buf4_, ((s->flags|on)&(~off))); + _c4dbgpf("state[{}]: add {} / rem {}: before={} after={}", s->level, buf1, buf2, buf3, buf4); + s->flags |= on; + s->flags &= ~off; +} + +template +void ParseEngine::rem_flags(ParserFlag_t off, ParserState * s) +{ + char buf1_[64], buf2_[64], buf3_[64]; + csubstr buf1 = detail::_parser_flags_to_str(buf1_, off); + csubstr buf2 = detail::_parser_flags_to_str(buf2_, s->flags); + csubstr buf3 = detail::_parser_flags_to_str(buf3_, s->flags&(~off)); + _c4dbgpf("state[{}]: rem {}: before={} after={}", s->level, buf1, buf2, buf3); + s->flags &= ~off; +} + +inline C4_NO_INLINE csubstr detail::_parser_flags_to_str(substr buf, ParserFlag_t flags) +{ + size_t pos = 0; + bool gotone = false; + + #define _prflag(fl) \ + if((flags & fl) == (fl)) \ + { \ + if(gotone) \ + { \ + if(pos + 1 < buf.len) \ + buf[pos] = '|'; \ + ++pos; \ + } \ + csubstr fltxt = #fl; \ + if(pos + fltxt.len <= buf.len) \ + memcpy(buf.str + pos, fltxt.str, fltxt.len); \ + pos += fltxt.len; \ + gotone = true; \ + } + + _prflag(RTOP); + _prflag(RUNK); + _prflag(RMAP); + _prflag(RSEQ); + _prflag(FLOW); + _prflag(BLCK); + _prflag(QMRK); + _prflag(RKEY); + _prflag(RVAL); + _prflag(RKCL); + _prflag(RNXT); + _prflag(SSCL); + _prflag(QSCL); + _prflag(RSET); + _prflag(RDOC); + _prflag(NDOC); + _prflag(USTY); + _prflag(RSEQIMAP); + + #undef _prflag + + if(pos == 0) + if(buf.len > 0) + buf[pos++] = '0'; + + RYML_CHECK(pos <= buf.len); + + return buf.first(pos); +} + +#endif // RYML_DBG !!! <---------------------------------- + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +csubstr ParseEngine::location_contents(Location const& loc) const +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, loc.offset < m_buf.len); + return m_buf.sub(loc.offset); +} + +template +Location ParseEngine::val_location(const char *val) const +{ + if(C4_UNLIKELY(val == nullptr)) + return {m_file, 0, 0, 0}; + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_options.locations()); + // NOTE: if any of these checks fails, the parser needs to be + // instantiated with locations enabled. + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf.str == m_newline_offsets_buf.str); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_buf.len == m_newline_offsets_buf.len); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_options.locations()); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, !_locations_dirty()); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_newline_offsets != nullptr); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_newline_offsets_size > 0); + // NOTE: the pointer needs to belong to the buffer that was used to parse. + csubstr src = m_buf; + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, val != nullptr || src.str == nullptr); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, (val >= src.begin() && val <= src.end()) || (src.str == nullptr && val == nullptr)); + // ok. search the first stored newline after the given ptr + using lineptr_type = size_t const* C4_RESTRICT; + lineptr_type lineptr = nullptr; + size_t offset = (size_t)(val - src.begin()); + if(m_newline_offsets_size < RYML_LOCATIONS_SMALL_THRESHOLD) + { + // just do a linear search if the size is small. + for(lineptr_type curr = m_newline_offsets, last = m_newline_offsets + m_newline_offsets_size; curr < last; ++curr) + { + if(*curr > offset) + { + lineptr = curr; + break; + } + } + } + else + { + // do a bisection search if the size is not small. + // + // We could use std::lower_bound but this is simple enough and + // spares the costly include of . + size_t count = m_newline_offsets_size; + size_t step; + lineptr_type it; + lineptr = m_newline_offsets; + while(count) + { + step = count >> 1; + it = lineptr + step; + if(*it < offset) + { + lineptr = ++it; + count -= step + 1; + } + else + { + count = step; + } + } + } + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, lineptr >= m_newline_offsets); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, lineptr <= m_newline_offsets + m_newline_offsets_size); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, *lineptr > offset); + Location loc; + loc.name = m_file; + loc.offset = offset; + loc.line = (size_t)(lineptr - m_newline_offsets); + if(lineptr > m_newline_offsets) + loc.col = (offset - *(lineptr-1) - 1u); + else + loc.col = offset; + return loc; +} + +template +void ParseEngine::_prepare_locations() +{ + m_newline_offsets_buf = m_buf; + size_t numnewlines = 1u + m_buf.count('\n'); + _resize_locations(numnewlines); + m_newline_offsets_size = 0; + for(size_t i = 0; i < m_buf.len; i++) + if(m_buf[i] == '\n') + m_newline_offsets[m_newline_offsets_size++] = i; + m_newline_offsets[m_newline_offsets_size++] = m_buf.len; + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_newline_offsets_size == numnewlines); +} + +template +void ParseEngine::_resize_locations(size_t numnewlines) +{ + if(numnewlines > m_newline_offsets_capacity) + { + if(m_newline_offsets) + _RYML_CB_FREE(m_evt_handler->m_stack.m_callbacks, m_newline_offsets, size_t, m_newline_offsets_capacity); + m_newline_offsets = _RYML_CB_ALLOC_HINT(m_evt_handler->m_stack.m_callbacks, size_t, numnewlines, m_newline_offsets); + m_newline_offsets_capacity = numnewlines; + } +} + +template +bool ParseEngine::_locations_dirty() const +{ + return !m_newline_offsets_size; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_flow_skip_whitespace() +{ + // don't assign to csubstr rem: otherwise, gcc12,13,14 -O3 -m32 misbuilds + if(m_evt_handler->m_curr->line_contents.rem.len > 0) + { + if(m_evt_handler->m_curr->line_contents.rem.str[0] == ' ' || m_evt_handler->m_curr->line_contents.rem.str[0] == '\t') + { + _c4dbgpf("starts with whitespace: '{}'", _c4prc(m_evt_handler->m_curr->line_contents.rem.str[0])); + _skipchars(" \t"); + } + // comments + if(m_evt_handler->m_curr->line_contents.rem.begins_with('#')) + { + _c4dbgpf("it's a comment: {}", m_evt_handler->m_curr->line_contents.rem); + _line_progressed(m_evt_handler->m_curr->line_contents.rem.len); + } + } +} + + +//----------------------------------------------------------------------------- + + +template +void ParseEngine::_handle_colon() +{ + size_t curr = m_evt_handler->m_curr->pos.line; + if(m_prev_colon != npos) + { + if(curr == m_prev_colon) + _c4err("two colons on same line"); + } + m_prev_colon = curr; +} + +template +void ParseEngine::_add_annotation(Annotation *C4_RESTRICT dst, csubstr str, size_t indentation, size_t line) +{ + _c4dbgpf("store annotation[{}]: '{}' indentation={} line={}", dst->num_entries, str, indentation, line); + if(C4_UNLIKELY(dst->num_entries >= C4_COUNTOF(dst->annotations))) // NOLINT(bugprone-sizeof-expression) + _c4err("too many annotations"); + dst->annotations[dst->num_entries].str = str; + dst->annotations[dst->num_entries].indentation = indentation; + dst->annotations[dst->num_entries].line = line; + ++dst->num_entries; +} + +template +void ParseEngine::_clear_annotations(Annotation *C4_RESTRICT dst) +{ + dst->num_entries = 0; +} + +#ifdef RYML_NO_COVERAGE__TO_BE_DELETED +template +bool ParseEngine::_handle_indentation_from_annotations() +{ + if(m_pending_anchors.num_entries == 1u || m_pending_tags.num_entries == 1u) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_anchors.num_entries < 2u && m_pending_tags.num_entries < 2u); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_anchors.annotations[0].line < m_evt_handler->m_curr->pos.line); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_tags.annotations[1].line < m_evt_handler->m_curr->pos.line); + size_t to_skip = m_evt_handler->m_curr->indref; + if(m_pending_anchors.num_entries) + to_skip = m_pending_anchors.annotations[0].indentation > to_skip ? m_pending_anchors.annotations[0].indentation : to_skip; + if(m_pending_tags.num_entries) + to_skip = m_pending_tags.annotations[0].indentation > to_skip ? m_pending_tags.annotations[0].indentation : to_skip; + _c4dbgpf("annotations pending, skip indentation up to {}!", to_skip); + _maybe_skipchars_up_to(' ', to_skip); + return true; + } + return false; +} +#endif + +template +bool ParseEngine::_annotations_require_key_container() const +{ + return m_pending_tags.num_entries > 1 || m_pending_anchors.num_entries > 1; +} + +template +void ParseEngine::_check_tag(csubstr tag) +{ + if(!tag.begins_with("!<")) + { + if(C4_UNLIKELY(tag.first_of("[]{},") != npos)) + _RYML_CB_ERR_(m_evt_handler->m_stack.m_callbacks, "tags must not contain any of '[]{},'", m_evt_handler->m_curr->pos); + } + else + { + if(C4_UNLIKELY(!tag.ends_with('>'))) + _RYML_CB_ERR_(m_evt_handler->m_stack.m_callbacks, "malformed tag", m_evt_handler->m_curr->pos); + } +} + +template +void ParseEngine::_handle_annotations_before_blck_key_scalar() +{ + _c4dbgpf("annotations_before_blck_key_scalar, node={}", m_evt_handler->m_curr->node_id); + if(m_pending_tags.num_entries) + { + _c4dbgpf("annotations_before_blck_key_scalar, #tags={}", m_pending_tags.num_entries); + if(C4_LIKELY(m_pending_tags.num_entries == 1)) + { + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_key_tag(m_pending_tags.annotations[0].str); + _clear_annotations(&m_pending_tags); + } + else + { + _c4err("too many tags"); + } + } + if(m_pending_anchors.num_entries) + { + _c4dbgpf("annotations_before_blck_key_scalar, #anchors={}", m_pending_anchors.num_entries); + if(C4_LIKELY(m_pending_anchors.num_entries == 1)) + { + m_evt_handler->set_key_anchor(m_pending_anchors.annotations[0].str); + _clear_annotations(&m_pending_anchors); + } + else + { + _c4err("too many anchors"); + } + } +} + +template +void ParseEngine::_handle_annotations_before_blck_val_scalar() +{ + _c4dbgpf("annotations_before_blck_val_scalar, node={}", m_evt_handler->m_curr->node_id); + if(m_pending_tags.num_entries) + { + _c4dbgpf("annotations_before_blck_val_scalar, #tags={}", m_pending_tags.num_entries); + if(C4_LIKELY(m_pending_tags.num_entries == 1)) + { + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_val_tag(m_pending_tags.annotations[0].str); + _clear_annotations(&m_pending_tags); + } + else + { + _c4err("too many tags"); + } + } + if(m_pending_anchors.num_entries) + { + _c4dbgpf("annotations_before_blck_val_scalar, #anchors={}", m_pending_anchors.num_entries); + if(C4_LIKELY(m_pending_anchors.num_entries == 1)) + { + m_evt_handler->set_val_anchor(m_pending_anchors.annotations[0].str); + _clear_annotations(&m_pending_anchors); + } + else + { + _c4err("too many anchors"); + } + } +} + +template +void ParseEngine::_handle_annotations_before_start_mapblck(size_t current_line) +{ + _c4dbgpf("annotations_before_start_mapblck, current_line={}", current_line); + if(m_pending_tags.num_entries == 2) + { + _c4dbgp("2 tags, setting entry 0"); + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_val_tag(m_pending_tags.annotations[0].str); + } + else if(m_pending_tags.num_entries == 1) + { + _c4dbgpf("1 tag. line={}, curr={}", m_pending_tags.annotations[0].line); + if(m_pending_tags.annotations[0].line < current_line) + { + _c4dbgp("...tag is for the map. setting it."); + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_val_tag(m_pending_tags.annotations[0].str); + _clear_annotations(&m_pending_tags); + } + } + // + if(m_pending_anchors.num_entries == 2) + { + _c4dbgp("2 anchors, setting entry 0"); + m_evt_handler->set_val_anchor(m_pending_anchors.annotations[0].str); + } + else if(m_pending_anchors.num_entries == 1) + { + _c4dbgpf("1 anchor. line={}, curr={}", m_pending_anchors.annotations[0].line); + if(m_pending_anchors.annotations[0].line < current_line) + { + _c4dbgp("...anchor is for the map. setting it."); + m_evt_handler->set_val_anchor(m_pending_anchors.annotations[0].str); + _clear_annotations(&m_pending_anchors); + } + } +} + +template +void ParseEngine::_handle_annotations_before_start_mapblck_as_key() +{ + _c4dbgp("annotations_before_start_mapblck_as_key"); + if(m_pending_tags.num_entries == 2) + { + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_key_tag(m_pending_tags.annotations[0].str); + } + if(m_pending_anchors.num_entries == 2) + { + m_evt_handler->set_key_anchor(m_pending_anchors.annotations[0].str); + } +} + +template +void ParseEngine::_handle_annotations_and_indentation_after_start_mapblck(size_t key_indentation, size_t key_line) +{ + _c4dbgp("annotations_after_start_mapblck"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_tags.num_entries <= 2); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_anchors.num_entries <= 2); + if(m_pending_anchors.num_entries || m_pending_tags.num_entries) + { + key_indentation = _select_indentation_from_annotations(key_indentation, key_line); + switch(m_pending_tags.num_entries) + { + case 1u: + _check_tag(m_pending_tags.annotations[0].str); + m_evt_handler->set_key_tag(m_pending_tags.annotations[0].str); + _clear_annotations(&m_pending_tags); + break; + case 2u: + _check_tag(m_pending_tags.annotations[1].str); + m_evt_handler->set_key_tag(m_pending_tags.annotations[1].str); + _clear_annotations(&m_pending_tags); + break; + } + switch(m_pending_anchors.num_entries) + { + case 1u: + m_evt_handler->set_key_anchor(m_pending_anchors.annotations[0].str); + _clear_annotations(&m_pending_anchors); + break; + case 2u: + m_evt_handler->set_key_anchor(m_pending_anchors.annotations[1].str); + _clear_annotations(&m_pending_anchors); + break; + } + } + _set_indentation(key_indentation); +} + +template +size_t ParseEngine::_select_indentation_from_annotations(size_t val_indentation, size_t val_line) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_pending_tags.num_entries || m_pending_anchors.num_entries); + // select the left-most annotation on the max line + auto const *C4_RESTRICT curr = m_pending_anchors.num_entries ? &m_pending_anchors.annotations[0] : &m_pending_tags.annotations[0]; + for(size_t i = 0; i < m_pending_anchors.num_entries; ++i) + { + auto const& C4_RESTRICT ann = m_pending_anchors.annotations[i]; + if(ann.line > curr->line) + curr = &ann; + else if(ann.indentation < curr->indentation) + curr = &ann; + } + for(size_t j = 0; j < m_pending_tags.num_entries; ++j) + { + auto const& C4_RESTRICT ann = m_pending_tags.annotations[j]; + if(ann.line > curr->line) + curr = &ann; + else if(ann.indentation < curr->indentation) + curr = &ann; + } + return curr->line < val_line ? val_indentation : curr->indentation; +} + +template +void ParseEngine::_handle_directive(csubstr rem) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.is_sub(m_evt_handler->m_curr->line_contents.rem)); + const size_t pos = rem.find('#'); + _c4dbgpf("handle_directive: pos={} rem={}", pos, rem); + if(pos == npos) // no comments + { + m_evt_handler->add_directive(rem); + _line_progressed(rem.len); + } + else + { + csubstr to_comment = rem.first(pos); + csubstr trimmed = to_comment.trimr(" \t"); + m_evt_handler->add_directive(trimmed); + _line_progressed(pos); + _skip_comment(); + } +} + +template +bool ParseEngine::_handle_bom() +{ + const csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(rem.len) + { + const csubstr rest = rem.sub(1); + // https://yaml.org/spec/1.2.2/#52-character-encodings + #define _rymlisascii(c) ((c) > '\0' && (c) <= '\x7f') // is the character ASCII? + if(rem.begins_with({"\x00\x00\xfe\xff", 4}) || (rem.begins_with({"\x00\x00\x00", 3}) && rem.len >= 4u && _rymlisascii(rem.str[3]))) + { + _c4dbgp("byte order mark: UTF32BE"); + _handle_bom(UTF32BE); + _line_progressed(4); + return true; + } + else if(rem.begins_with("\xff\xfe\x00\x00") || (rest.begins_with({"\x00\x00\x00", 3}) && rem.len >= 4u && _rymlisascii(rem.str[0]))) + { + _c4dbgp("byte order mark: UTF32LE"); + _handle_bom(UTF32LE); + _line_progressed(4); + return true; + } + else if(rem.begins_with("\xfe\xff") || (rem.begins_with('\x00') && rem.len >= 2u && _rymlisascii(rem.str[1]))) + { + _c4dbgp("byte order mark: UTF16BE"); + _handle_bom(UTF16BE); + _line_progressed(2); + return true; + } + else if(rem.begins_with("\xff\xfe") || (rest.begins_with('\x00') && rem.len >= 2u && _rymlisascii(rem.str[0]))) + { + _c4dbgp("byte order mark: UTF16LE"); + _handle_bom(UTF16LE); + _line_progressed(2); + return true; + } + else if(rem.begins_with("\xef\xbb\xbf")) + { + _c4dbgp("byte order mark: UTF8"); + _handle_bom(UTF8); + _line_progressed(3); + return true; + } + #undef _rymlisascii + } + return false; +} + +template +void ParseEngine::_handle_bom(Encoding_e enc) +{ + if(m_encoding == NOBOM) + { + const bool is_beginning_of_file = m_evt_handler->m_curr->line_contents.rem.str == m_buf.str; + if(enc == UTF8 || is_beginning_of_file) + m_encoding = enc; + else + _c4err("non-UTF8 byte order mark can appear only at the beginning of the file"); + } + else if(enc != m_encoding) + { + _c4err("byte order mark can only be set once"); + } +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_seq_json() +{ +seqjson_start: + _c4dbgpf("handle2_seq_json: node_id={} level={} indentation={}", m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL|RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RVAL) != has_all(RNXT)); + + _handle_flow_skip_whitespace(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqjson_again; + + if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + const char first = rem.str[0]; + _c4dbgpf("mapjson[RVAL]: '{}'", first); + switch(first) + { + case '"': + { + _c4dbgp("seqjson[RVAL]: scanning double-quoted scalar"); + ScannedScalar sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + break; + } + case '[': + { + _c4dbgp("seqjson[RVAL]: start child seqjson"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RVAL, RNXT); + _line_progressed(1); + break; + } + case '{': + { + _c4dbgp("seqjson[RVAL]: start child mapjson"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|RKEY, RSEQ|RVAL|RNXT); + _line_progressed(1); + goto seqjson_finish; + } + case ']': // this happens on a trailing comma like ", ]" + { + _c4dbgp("seqjson[RVAL]: end!"); + rem_flags(RSEQ); + m_evt_handler->end_seq(); + _line_progressed(1); + if(!has_all(RSEQ|FLOW)) + goto seqjson_finish; + break; + } + default: + { + ScannedScalar sc; + if(_scan_scalar_seq_json(&sc)) + { + _c4dbgp("seqjson[RVAL]: it's a plain scalar."); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + _c4err("parse error"); + } + } + } + } + else // RNXT + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + const char first = rem.str[0]; + _c4dbgpf("mapjson[RNXT]: '{}'", first); + switch(first) + { + case ',': + { + _c4dbgp("seqjson[RNXT]: expect next val"); + addrem_flags(RVAL, RNXT); + m_evt_handler->add_sibling(); + _line_progressed(1); + break; + } + case ']': + { + _c4dbgp("seqjson[RNXT]: end!"); + m_evt_handler->end_seq(); + _line_progressed(1); + goto seqjson_finish; + } + default: + _c4err("parse error"); + } + } + + seqjson_again: + _c4dbgt("seqjson: go again", 0); + if(_finished_line()) + { + if(C4_LIKELY(!_finished_file())) + { + _line_ended(); + _scan_line(); + _c4dbgnextline(); + } + else + { + _c4err("missing terminating ]"); + } + } + goto seqjson_start; + + seqjson_finish: + _c4dbgp("seqjson: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_map_json() +{ +mapjson_start: + _c4dbgpf("handle2_map_json: node_id={} level={} indentation={}", m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RKCL|RVAL|RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, 1 == (has_any(RKEY) + has_any(RKCL) + has_any(RVAL) + has_any(RNXT))); + + _handle_flow_skip_whitespace(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapjson_again; + + if(has_any(RKEY)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapjson[RKEY]: '{}'", first); + switch(first) + { + case '"': + { + _c4dbgp("mapjson[RKEY]: scanning double-quoted scalar"); + ScannedScalar sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RKCL, RKEY); + break; + } + case '}': // this happens on a trailing comma like ", }" + { + _c4dbgp("mapjson[RKEY]: end!"); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapjson_finish; + } + default: + _c4err("parse error"); + } + } + else if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapjson[RVAL]: '{}'", first); + switch(first) + { + case '"': + { + _c4dbgp("mapjson[RVAL]: scanning double-quoted scalar"); + ScannedScalar sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + break; + } + case '[': + { + _c4dbgp("mapjson[RVAL]: start val seqjson"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_seq_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RSEQ|RVAL, RMAP|RNXT); + _line_progressed(1); + goto mapjson_finish; + } + case '{': + { + _c4dbgp("mapjson[RVAL]: start val mapjson"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RKEY, RNXT); + _line_progressed(1); + // keep going in this function + break; + } + default: + { + ScannedScalar sc; + if(_scan_scalar_map_json(&sc)) + { + _c4dbgp("mapjson[RVAL]: plain scalar."); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + _c4err("parse error"); + } + break; + } + } + } + else if(has_any(RKCL)) // read the key colon + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapjson[RKCL]: '{}'", first); + if(first == ':') + { + _c4dbgp("mapjson[RKCL]: found the colon"); + addrem_flags(RVAL, RKCL); + _line_progressed(1); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RNXT)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _c4dbgpf("mapjson[RNXT]: '{}'", rem.str[0]); + if(rem.begins_with(',')) + { + _c4dbgp("mapjson[RNXT]: expect next keyval"); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + _line_progressed(1); + } + else if(rem.begins_with('}')) + { + _c4dbgp("mapjson[RNXT]: end!"); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapjson_finish; + } + else + { + _c4err("parse error"); + } + } + + mapjson_again: + _c4dbgt("mapjson: go again", 0); + if(_finished_line()) + { + if(C4_LIKELY(!_finished_file())) + { + _line_ended(); + _scan_line(); + _c4dbgnextline(); + } + else + { + _c4err("missing terminating }"); + } + } + goto mapjson_start; + + mapjson_finish: + _c4dbgp("mapjson: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_seq_imap() +{ +seqimap_start: + _c4dbgpf("handle2_seq_imap: node_id={} level={} indref={}", m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RSEQIMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL|RNXT|QMRK|RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, 1 == has_all(RVAL) + has_all(RNXT) + has_all(QMRK) + has_all(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_stack.size() >= 3); + + _handle_flow_skip_whitespace(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqimap_again; + + if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + const char first = rem.str[0]; + _c4dbgpf("seqimap[RVAL]: '{}'", _c4prc(first)); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("seqimap[RVAL]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else if(first == '"') + { + _c4dbgp("seqimap[RVAL]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + m_evt_handler->end_map(); + goto seqimap_finish; + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_map_flow(&sc)) + { + _c4dbgp("seqimap[RVAL]: it's a scalar."); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else if(first == '[') + { + _c4dbgp("seqimap[RVAL]: start child seqflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RVAL, RNXT|RSEQIMAP); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto seqimap_finish; + } + else if(first == '{') + { + _c4dbgp("seqimap[RVAL]: start child mapflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|RKEY, RSEQ|RVAL|RSEQIMAP|RNXT); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto seqimap_finish; + } + else if(first == ',' || first == ']') + { + _c4dbgp("seqimap[RVAL]: finish without val."); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgp("seqimap[RVAL]: anchor!"); + m_evt_handler->set_val_anchor(anchor); + } + else if(first == '*') + { + csubstr ref = _scan_ref_seq(); + _c4dbgp("seqimap[RVAL]: ref!"); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RNXT)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + const char first = rem.str[0]; + _c4dbgpf("seqimap[RNXT]: '{}'", _c4prc(first)); + if(first == ',' || first == ']') + { + // we may get here because a map or a seq started and we + // return later + _c4dbgp("seqimap: done"); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else + { + _c4err("parse error"); + } + } + else if(has_any(QMRK)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + const char first = rem.str[0]; + _c4dbgpf("seqimap[QMRK]: '{}'", _c4prc(first)); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("seqimap[QMRK]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + goto seqimap_again; + } + else if(first == '"') + { + _c4dbgp("seqimap[QMRK]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + goto seqimap_again; + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_map_flow(&sc)) + { + _c4dbgp("seqimap[QMRK]: it's a scalar."); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RKCL, QMRK); + goto seqimap_again; + } + else if(first == '[') + { + _c4dbgp("seqimap[QMRK]: start child seqflow"); + addrem_flags(RKCL, QMRK); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RSEQ|RVAL, RKCL|RSEQIMAP); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto seqimap_finish; + } + else if(first == '{') + { + _c4dbgp("seqimap[QMRK]: start child mapflow"); + addrem_flags(RKCL, QMRK); + m_evt_handler->begin_map_key_flow(); + addrem_flags(RMAP|RKEY, RSEQ|RKCL|RSEQIMAP); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto seqimap_finish; + } + else if(first == ',' || first == ']') + { + _c4dbgp("seqimap[QMRK]: finish without key."); + m_evt_handler->set_key_scalar_plain_empty(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgp("seqimap[QMRK]: anchor!"); + m_evt_handler->set_key_anchor(anchor); + } + else if(first == '*') + { + csubstr ref = _scan_ref_seq(); + _c4dbgp("seqimap[QMRK]: ref!"); + m_evt_handler->set_key_ref(ref); + addrem_flags(RKCL, QMRK); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RKCL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKCL)); + const char first = rem.str[0]; + _c4dbgpf("seqimap[RKCL]: '{}'", _c4prc(first)); + if(first == ':') + { + _c4dbgp("seqimap[RKCL]: found ':'"); + addrem_flags(RVAL, RKCL); + _line_progressed(1); + goto seqimap_again; + } + else if(first == ',' || first == ']') + { + _c4dbgp("seqimap[RKCL]: found ','. finish without val"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + goto seqimap_finish; + } + else + { + _c4err("parse error"); + } + } + + seqimap_again: + _c4dbgt("seqimap: go again", 0); + if(_finished_line()) + { + if(C4_LIKELY(!_finished_file())) + { + _line_ended(); + _scan_line(); + _c4dbgnextline(); + } + else + { + _c4err("parse error"); + } + } + goto seqimap_start; + + seqimap_finish: + _c4dbgp("seqimap: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_seq_flow() +{ +seqflow_start: + _c4dbgpf("handle2_seq_flow: node_id={} level={} indentation={}", m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL|RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RVAL) != has_all(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->indref != npos); + + _handle_flow_skip_whitespace(); + // don't assign to csubstr rem: otherwise, gcc12,13,14 -O3 -m32 misbuilds + if(!m_evt_handler->m_curr->line_contents.rem.len) + goto seqflow_again; + + if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + const char first = m_evt_handler->m_curr->line_contents.rem.str[0]; + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("seqflow[RVAL]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '"') + { + _c4dbgp("seqflow[RVAL]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_seq_flow(&sc)) + { + _c4dbgp("seqflow[RVAL]: it's a scalar."); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '[') + { + _c4dbgp("seqflow[RVAL]: start child seqflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_seq_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RVAL, RNXT); + _line_progressed(1); + } + else if(first == '{') + { + _c4dbgp("seqflow[RVAL]: start child mapflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RMAP|RKEY, RSEQ|RVAL|RNXT); + _line_progressed(1); + goto seqflow_finish; + } + else if(first == ']') // this happens on a trailing comma like ", ]" + { + _c4dbgp("seqflow[RVAL]: end!"); + _line_progressed(1); + m_evt_handler->end_seq(); + goto seqflow_finish; + } + else if(first == '*') + { + csubstr ref = _scan_ref_seq(); + _c4dbgpf("seqflow[RVAL]: ref! [{}]~~~{}~~~", ref.len, ref); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("seqflow[RVAL]: anchor! [{}]~~~{}~~~", anchor.len, anchor); + m_evt_handler->set_val_anchor(anchor); + if(_maybe_scan_following_comma()) + { + _c4dbgp("seqflow[RVAL]: empty scalar!"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + } + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("seqflow[RVAL]: tag! [{}]~~~{}~~~", tag.len, tag); + _check_tag(tag); + m_evt_handler->set_val_tag(tag); + if(_maybe_scan_following_comma()) + { + _c4dbgp("seqflow[RVAL]: empty scalar!"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + } + } + else if(first == ':') + { + _c4dbgpf("seqflow[RVAL]: actually seqimap at node[{}], with empty key", m_evt_handler->m_curr->node_id); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RSEQIMAP|RVAL, RSEQ|RNXT); + _line_progressed(1); + goto seqflow_finish; + } + else if(first == '?') + { + _c4dbgp("seqflow[RVAL]: start child mapflow, explicit key"); + addrem_flags(RNXT, RVAL); + m_was_inside_qmrk = true; + m_evt_handler->begin_map_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RSEQIMAP|QMRK, RSEQ|RNXT); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto seqflow_finish; + } + else + { + _c4err("parse error"); + } + } + else // RNXT + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + const char first = m_evt_handler->m_curr->line_contents.rem.str[0]; + if(first == ',') + { + _c4dbgp("seqflow[RNXT]: expect next val"); + addrem_flags(RVAL, RNXT); + m_evt_handler->add_sibling(); + _line_progressed(1); + } + else if(first == ']') + { + _c4dbgp("seqflow[RNXT]: end!"); + m_evt_handler->end_seq(); + _line_progressed(1); + goto seqflow_finish; + } + else if(first == ':') + { + _c4dbgpf("seqflow[RNXT]: actually seqimap at node[{}]", m_evt_handler->m_curr->node_id); + m_evt_handler->actually_val_is_first_key_of_new_map_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + addrem_flags(RSEQIMAP|RVAL, RNXT); + goto seqflow_finish; + } + else + { + _c4err("parse error"); + } + } + + seqflow_again: + _c4dbgt("seqflow: go again", 0); + if(_finished_line()) + { + if(C4_LIKELY(!_finished_file())) + { + _line_ended(); + _scan_line(); + _c4dbgnextline(); + } + else + { + _c4err("missing terminating ]"); + } + } + goto seqflow_start; + + seqflow_finish: + _c4dbgp("seqflow: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_map_flow() +{ +mapflow_start: + _c4dbgpf("handle2_map_flow: node_id={} level={} indentation={}", m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(FLOW)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RKCL|RVAL|RNXT|QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, 1 == (has_any(RKEY) + has_any(RKCL) + has_any(RVAL) + has_any(RNXT) + has_any(QMRK))); + + _handle_flow_skip_whitespace(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapflow_again; + + if(has_any(RKEY)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapflow[RKEY]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapflow[RKEY]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RKCL, RKEY|QMRK); + } + else if(first == '"') + { + _c4dbgp("mapflow[RKEY]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RKCL, RKEY|QMRK); + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_map_flow(&sc)) + { + _c4dbgp("mapflow[RKEY]: plain scalar"); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RKCL, RKEY|QMRK); + } + else if(first == '?') + { + _c4dbgp("mapflow[RKEY]: explicit key"); + _line_progressed(1); + addrem_flags(QMRK, RKEY); + _maybe_skip_whitespace_tokens(); + } + else if(first == ':') + { + _c4dbgp("mapflow[RKEY]: setting empty key"); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RVAL, RKEY|QMRK); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == ',') + { + _c4dbgp("mapflow[RKEY]: empty key+val!"); + m_evt_handler->set_key_scalar_plain_empty(); + m_evt_handler->set_val_scalar_plain_empty(); + addrem_flags(RNXT, RKEY|QMRK); + // keep going in this function + } + else if(first == '}') // this happens on a trailing comma like ", }" + { + _c4dbgp("mapflow[RKEY]: end!"); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapflow[RKEY]: key anchor! [{}]~~~{}~~~", anchor.len, anchor); + m_evt_handler->set_key_anchor(anchor); + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapflow[RKEY]: key ref! [{}]~~~{}~~~", ref.len, ref); + m_evt_handler->set_key_ref(ref); + addrem_flags(RKCL, RKEY); + } + else if(first == '[') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree sink. Other sink types may be + // able to handle it. + _c4dbgp("mapflow[RKEY]: start child seqflow (!)"); + addrem_flags(RKCL, RKEY); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RSEQ|RVAL, RMAP|RKCL); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == '{') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree sink. Other sink types may be + // able to handle it. + _c4dbgp("mapflow[RKEY]: start child mapflow (!)"); + addrem_flags(RKCL, RKEY); + m_evt_handler->begin_map_key_flow(); + addrem_flags(RKEY, RVAL|RKCL); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + // keep going in this function + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapflow[RKEY]: tag! [{}]~~~{}~~~", tag.len, tag); + _check_tag(tag); + m_evt_handler->set_key_tag(tag); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RKCL)) // read the key colon + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapflow[RKCL]: '{}'", first); + if(first == ':') + { + _c4dbgp("mapflow[RKCL]: found the colon"); + addrem_flags(RVAL, RKCL); + _line_progressed(1); + } + else if(first == '}') + { + _c4dbgp("mapflow[RKCL]: end with missing val!"); + addrem_flags(RVAL, RKCL); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == ',') + { + _c4dbgp("mapflow[RKCL]: got comma. val is missing"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RKCL); + _line_progressed(1); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + const char first = rem.str[0]; + _c4dbgpf("mapflow[RVAL]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapflow[RVAL]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '"') + { + _c4dbgp("mapflow[RVAL]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_map_flow(&sc)) + { + _c4dbgp("mapflow[RVAL]: plain scalar."); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '[') + { + _c4dbgp("mapflow[RVAL]: start val seqflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_seq_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RSEQ|RVAL, RMAP|RNXT); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == '{') + { + _c4dbgp("mapflow[RVAL]: start val mapflow"); + addrem_flags(RNXT, RVAL); + m_evt_handler->begin_map_val_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RKEY, RNXT); + _line_progressed(1); + // keep going in this function + } + else if(first == '}') + { + _c4dbgp("mapflow[RVAL]: end!"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == ',') + { + _c4dbgp("mapflow[RVAL]: empty val!"); + m_evt_handler->set_val_scalar_plain_empty(); + addrem_flags(RNXT, RVAL); + // keep going in this function + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapflow[RVAL]: key ref! [{}]~~~{}~~~", ref.len, ref); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapflow[RVAL]: key anchor! [{}]~~~{}~~~", anchor.len, anchor); + m_evt_handler->set_val_anchor(anchor); + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapflow[RVAL]: tag! [{}]~~~{}~~~", tag.len, tag); + _check_tag(tag); + m_evt_handler->set_val_tag(tag); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RNXT)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _c4dbgpf("mapflow[RNXT]: '{}'", rem.str[0]); + if(rem.begins_with(',')) + { + _c4dbgp("mapflow[RNXT]: expect next keyval"); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + _line_progressed(1); + } + else if(rem.begins_with('}')) + { + _c4dbgp("mapflow[RNXT]: end!"); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapflow_finish; + } + else + { + _c4err("parse error"); + } + } + else if(has_any(QMRK)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + const char first = rem.str[0]; + _c4dbgpf("mapflow[QMRK]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapflow[QMRK]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else if(first == '"') + { + _c4dbgp("mapflow[QMRK]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + // block scalars (ie | and >) cannot appear in flow containers + else if(_scan_scalar_plain_map_flow(&sc)) + { + _c4dbgp("mapflow[QMRK]: plain scalar"); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else if(first == ':') + { + _c4dbgp("mapflow[QMRK]: setting empty key"); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RVAL, QMRK); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '}') // this happens on a trailing comma like ", }" + { + _c4dbgp("mapflow[QMRK]: end!"); + m_evt_handler->set_key_scalar_plain_empty(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_map(); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == ',') + { + _c4dbgp("mapflow[QMRK]: empty key+val!"); + m_evt_handler->set_key_scalar_plain_empty(); + m_evt_handler->set_val_scalar_plain_empty(); + addrem_flags(RNXT, QMRK); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapflow[QMRK]: key anchor! [{}]~~~{}~~~", anchor.len, anchor); + m_evt_handler->set_key_anchor(anchor); + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapflow[QMRK]: key ref! [{}]~~~{}~~~", ref.len, ref); + m_evt_handler->set_key_ref(ref); + addrem_flags(RKCL, QMRK); + } + else if(first == '[') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree sink. Other sink types may be + // able to handle it. + _c4dbgp("mapflow[QMRK]: start child seqflow (!)"); + addrem_flags(RKCL, QMRK); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RSEQ|RVAL, RMAP|RKCL); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto mapflow_finish; + } + else if(first == '{') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree sink. Other sink types may be + // able to handle it. + _c4dbgp("mapflow[QMRK]: start child mapflow (!)"); + addrem_flags(RKCL, QMRK); + m_evt_handler->begin_map_key_flow(); + _set_indentation(m_evt_handler->m_parent->indref); + addrem_flags(RKEY, RKCL); + _line_progressed(1); + // keep going in this function + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapflow[QMRK]: tag! [{}]~~~{}~~~", tag.len, tag); + _check_tag(tag); + m_evt_handler->set_key_tag(tag); + } + else + { + _c4err("parse error"); + } + } + + mapflow_again: + _c4dbgt("mapflow: go again", 0); + if(_finished_line()) + { + if(C4_LIKELY(!_finished_file())) + { + _line_ended(); + _scan_line(); + _c4dbgnextline(); + } + else + { + _c4err("missing terminating }"); + } + } + goto mapflow_start; + + mapflow_finish: + _c4dbgp("mapflow: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_seq_block() +{ +seqblck_start: + _c4dbgpf("handle2_seq_block: seq_id={} node_id={} level={} indent={}", m_evt_handler->m_parent->node_id, m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RSEQ)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RVAL|RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, 1 == (has_any(RVAL) + has_any(RNXT))); + + _maybe_skip_comment(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + + if(has_any(RVAL)) + { + _c4dbgpf("seqblck[RVAL]: col={}", m_evt_handler->m_curr->pos.col); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + if(m_evt_handler->m_curr->at_line_beginning()) + { + _c4dbgpf("seqblck[RVAL]: indref={} indentation={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->line_contents.indentation); + if(m_evt_handler->m_curr->indentation_ge()) + { + _c4dbgpf("seqblck[RVAL]: skip {} from indentation", m_evt_handler->m_curr->line_contents.indentation); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("seqblck[RVAL]: smaller indentation!"); + _handle_indentation_pop_from_block_seq(); + goto seqblck_finish; + } + else if(m_evt_handler->m_curr->line_contents.indentation == npos) + { + _c4dbgp("seqblck[RVAL]: empty line!"); + _line_progressed(m_evt_handler->m_curr->line_contents.rem.len); + goto seqblck_again; + } + } + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + else + { + // accomodate annotation on the previous line. eg: + // - &elm + // foo # <-- on this line + // - &elm + // &foo foo: bar # <-- on this line + if(rem.str[0] == ' ') + { + if(_handle_indentation_from_annotations()) + { + _c4dbgp("seqblck[RVAL]: annotations!"); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + } + } + } + #endif + _RYML_CB_ASSERT(callbacks(), rem.len); + _c4dbgpf("seqblck[RVAL]: '{}' node_id={}", rem.str[0], m_evt_handler->m_curr->node_id); + const char first = rem.str[0]; + const size_t startline = m_evt_handler->m_curr->pos.line; + // warning: the gcc optimizer on x86 builds is brittle with + // this function: + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("seqblck[RVAL]: single-quoted scalar"); + sc = _scan_scalar_squot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("seqblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); // VAL! + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + _c4dbgp("seqblck[RVAL]: start mapblck, set scalar as key"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); // KEY! + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RMAP|RVAL, RSEQ|RNXT); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + } + else if(first == '"') + { + _c4dbgp("seqblck[RVAL]: double-quoted scalar"); + sc = _scan_scalar_dquot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("seqblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); // VAL! + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + _c4dbgp("seqblck[RVAL]: start mapblck, set scalar as key"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); // KEY! + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RMAP|RVAL, RSEQ|RNXT); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + } + // block scalars can only appear as keys when in QMRK scope + // (ie, after ? tokens), so no need to scan following colon in + // here. + else if(first == '|') + { + _c4dbgp("seqblck[RVAL]: block-literal scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_literal(sb); + m_evt_handler->set_val_scalar_literal(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '>') + { + _c4dbgp("seqblck[RVAL]: block-folded scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_folded(sb); + m_evt_handler->set_val_scalar_folded(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(_scan_scalar_plain_seq_blck(&sc)) + { + _c4dbgp("seqblck[RVAL]: plain scalar."); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("seqblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); // VAL! + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + if(startindent > m_evt_handler->m_curr->indref) + { + _c4dbgp("seqblck[RVAL]: start mapblck, set scalar as key"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); // KEY! + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RMAP|RVAL, RSEQ|RNXT); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + else if(m_evt_handler->m_parent && m_evt_handler->m_parent->indref == startindent && has_any(RMAP|BLCK, m_evt_handler->m_parent)) + { + _c4dbgp("seqblck[RVAL]: empty val + end indentless seq + set key"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->end_seq(); + m_evt_handler->add_sibling(); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); // KEY! + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RVAL, RNXT|RKEY); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + else + { + _c4err("parse error"); + } + } + } + else if(first == '[') + { + _c4dbgp("seqblck[RVAL]: start child seqflow"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(FLOW|RVAL, BLCK|RNXT); + _line_progressed(1); + _set_indentation(m_evt_handler->m_parent->indref + 1u); + goto seqblck_finish; + } + else if(first == '{') + { + _c4dbgp("seqblck[RVAL]: start child mapflow"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|RKEY|FLOW, BLCK|RSEQ|RVAL|RNXT); + _line_progressed(1); + _set_indentation(m_evt_handler->m_parent->indref + 1u); + goto seqblck_finish; + } + else if(first == '-') + { + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("seqblck[RVAL]: prev val was empty"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + // keep in RVAL, but for the next sibling + m_evt_handler->add_sibling(); + } + else + { + _c4dbgp("seqblck[RVAL]: start child seqblck"); + _RYML_CB_ASSERT(this->callbacks(), startindent > m_evt_handler->m_curr->indref); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_block(); + addrem_flags(RVAL, RNXT); + _save_indentation(); + // keep going on inside this function + } + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == ':') + { + _c4dbgp("seqblck[RVAL]: start child mapblck with empty key"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RMAP|RVAL, RSEQ|RNXT); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + else if(first == '&') + { + const csubstr anchor = _scan_anchor(); + _c4dbgpf("seqblck[RVAL]: anchor! [{}]~~~{}~~~", anchor.len, anchor); + // we need to buffer the anchors, as there may be two + // consecutive anchors in here + _add_annotation(&m_pending_anchors, anchor, startindent, startline); + } + else if(first == '*') + { + csubstr ref = _scan_ref_seq(); + _c4dbgpf("seqblck[RVAL]: ref! [{}]~~~{}~~~", ref.len, ref); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("seqblck[RVAL]: set ref as val!"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + else + { + _c4dbgp("seqblck[RVAL]: ref is key of map"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_ref(ref); + addrem_flags(RMAP|RVAL, RSEQ|RNXT); + _set_indentation(startindent); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("seqblck[RVAL]: val tag! [{}]~~~{}~~~", tag.len, tag); + // we need to buffer the tags, as there may be two + // consecutive tags in here + _add_annotation(&m_pending_tags, tag, startindent, startline); + } + else if(first == '?') + { + _c4dbgp("seqblck[RVAL]: start child mapblck, explicit key"); + addrem_flags(RNXT, RVAL); + m_was_inside_qmrk = true; + m_evt_handler->begin_map_val_block(); + addrem_flags(RMAP|QMRK, RSEQ|RNXT); + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + else + { + _c4err("parse error"); + } + } + else // RNXT + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + // + // handle indentation + // + _c4dbgpf("seqblck[RNXT]: indref={} indentation={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->line_contents.indentation); + if(C4_LIKELY(_at_line_begin())) + { + _c4dbgp("seqblck[RNXT]: at line begin"); + if(m_evt_handler->m_curr->indentation_ge()) + { + _c4dbgpf("seqblck[RNXT]: skip {} from indref", m_evt_handler->m_curr->indref); + _line_progressed(m_evt_handler->m_curr->indref); + _maybe_skip_whitespace_tokens(); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("seqblck[RNXT]: smaller indentation!"); + _handle_indentation_pop_from_block_seq(); + if(has_all(RSEQ|BLCK)) + { + _c4dbgp("seqblck[RNXT]: still seqblck!"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RNXT)); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + } + else + { + _c4dbgp("seqblck[RNXT]: no longer seqblck!"); + goto seqblck_finish; + } + } + else if(m_evt_handler->m_curr->line_contents.indentation == npos) + { + _c4dbgpf("seqblck[RNXT]: blank line, len={}", m_evt_handler->m_curr->line_contents.rem); + _line_progressed(m_evt_handler->m_curr->line_contents.rem.len); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto seqblck_again; + } + } + else + { + _c4dbgp("seqblck[RNXT]: NOT at line begin"); + if(!rem.begins_with_any(" \t")) + { + _c4err("parse error"); + } + else + { + _skipchars(" \t"); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + { + _c4dbgp("seqblck[RNXT]: again"); + goto seqblck_again; + } + } + } + // + // now handle the tokens + // + const char first = rem.str[0]; + _c4dbgpf("seqblck[RNXT]: '{}' node_id={}", first, m_evt_handler->m_curr->node_id); + if(first == '-') + { + if(m_evt_handler->m_curr->indref > 0 || m_evt_handler->m_curr->line_contents.indentation > 0 || !_is_doc_begin_token(rem)) + { + _c4dbgp("seqblck[RNXT]: expect next val"); + addrem_flags(RVAL, RNXT); + m_evt_handler->add_sibling(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else + { + _c4dbgp("seqblck[RNXT]: start doc"); + _start_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + } + else if(first == ':') + { + // This happens for example in `- [a: b]: c` (after + // terminating the seq, ie, after `]`). All other cases + // (ie colon after scalars) are caught elsewhere (ie, in + // RVAL state). + auto const *C4_RESTRICT prev_state = m_evt_handler->m_parent; + if(C4_LIKELY(prev_state && (prev_state->flags & RMAP))) + { + _c4dbgp("seqblck[RNXT]: actually this seq was '?' key of parent map"); + m_evt_handler->end_seq(); + goto seqblck_finish; + } + else + { + _c4err("parse error"); + } + } + else if(first == '.') + { + _c4dbgp("seqblck[RNXT]: maybe doc?"); + csubstr rs = rem.sub(1); + if(rs == ".." || rs.begins_with(".. ")) + { + _c4dbgp("seqblck[RNXT]: end+start doc"); + _end_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto seqblck_finish; + } + else + { + _c4err("parse error"); + } + } + else + { + // may be an indentless sequence nested in a map... + //if(m_evt_handler->m_stack.size() >= 2) + #ifdef RYML_DBG + char flagbuf_[128]; + for(auto const& s : m_evt_handler->m_stack) + { + _dbg_printf("state[{}]: ind={} node={} flags={}\n", s.level, s.indref, s.node_id, detail::_parser_flags_to_str(flagbuf_, s.flags)); + } + #endif + if(m_evt_handler->m_parent && has_all(RMAP|BLCK, m_evt_handler->m_parent) && m_evt_handler->m_curr->indref == m_evt_handler->m_parent->indref) + { + _c4dbgpf("seqblck[RNXT]: end indentless seq, go to parent={}. node={}", m_evt_handler->m_parent->node_id, m_evt_handler->m_curr->node_id); + _RYML_CB_ASSERT(this->callbacks(), m_evt_handler->m_curr != m_evt_handler->m_parent); + _handle_indentation_pop(m_evt_handler->m_parent); + _RYML_CB_ASSERT(this->callbacks(), has_all(RMAP|BLCK)); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + goto seqblck_finish; + } + else //if(first != '*') + { + _c4err("parse error"); + } + } + } + + seqblck_again: + _c4dbgt("seqblck: go again", 0); + if(_finished_line()) + { + _line_ended(); + _scan_line(); + if(_finished_file()) + { + _c4dbgp("seqblck: finish!"); + _end_seq_blck(); + goto seqblck_finish; + } + _c4dbgnextline(); + } + goto seqblck_start; + + seqblck_finish: + _c4dbgp("seqblck: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_map_block() +{ +mapblck_start: + _c4dbgpf("handle2_map_block: map_id={} node_id={} level={} indref={}", m_evt_handler->m_parent->node_id, m_evt_handler->m_curr->node_id, m_evt_handler->m_curr->level, m_evt_handler->m_curr->indref); + + // states: RKEY|QMRK -> RKCL -> RVAL -> RNXT + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(BLCK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY|RKCL|RVAL|RNXT|QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, 1 == (has_any(RKEY) + has_any(RKCL) + has_any(RVAL) + has_any(RNXT) + has_any(QMRK))); + + _maybe_skip_comment(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + + if(has_any(RKEY)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + // + // handle indentation + // + if(m_evt_handler->m_curr->at_line_beginning()) + { + if(m_evt_handler->m_curr->indentation_eq()) + { + _c4dbgpf("mapblck[RKEY]: skip {} from indref", m_evt_handler->m_curr->indref); + _line_progressed(m_evt_handler->m_curr->indref); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("mapblck[RKEY]: smaller indentation!"); + _handle_indentation_pop_from_block_map(); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + if(has_all(RMAP|BLCK)) + { + _c4dbgp("mapblck[RKEY]: still mapblck!"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(RKEY)); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else + { + _c4dbgp("mapblck[RKEY]: no longer mapblck!"); + goto mapblck_finish; + } + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->indentation_gt()); + _c4err("invalid indentation"); + } + } + // + // now handle the tokens + // + const char first = rem.str[0]; + const size_t startline = m_evt_handler->m_curr->pos.line; + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(); + _c4dbgpf("mapblck[RKEY]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapblck[RKEY]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RVAL, RKEY); + if(!_maybe_scan_following_colon()) + _c4err("could not find ':' colon after key"); + _maybe_skip_whitespace_tokens(); + } + else if(first == '"') + { + _c4dbgp("mapblck[RKEY]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RVAL, RKEY); + if(!_maybe_scan_following_colon()) + _c4err("could not find ':' colon after key"); + _maybe_skip_whitespace_tokens(); + } + // block scalars (| and >) can not be used as keys unless they + // appear in an explicit QMRK scope (ie, after the ? token), + else if(C4_UNLIKELY(first == '|')) + { + _c4err("block literal keys must be enclosed in '?'"); + } + else if(C4_UNLIKELY(first == '>')) + { + _c4err("block literal keys must be enclosed in '?'"); + } + else if(_scan_scalar_plain_map_blck(&sc)) + { + _c4dbgp("mapblck[RKEY]: plain scalar"); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RVAL, RKEY); + if(!_maybe_scan_following_colon()) + _c4err("could not find ':' colon after key"); + _maybe_skip_whitespace_tokens(); + } + else if(first == '?') + { + _c4dbgp("mapblck[RKEY]: key token!"); + addrem_flags(QMRK, RKEY); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + m_was_inside_qmrk = true; + goto mapblck_again; + } + else if(first == ':') + { + _c4dbgp("mapblck[RKEY]: setting empty key"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RVAL, RKEY); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapblck[RKEY]: key ref! [{}]~~~{}~~~", ref.len, ref); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_ref(ref); + addrem_flags(RVAL, RKEY); + if(!_maybe_scan_following_colon()) + _c4err("could not find ':' colon after key"); + _maybe_skip_whitespace_tokens(); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapblck[RKEY]: key anchor! [{}]~~~{}~~~", anchor.len, anchor); + _add_annotation(&m_pending_anchors, anchor, startindent, startline); + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapblck[RKEY]: key tag! [{}]~~~{}~~~", tag.len, tag); + _add_annotation(&m_pending_tags, tag, startindent, startline); + } + else if(first == '[') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree handler. Other handlers may be + // able to handle it. + _c4dbgp("mapblck[RKEY]: start child seqflow (!)"); + addrem_flags(RKCL, RKEY); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RMAP|BLCK|RKCL); + _line_progressed(1); + _set_indentation(startindent); + goto mapblck_finish; + } + else if(first == '{') + { + // RYML's tree cannot store container keys, but that's + // handled inside the tree handler. Other handlers may be + // able to handle it. + _c4dbgp("mapblck[RKEY]: start child mapflow (!)"); + addrem_flags(RKCL, RKEY); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->begin_map_key_flow(); + addrem_flags(FLOW|RKEY, BLCK|RKCL); + _line_progressed(1); + _set_indentation(startindent); + goto mapblck_finish; + } + else if(first == '-') + { + _c4dbgp("mapblck[RKEY]: maybe doc?"); + if(m_evt_handler->m_curr->line_contents.indentation == 0 && _is_doc_begin_token(rem)) + { + _c4dbgp("mapblck[RKEY]: end+start doc"); + _start_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + else if(first == '.') + { + _c4dbgp("mapblck[RKEY]: maybe end doc?"); + if(m_evt_handler->m_curr->line_contents.indentation == 0 && _is_doc_end_token(rem)) + { + _c4dbgp("mapblck[RKEY]: end doc"); + _end_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + _RYML_WITH_TAB_TOKENS( + else if(first == '\t') + { + _c4dbgp("mapblck[RKEY]: skip tabs"); + _maybe_skipchars('\t'); + }) + else + { + _c4err("parse error"); + } + } + else if(has_any(RKCL)) // read the key colon + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + // + // handle indentation + // + if(m_evt_handler->m_curr->at_line_beginning()) + { + if(m_evt_handler->m_curr->indentation_eq()) + { + _c4dbgpf("mapblck[RKCL]: skip {} from indref", m_evt_handler->m_curr->indref); + _line_progressed(m_evt_handler->m_curr->indref); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else if(C4_UNLIKELY(m_evt_handler->m_curr->indentation_lt())) + { + _c4err("invalid indentation"); + } + } + const char first = rem.str[0]; + _c4dbgpf("mapblck[RKCL]: '{}'", first); + if(first == ':') + { + _c4dbgp("mapblck[RKCL]: found the colon"); + addrem_flags(RVAL, RKCL); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '?') + { + _c4dbgp("mapblck[RKCL]: got '?'. val was empty"); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_was_inside_qmrk); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + addrem_flags(QMRK, RKCL); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '-') + { + if(m_evt_handler->m_curr->indref == 0 || m_evt_handler->m_curr->line_contents.indentation == 0 || _is_doc_begin_token(rem)) + { + _c4dbgp("mapblck[RKCL]: end+start doc"); + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, _is_doc_begin_token(rem)); + _start_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + else if(first == '.') + { + _c4dbgp("mapblck[RKCL]: maybe end doc?"); + csubstr rs = rem.sub(1); + if(rs == ".." || rs.begins_with(".. ")) + { + _c4dbgp("mapblck[RKCL]: end+start doc"); + _end_doc_suddenly(); + _line_progressed(3); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + else if(m_was_inside_qmrk) + { + _RYML_CB_CHECK(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->indentation_eq()); + _c4dbgp("mapblck[RKCL]: missing :"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + m_was_inside_qmrk = false; + addrem_flags(RKEY, RKCL); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(RVAL)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + // + // handle indentation + // + if(m_evt_handler->m_curr->at_line_beginning()) + { + _c4dbgpf("mapblck[RVAL]: indref={} indentation={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->line_contents.indentation); + m_evt_handler->m_curr->more_indented = false; + if(m_evt_handler->m_curr->indref == npos) + { + _c4dbgpf("mapblck[RVAL]: setting indentation={}", m_evt_handler->m_parent->indref); + _set_indentation(m_evt_handler->m_curr->line_contents.indentation); + _line_progressed(m_evt_handler->m_curr->indref); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else if(m_evt_handler->m_curr->indentation_eq()) + { + _c4dbgp("mapblck[RVAL]: skip indentation!"); + _line_progressed(m_evt_handler->m_curr->indref); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + // TODO: this is valid: + // + // ```yaml + // a: + // b: + // --- + // a: + // b + // --- + // a: + // b: c + // ``` + // + // ... but this is not: + // + // ```yaml + // a: + // v + // --- + // a: b: c + // ``` + // + // here, we probably need to set a boolean on the state + // to disambiguate between these cases. + } + else if(m_evt_handler->m_curr->indentation_gt()) + { + _c4dbgp("mapblck[RVAL]: more indented!"); + m_evt_handler->m_curr->more_indented = true; + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("mapblck[RVAL]: smaller indentation!"); + _handle_indentation_pop_from_block_map(); + if(has_all(RMAP|BLCK)) + { + _c4dbgp("mapblck[RVAL]: still mapblck!"); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + if(has_any(RNXT)) + { + _c4dbgp("mapblck[RVAL]: speculatively expect next keyval"); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + } + goto mapblck_again; + } + else + { + _c4dbgp("mapblck[RVAL]: no longer mapblck!"); + goto mapblck_finish; + } + } + else if(m_evt_handler->m_curr->line_contents.indentation == npos) + { + _c4dbgp("mapblck[RVAL]: empty line!"); + _line_progressed(m_evt_handler->m_curr->line_contents.rem.len); + goto mapblck_again; + } + } + // + // now handle the tokens + // + const char first = rem.str[0]; + const size_t startline = m_evt_handler->m_curr->pos.line; + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(); + _c4dbgpf("mapblck[RVAL]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapblck[RVAL]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); // VAL! + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + if(startindent != m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: start new block map, set scalar as key"); + _handle_annotations_before_start_mapblck(startline); + addrem_flags(RNXT, RVAL); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); // KEY! + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + // keep the child state on RVAL + addrem_flags(RVAL, RNXT); + } + else + { + _c4dbgp("mapblck[RVAL]: prev val empty+this is a key"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); // KEY! + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + // keep going on RVAL + _maybe_skip_whitespace_tokens(); + } + } + } + else if(first == '"') + { + _c4dbgp("mapblck[RVAL]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); // VAL! + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + if(startindent != m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: start new block map, set scalar as key"); + _handle_annotations_before_start_mapblck(startline); + addrem_flags(RNXT, RVAL); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); // KEY! + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + // keep the child state on RVAL + addrem_flags(RVAL, RNXT); + } + else + { + _c4dbgp("mapblck[RVAL]: prev val empty+this is a key"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); // KEY! + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + // keep going on RVAL + _maybe_skip_whitespace_tokens(); + } + } + } + // block scalars can only appear as keys when in QMRK scope + // (ie, after ? tokens), so no need to scan following colon + else if(first == '|') + { + _c4dbgp("mapblck[RVAL]: scanning block-literal scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_literal(sb); + m_evt_handler->set_val_scalar_literal(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(first == '>') + { + _c4dbgp("mapblck[RVAL]: scanning block-folded scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_folded(sb); + m_evt_handler->set_val_scalar_folded(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else if(_scan_scalar_plain_map_blck(&sc)) + { + _c4dbgp("mapblck[RVAL]: plain scalar."); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[RVAL]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, m_evt_handler->m_curr->indref); // VAL! + m_evt_handler->set_val_scalar_plain(maybe_filtered); + addrem_flags(RNXT, RVAL); + } + else + { + if(startindent != m_evt_handler->m_curr->indref) + { + _c4dbgpf("mapblck[RVAL]: start new block map, set scalar as key {}", m_evt_handler->m_curr->indref); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); // KEY! + m_evt_handler->set_key_scalar_plain(maybe_filtered); + _maybe_skip_whitespace_tokens(); + // keep the child state on RVAL + addrem_flags(RVAL, RNXT); + } + else + { + _c4dbgp("mapblck[RVAL]: prev val empty+this is a key"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); // KEY! + m_evt_handler->set_key_scalar_plain(maybe_filtered); + // keep going on RVAL + _maybe_skip_whitespace_tokens(); + } + } + } + else if(first == '-') + { + if(rem.len == 1 || rem.str[1] == ' ' _RYML_WITH_TAB_TOKENS(|| rem.str[1] == '\t')) + { + _c4dbgp("mapblck[RVAL]: start val seqblck"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_block(); + addrem_flags(RSEQ|RVAL, RMAP|RNXT); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else if(m_evt_handler->m_curr->indref == 0 || m_evt_handler->m_curr->line_contents.indentation == 0 || _is_doc_begin_token(rem)) + { + _c4dbgp("mapblck[RVAL]: end+start doc"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, _is_doc_begin_token(rem)); + _start_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + else if(first == '[') + { + _c4dbgp("mapblck[RVAL]: start val seqflow"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RMAP|BLCK|RNXT); + _set_indentation(m_evt_handler->m_curr->indref + 1u); + _line_progressed(1); + goto mapblck_finish; + } + else if(first == '{') + { + _c4dbgp("mapblck[RVAL]: start val mapflow"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RKEY|FLOW, BLCK|RVAL|RNXT); + m_evt_handler->m_curr->scalar_col = m_evt_handler->m_curr->line_contents.indentation; + _set_indentation(m_evt_handler->m_curr->indref + 1u); + _line_progressed(1); + goto mapblck_finish; + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapblck[RVAL]: ref! [{}]~~~{}~~~", ref.len, ref); + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgpf("mapblck[RVAL]: same indentation {}", startindent); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + else + { + _c4dbgpf("mapblck[RVAL]: larger indentation {}>{}", startindent, m_evt_handler->m_curr->indref); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, startindent > m_evt_handler->m_curr->indref); + if(_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[RVAL]: start child map, block"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_block(); + m_evt_handler->set_key_ref(ref); + _set_indentation(startindent); + // keep going in RVAL + addrem_flags(RVAL, RNXT); + } + else + { + _c4dbgp("mapblck[RVAL]: was val ref"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_ref(ref); + addrem_flags(RNXT, RVAL); + } + } + _maybe_skip_whitespace_tokens(); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapblck[RVAL]: anchor! [{}]~~~{}~~~", anchor.len, anchor); + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: anchor for next key. val is missing!"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RVAL); + } + // we need to buffer the anchors, as there may be two + // consecutive anchors in here + _add_annotation(&m_pending_anchors, anchor, startindent, startline); + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapblck[RVAL]: tag! [{}]~~~{}~~~", tag.len, tag); + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: tag for next key. val is missing!"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RVAL); + } + // we need to buffer the tags, as there may be two + // consecutive tags in here + _add_annotation(&m_pending_tags, tag, startindent, startline); + } + else if(first == '?') + { + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: got '?'. val was empty"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + addrem_flags(QMRK, RVAL); + } + else if(startindent > m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: start val mapblck"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_block(); + addrem_flags(QMRK|BLCK, RNXT); + _set_indentation(startindent); + } + else + { + _c4err("parse error"); + } + m_was_inside_qmrk = true; + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto mapblck_again; + } + else if(first == ':') + { + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: got ':'. val was empty, next key as well"); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + m_evt_handler->set_key_scalar_plain_empty(); + } + else if(startindent > m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[RVAL]: start val mapblck"); + addrem_flags(RNXT, RVAL); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_plain_empty(); + // keep the child state on RVAL + addrem_flags(RVAL, RNXT); + } + else + { + _c4err("parse error"); + } + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + goto mapblck_again; + } + else if(first == '.') + { + _c4dbgp("mapblck[RVAL]: maybe doc?"); + csubstr rs = rem.sub(1); + if(rs == ".." || rs.begins_with(".. ")) + { + _c4dbgp("seqblck[RVAL]: end doc expl"); + _end_doc_suddenly(); + _line_progressed(3); + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + _RYML_WITH_TAB_TOKENS( + else if(first == '\t') + { + _c4dbgp("mapblck[RVAL]: skip tabs"); + _maybe_skipchars('\t'); + }) + else + { + _c4err("parse error"); + } + } + else if(has_any(RNXT)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(QMRK)); + // + // handle indentation + // + if(m_evt_handler->m_curr->at_line_beginning()) + { + _c4dbgpf("mapblck[RNXT]: indref={} indentation={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->line_contents.indentation); + if(m_evt_handler->m_curr->indentation_eq()) + { + _c4dbgpf("mapblck[RNXT]: skip {} from indref", m_evt_handler->m_curr->indref); + _line_progressed(m_evt_handler->m_curr->indref); + _c4dbgp("mapblck[RNXT]: speculatively expect next keyval"); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + goto mapblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("mapblck[RNXT]: smaller indentation!"); + _handle_indentation_pop_from_block_map(); + if(has_all(RMAP|BLCK)) + { + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + if(!has_any(RKCL)) + { + _c4dbgp("mapblck[RNXT]: speculatively expect next keyval"); + m_evt_handler->add_sibling(); + addrem_flags(RKEY, RNXT); + } + goto mapblck_again; + } + else + { + goto mapblck_finish; + } + } + } + else + { + _c4dbgp("mapblck[RNXT]: NOT at line begin"); + if(!rem.begins_with_any(" \t")) + { + _c4err("parse error"); + } + else + { + _skipchars(" \t"); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + { + _c4dbgp("seqblck[RNXT]: again"); + goto mapblck_again; + } + } + } + // + // handle tokens + // + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.len > 0); + const char first = rem.str[0]; + _c4dbgpf("mapblck[RNXT]: '{}'", _c4prc(first)); + if(first == ':') + { + if(m_evt_handler->m_curr->more_indented) + { + _c4dbgp("mapblck[RNXT]: start child block map"); + C4_NOT_IMPLEMENTED(); + //m_evt_handler->actually_as_block_map(); + _line_progressed(1); + _set_indentation(m_evt_handler->m_curr->scalar_col); + m_evt_handler->m_curr->more_indented = false; + goto mapblck_again; + } + else + { + _c4err("parse error"); + } + } + else if(first == ' ') + { + _c4dbgp("mapblck[RNXT]: skip spaces"); + _maybe_skip_whitespace_tokens(); + } + else + { + _c4err("parse error"); + } + } + else if(has_any(QMRK)) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKEY)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RKCL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RVAL)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT)); + // + // handle indentation + // + if(m_evt_handler->m_curr->at_line_beginning()) + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_curr->line_contents.indentation != npos); + if(m_evt_handler->m_curr->indentation_eq()) + { + _c4dbgpf("mapblck[QMRK]: skip {} from indref", m_evt_handler->m_curr->indref); + _line_progressed(m_evt_handler->m_curr->indref); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else if(m_evt_handler->m_curr->indentation_lt()) + { + _c4dbgp("mapblck[QMRK]: smaller indentation!"); + _handle_indentation_pop_from_block_map(); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + if(has_all(RMAP|BLCK)) + { + _c4dbgp("mapblck[QMRK]: still mapblck!"); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_any(QMRK)); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + else + { + _c4dbgp("mapblck[QMRK]: no longer mapblck!"); + goto mapblck_finish; + } + } + // indentation can be larger in QMRK state + else + { + _c4dbgp("mapblck[QMRK]: larger indentation !"); + _line_progressed(m_evt_handler->m_curr->line_contents.indentation); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + goto mapblck_again; + } + } + // + // now handle the tokens + // + const char first = rem.str[0]; + const size_t startline = m_evt_handler->m_curr->pos.line; + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(); + _c4dbgpf("mapblck[QMRK]: '{}'", first); + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("mapblck[QMRK]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); // KEY! + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[QMRK]: set as key"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else + { + _c4dbgp("mapblck[QMRK]: start new block map as key (!), set scalar as key"); + addrem_flags(RKCL, QMRK); + _handle_annotations_before_start_mapblck_as_key(); + m_evt_handler->begin_map_key_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + // keep the child state on RVAL + addrem_flags(RVAL, RKCL|QMRK); + } + } + else if(first == '"') + { + _c4dbgp("mapblck[QMRK]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); // KEY! + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[QMRK]: set as key"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else + { + _c4dbgp("mapblck[QMRK]: start new block map as key (!), set scalar as key"); + addrem_flags(RKCL, QMRK); + _handle_annotations_before_start_mapblck_as_key(); + m_evt_handler->begin_map_key_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + // keep the child state on RVAL + addrem_flags(RVAL, RKCL|QMRK); + } + } + else if(first == '|') + { + _c4dbgp("mapblck[QMRK]: scanning block-literal scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + csubstr maybe_filtered = _maybe_filter_key_scalar_literal(sb); // KEY! + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_literal(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else if(first == '>') + { + _c4dbgp("mapblck[QMRK]: scanning block-literal scalar"); + ScannedBlock sb; + _scan_block(&sb, m_evt_handler->m_curr->indref + 1); + csubstr maybe_filtered = _maybe_filter_key_scalar_folded(sb); // KEY! + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_folded(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else if(_scan_scalar_plain_map_blck(&sc)) + { + _c4dbgp("mapblck[QMRK]: plain scalar"); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, m_evt_handler->m_curr->indref); // KEY! + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[QMRK]: set as key"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + addrem_flags(RKCL, QMRK); + } + else + { + _c4dbgp("mapblck[QMRK]: start new block map as key (!), set scalar as key"); + addrem_flags(RKCL, QMRK); + _handle_annotations_before_start_mapblck_as_key(); + m_evt_handler->begin_map_key_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + // keep the child state on RVAL + addrem_flags(RVAL, RKCL|QMRK); + } + } + else if(first == ':') + { + if(startindent == m_evt_handler->m_curr->indref) + { + _c4dbgp("mapblck[QMRK]: empty key"); + addrem_flags(RVAL, QMRK); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_scalar_plain_empty(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else + { + _c4dbgp("mapblck[QMRK]: start new block map as key (!), empty key"); + addrem_flags(RKCL, QMRK); + _handle_annotations_before_start_mapblck_as_key(); + m_evt_handler->begin_map_key_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_plain_empty(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + // keep the child state on RVAL + addrem_flags(RVAL, RKCL|QMRK); + } + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("mapblck[QMRK]: key ref! [{}]~~~{}~~~", ref.len, ref); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("mapblck[QMRK]: set ref as key"); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->set_key_ref(ref); + addrem_flags(RKCL, QMRK); + } + else + { + _c4dbgp("mapblck[QMRK]: start new block map as key (!), set ref as key"); + addrem_flags(RKCL, QMRK); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->begin_map_key_block(); + m_evt_handler->set_key_ref(ref); + _set_indentation(startindent); + // keep the child state on RVAL + addrem_flags(RVAL, RKCL|QMRK); + } + _maybe_skip_whitespace_tokens(); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("mapblck[QMRK]: key anchor! [{}]~~~{}~~~", anchor.len, anchor); + _add_annotation(&m_pending_anchors, anchor, startindent, startline); + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("mapblck[QMRK]: key tag! [{}]~~~{}~~~", tag.len, tag); + _add_annotation(&m_pending_tags, tag, startindent, startline); + } + else if(first == '-') + { + _c4dbgp("mapblck[QMRK]: maybe doc?"); + csubstr rs = rem.sub(1); + if(rs == "--" || rs.begins_with("-- ")) + { + _c4dbgp("mapblck[QMRK]: end+start doc"); + _start_doc_suddenly(); + _line_progressed(3); + } + else + { + _c4dbgp("mapblck[QMRK]: start child seqblck (!)"); + addrem_flags(RKCL, RKEY|QMRK); + _handle_annotations_before_blck_key_scalar(); + m_evt_handler->begin_seq_key_block(); + addrem_flags(RVAL|RSEQ, RMAP|RKCL|QMRK); + _set_indentation(startindent); + _line_progressed(1); + } + _maybe_skip_whitespace_tokens(); + goto mapblck_finish; + } + else if(first == '[') + { + _c4dbgp("mapblck[QMRK]: start child seqflow (!)"); + addrem_flags(RKCL, RKEY|QMRK); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RVAL|RSEQ|FLOW, RMAP|RKCL|QMRK|BLCK); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto mapblck_finish; + } + else if(first == '{') + { + _c4dbgp("mapblck[QMRK]: start child mapblck (!)"); + addrem_flags(RKCL, RKEY|QMRK); + m_evt_handler->begin_map_key_flow(); + addrem_flags(RKEY|FLOW, RVAL|RKCL|QMRK|BLCK); + _set_indentation(m_evt_handler->m_parent->indref); + _line_progressed(1); + goto mapblck_finish; + } + else if(first == '?') + { + _c4dbgp("mapblck[QMRK]: another QMRK '?'"); + m_evt_handler->set_key_scalar_plain_empty(); + m_evt_handler->set_val_scalar_plain_empty(); + m_evt_handler->add_sibling(); + _line_progressed(1); + } + else if(first == '.') + { + _c4dbgp("mapblck[QMRK]: maybe end doc?"); + csubstr rs = rem.sub(1); + if(rs == ".." || rs.begins_with(".. ")) + { + _c4dbgp("mapblck[QMRK]: end+start doc"); + _end_doc_suddenly(); + _line_progressed(3); + goto mapblck_finish; + } + else + { + _c4err("parse error"); + } + } + else + { + _c4err("parse error"); + } + } + + mapblck_again: + _c4dbgt("mapblck: again", 0); + if(_finished_line()) + { + _line_ended(); + _scan_line(); + if(_finished_file()) + { + _c4dbgp("mapblck: file finished!"); + _end_map_blck(); + goto mapblck_finish; + } + _c4dbgnextline(); + } + goto mapblck_start; + + mapblck_finish: + _c4dbgp("mapblck: finish"); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_unk_json() +{ + _c4dbgpf("handle_unk_json indref={} target={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->node_id); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT|RSEQ|RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RTOP)); + + _maybe_skip_comment(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + + size_t pos = rem.first_not_of(" \t"); + if(pos) + { + pos = pos != npos ? pos : rem.len; + _c4dbgpf("skipping indentation of {}", pos); + _line_progressed(pos); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + _c4dbgpf("rem is now [{}]~~~{}~~~", rem.len, rem); + } + + if(rem.begins_with('[')) + { + _c4dbgp("it's a seq"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RUNK|RTOP|RDOC); + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + m_doc_empty = false; + _line_progressed(1); + } + else if(rem.begins_with('{')) + { + _c4dbgp("it's a map"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|FLOW|RKEY, RVAL|RTOP|RUNK|RDOC); + m_doc_empty = false; + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + _line_progressed(1); + } + else if(_handle_bom()) + { + _c4dbgp("byte order mark"); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(SSCL)); + _maybe_skip_whitespace_tokens(); + csubstr s = m_evt_handler->m_curr->line_contents.rem; + if(!s.len) + return; + const size_t startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const char first = s.str[0]; + ScannedScalar sc; + if(first == '"') + { + _c4dbgp("runk_json: scanning double-quoted scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + sc = _scan_scalar_dquot(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk_json: set as val"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + } + else + { + _c4err("parse error"); + } + } + else if(_scan_scalar_plain_unk(&sc)) + { + _c4dbgp("runk_json: got a plain scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk_json: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, startindent); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + } + else + { + _c4err("parse error"); + } + } + else + { + _c4err("parse error"); + } + } +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::_handle_unk() +{ + _c4dbgpf("handle_unk indref={} target={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->node_id); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(RNXT|RSEQ|RMAP)); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RTOP)); + + _maybe_skip_comment(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + + size_t pos = rem.first_not_of(" \t"); + if(pos) + { + pos = pos != npos ? pos : rem.len; + _c4dbgpf("skipping {} whitespace characters", pos); + _line_progressed(pos); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + _c4dbgpf("rem is now [{}]~~~{}~~~", rem.len, rem); + } + + if(m_evt_handler->m_curr->line_contents.indentation == 0u && _at_line_begin()) + { + _c4dbgp("rtop: zero indent + at line begin"); + if(_handle_bom()) + { + _c4dbgp("byte order mark!"); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + } + const char first = rem.str[0]; + if(first == '-') + { + _c4dbgp("rtop: suspecting doc"); + if(_is_doc_begin_token(rem)) + { + _c4dbgp("rtop: begin doc"); + _maybe_end_doc(); + _begin2_doc_expl(); + _set_indentation(0); + addrem_flags(RDOC|RUNK, NDOC); + _line_progressed(3u); + _maybe_skip_whitespace_tokens(); + return; + } + } + else if(first == '.') + { + _c4dbgp("rtop: suspecting doc end"); + if(_is_doc_end_token(rem)) + { + _c4dbgp("rtop: end doc"); + if(has_any(RDOC)) + { + _end2_doc_expl(); + } + else + { + _c4dbgp("rtop: ignore end doc"); + } + addrem_flags(NDOC|RUNK, RDOC); + _line_progressed(3u); + _maybe_skip_whitespace_tokens(); + return; + } + } + else if(first == '%') + { + _c4dbgpf("directive: {}", rem); + if(C4_UNLIKELY(!m_doc_empty && has_none(NDOC))) + _RYML_CB_ERR(m_evt_handler->m_stack.m_callbacks, "need document footer before directives"); + _handle_directive(rem); + return; + } + } + + /* no else-if! */ + char first = rem.str[0]; + + if(first == '[') + { + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + m_doc_empty = false; + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(rem); + if(C4_LIKELY( ! _annotations_require_key_container())) + { + _c4dbgp("it's a seq, flow"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RUNK|RTOP|RDOC); + _set_indentation(startindent); + } + else + { + _c4dbgp("start new block map, set flow seq as key (!)"); + _handle_annotations_before_start_mapblck(m_evt_handler->m_curr->pos.line); + m_evt_handler->begin_map_val_block(); + addrem_flags(RMAP|BLCK|RKCL, RUNK|RTOP|RDOC); + _handle_annotations_and_indentation_after_start_mapblck(startindent, m_evt_handler->m_curr->pos.line); + m_evt_handler->begin_seq_key_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RMAP|BLCK|RKCL); + _set_indentation(startindent); + } + _line_progressed(1); + } + else if(first == '{') + { + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + m_doc_empty = false; + const size_t startindent = m_evt_handler->m_curr->line_contents.current_col(rem); + if(C4_LIKELY( ! _annotations_require_key_container())) + { + _c4dbgp("it's a map, flow"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|FLOW|RKEY, RVAL|RTOP|RUNK|RDOC); + _set_indentation(startindent); + } + else + { + _c4dbgp("start new block map, set flow map as key (!)"); + _handle_annotations_before_start_mapblck(m_evt_handler->m_curr->pos.line); + m_evt_handler->begin_map_val_block(); + addrem_flags(RMAP|BLCK|RKCL, RUNK|RTOP|RDOC); + _handle_annotations_and_indentation_after_start_mapblck(startindent, m_evt_handler->m_curr->pos.line); + m_evt_handler->begin_map_key_flow(); + addrem_flags(RMAP|FLOW|RKEY, BLCK|RKCL); + _set_indentation(startindent); + } + _line_progressed(1); + } + else if(first == '-' && _is_blck_token(rem)) + { + _c4dbgp("it's a seq, block"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_block(); + addrem_flags(RSEQ|BLCK|RVAL, RNXT|RTOP|RUNK|RDOC); + m_doc_empty = false; + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '?' && _is_blck_token(rem)) + { + _c4dbgp("it's a map + this key is complex"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_block(); + addrem_flags(RMAP|BLCK|QMRK, RKEY|RVAL|RTOP|RUNK); + m_doc_empty = false; + m_was_inside_qmrk = true; + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == ':' && _is_blck_token(rem)) + { + if(m_doc_empty) + { + _c4dbgp("it's a map with an empty key"); + const size_t startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const size_t startline = m_evt_handler->m_curr->pos.line; // save + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_scalar_plain_empty(); + m_doc_empty = false; + _set_indentation(startindent); + } + else + { + _c4dbgp("actually prev val is a key!"); + size_t prev_indentation = m_evt_handler->m_curr->indref; + m_evt_handler->actually_val_is_first_key_of_new_map_block(); + _set_indentation(prev_indentation); + } + addrem_flags(RMAP|BLCK|RVAL, RTOP|RUNK|RDOC); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("anchor! [{}]~~~{}~~~", anchor.len, anchor); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_anchors, anchor, indentation, line); + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + m_doc_empty = false; + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("ref! [{}]~~~{}~~~", ref.len, ref); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + m_doc_empty = false; + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk: set val ref"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_ref(ref); + } + else + { + _c4dbgp("runk: start new block map, set ref as key"); + const size_t startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const size_t startline = m_evt_handler->m_curr->pos.line; // save + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_ref(ref); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RTOP|RUNK|RDOC); + } + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("unk: val tag! [{}]~~~{}~~~", tag.len, tag); + // we need to buffer the tags, as there may be two + // consecutive tags in here + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_tags, tag, indentation, line); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(SSCL)); + _maybe_skip_whitespace_tokens(); + csubstr s = m_evt_handler->m_curr->line_contents.rem; + if(!s.len) + return; + const size_t startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const size_t startline = m_evt_handler->m_curr->pos.line; // save + first = s.str[0]; + ScannedScalar sc; + if(first == '\'') + { + _c4dbgp("runk: scanning single-quoted scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + sc = _scan_scalar_squot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + } + else + { + _c4dbgp("runk: start new block map, set scalar as key"); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RTOP|RUNK|RDOC); + } + } + else if(first == '"') + { + _c4dbgp("runk: scanning double-quoted scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + sc = _scan_scalar_dquot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + } + else + { + _c4dbgp("runk: start new block map, set double-quoted scalar as key"); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_colon(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RTOP|RUNK|RDOC); + } + } + else if(first == '|') + { + _c4dbgp("runk: scanning block-literal scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + ScannedBlock sb; + _scan_block(&sb, startindent); + if(C4_LIKELY(!_maybe_scan_following_colon())) + { + _c4dbgp("runk: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_literal(sb); + m_evt_handler->set_val_scalar_literal(maybe_filtered); + } + else + { + _c4err("block literal keys must be enclosed in '?'"); + } + } + else if(first == '>') + { + _c4dbgp("runk: scanning block-folded scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + ScannedBlock sb; + _scan_block(&sb, startindent); + if(C4_LIKELY(!_maybe_scan_following_colon())) + { + _c4dbgp("runk: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_folded(sb); + m_evt_handler->set_val_scalar_folded(maybe_filtered); + } + else + { + _c4err("block folded keys must be enclosed in '?'"); + } + } + else if(_scan_scalar_plain_unk(&sc)) + { + _c4dbgp("runk: got a plain scalar"); + m_evt_handler->check_trailing_doc_token(); + _maybe_begin_doc(); + add_flags(RDOC); + m_doc_empty = false; + if(!_maybe_scan_following_colon()) + { + _c4dbgp("runk: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, startindent); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + } + else + { + _c4dbgp("runk: start new block map, set scalar as key"); + _handle_annotations_before_start_mapblck(startline); + _handle_colon(); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, startindent); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RTOP|RUNK|RDOC); + } + } + } +} + + +//----------------------------------------------------------------------------- + +template +C4_COLD void ParseEngine::_handle_usty() +{ + _c4dbgpf("handle_usty target={}", m_evt_handler->m_curr->indref, m_evt_handler->m_curr->node_id); + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_none(BLCK|FLOW)); + + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + if(has_any(RNXT)) + { + _c4dbgp("usty[RNXT]: finishing!"); + _end_stream(); + } + #endif + + _maybe_skip_comment(); + csubstr rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + + size_t pos = rem.first_not_of(" \t"); + if(pos) + { + pos = pos != npos ? pos : rem.len; + _c4dbgpf("skipping indentation of {}", pos); + _line_progressed(pos); + rem = m_evt_handler->m_curr->line_contents.rem; + if(!rem.len) + return; + _c4dbgpf("rem is now [{}]~~~{}~~~", rem.len, rem); + } + + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, rem.len > 0); + size_t startindent = m_evt_handler->m_curr->line_contents.indentation; // save + char first = rem.str[0]; + if(has_any(RSEQ)) // destination is a sequence + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(RMAP)); + _c4dbgpf("usty[RSEQ]: first='{}'", _c4prc(first)); + if(first == '[') + { + _c4dbgp("usty[RSEQ]: it's a flow seq. merging it"); + add_flags(RNXT); + m_evt_handler->_push(); + addrem_flags(FLOW|RVAL, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '-' && _is_blck_token(rem)) + { + _c4dbgp("usty[RSEQ]: it's a block seq. merging it"); + add_flags(RNXT); + m_evt_handler->_push(); + addrem_flags(BLCK|RVAL, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else + { + _c4err("can only parse a seq into an existing seq"); + } + } + else if(has_any(RMAP)) // destination is a map + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(RSEQ)); + _c4dbgpf("usty[RMAP]: first='{}'", _c4prc(first)); + if(first == '{') + { + _c4dbgp("usty[RMAP]: it's a flow map. merging it"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->_push(); + addrem_flags(RMAP|FLOW|RKEY, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '?' && _is_blck_token(rem)) + { + _c4dbgp("usty[RMAP]: it's a block map + this key is complex"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->_push(); + addrem_flags(RMAP|BLCK|QMRK, RNXT|USTY); + m_was_inside_qmrk = true; + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == ':' && _is_blck_token(rem)) + { + _c4dbgp("usty[RMAP]: it's a map with an empty key"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->_push(); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(rem.begins_with('&')) + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("usty[RMAP]: anchor! [{}]~~~{}~~~", anchor.len, anchor); + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_anchors, anchor, indentation, line); + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("usty[RMAP]: ref! [{}]~~~{}~~~", ref.len, ref); + if(!_maybe_scan_following_colon()) + { + _c4err("cannot read a VAL to a map"); + } + else + { + _c4dbgp("usty[RMAP]: start new block map, set ref as key"); + const size_t startline = m_evt_handler->m_curr->pos.line; // save + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->_push(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_ref(ref); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + } + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("usty[RMAP]: val tag! [{}]~~~{}~~~", tag.len, tag); + // we need to buffer the tags, as there may be two + // consecutive tags in here + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_tags, tag, indentation, line); + } + else if(first == '[' || (first == '-' && _is_blck_token(rem))) + { + _c4err("cannot parse a seq into an existing map"); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(SSCL)); + startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const size_t startline = m_evt_handler->m_curr->pos.line; // save + ScannedScalar sc; + _c4dbgpf("usty[RMAP]: maybe scalar. first='{}'", _c4prc(first)); + if(first == '\'') + { + _c4dbgp("usty[RMAP]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + if(!_maybe_scan_following_colon()) + { + _c4err("cannot read a VAL to a map"); + } + else + { + _c4dbgp("usty[RMAP]: start new block map, set scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->_push(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else if(first == '"') + { + _c4dbgp("usty[RMAP]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + if(!_maybe_scan_following_colon()) + { + _c4err("cannot read a VAL to a map"); + } + else + { + _c4dbgp("usty[RMAP]: start new block map, set double-quoted scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->_push(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else if(first == '|') + { + _c4err("block literal keys must be enclosed in '?'"); + } + else if(first == '>') + { + _c4err("block literal keys must be enclosed in '?'"); + } + else if(_scan_scalar_plain_unk(&sc)) + { + _c4dbgp("usty[RMAP]: got a plain scalar"); + if(!_maybe_scan_following_colon()) + { + _c4err("cannot read a VAL to a map"); + } + else + { + _c4dbgp("usty[RMAP]: start new block map, set scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->_push(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, startindent); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else + { + _c4err("parse error"); + } + } + } + else // destination is unknown + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(RSEQ)); + _c4dbgpf("usty[UNK]: first='{}'", _c4prc(first)); + if(first == '[') + { + _c4dbgp("usty[UNK]: it's a flow seq"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_flow(); + addrem_flags(RSEQ|FLOW|RVAL, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '-' && _is_blck_token(rem)) + { + _c4dbgp("usty[UNK]: it's a block seq"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_seq_val_block(); + addrem_flags(RSEQ|BLCK|RVAL, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '{') + { + _c4dbgp("usty[UNK]: it's a flow map"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_flow(); + addrem_flags(RMAP|FLOW|RKEY, RNXT|USTY); + _set_indentation(startindent); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '?' && _is_blck_token(rem)) + { + _c4dbgp("usty[UNK]: it's a map + this key is complex"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_block(); + addrem_flags(RMAP|BLCK|QMRK, RNXT|USTY); + m_was_inside_qmrk = true; + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == ':' && _is_blck_token(rem)) + { + _c4dbgp("usty[UNK]: it's a map with an empty key"); + add_flags(RNXT); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->begin_map_val_block(); + m_evt_handler->set_key_scalar_plain_empty(); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _save_indentation(); + _line_progressed(1); + _maybe_skip_whitespace_tokens(); + } + else if(first == '&') + { + csubstr anchor = _scan_anchor(); + _c4dbgpf("usty[UNK]: anchor! [{}]~~~{}~~~", anchor.len, anchor); + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_anchors, anchor, indentation, line); + _set_indentation(m_evt_handler->m_curr->line_contents.current_col(rem)); + } + else if(first == '*') + { + csubstr ref = _scan_ref_map(); + _c4dbgpf("usty[UNK]: ref! [{}]~~~{}~~~", ref.len, ref); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("usty[UNK]: set val ref"); + _handle_annotations_before_blck_val_scalar(); + m_evt_handler->set_val_ref(ref); + } + else + { + _c4dbgp("usty[UNK]: start new block map, set ref as key"); + const size_t startline = m_evt_handler->m_curr->pos.line; // save + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + m_evt_handler->set_key_ref(ref); + _maybe_skip_whitespace_tokens(); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + } + } + else if(first == '!') + { + csubstr tag = _scan_tag(); + _c4dbgpf("usty[UNK]: val tag! [{}]~~~{}~~~", tag.len, tag); + // we need to buffer the tags, as there may be two + // consecutive tags in here + const size_t indentation = m_evt_handler->m_curr->line_contents.current_col(rem); + const size_t line = m_evt_handler->m_curr->pos.line; + _add_annotation(&m_pending_tags, tag, indentation, line); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! has_any(SSCL)); + startindent = m_evt_handler->m_curr->line_contents.indentation; // save + const size_t startline = m_evt_handler->m_curr->pos.line; // save + first = rem.str[0]; + ScannedScalar sc; + _c4dbgpf("usty[UNK]: maybe scalar. first='{}'", _c4prc(first)); + if(first == '\'') + { + _c4dbgp("usty[UNK]: scanning single-quoted scalar"); + sc = _scan_scalar_squot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("usty[UNK]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_squot(sc); + m_evt_handler->set_val_scalar_squoted(maybe_filtered); + _end_stream(); + } + else + { + _c4dbgp("usty[UNK]: start new block map, set scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_squot(sc); + m_evt_handler->set_key_scalar_squoted(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else if(first == '"') + { + _c4dbgp("usty[UNK]: scanning double-quoted scalar"); + sc = _scan_scalar_dquot(); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("usty[UNK]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_dquot(sc); + m_evt_handler->set_val_scalar_dquoted(maybe_filtered); + _end_stream(); + } + else + { + _c4dbgp("usty[UNK]: start new block map, set double-quoted scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_dquot(sc); + m_evt_handler->set_key_scalar_dquoted(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else if(first == '|') + { + _c4dbgp("usty[UNK]: scanning block-literal scalar"); + ScannedBlock sb; + _scan_block(&sb, startindent); + _c4dbgp("usty[UNK]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_literal(sb); + m_evt_handler->set_val_scalar_literal(maybe_filtered); + _end_stream(); + } + else if(first == '>') + { + _c4dbgp("usty[UNK]: scanning block-folded scalar"); + ScannedBlock sb; + _scan_block(&sb, startindent); + _c4dbgp("usty[UNK]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_folded(sb); + m_evt_handler->set_val_scalar_folded(maybe_filtered); + _end_stream(); + } + else if(_scan_scalar_plain_unk(&sc)) + { + _c4dbgp("usty[UNK]: got a plain scalar"); + if(!_maybe_scan_following_colon()) + { + _c4dbgp("usty[UNK]: set as val"); + _handle_annotations_before_blck_val_scalar(); + csubstr maybe_filtered = _maybe_filter_val_scalar_plain(sc, startindent); + m_evt_handler->set_val_scalar_plain(maybe_filtered); + _end_stream(); + } + else + { + _c4dbgp("usty[UNK]: start new block map, set scalar as key"); + add_flags(RNXT); + _handle_annotations_before_start_mapblck(startline); + m_evt_handler->begin_map_val_block(); + _handle_annotations_and_indentation_after_start_mapblck(startindent, startline); + csubstr maybe_filtered = _maybe_filter_key_scalar_plain(sc, startindent); + m_evt_handler->set_key_scalar_plain(maybe_filtered); + _set_indentation(startindent); + addrem_flags(RMAP|BLCK|RVAL, RNXT|USTY); + _maybe_skip_whitespace_tokens(); + } + } + else + { + _c4err("parse error"); + } + } + } +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::parse_json_in_place_ev(csubstr filename, substr src) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_stack.size() >= 1); + m_file = filename; + m_buf = src; + _reset(); + m_evt_handler->start_parse(filename.str, &_s_relocate_arena, this); + m_evt_handler->begin_stream(); + while( ! _finished_file()) + { + _scan_line(); + while( ! _finished_line()) + { + _c4dbgnextline(); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! m_evt_handler->m_curr->line_contents.rem.empty()); + if(has_any(RSEQ)) + { + _handle_seq_json(); + } + else if(has_any(RMAP)) + { + _handle_map_json(); + } + else if(has_any(RUNK)) + { + _handle_unk_json(); + } + else + { + _c4err("internal error"); + } + } + if(_finished_file()) + break; // it may have finished because of multiline blocks + _line_ended(); + } + _end_stream(); + m_evt_handler->finish_parse(); +} + + +//----------------------------------------------------------------------------- + +template +void ParseEngine::parse_in_place_ev(csubstr filename, substr src) +{ + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, m_evt_handler->m_stack.size() >= 1); + m_file = filename; + m_buf = src; + _reset(); + m_evt_handler->start_parse(filename.str, &_s_relocate_arena, this); + m_evt_handler->begin_stream(); + while( ! _finished_file()) + { + _scan_line(); + while( ! _finished_line()) + { + _c4dbgnextline(); + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, ! m_evt_handler->m_curr->line_contents.rem.empty()); + if(has_any(FLOW)) + { + if(has_none(RSEQIMAP)) + { + if(has_any(RSEQ)) + { + _handle_seq_flow(); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RMAP)); + _handle_map_flow(); + } + } + else + { + _handle_seq_imap(); + } + } + else if(has_any(BLCK)) + { + if(has_any(RSEQ)) + { + _handle_seq_block(); + } + else + { + _RYML_CB_ASSERT(m_evt_handler->m_stack.m_callbacks, has_all(RMAP)); + _handle_map_block(); + } + } + else if(has_any(RUNK)) + { + _handle_unk(); + } + else if(has_any(USTY)) + { + _handle_usty(); + } + else + { + _c4err("internal error"); + } + } + if(_finished_file()) + break; // it may have finished because of multiline blocks + _line_ended(); + } + _end_stream(); + m_evt_handler->finish_parse(); +} +/** @endcond */ + +} // namespace yml +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise,cppcoreguidelines-avoid-goto,hicpp-avoid-goto,hicpp-multiway-paths-covered) + +#undef _c4dbgnextline + +#if defined(_MSC_VER) +# pragma warning(pop) +#elif defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif // _C4_YML_PARSE_ENGINE_DEF_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/parse_engine.hpp b/3rdparty/rapidyaml/include/c4/yml/parse_engine.hpp new file mode 100644 index 0000000000..656b87ea5d --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/parse_engine.hpp @@ -0,0 +1,799 @@ +#ifndef _C4_YML_PARSE_ENGINE_HPP_ +#define _C4_YML_PARSE_ENGINE_HPP_ + +#ifndef _C4_YML_PARSER_STATE_HPP_ +#include "c4/yml/parser_state.hpp" +#endif + + +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable: 4251/*needs to have dll-interface to be used by clients of struct*/) +#endif + +// NOLINTBEGIN(hicpp-signed-bitwise) + +namespace c4 { +namespace yml { + +/** @addtogroup doc_parse + * @{ */ + +/** @defgroup doc_event_handlers Event Handlers + * + * @brief rapidyaml implements its parsing logic with a two-level + * model, where a @ref ParseEngine object reads through the YAML + * source, and dispatches events to an EventHandler bound to the @ref + * ParseEngine. Because @ref ParseEngine is templated on the event + * handler, the binding uses static polymorphism, without any virtual + * functions. The actual handler object can be changed at run time, + * (but of course needs to be the type of the template parameter). + * This is thus a very efficient architecture, and further enables the + * user to provide his own custom handler if he wishes to bypass the + * rapidyaml @ref Tree. + * + * There are two handlers implemented in this project: + * + * - @ref EventHandlerTree is the handler responsible for creating the + * ryml @ref Tree + * + * - @ref extra::EventHandlerInts parses YAML into an integer array + representation of the tree and scalars. + * + * - @ref extra::EventHandlerTestSuite is the handler responsible for emitting + * standardized [YAML test suite + * events](https://github.com/yaml/yaml-test-suite), used (only) in + * the CI of this project. + * + * + * ### Event model + * + * The event model used by the parse engine and event handlers follows + * very closely the event model in the [YAML test + * suite](https://github.com/yaml/yaml-test-suite). + * + * Consider for example this YAML, + * ```yaml + * {foo: bar,foo2: bar2} + * ``` + * which would produce these events in the test-suite parlance: + * ``` + * +STR + * +DOC + * +MAP {} + * =VAL :foo + * =VAL :bar + * =VAL :foo2 + * =VAL :bar2 + * -MAP + * -DOC + * -STR + * ``` + * + * For reference, the @ref ParseEngine object will produce this + * sequence of calls to its bound EventHandler: + * ```cpp + * handler.begin_stream(); + * handler.begin_doc(); + * handler.begin_map_val_flow(); + * handler.set_key_scalar_plain("foo"); + * handler.set_val_scalar_plain("bar"); + * handler.add_sibling(); + * handler.set_key_scalar_plain("foo2"); + * handler.set_val_scalar_plain("bar2"); + * handler.end_map(); + * handler.end_doc(); + * handler.end_stream(); + * ``` + * + * For many other examples of all areas of YAML and how ryml's parse + * model corresponds to the YAML standard model, refer to the [unit + * tests for the parse + * engine](https://github.com/biojppm/rapidyaml/tree/master/test/test_parse_engine.cpp). + * + * + * ### Special events + * + * Most of the parsing events adopted by rapidyaml in its event model + * are fairly obvious, but there are two less-obvious events requiring + * some explanation. + * + * These events exist to make it easier to parse some special YAML + * cases. They are called by the parser when a just-handled + * value/container is actually the first key of a new map: + * + * - `actually_val_is_first_key_of_new_map_flow()` (@ref EventHandlerTree::actually_val_is_first_key_of_new_map_flow() "see implementation in EventHandlerTree" / @ref EventHandlerTestSuite::actually_val_is_first_key_of_new_map_flow() "see implementation in EventHandlerTestSuite") + * - `actually_val_is_first_key_of_new_map_block()` (@ref EventHandlerTree::actually_val_is_first_key_of_new_map_block() "see implementation in EventHandlerTree" / @ref EventHandlerTestSuite::actually_val_is_first_key_of_new_map_block() "see implementation in EventHandlerTestSuite") + * + * For example, consider an implicit map inside a seq: `[a: b, c: + * d]` which is parsed as `[{a: b}, {c: d}]`. The standard event + * sequence for this YAML would be the following: + * ```cpp + * handler.begin_seq_val_flow(); + * handler.begin_map_val_flow(); + * handler.set_key_scalar_plain("a"); + * handler.set_val_scalar_plain("b"); + * handler.end_map(); + * handler.add_sibling(); + * handler.begin_map_val_flow(); + * handler.set_key_scalar_plain("c"); + * handler.set_val_scalar_plain("d"); + * handler.end_map(); + * handler.end_seq(); + * ``` + * The problem with this event sequence is that it forces the + * parser to delay setting the val scalar (in this case "a" and + * "c") until it knows whether the scalar is a key or a val. This + * would require the parser to store the scalar until this + * time. For instance, in the example above, the parser should + * delay setting "a" and "c", because they are in fact keys and + * not vals. Until then, the parser would have to store "a" and + * "c" in its internal state. The downside is that this complexity + * cost would apply even if there is no implicit map -- every val + * in a seq would have to be delayed until one of the + * disambiguating subsequent tokens `,-]:` is found. + * By calling this function, the parser can avoid this complexity, + * by preemptively setting the scalar as a val. Then a call to + * this function will create the map and rearrange the scalar as + * key. Now the cost applies only once: when a seqimap starts. So + * the following (easier and cheaper) event sequence below has the + * same effect as the event sequence above: + * ```cpp + * handler.begin_seq_val_flow(); + * handler.set_val_scalar_plain("notmap"); + * handler.set_val_scalar_plain("a"); // preemptively set "a" as val! + * handler.actually_as_new_map_key(); // create a map, move the "a" val as the key of the first child of the new map + * handler.set_val_scalar_plain("b"); // now "a" is a key and "b" the val + * handler.end_map(); + * handler.set_val_scalar_plain("c"); // "c" also as val! + * handler.actually_as_block_flow(); // likewise + * handler.set_val_scalar_plain("d"); // now "c" is a key and "b" the val + * handler.end_map(); + * handler.end_seq(); + * ``` + * This also applies to container keys (although ryml's tree + * cannot accomodate these): the parser can preemptively set a + * container as a val, and call this event to turn that container + * into a key. For example, consider this yaml: + * ```yaml + * [aa, bb]: [cc, dd] + * # ^ ^ ^ + * # | | | + * # (2) (1) (3) <- event sequence + * ``` + * The standard event sequence for this YAML would be the + * following: + * ```cpp + * handler.begin_map_val_block(); // (1) + * handler.begin_seq_key_flow(); // (2) + * handler.set_val_scalar_plain("aa"); + * handler.add_sibling(); + * handler.set_val_scalar_plain("bb"); + * handler.end_seq(); + * handler.begin_seq_val_flow(); // (3) + * handler.set_val_scalar_plain("cc"); + * handler.add_sibling(); + * handler.set_val_scalar_plain("dd"); + * handler.end_seq(); + * handler.end_map(); + * ``` + * The problem with the sequence above is that, reading from + * left-to-right, the parser can only detect the proper calls at + * (1) and (2) once it reaches (1) in the YAML source. So, the + * parser would have to buffer the entire event sequence starting + * from the beginning until it reaches (1). Using this function, + * the parser can do instead: + * ```cpp + * handler.begin_seq_val_flow(); // (2) -- preemptively as val! + * handler.set_val_scalar_plain("aa"); + * handler.add_sibling(); + * handler.set_val_scalar_plain("bb"); + * handler.end_seq(); + * handler.actually_as_new_map_key(); // (1) -- adjust when finding that the prev val was actually a key. + * handler.begin_seq_val_flow(); // (3) -- go on as before + * handler.set_val_scalar_plain("cc"); + * handler.add_sibling(); + * handler.set_val_scalar_plain("dd"); + * handler.end_seq(); + * handler.end_map(); + * ``` + */ + +class Tree; +class NodeRef; +class ConstNodeRef; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** Options to give to the parser to control its behavior. */ +struct RYML_EXPORT ParserOptions +{ +private: + + typedef enum : uint32_t { + SCALAR_FILTERING = (1u << 0u), + LOCATIONS = (1u << 1u), + DEFAULTS = SCALAR_FILTERING, + } Flags_e; + + uint32_t flags = DEFAULTS; + +public: + + ParserOptions() = default; + +public: + + /** @name source location tracking */ + /** @{ */ + + /** enable/disable source location tracking */ + ParserOptions& locations(bool enabled) noexcept + { + if(enabled) + flags |= LOCATIONS; + else + flags &= ~LOCATIONS; + return *this; + } + /** query source location tracking status */ + C4_ALWAYS_INLINE bool locations() const noexcept { return (flags & LOCATIONS); } + + /** @} */ + +public: + + /** @name scalar filtering status (experimental; disable at your discretion) */ + /** @{ */ + + /** enable/disable scalar filtering while parsing */ + ParserOptions& scalar_filtering(bool enabled) noexcept + { + if(enabled) + flags |= SCALAR_FILTERING; + else + flags &= ~SCALAR_FILTERING; + return *this; + } + /** query scalar filtering status */ + C4_ALWAYS_INLINE bool scalar_filtering() const noexcept { return (flags & SCALAR_FILTERING); } + + /** @} */ +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** This is the main driver of parsing logic: it scans the YAML or + * JSON source for tokens, and emits the appropriate sequence of + * parsing events to its event handler. The parse engine itself has no + * special limitations, and *can* accomodate containers as keys; it is the + * event handler may introduce additional constraints. + * + * There are two implemented handlers (see @ref doc_event_handlers, + * which has important notes about the event model): + * + * - @ref EventHandlerTree is the handler responsible for creating the + * ryml @ref Tree + * + * - @ref extra::EventHandlerTestSuite is a handler responsible for emitting + * standardized [YAML test suite + * events](https://github.com/yaml/yaml-test-suite), used (only) in + * the CI of this project. This is not part of the library and is + * not installed. + * + * - @ref extra::EventHandlerInts is the handler responsible for + * emitting integer-coded events. It is intended for implementing + * fully-conformant parsing in other programming languages + * (integration is currently under work for + * [YamlScript](https://github.com/yaml/yamlscript) and + * [go-yaml](https://github.com/yaml/go-yaml/)). It is not part of + * the library and is not installed. + * + */ +template +class ParseEngine +{ +public: + + using handler_type = EventHandler; + +public: + + /** @name construction and assignment */ + /** @{ */ + + ParseEngine(EventHandler *evt_handler, ParserOptions opts={}); + ~ParseEngine(); + + ParseEngine(ParseEngine &&) noexcept; + ParseEngine(ParseEngine const&); + ParseEngine& operator=(ParseEngine &&) noexcept; + ParseEngine& operator=(ParseEngine const&); + + /** @} */ + +public: + + /** @name modifiers */ + /** @{ */ + + /** Reserve a certain capacity for the parsing stack. + * This should be larger than the expected depth of the parsed + * YAML tree. + * + * The parsing stack is the only (potential) heap memory used + * directly by the parser. + * + * If the requested capacity is below the default + * stack size of 16, the memory is used directly in the parser + * object; otherwise it will be allocated from the heap. + * + * @note this reserves memory only for the parser itself; all the + * allocations for the parsed tree will go through the tree's + * allocator (when different). + * + * @note for maximum efficiency, the tree and the arena can (and + * should) also be reserved. */ + void reserve_stack(id_type capacity) + { + m_evt_handler->m_stack.reserve(capacity); + } + + /** Reserve a certain capacity for the array used to track node + * locations in the source buffer. */ + void reserve_locations(size_t num_source_lines) + { + _resize_locations(num_source_lines); + } + + RYML_DEPRECATED("filter arena no longer needed") + void reserve_filter_arena(size_t) {} + + /** @} */ + +public: + + /** @name getters */ + /** @{ */ + + /** Get the options used to build this parser object. */ + ParserOptions const& options() const { return m_options; } + + /** Get the current callbacks in the parser. */ + Callbacks const& callbacks() const { RYML_ASSERT(m_evt_handler); return m_evt_handler->m_stack.m_callbacks; } + + /** Get the name of the latest file parsed by this object. */ + csubstr filename() const { return m_file; } + + /** Get the latest YAML buffer parsed by this object. */ + csubstr source() const { return m_buf; } + + /** Get the encoding of the latest YAML buffer parsed by this object. + * If no encoding was specified, UTF8 is assumed as per the YAML standard. */ + Encoding_e encoding() const { return m_encoding != NOBOM ? m_encoding : UTF8; } + + id_type stack_capacity() const { RYML_ASSERT(m_evt_handler); return m_evt_handler->m_stack.capacity(); } + size_t locations_capacity() const { return m_newline_offsets_capacity; } + + RYML_DEPRECATED("filter arena no longer needed") + size_t filter_arena_capacity() const { return 0u; } + + /** @} */ + +public: + + /** @name parse methods */ + /** @{ */ + + /** parse YAML in place, emitting events to the current handler */ + void parse_in_place_ev(csubstr filename, substr src); + + /** parse JSON in place, emitting events to the current handler */ + void parse_json_in_place_ev(csubstr filename, substr src); + + /** @} */ + +public: + + // deprecated parse methods + + /** @cond dev */ + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place(csubstr filename, substr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place( substr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place(csubstr filename, substr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place( substr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place(csubstr filename, substr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place( substr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place(csubstr filename, substr yaml ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_place( substr yaml ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, csubstr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena( csubstr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, csubstr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena( csubstr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, csubstr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena( csubstr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, csubstr yaml ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding function in parse.hpp.") typename std::enable_if::type parse_in_arena( csubstr yaml ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, substr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena( substr yaml, Tree *t, size_t node_id); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, substr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena( substr yaml, Tree *t ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, substr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena( substr yaml, NodeRef node ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena(csubstr filename, substr yaml ); + template RYML_DEPRECATED("removed, deliberately undefined. use the freestanding csubstr version in parse.hpp.") typename std::enable_if::type parse_in_arena( substr yaml ); + /** @endcond */ + +public: + + /** @name locations */ + /** @{ */ + + /** Get the string starting at a particular location, to the end + * of the parsed source buffer. */ + csubstr location_contents(Location const& loc) const; + + /** Given a pointer to a buffer position, get the location. + * @param[in] val must be pointing to somewhere in the source + * buffer that was last parsed by this object. */ + Location val_location(const char *val) const; + + /** @} */ + +public: + + /** @cond dev */ + template + RYML_DEPRECATED("moved to Tree::location(Parser const&). deliberately undefined here.") + auto location(Tree const&, id_type node) const -> typename std::enable_if::type; + + template + RYML_DEPRECATED("moved to ConstNodeRef::location(Parser const&), deliberately undefined here.") + auto location(ConstNodeRef const&) const -> typename std::enable_if::type; + /** @endcond */ + +public: + + /** @name scalar filtering */ + /** @{*/ + + /** filter a plain scalar */ + FilterResult filter_scalar_plain(csubstr scalar, substr dst, size_t indentation); + /** filter a plain scalar in place */ + FilterResult filter_scalar_plain_in_place(substr scalar, size_t cap, size_t indentation); + + /** filter a single-quoted scalar */ + FilterResult filter_scalar_squoted(csubstr scalar, substr dst); + /** filter a single-quoted scalar in place */ + FilterResult filter_scalar_squoted_in_place(substr scalar, size_t cap); + + /** filter a double-quoted scalar */ + FilterResult filter_scalar_dquoted(csubstr scalar, substr dst); + /** filter a double-quoted scalar in place */ + FilterResultExtending filter_scalar_dquoted_in_place(substr scalar, size_t cap); + + /** filter a block-literal scalar */ + FilterResult filter_scalar_block_literal(csubstr scalar, substr dst, size_t indentation, BlockChomp_e chomp); + /** filter a block-literal scalar in place */ + FilterResult filter_scalar_block_literal_in_place(substr scalar, size_t cap, size_t indentation, BlockChomp_e chomp); + + /** filter a block-folded scalar */ + FilterResult filter_scalar_block_folded(csubstr scalar, substr dst, size_t indentation, BlockChomp_e chomp); + /** filter a block-folded scalar in place */ + FilterResult filter_scalar_block_folded_in_place(substr scalar, size_t cap, size_t indentation, BlockChomp_e chomp); + + /** @} */ + +private: + + struct ScannedScalar + { + substr scalar; + bool needs_filter; + }; + + struct ScannedBlock + { + substr scalar; + size_t indentation; + BlockChomp_e chomp; + }; + + bool _is_doc_begin(csubstr s); + bool _is_doc_end(csubstr s); + + bool _scan_scalar_plain_blck(ScannedScalar *C4_RESTRICT sc, size_t indentation); + bool _scan_scalar_plain_seq_flow(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_plain_seq_blck(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_plain_map_flow(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_plain_map_blck(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_map_json(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_seq_json(ScannedScalar *C4_RESTRICT sc); + bool _scan_scalar_plain_unk(ScannedScalar *C4_RESTRICT sc); + bool _is_valid_start_scalar_plain_flow(csubstr s); + + ScannedScalar _scan_scalar_squot(); + ScannedScalar _scan_scalar_dquot(); + + void _scan_block(ScannedBlock *C4_RESTRICT sb, size_t indref); + + csubstr _scan_anchor(); + csubstr _scan_ref_seq(); + csubstr _scan_ref_map(); + csubstr _scan_tag(); + +public: // exposed for testing + + /** @cond dev */ + csubstr _filter_scalar_plain(substr s, size_t indentation); + csubstr _filter_scalar_squot(substr s); + csubstr _filter_scalar_dquot(substr s); + csubstr _filter_scalar_literal(substr s, size_t indentation, BlockChomp_e chomp); + csubstr _filter_scalar_folded(substr s, size_t indentation, BlockChomp_e chomp); + csubstr _move_scalar_left_and_add_newline(substr s); + + csubstr _maybe_filter_key_scalar_plain(ScannedScalar const& sc, size_t indendation); + csubstr _maybe_filter_val_scalar_plain(ScannedScalar const& sc, size_t indendation); + csubstr _maybe_filter_key_scalar_squot(ScannedScalar const& sc); + csubstr _maybe_filter_val_scalar_squot(ScannedScalar const& sc); + csubstr _maybe_filter_key_scalar_dquot(ScannedScalar const& sc); + csubstr _maybe_filter_val_scalar_dquot(ScannedScalar const& sc); + csubstr _maybe_filter_key_scalar_literal(ScannedBlock const& sb); + csubstr _maybe_filter_val_scalar_literal(ScannedBlock const& sb); + csubstr _maybe_filter_key_scalar_folded(ScannedBlock const& sb); + csubstr _maybe_filter_val_scalar_folded(ScannedBlock const& sb); + /** @endcond */ + +private: + + void _handle_map_block(); + void _handle_seq_block(); + void _handle_map_flow(); + void _handle_seq_flow(); + void _handle_seq_imap(); + void _handle_map_json(); + void _handle_seq_json(); + + void _handle_unk(); + void _handle_unk_json(); + void _handle_usty(); + + void _handle_flow_skip_whitespace(); + + void _end_map_blck(); + void _end_seq_blck(); + void _end2_map(); + void _end2_seq(); + + void _begin2_doc(); + void _begin2_doc_expl(); + void _end2_doc(); + void _end2_doc_expl(); + + void _maybe_begin_doc(); + void _maybe_end_doc(); + + void _start_doc_suddenly(); + void _end_doc_suddenly(); + void _end_doc_suddenly__pop(); + void _end_stream(); + + void _set_indentation(size_t indentation); + void _save_indentation(); + void _handle_indentation_pop_from_block_seq(); + void _handle_indentation_pop_from_block_map(); + void _handle_indentation_pop(ParserState const* dst); + + void _maybe_skip_comment(); + void _skip_comment(); + void _maybe_skip_whitespace_tokens(); + void _maybe_skipchars(char c); + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + void _maybe_skipchars_up_to(char c, size_t max_to_skip); + #endif + template + void _skipchars(const char (&chars)[N]); + bool _maybe_scan_following_colon() noexcept; + bool _maybe_scan_following_comma() noexcept; + +public: + + /** @cond dev */ + template auto _filter_plain(FilterProcessor &C4_RESTRICT proc, size_t indentation) -> decltype(proc.result()); + template auto _filter_squoted(FilterProcessor &C4_RESTRICT proc) -> decltype(proc.result()); + template auto _filter_dquoted(FilterProcessor &C4_RESTRICT proc) -> decltype(proc.result()); + template auto _filter_block_literal(FilterProcessor &C4_RESTRICT proc, size_t indentation, BlockChomp_e chomp) -> decltype(proc.result()); + template auto _filter_block_folded(FilterProcessor &C4_RESTRICT proc, size_t indentation, BlockChomp_e chomp) -> decltype(proc.result()); + /** @endcond */ + +public: + + /** @cond dev */ + template void _filter_nl_plain(FilterProcessor &C4_RESTRICT proc, size_t indentation); + template void _filter_nl_squoted(FilterProcessor &C4_RESTRICT proc); + template void _filter_nl_dquoted(FilterProcessor &C4_RESTRICT proc); + + template bool _filter_ws_handle_to_first_non_space(FilterProcessor &C4_RESTRICT proc); + template void _filter_ws_copy_trailing(FilterProcessor &C4_RESTRICT proc); + template void _filter_ws_skip_trailing(FilterProcessor &C4_RESTRICT proc); + + template void _filter_dquoted_backslash(FilterProcessor &C4_RESTRICT proc); + + template void _filter_chomp(FilterProcessor &C4_RESTRICT proc, BlockChomp_e chomp, size_t indentation); + template size_t _handle_all_whitespace(FilterProcessor &C4_RESTRICT proc, BlockChomp_e chomp); + template size_t _extend_to_chomp(FilterProcessor &C4_RESTRICT proc, size_t contents_len); + template void _filter_block_indentation(FilterProcessor &C4_RESTRICT proc, size_t indentation); + template void _filter_block_folded_newlines(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len); + template size_t _filter_block_folded_newlines_compress(FilterProcessor &C4_RESTRICT proc, size_t num_newl, size_t wpos_at_first_newl); + template void _filter_block_folded_newlines_leading(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len); + template void _filter_block_folded_indented_block(FilterProcessor &C4_RESTRICT proc, size_t indentation, size_t len, size_t curr_indentation) noexcept; + + /** @endcond */ + +private: + + void _line_progressed(size_t ahead); + void _line_ended(); + void _line_ended_undo(); + + bool _finished_file() const; + bool _finished_line() const; + + void _scan_line(); + substr _peek_next_line(size_t pos=npos) const; + + bool _at_line_begin() const + { + return m_evt_handler->m_curr->line_contents.rem.begin() == m_evt_handler->m_curr->line_contents.full.begin(); + } + + void _relocate_arena(csubstr prev_arena, substr next_arena); + static void _s_relocate_arena(void*, csubstr prev_arena, substr next_arena); + +private: + + C4_ALWAYS_INLINE bool has_all(ParserFlag_t f) const noexcept { return (m_evt_handler->m_curr->flags & f) == f; } + C4_ALWAYS_INLINE bool has_any(ParserFlag_t f) const noexcept { return (m_evt_handler->m_curr->flags & f) != 0; } + C4_ALWAYS_INLINE bool has_none(ParserFlag_t f) const noexcept { return (m_evt_handler->m_curr->flags & f) == 0; } + static C4_ALWAYS_INLINE bool has_all(ParserFlag_t f, ParserState const* C4_RESTRICT s) noexcept { return (s->flags & f) == f; } + static C4_ALWAYS_INLINE bool has_any(ParserFlag_t f, ParserState const* C4_RESTRICT s) noexcept { return (s->flags & f) != 0; } + static C4_ALWAYS_INLINE bool has_none(ParserFlag_t f, ParserState const* C4_RESTRICT s) noexcept { return (s->flags & f) == 0; } + + #ifndef RYML_DBG + C4_ALWAYS_INLINE static void add_flags(ParserFlag_t on, ParserState *C4_RESTRICT s) noexcept { s->flags |= on; } + C4_ALWAYS_INLINE static void addrem_flags(ParserFlag_t on, ParserFlag_t off, ParserState *C4_RESTRICT s) noexcept { s->flags &= ~off; s->flags |= on; } + C4_ALWAYS_INLINE static void rem_flags(ParserFlag_t off, ParserState *C4_RESTRICT s) noexcept { s->flags &= ~off; } + C4_ALWAYS_INLINE void add_flags(ParserFlag_t on) noexcept { m_evt_handler->m_curr->flags |= on; } + C4_ALWAYS_INLINE void addrem_flags(ParserFlag_t on, ParserFlag_t off) noexcept { m_evt_handler->m_curr->flags &= ~off; m_evt_handler->m_curr->flags |= on; } + C4_ALWAYS_INLINE void rem_flags(ParserFlag_t off) noexcept { m_evt_handler->m_curr->flags &= ~off; } + #else + static void add_flags(ParserFlag_t on, ParserState *C4_RESTRICT s); + static void addrem_flags(ParserFlag_t on, ParserFlag_t off, ParserState *C4_RESTRICT s); + static void rem_flags(ParserFlag_t off, ParserState *C4_RESTRICT s); + C4_ALWAYS_INLINE void add_flags(ParserFlag_t on) noexcept { add_flags(on, m_evt_handler->m_curr); } + C4_ALWAYS_INLINE void addrem_flags(ParserFlag_t on, ParserFlag_t off) noexcept { addrem_flags(on, off, m_evt_handler->m_curr); } + C4_ALWAYS_INLINE void rem_flags(ParserFlag_t off) noexcept { rem_flags(off, m_evt_handler->m_curr); } + #endif + +private: + + void _prepare_locations(); + void _resize_locations(size_t sz); + bool _locations_dirty() const; + +private: + + void _reset(); + void _free(); + void _clr(); + + #ifdef RYML_DBG + template void _dbg(csubstr fmt, Args const& C4_RESTRICT ...args) const; + #endif + template void _err(csubstr fmt, Args const& C4_RESTRICT ...args) const; + template void _errloc(csubstr fmt, Location const& loc, Args const& C4_RESTRICT ...args) const; + + template void _fmt_msg(DumpFn &&dumpfn) const; + +private: + + /** store pending tag or anchor/ref annotations */ + struct Annotation + { + struct Entry + { + csubstr str; + size_t indentation; + size_t line; + }; + Entry annotations[2]; + size_t num_entries; + }; + + void _handle_colon(); + void _add_annotation(Annotation *C4_RESTRICT dst, csubstr str, size_t indentation, size_t line); + void _clear_annotations(Annotation *C4_RESTRICT dst); + bool _has_pending_annotations() const { return m_pending_tags.num_entries || m_pending_anchors.num_entries; } + #ifdef RYML_NO_COVERAGE__TO_BE_DELETED + bool _handle_indentation_from_annotations(); + #endif + bool _annotations_require_key_container() const; + void _handle_annotations_before_blck_key_scalar(); + void _handle_annotations_before_blck_val_scalar(); + void _handle_annotations_before_start_mapblck(size_t current_line); + void _handle_annotations_before_start_mapblck_as_key(); + void _handle_annotations_and_indentation_after_start_mapblck(size_t key_indentation, size_t key_line); + size_t _select_indentation_from_annotations(size_t val_indentation, size_t val_line); + void _handle_directive(csubstr rem); + bool _handle_bom(); + void _handle_bom(Encoding_e enc); + + void _check_tag(csubstr tag); + +private: + + ParserOptions m_options; + + csubstr m_file; + substr m_buf; + +public: + + /** @cond dev */ + EventHandler *C4_RESTRICT m_evt_handler; // NOLINT + /** @endcond */ + +private: + + Annotation m_pending_anchors; + Annotation m_pending_tags; + + bool m_was_inside_qmrk; + bool m_doc_empty = true; + size_t m_prev_colon = npos; + + Encoding_e m_encoding = UTF8; + +private: + + size_t *m_newline_offsets; + size_t m_newline_offsets_size; + size_t m_newline_offsets_capacity; + csubstr m_newline_offsets_buf; + +}; + + +/** Quickly inspect the source to estimate the number of nodes the + * resulting tree is likely have. If a tree is empty before + * parsing, considerable time will be spent growing it, so calling + * this to reserve the tree size prior to parsing is likely to + * result in a time gain. We encourage using this method before + * parsing, but as always measure its impact in performance to + * obtain a good trade-off. + * + * @note since this method is meant for optimizing performance, it + * is approximate. The result may be actually smaller than the + * resulting number of nodes, notably if the YAML uses implicit + * maps as flow seq members as in `[these: are, individual: + * maps]`. */ +RYML_EXPORT id_type estimate_tree_capacity(csubstr src); // NOLINT(readability-redundant-declaration) + +/** @} */ + +} // namespace yml +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise) + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif + +#endif /* _C4_YML_PARSE_ENGINE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/parser_state.hpp b/3rdparty/rapidyaml/include/c4/yml/parser_state.hpp new file mode 100644 index 0000000000..beabb8948e --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/parser_state.hpp @@ -0,0 +1,212 @@ +#ifndef _C4_YML_PARSER_STATE_HPP_ +#define _C4_YML_PARSER_STATE_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "c4/yml/common.hpp" +#endif + +// NOLINTBEGIN(hicpp-signed-bitwise) + +namespace c4 { +namespace yml { + +/** data type for @ref ParserState_e */ +using ParserFlag_t = int; + +/** Enumeration of the state flags for the parser */ +typedef enum : ParserFlag_t { + RTOP = 0x01 << 0, ///< reading at top level + RUNK = 0x01 << 1, ///< reading unknown state (when starting): must determine whether scalar, map or seq + RMAP = 0x01 << 2, ///< reading a map + RSEQ = 0x01 << 3, ///< reading a seq + FLOW = 0x01 << 4, ///< reading is inside explicit flow chars: [] or {} + BLCK = 0x01 << 5, ///< reading in block mode + QMRK = 0x01 << 6, ///< reading an explicit key (`? key`) + RKEY = 0x01 << 7, ///< reading a scalar as key + RVAL = 0x01 << 9, ///< reading a scalar as val + RKCL = 0x01 << 8, ///< reading the key colon (ie the : after the key in the map) + RNXT = 0x01 << 10, ///< read next val or keyval + SSCL = 0x01 << 11, ///< there's a stored scalar + QSCL = 0x01 << 12, ///< stored scalar was quoted + RSET = 0x01 << 13, ///< the (implicit) map being read is a !!set. @see https://yaml.org/type/set.html + RDOC = 0x01 << 14, ///< reading a document + NDOC = 0x01 << 15, ///< no document mode. a document has ended and another has not started yet. + USTY = 0x01 << 16, ///< reading in unknown style mode - must determine FLOW or BLCK + //! reading an implicit map nested in an explicit seq. + //! eg, {key: [key2: value2, key3: value3]} + //! is parsed as {key: [{key2: value2}, {key3: value3}]} + RSEQIMAP = 0x01 << 17, +} ParserState_e; + +#ifdef RYML_DBG +/** @cond dev */ +namespace detail { +csubstr _parser_flags_to_str(substr buf, ParserFlag_t flags); +} // namespace +/** @endcond */ +#endif + + +/** Helper to control the line contents while parsing a buffer */ +struct LineContents +{ + substr rem; ///< the stripped line remainder; initially starts at the first non-space character + size_t indentation; ///< the number of spaces on the beginning of the line + substr full; ///< the full line, including newlines on the right + substr stripped; ///< the stripped line, excluding newlines on the right + + LineContents() = default; + + void reset_with_next_line(substr buf, size_t offset) + { + RYML_ASSERT(offset <= buf.len); + size_t e = offset; + // get the current line stripped of newline chars + while(e < buf.len && (buf.str[e] != '\n' && buf.str[e] != '\r')) + ++e; + RYML_ASSERT(e >= offset); + const substr stripped_ = buf.range(offset, e); + #if defined(__GNUC__) && __GNUC__ == 11 + C4_DONT_OPTIMIZE(stripped_); + #endif + // advance pos to include the first line ending + if(e < buf.len && buf.str[e] == '\r') + ++e; + if(e < buf.len && buf.str[e] == '\n') + ++e; + const substr full_ = buf.range(offset, e); + reset(full_, stripped_); + } + + void reset(substr full_, substr stripped_) + { + rem = stripped_; + indentation = stripped_.first_not_of(' '); // find the first column where the character is not a space + full = full_; + stripped = stripped_; + } + + C4_ALWAYS_INLINE size_t current_col() const RYML_NOEXCEPT + { + // WARNING: gcc x86 release builds were wrong (eg returning 0 + // when the result should be 4 ) when this function was like + // this: + // + //return current_col(rem); + // + // (see below for the full definition of the called overload + // of current_col()) + // + // ... so we explicitly inline the code in here: + RYML_ASSERT(rem.str >= full.str); + size_t col = static_cast(rem.str - full.str); + return col; + // + // this was happening only on builds specifically with (gcc + // AND x86 AND release); no other builds were having the + // problem: not in debug, not in x64, not in other + // architectures, not in clang, not in visual studio. WTF!? + // + // Enabling debug prints with RYML_DBG made the problem go + // away, so these could not be used to debug the + // problem. Adding prints inside the called current_col() also + // made the problem go away! WTF!??? + // + // a prize will be offered to anybody able to explain why this + // was happening. + } + + C4_ALWAYS_INLINE size_t current_col(csubstr s) const RYML_NOEXCEPT + { + RYML_ASSERT(s.str >= full.str); + RYML_ASSERT(full.is_super(s)); + size_t col = static_cast(s.str - full.str); + return col; + } +}; +static_assert(std::is_standard_layout::value, "LineContents not standard"); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +struct ParserState +{ + LineContents line_contents; + Location pos; + ParserFlag_t flags; + size_t indref; ///< the reference indentation in the current block scope + id_type level; + id_type node_id; ///< don't hold a pointer to the node as it will be relocated during tree resizes + size_t scalar_col; // the column where the scalar (or its quotes) begin + bool more_indented; + bool has_children; + + ParserState() = default; + + void start_parse(const char *file, id_type node_id_) + { + level = 0; + pos.name = to_csubstr(file); + pos.offset = 0; + pos.line = 1; + pos.col = 1; + node_id = node_id_; + more_indented = false; + scalar_col = 0; + indref = 0; + has_children = false; + } + + void reset_after_push() + { + node_id = NONE; + indref = npos; + more_indented = false; + ++level; + has_children = false; + } + + C4_ALWAYS_INLINE void reset_before_pop(ParserState const& to_pop) + { + pos = to_pop.pos; + line_contents = to_pop.line_contents; + } + +public: + + C4_ALWAYS_INLINE bool at_line_beginning() const noexcept + { + return line_contents.rem.str == line_contents.full.str; + } + C4_ALWAYS_INLINE bool indentation_eq() const noexcept + { + RYML_ASSERT(indref != npos); + return line_contents.indentation != npos && line_contents.indentation == indref; + } + C4_ALWAYS_INLINE bool indentation_ge() const noexcept + { + RYML_ASSERT(indref != npos); + return line_contents.indentation != npos && line_contents.indentation >= indref; + } + C4_ALWAYS_INLINE bool indentation_gt() const noexcept + { + RYML_ASSERT(indref != npos); + return line_contents.indentation != npos && line_contents.indentation > indref; + } + C4_ALWAYS_INLINE bool indentation_lt() const noexcept + { + RYML_ASSERT(indref != npos); + return line_contents.indentation != npos && line_contents.indentation < indref; + } +}; +static_assert(std::is_standard_layout::value, "ParserState not standard"); + + +} // namespace yml +} // namespace c4 + +// NOLINTEND(hicpp-signed-bitwise) + +#endif /* _C4_YML_PARSER_STATE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/preprocess.hpp b/3rdparty/rapidyaml/include/c4/yml/preprocess.hpp new file mode 100644 index 0000000000..3db4f700b5 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/preprocess.hpp @@ -0,0 +1,97 @@ +#ifndef _C4_YML_PREPROCESS_HPP_ +#define _C4_YML_PREPROCESS_HPP_ + +/** @file preprocess.hpp Functions for preprocessing YAML prior to parsing. */ + +#ifndef _C4_YML_COMMON_HPP_ +#include "./common.hpp" +#endif +#include + + +namespace c4 { +namespace yml { + +/** @addtogroup doc_preprocessors + * @{ + */ + +/** @cond dev */ +namespace detail { +using Preprocessor = size_t(csubstr, substr); +template +substr preprocess_into_container(csubstr input, CharContainer *out) +{ + // try to write once. the preprocessor will stop writing at the end of + // the container, but will process all the input to determine the + // required container size. + size_t sz = PP(input, to_substr(*out)); + // if the container size is not enough, resize, and run again in the + // resized container + if(sz > out->size()) + { + out->resize(sz); + sz = PP(input, to_substr(*out)); + } + return to_substr(*out).first(sz); +} +} // namespace detail +/** @endcond */ + + +//----------------------------------------------------------------------------- + +/** @defgroup doc_preprocess_rxmap preprocess_rxmap + * + * @brief Convert flow-type relaxed maps (with implicit bools) into strict YAML + * flow map: + * + * @code{.yaml} + * {a, b, c, d: [e, f], g: {a, b}} + * # is converted into this: + * {a: 1, b: 1, c: 1, d: [e, f], g: {a, b}} + * @endcode + + * @note this is NOT recursive - conversion happens only in the top-level map + * @param rxmap A relaxed map + * @param buf output buffer + * @param out output container + * + * @{ + */ + +/** Write into a given output buffer. This function is safe to call with + * empty or small buffers; it won't write beyond the end of the buffer. + * + * @return the number of characters required for output + */ +RYML_EXPORT size_t preprocess_rxmap(csubstr rxmap, substr buf); + + +/** Write into an existing container. It is resized to contained the output. + * @return a substr of the container + * @overload preprocess_rxmap */ +template +substr preprocess_rxmap(csubstr rxmap, CharContainer *out) +{ + return detail::preprocess_into_container(rxmap, out); +} + + +/** Create a container with the result. + * @overload preprocess_rxmap */ +template +CharContainer preprocess_rxmap(csubstr rxmap) +{ + CharContainer out; + preprocess_rxmap(rxmap, &out); + return out; +} + +/** @} */ // preprocess_rxmap +/** @} */ // group + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_PREPROCESS_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/reference_resolver.hpp b/3rdparty/rapidyaml/include/c4/yml/reference_resolver.hpp new file mode 100644 index 0000000000..7f277131ea --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/reference_resolver.hpp @@ -0,0 +1,88 @@ +#ifndef _C4_YML_REFERENCE_RESOLVER_HPP_ +#define _C4_YML_REFERENCE_RESOLVER_HPP_ + +#include "c4/yml/tree.hpp" +#include "c4/yml/detail/stack.hpp" + + +namespace c4 { +namespace yml { + +/** @addtogroup doc_ref_utils + * @{ + */ + +/** Reusable object to resolve references/aliases in a @ref Tree. */ +struct RYML_EXPORT ReferenceResolver +{ + ReferenceResolver() = default; + + /** Resolve references: for each reference, look for a matching + * anchor, and copy its contents to the ref node. + * + * @p tree the subject tree + * + * @p clear_anchors whether to clear existing anchors after + * resolving + * + * This method first does a full traversal of the tree to gather + * all anchors and references in a separate collection, then it + * goes through that collection to locate the names, which it does + * by obeying the YAML standard diktat that "an alias node refers + * to the most recent node in the serialization having the + * specified anchor" + * + * So, depending on the number of anchor/alias nodes, this is a + * potentially expensive operation, with a best-case linear + * complexity (from the initial traversal). This potential cost is + * one of the reasons for requiring an explicit call. + * + * The @ref Tree has an `Tree::resolve()` overload set forwarding + * here. Previously this operation was done there, using a + * discarded object; using this separate class offers opportunity + * for reuse of the object. + * + * @warning resolving references opens an attack vector when the + * data is malicious or severely malformed, as the tree can expand + * exponentially. See for example the [Billion Laughs + * Attack](https://en.wikipedia.org/wiki/Billion_laughs_attack). + * + */ + void resolve(Tree *tree, bool clear_anchors=true); + +public: + + /** @cond dev */ + + struct RefData + { + NodeType type; + id_type node; + id_type prev_anchor; + id_type target; + id_type parent_ref; + id_type parent_ref_sibling; + }; + + void reset_(Tree *t_); + void resolve_(); + void gather_anchors_and_refs_(); + void gather_anchors_and_refs__(id_type n); + id_type count_anchors_and_refs_(id_type n); + + id_type lookup_(RefData const* C4_RESTRICT ra); + + Tree *C4_RESTRICT m_tree; + /** We're using this stack purely as an array. */ + detail::stack m_refs; + + /** @endcond */ +}; + +/** @} */ + +} // namespace ryml +} // namespace c4 + + +#endif // _C4_YML_REFERENCE_RESOLVER_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/std/map.hpp b/3rdparty/rapidyaml/include/c4/yml/std/map.hpp new file mode 100644 index 0000000000..dc07c67f54 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/std/map.hpp @@ -0,0 +1,46 @@ +#ifndef _C4_YML_STD_MAP_HPP_ +#define _C4_YML_STD_MAP_HPP_ + +/** @file map.hpp write/read std::map to/from a YAML tree. */ + +#include "c4/yml/node.hpp" +#include + +namespace c4 { +namespace yml { + +// std::map requires child nodes in the data +// tree hierarchy (a MAP node in ryml parlance). +// So it should be serialized via write()/read(). + +template +void write(c4::yml::NodeRef *n, std::map const& m) +{ + *n |= c4::yml::MAP; + for(auto const& C4_RESTRICT p : m) + { + auto ch = n->append_child(); + ch << c4::yml::key(p.first); + ch << p.second; + } +} + +/** read the node members, assigning into the existing map. If a key + * is already present in the map, then its value will be + * move-assigned. */ +template +bool read(c4::yml::ConstNodeRef const& n, std::map * m) +{ + for(auto const& C4_RESTRICT ch : n) + { + K k{}; + ch >> c4::yml::key(k); + ch >> (*m)[k]; + } + return true; +} + +} // namespace yml +} // namespace c4 + +#endif // _C4_YML_STD_MAP_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/std/std.hpp b/3rdparty/rapidyaml/include/c4/yml/std/std.hpp new file mode 100644 index 0000000000..08e80d1557 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/std/std.hpp @@ -0,0 +1,8 @@ +#ifndef _C4_YML_STD_STD_HPP_ +#define _C4_YML_STD_STD_HPP_ + +#include "c4/yml/std/string.hpp" +#include "c4/yml/std/vector.hpp" +#include "c4/yml/std/map.hpp" + +#endif // _C4_YML_STD_STD_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/std/string.hpp b/3rdparty/rapidyaml/include/c4/yml/std/string.hpp new file mode 100644 index 0000000000..e3318f91c1 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/std/string.hpp @@ -0,0 +1,9 @@ +#ifndef C4_YML_STD_STRING_HPP_ +#define C4_YML_STD_STRING_HPP_ + +/** @file string.hpp substring conversions for/from std::string */ + +// everything we need is implemented here: +#include + +#endif // C4_YML_STD_STRING_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/std/vector.hpp b/3rdparty/rapidyaml/include/c4/yml/std/vector.hpp new file mode 100644 index 0000000000..68aa39a241 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/std/vector.hpp @@ -0,0 +1,59 @@ +#ifndef _C4_YML_STD_VECTOR_HPP_ +#define _C4_YML_STD_VECTOR_HPP_ + +#include "c4/yml/node.hpp" +#include +#include + +namespace c4 { +namespace yml { + +// vector is a sequence-like type, and it requires child nodes +// in the data tree hierarchy (a SEQ node in ryml parlance). +// So it should be serialized via write()/read(). + + +template +void write(c4::yml::NodeRef *n, std::vector const& vec) +{ + *n |= c4::yml::SEQ; + for(V const& v : vec) + n->append_child() << v; +} + +/** read the node members, overwriting existing vector entries. */ +template +bool read(c4::yml::ConstNodeRef const& n, std::vector *vec) +{ + C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wuseless-cast") + vec->resize(static_cast(n.num_children())); + C4_SUPPRESS_WARNING_GCC_POP + size_t pos = 0; + for(ConstNodeRef const child : n) + child >> (*vec)[pos++]; + return true; +} + +/** read the node members, overwriting existing vector entries. + * specialization: std::vector uses std::vector::reference as + * the return value of its operator[]. */ +template +bool read(c4::yml::ConstNodeRef const& n, std::vector *vec) +{ + C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wuseless-cast") + vec->resize(static_cast(n.num_children())); + C4_SUPPRESS_WARNING_GCC_POP + size_t pos = 0; + bool tmp = {}; + for(ConstNodeRef const child : n) + { + child >> tmp; + (*vec)[pos++] = tmp; + } + return true; +} + +} // namespace yml +} // namespace c4 + +#endif // _C4_YML_STD_VECTOR_HPP_ diff --git a/3rdparty/rapidyaml/include/c4/yml/tag.hpp b/3rdparty/rapidyaml/include/c4/yml/tag.hpp new file mode 100644 index 0000000000..1915b3ba67 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/tag.hpp @@ -0,0 +1,83 @@ +#ifndef _C4_YML_TAG_HPP_ +#define _C4_YML_TAG_HPP_ + +#include + +namespace c4 { +namespace yml { + +class Tree; + +/** @addtogroup doc_tag_utils + * + * @{ + */ + + +#ifndef RYML_MAX_TAG_DIRECTIVES +/** the maximum number of tag directives in a Tree */ +#define RYML_MAX_TAG_DIRECTIVES 4 +#endif + +/** the integral type necessary to cover all the bits marking node tags */ +using tag_bits = uint16_t; + +/** a bit mask for marking tags for types */ +typedef enum : tag_bits { + TAG_NONE = 0, + // container types + TAG_MAP = 1, /**< !!map Unordered set of key: value pairs without duplicates. @see https://yaml.org/type/map.html */ + TAG_OMAP = 2, /**< !!omap Ordered sequence of key: value pairs without duplicates. @see https://yaml.org/type/omap.html */ + TAG_PAIRS = 3, /**< !!pairs Ordered sequence of key: value pairs allowing duplicates. @see https://yaml.org/type/pairs.html */ + TAG_SET = 4, /**< !!set Unordered set of non-equal values. @see https://yaml.org/type/set.html */ + TAG_SEQ = 5, /**< !!seq Sequence of arbitrary values. @see https://yaml.org/type/seq.html */ + // scalar types + TAG_BINARY = 6, /**< !!binary A sequence of zero or more octets (8 bit values). @see https://yaml.org/type/binary.html */ + TAG_BOOL = 7, /**< !!bool Mathematical Booleans. @see https://yaml.org/type/bool.html */ + TAG_FLOAT = 8, /**< !!float Floating-point approximation to real numbers. https://yaml.org/type/float.html */ + TAG_INT = 9, /**< !!float Mathematical integers. https://yaml.org/type/int.html */ + TAG_MERGE = 10, /**< !!merge Specify one or more mapping to be merged with the current one. https://yaml.org/type/merge.html */ + TAG_NULL = 11, /**< !!null Devoid of value. https://yaml.org/type/null.html */ + TAG_STR = 12, /**< !!str A sequence of zero or more Unicode characters. https://yaml.org/type/str.html */ + TAG_TIMESTAMP = 13, /**< !!timestamp A point in time https://yaml.org/type/timestamp.html */ + TAG_VALUE = 14, /**< !!value Specify the default value of a mapping https://yaml.org/type/value.html */ + TAG_YAML = 15, /**< !!yaml Specify the default value of a mapping https://yaml.org/type/yaml.html */ +} YamlTag_e; + +RYML_EXPORT YamlTag_e to_tag(csubstr tag); +RYML_EXPORT csubstr from_tag(YamlTag_e tag); +RYML_EXPORT csubstr from_tag_long(YamlTag_e tag); +RYML_EXPORT csubstr normalize_tag(csubstr tag); +RYML_EXPORT csubstr normalize_tag_long(csubstr tag); +RYML_EXPORT csubstr normalize_tag_long(csubstr tag, substr output); + +RYML_EXPORT bool is_custom_tag(csubstr tag); + + +struct RYML_EXPORT TagDirective +{ + /** Eg
!e!
in
%TAG !e! tag:example.com,2000:app/
*/ + csubstr handle; + /** Eg
tag:example.com,2000:app/
in
%TAG !e! tag:example.com,2000:app/
*/ + csubstr prefix; + /** The next node to which this tag directive applies */ + id_type next_node_id; + + bool create_from_str(csubstr directive_); ///< leaves next_node_id unfilled + size_t transform(csubstr tag, substr output, Callbacks const& callbacks, bool with_brackets=true) const; +}; + +struct RYML_EXPORT TagDirectiveRange +{ + TagDirective const* C4_RESTRICT b; + TagDirective const* C4_RESTRICT e; + C4_ALWAYS_INLINE TagDirective const* begin() const noexcept { return b; } + C4_ALWAYS_INLINE TagDirective const* end() const noexcept { return e; } +}; + +/** @} */ + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_TAG_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/tree.hpp b/3rdparty/rapidyaml/include/c4/yml/tree.hpp new file mode 100644 index 0000000000..2d93dd7b04 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/tree.hpp @@ -0,0 +1,1561 @@ +#ifndef _C4_YML_TREE_HPP_ +#define _C4_YML_TREE_HPP_ + +/** @file tree.hpp */ + +#include "c4/error.hpp" +#include "c4/types.hpp" +#ifndef _C4_YML_FWD_HPP_ +#include "c4/yml/fwd.hpp" +#endif +#ifndef _C4_YML_COMMON_HPP_ +#include "c4/yml/common.hpp" +#endif +#ifndef C4_YML_NODE_TYPE_HPP_ +#include "c4/yml/node_type.hpp" +#endif +#ifndef _C4_YML_TAG_HPP_ +#include "c4/yml/tag.hpp" +#endif +#ifndef _C4_CHARCONV_HPP_ +#include +#endif + +#include +#include + + +C4_SUPPRESS_WARNING_MSVC_PUSH +C4_SUPPRESS_WARNING_MSVC(4251) // needs to have dll-interface to be used by clients of struct +C4_SUPPRESS_WARNING_MSVC(4296) // expression is always 'boolean_value' +C4_SUPPRESS_WARNING_GCC_CLANG_PUSH +C4_SUPPRESS_WARNING_GCC_CLANG("-Wold-style-cast") +C4_SUPPRESS_WARNING_GCC("-Wuseless-cast") +C4_SUPPRESS_WARNING_GCC("-Wtype-limits") + + +namespace c4 { +namespace yml { + +template inline auto read(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value, bool>::type; +template inline auto read(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value && !std::is_floating_point::value, bool>::type; +template inline auto read(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value, bool>::type; + +template inline auto readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value, bool>::type; +template inline auto readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value && !std::is_floating_point::value, bool>::type; +template inline auto readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) -> typename std::enable_if::value, bool>::type; + +template size_t to_chars_float(substr buf, T val); +template bool from_chars_float(csubstr buf, T *C4_RESTRICT val); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + + +/** @addtogroup doc_tree + * + * @{ + */ + +/** a node scalar is a csubstr, which may be tagged and anchored. */ +struct NodeScalar +{ + csubstr tag; + csubstr scalar; + csubstr anchor; + +public: + + /// initialize as an empty scalar + NodeScalar() noexcept : tag(), scalar(), anchor() {} // NOLINT + + /// initialize as an untagged scalar + template + NodeScalar(const char (&s)[N]) noexcept : tag(), scalar(s), anchor() {} + NodeScalar(csubstr s ) noexcept : tag(), scalar(s), anchor() {} + + /// initialize as a tagged scalar + template + NodeScalar(const char (&t)[N], const char (&s)[N]) noexcept : tag(t), scalar(s), anchor() {} + NodeScalar(csubstr t , csubstr s ) noexcept : tag(t), scalar(s), anchor() {} + +public: + + ~NodeScalar() noexcept = default; + NodeScalar(NodeScalar &&) noexcept = default; + NodeScalar(NodeScalar const&) noexcept = default; + NodeScalar& operator= (NodeScalar &&) noexcept = default; + NodeScalar& operator= (NodeScalar const&) noexcept = default; + +public: + + bool empty() const noexcept { return tag.empty() && scalar.empty() && anchor.empty(); } + + void clear() noexcept { tag.clear(); scalar.clear(); anchor.clear(); } + + void set_ref_maybe_replacing_scalar(csubstr ref, bool has_scalar) RYML_NOEXCEPT + { + csubstr trimmed = ref.begins_with('*') ? ref.sub(1) : ref; + anchor = trimmed; + if((!has_scalar) || !scalar.ends_with(trimmed)) + scalar = ref; + } +}; +C4_MUST_BE_TRIVIAL_COPY(NodeScalar); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** convenience class to initialize nodes */ +struct NodeInit +{ + + NodeType type; + NodeScalar key; + NodeScalar val; + +public: + + /// initialize as an empty node + NodeInit() : type(NOTYPE), key(), val() {} + /// initialize as a typed node + NodeInit(NodeType_e t) : type(t), key(), val() {} + /// initialize as a sequence member + NodeInit(NodeScalar const& v) : type(VAL), key(), val(v) { _add_flags(); } + /// initialize as a sequence member with explicit type + NodeInit(NodeScalar const& v, NodeType_e t) : type(t|VAL), key(), val(v) { _add_flags(); } + /// initialize as a mapping member + NodeInit( NodeScalar const& k, NodeScalar const& v) : type(KEYVAL), key(k), val(v) { _add_flags(); } + /// initialize as a mapping member with explicit type + NodeInit(NodeType_e t, NodeScalar const& k, NodeScalar const& v) : type(t), key(k), val(v) { _add_flags(); } + /// initialize as a mapping member with explicit type (eg for SEQ or MAP) + NodeInit(NodeType_e t, NodeScalar const& k ) : type(t), key(k), val( ) { _add_flags(KEY); } + +public: + + void clear() + { + type.clear(); + key.clear(); + val.clear(); + } + + void _add_flags(type_bits more_flags=0) + { + type = (type|more_flags); + if( ! key.tag.empty()) + type = (type|KEYTAG); + if( ! val.tag.empty()) + type = (type|VALTAG); + if( ! key.anchor.empty()) + type = (type|KEYANCH); + if( ! val.anchor.empty()) + type = (type|VALANCH); + } + + bool _check() const + { + // key cannot be empty + RYML_ASSERT(key.scalar.empty() == ((type & KEY) == 0)); + // key tag cannot be empty + RYML_ASSERT(key.tag.empty() == ((type & KEYTAG) == 0)); + // val may be empty even though VAL is set. But when VAL is not set, val must be empty + RYML_ASSERT(((type & VAL) != 0) || val.scalar.empty()); + // val tag cannot be empty + RYML_ASSERT(val.tag.empty() == ((type & VALTAG) == 0)); + return true; + } +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** contains the data for each YAML node. */ +struct NodeData +{ + NodeType m_type; + + NodeScalar m_key; + NodeScalar m_val; + + id_type m_parent; + id_type m_first_child; + id_type m_last_child; + id_type m_next_sibling; + id_type m_prev_sibling; +}; +C4_MUST_BE_TRIVIAL_COPY(NodeData); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +class RYML_EXPORT Tree +{ +public: + + /** @name construction and assignment */ + /** @{ */ + + Tree() : Tree(get_callbacks()) {} + Tree(Callbacks const& cb); + Tree(id_type node_capacity, size_t arena_capacity=0) : Tree(node_capacity, arena_capacity, get_callbacks()) {} + Tree(id_type node_capacity, size_t arena_capacity, Callbacks const& cb); + + ~Tree(); + + Tree(Tree const& that); + Tree(Tree && that) noexcept; + + Tree& operator= (Tree const& that); + Tree& operator= (Tree && that) noexcept; + + /** @} */ + +public: + + /** @name memory and sizing */ + /** @{ */ + + void reserve(id_type node_capacity); + + /** clear the tree and zero every node + * @note does NOT clear the arena + * @see clear_arena() */ + void clear(); + void clear_arena() { m_arena_pos = 0; } + + bool empty() const { return m_size == 0; } + + id_type size() const { return m_size; } + id_type capacity() const { return m_cap; } + id_type slack() const { RYML_ASSERT(m_cap >= m_size); return m_cap - m_size; } + + Callbacks const& callbacks() const { return m_callbacks; } + void callbacks(Callbacks const& cb) { m_callbacks = cb; } + + /** @} */ + +public: + + /** @name node getters */ + /** @{ */ + + //! get the index of a node belonging to this tree. + //! @p n can be nullptr, in which case NONE is returned + id_type id(NodeData const* n) const + { + if( ! n) + return NONE; + _RYML_CB_ASSERT(m_callbacks, n >= m_buf && n < m_buf + m_cap); + return static_cast(n - m_buf); + } + + //! get a pointer to a node's NodeData. + //! i can be NONE, in which case a nullptr is returned + NodeData *get(id_type node) // NOLINT(readability-make-member-function-const) + { + if(node == NONE) + return nullptr; + _RYML_CB_ASSERT(m_callbacks, node >= 0 && node < m_cap); + return m_buf + node; + } + //! get a pointer to a node's NodeData. + //! i can be NONE, in which case a nullptr is returned. + NodeData const *get(id_type node) const + { + if(node == NONE) + return nullptr; + _RYML_CB_ASSERT(m_callbacks, node >= 0 && node < m_cap); + return m_buf + node; + } + + //! An if-less form of get() that demands a valid node index. + //! This function is implementation only; use at your own risk. + NodeData * _p(id_type node) { _RYML_CB_ASSERT(m_callbacks, node != NONE && node >= 0 && node < m_cap); return m_buf + node; } // NOLINT(readability-make-member-function-const) + //! An if-less form of get() that demands a valid node index. + //! This function is implementation only; use at your own risk. + NodeData const * _p(id_type node) const { _RYML_CB_ASSERT(m_callbacks, node != NONE && node >= 0 && node < m_cap); return m_buf + node; } + + //! Get the id of the root node + id_type root_id() { if(m_cap == 0) { reserve(16); } _RYML_CB_ASSERT(m_callbacks, m_cap > 0 && m_size > 0); return 0; } + //! Get the id of the root node + id_type root_id() const { _RYML_CB_ASSERT(m_callbacks, m_cap > 0 && m_size > 0); return 0; } + + //! Get a NodeRef of a node by id + NodeRef ref(id_type node); + //! Get a NodeRef of a node by id + ConstNodeRef ref(id_type node) const; + //! Get a NodeRef of a node by id + ConstNodeRef cref(id_type node) const; + + //! Get the root as a NodeRef + NodeRef rootref(); + //! Get the root as a ConstNodeRef + ConstNodeRef rootref() const; + //! Get the root as a ConstNodeRef + ConstNodeRef crootref() const; + + //! get the i-th document of the stream + //! @note @p i is NOT the node id, but the doc position within the stream + NodeRef docref(id_type i); + //! get the i-th document of the stream + //! @note @p i is NOT the node id, but the doc position within the stream + ConstNodeRef docref(id_type i) const; + //! get the i-th document of the stream + //! @note @p i is NOT the node id, but the doc position within the stream + ConstNodeRef cdocref(id_type i) const; + + //! find a root child by name, return it as a NodeRef + //! @note requires the root to be a map. + NodeRef operator[] (csubstr key); + //! find a root child by name, return it as a NodeRef + //! @note requires the root to be a map. + ConstNodeRef operator[] (csubstr key) const; + + //! find a root child by index: return the root node's @p i-th child as a NodeRef + //! @note @p i is NOT the node id, but the child's position + NodeRef operator[] (id_type i); + //! find a root child by index: return the root node's @p i-th child as a NodeRef + //! @note @p i is NOT the node id, but the child's position + ConstNodeRef operator[] (id_type i) const; + + /** @} */ + +public: + + /** @name node property getters */ + /** @{ */ + + NodeType type(id_type node) const { return _p(node)->m_type; } + const char* type_str(id_type node) const { return NodeType::type_str(_p(node)->m_type); } + + csubstr const& key (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); return _p(node)->m_key.scalar; } + csubstr const& key_tag (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key_tag(node)); return _p(node)->m_key.tag; } + csubstr const& key_ref (id_type node) const { _RYML_CB_ASSERT(m_callbacks, is_key_ref(node)); return _p(node)->m_key.anchor; } + csubstr const& key_anchor(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key_anchor(node)); return _p(node)->m_key.anchor; } + NodeScalar const& keysc (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); return _p(node)->m_key; } + + csubstr const& val (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); return _p(node)->m_val.scalar; } + csubstr const& val_tag (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val_tag(node)); return _p(node)->m_val.tag; } + csubstr const& val_ref (id_type node) const { _RYML_CB_ASSERT(m_callbacks, is_val_ref(node)); return _p(node)->m_val.anchor; } + csubstr const& val_anchor(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val_anchor(node)); return _p(node)->m_val.anchor; } + NodeScalar const& valsc (id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); return _p(node)->m_val; } + + /** @} */ + +public: + + /** @name node type predicates */ + /** @{ */ + + C4_ALWAYS_INLINE bool type_has_any(id_type node, NodeType_e bits) const { return _p(node)->m_type.has_any(bits); } + C4_ALWAYS_INLINE bool type_has_all(id_type node, NodeType_e bits) const { return _p(node)->m_type.has_all(bits); } + C4_ALWAYS_INLINE bool type_has_none(id_type node, NodeType_e bits) const { return _p(node)->m_type.has_none(bits); } + + C4_ALWAYS_INLINE bool is_stream(id_type node) const { return _p(node)->m_type.is_stream(); } + C4_ALWAYS_INLINE bool is_doc(id_type node) const { return _p(node)->m_type.is_doc(); } + C4_ALWAYS_INLINE bool is_container(id_type node) const { return _p(node)->m_type.is_container(); } + C4_ALWAYS_INLINE bool is_map(id_type node) const { return _p(node)->m_type.is_map(); } + C4_ALWAYS_INLINE bool is_seq(id_type node) const { return _p(node)->m_type.is_seq(); } + C4_ALWAYS_INLINE bool has_key(id_type node) const { return _p(node)->m_type.has_key(); } + C4_ALWAYS_INLINE bool has_val(id_type node) const { return _p(node)->m_type.has_val(); } + C4_ALWAYS_INLINE bool is_val(id_type node) const { return _p(node)->m_type.is_val(); } + C4_ALWAYS_INLINE bool is_keyval(id_type node) const { return _p(node)->m_type.is_keyval(); } + C4_ALWAYS_INLINE bool has_key_tag(id_type node) const { return _p(node)->m_type.has_key_tag(); } + C4_ALWAYS_INLINE bool has_val_tag(id_type node) const { return _p(node)->m_type.has_val_tag(); } + C4_ALWAYS_INLINE bool has_key_anchor(id_type node) const { return _p(node)->m_type.has_key_anchor(); } + C4_ALWAYS_INLINE bool has_val_anchor(id_type node) const { return _p(node)->m_type.has_val_anchor(); } + C4_ALWAYS_INLINE bool has_anchor(id_type node) const { return _p(node)->m_type.has_anchor(); } + C4_ALWAYS_INLINE bool is_key_ref(id_type node) const { return _p(node)->m_type.is_key_ref(); } + C4_ALWAYS_INLINE bool is_val_ref(id_type node) const { return _p(node)->m_type.is_val_ref(); } + C4_ALWAYS_INLINE bool is_ref(id_type node) const { return _p(node)->m_type.is_ref(); } + + C4_ALWAYS_INLINE bool parent_is_seq(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_parent(node)); return is_seq(_p(node)->m_parent); } + C4_ALWAYS_INLINE bool parent_is_map(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_parent(node)); return is_map(_p(node)->m_parent); } + + /** true when the node has an anchor named a */ + C4_ALWAYS_INLINE bool has_anchor(id_type node, csubstr a) const { return _p(node)->m_key.anchor == a || _p(node)->m_val.anchor == a; } + + /** true if the node key is empty, or its scalar verifies @ref scalar_is_null(). + * @warning the node must verify @ref Tree::has_key() (asserted) (ie must be a member of a map) + * @see https://github.com/biojppm/rapidyaml/issues/413 */ + C4_ALWAYS_INLINE bool key_is_null(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_key_quoted() && (n->m_type.key_is_null() || scalar_is_null(n->m_key.scalar)); } + /** true if the node val is empty, or its scalar verifies @ref scalar_is_null(). + * @warning the node must verify @ref Tree::has_val() (asserted) (ie must be a scalar / must not be a container) + * @see https://github.com/biojppm/rapidyaml/issues/413 */ + C4_ALWAYS_INLINE bool val_is_null(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_val_quoted() && (n->m_type.val_is_null() || scalar_is_null(n->m_val.scalar)); } + + /// true if the key was a scalar requiring filtering and was left + /// unfiltered during the parsing (see ParserOptions) + C4_ALWAYS_INLINE bool is_key_unfiltered(id_type node) const { return _p(node)->m_type.is_key_unfiltered(); } + /// true if the val was a scalar requiring filtering and was left + /// unfiltered during the parsing (see ParserOptions) + C4_ALWAYS_INLINE bool is_val_unfiltered(id_type node) const { return _p(node)->m_type.is_val_unfiltered(); } + + RYML_DEPRECATED("use has_key_anchor()") bool is_key_anchor(id_type node) const { return _p(node)->m_type.has_key_anchor(); } + RYML_DEPRECATED("use has_val_anchor()") bool is_val_anchor(id_type node) const { return _p(node)->m_type.has_val_anchor(); } + RYML_DEPRECATED("use has_anchor()") bool is_anchor(id_type node) const { return _p(node)->m_type.has_anchor(); } + RYML_DEPRECATED("use has_anchor_or_ref()") bool is_anchor_or_ref(id_type node) const { return _p(node)->m_type.has_anchor() || _p(node)->m_type.is_ref(); } + + /** @} */ + +public: + + /** @name hierarchy predicates */ + /** @{ */ + + bool is_root(id_type node) const { _RYML_CB_ASSERT(m_callbacks, _p(node)->m_parent != NONE || node == 0); return _p(node)->m_parent == NONE; } + + bool has_parent(id_type node) const { return _p(node)->m_parent != NONE; } + + /** true when ancestor is parent or parent of a parent of node */ + bool is_ancestor(id_type node, id_type ancestor) const; + + /** true when key and val are empty, and has no children */ + bool empty(id_type node) const { return ! has_children(node) && _p(node)->m_key.empty() && (( ! (_p(node)->m_type & VAL)) || _p(node)->m_val.empty()); } + + /** true if @p node has a child with id @p ch */ + bool has_child(id_type node, id_type ch) const { return _p(ch)->m_parent == node; } + /** true if @p node has a child with key @p key */ + bool has_child(id_type node, csubstr key) const { return find_child(node, key) != NONE; } + /** true if @p node has any children key */ + bool has_children(id_type node) const { return _p(node)->m_first_child != NONE; } + + /** true if @p node has a sibling with id @p sib */ + bool has_sibling(id_type node, id_type sib) const { return _p(node)->m_parent == _p(sib)->m_parent; } + /** true if one of the node's siblings has the given key */ + bool has_sibling(id_type node, csubstr key) const { return find_sibling(node, key) != NONE; } + /** true if node is not a single child */ + bool has_other_siblings(id_type node) const + { + NodeData const *n = _p(node); + if(C4_LIKELY(n->m_parent != NONE)) + { + n = _p(n->m_parent); + return n->m_first_child != n->m_last_child; + } + return false; + } + + RYML_DEPRECATED("use has_other_siblings()") static bool has_siblings(id_type /*node*/) { return true; } + + /** @} */ + +public: + + /** @name hierarchy getters */ + /** @{ */ + + id_type parent(id_type node) const { return _p(node)->m_parent; } + + id_type prev_sibling(id_type node) const { return _p(node)->m_prev_sibling; } + id_type next_sibling(id_type node) const { return _p(node)->m_next_sibling; } + + /** O(#num_children) */ + id_type num_children(id_type node) const; + id_type child_pos(id_type node, id_type ch) const; + id_type first_child(id_type node) const { return _p(node)->m_first_child; } + id_type last_child(id_type node) const { return _p(node)->m_last_child; } + id_type child(id_type node, id_type pos) const; + id_type find_child(id_type node, csubstr const& key) const; + + /** O(#num_siblings) */ + /** counts with this */ + id_type num_siblings(id_type node) const { return is_root(node) ? 1 : num_children(_p(node)->m_parent); } + /** does not count with this */ + id_type num_other_siblings(id_type node) const { id_type ns = num_siblings(node); _RYML_CB_ASSERT(m_callbacks, ns > 0); return ns-1; } + id_type sibling_pos(id_type node, id_type sib) const { _RYML_CB_ASSERT(m_callbacks, ! is_root(node) || node == root_id()); return child_pos(_p(node)->m_parent, sib); } + id_type first_sibling(id_type node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_first_child; } + id_type last_sibling(id_type node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_last_child; } + id_type sibling(id_type node, id_type pos) const { return child(_p(node)->m_parent, pos); } + id_type find_sibling(id_type node, csubstr const& key) const { return find_child(_p(node)->m_parent, key); } + + id_type doc(id_type i) const { id_type rid = root_id(); _RYML_CB_ASSERT(m_callbacks, is_stream(rid)); return child(rid, i); } //!< gets the @p i document node index. requires that the root node is a stream. + + id_type depth_asc(id_type node) const; /**< O(log(num_tree_nodes)) get the ascending depth of the node: number of levels between root and node */ + id_type depth_desc(id_type node) const; /**< O(num_tree_nodes) get the descending depth of the node: number of levels between node and deepest child */ + + /** @} */ + +public: + + /** @name node style predicates and modifiers. see the corresponding predicate in NodeType */ + /** @{ */ + + C4_ALWAYS_INLINE bool is_container_styled(id_type node) const { return _p(node)->m_type.is_container_styled(); } + C4_ALWAYS_INLINE bool is_block(id_type node) const { return _p(node)->m_type.is_block(); } + C4_ALWAYS_INLINE bool is_flow_sl(id_type node) const { return _p(node)->m_type.is_flow_sl(); } + C4_ALWAYS_INLINE bool is_flow_ml(id_type node) const { return _p(node)->m_type.is_flow_ml(); } + C4_ALWAYS_INLINE bool is_flow(id_type node) const { return _p(node)->m_type.is_flow(); } + + C4_ALWAYS_INLINE bool is_key_styled(id_type node) const { return _p(node)->m_type.is_key_styled(); } + C4_ALWAYS_INLINE bool is_val_styled(id_type node) const { return _p(node)->m_type.is_val_styled(); } + C4_ALWAYS_INLINE bool is_key_literal(id_type node) const { return _p(node)->m_type.is_key_literal(); } + C4_ALWAYS_INLINE bool is_val_literal(id_type node) const { return _p(node)->m_type.is_val_literal(); } + C4_ALWAYS_INLINE bool is_key_folded(id_type node) const { return _p(node)->m_type.is_key_folded(); } + C4_ALWAYS_INLINE bool is_val_folded(id_type node) const { return _p(node)->m_type.is_val_folded(); } + C4_ALWAYS_INLINE bool is_key_squo(id_type node) const { return _p(node)->m_type.is_key_squo(); } + C4_ALWAYS_INLINE bool is_val_squo(id_type node) const { return _p(node)->m_type.is_val_squo(); } + C4_ALWAYS_INLINE bool is_key_dquo(id_type node) const { return _p(node)->m_type.is_key_dquo(); } + C4_ALWAYS_INLINE bool is_val_dquo(id_type node) const { return _p(node)->m_type.is_val_dquo(); } + C4_ALWAYS_INLINE bool is_key_plain(id_type node) const { return _p(node)->m_type.is_key_plain(); } + C4_ALWAYS_INLINE bool is_val_plain(id_type node) const { return _p(node)->m_type.is_val_plain(); } + C4_ALWAYS_INLINE bool is_key_quoted(id_type node) const { return _p(node)->m_type.is_key_quoted(); } + C4_ALWAYS_INLINE bool is_val_quoted(id_type node) const { return _p(node)->m_type.is_val_quoted(); } + C4_ALWAYS_INLINE bool is_quoted(id_type node) const { return _p(node)->m_type.is_quoted(); } + + C4_ALWAYS_INLINE NodeType key_style(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); return _p(node)->m_type.key_style(); } + C4_ALWAYS_INLINE NodeType val_style(id_type node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node) || is_root(node)); return _p(node)->m_type.val_style(); } + + C4_ALWAYS_INLINE void set_container_style(id_type node, NodeType_e style) { _RYML_CB_ASSERT(m_callbacks, is_container(node)); _p(node)->m_type.set_container_style(style); } + C4_ALWAYS_INLINE void set_key_style(id_type node, NodeType_e style) { _RYML_CB_ASSERT(m_callbacks, has_key(node)); _p(node)->m_type.set_key_style(style); } + C4_ALWAYS_INLINE void set_val_style(id_type node, NodeType_e style) { _RYML_CB_ASSERT(m_callbacks, has_val(node)); _p(node)->m_type.set_val_style(style); } + + void clear_style(id_type node, bool recurse=false); + void set_style_conditionally(id_type node, + NodeType type_mask, + NodeType rem_style_flags, + NodeType add_style_flags, + bool recurse=false); + /** @} */ + +public: + + /** @name node type modifiers */ + /** @{ */ + + void to_keyval(id_type node, csubstr key, csubstr val, type_bits more_flags=0); + void to_map(id_type node, csubstr key, type_bits more_flags=0); + void to_seq(id_type node, csubstr key, type_bits more_flags=0); + void to_val(id_type node, csubstr val, type_bits more_flags=0); + void to_map(id_type node, type_bits more_flags=0); + void to_seq(id_type node, type_bits more_flags=0); + void to_doc(id_type node, type_bits more_flags=0); + void to_stream(id_type node, type_bits more_flags=0); + + void set_key(id_type node, csubstr key) { _RYML_CB_ASSERT(m_callbacks, has_key(node)); _p(node)->m_key.scalar = key; } + void set_val(id_type node, csubstr val) { _RYML_CB_ASSERT(m_callbacks, has_val(node)); _p(node)->m_val.scalar = val; } + + void set_key_tag(id_type node, csubstr tag) { _RYML_CB_ASSERT(m_callbacks, has_key(node)); _p(node)->m_key.tag = tag; _add_flags(node, KEYTAG); } + void set_val_tag(id_type node, csubstr tag) { _RYML_CB_ASSERT(m_callbacks, has_val(node) || is_container(node)); _p(node)->m_val.tag = tag; _add_flags(node, VALTAG); } + + void set_key_anchor(id_type node, csubstr anchor) { _RYML_CB_ASSERT(m_callbacks, ! is_key_ref(node)); _p(node)->m_key.anchor = anchor.triml('&'); _add_flags(node, KEYANCH); } + void set_val_anchor(id_type node, csubstr anchor) { _RYML_CB_ASSERT(m_callbacks, ! is_val_ref(node)); _p(node)->m_val.anchor = anchor.triml('&'); _add_flags(node, VALANCH); } + void set_key_ref (id_type node, csubstr ref ) { _RYML_CB_ASSERT(m_callbacks, ! has_key_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_key.set_ref_maybe_replacing_scalar(ref, n->m_type.has_key()); _add_flags(node, KEY|KEYREF); } + void set_val_ref (id_type node, csubstr ref ) { _RYML_CB_ASSERT(m_callbacks, ! has_val_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_val.set_ref_maybe_replacing_scalar(ref, n->m_type.has_val()); _add_flags(node, VAL|VALREF); } + + void rem_key_anchor(id_type node) { _p(node)->m_key.anchor.clear(); _rem_flags(node, KEYANCH); } + void rem_val_anchor(id_type node) { _p(node)->m_val.anchor.clear(); _rem_flags(node, VALANCH); } + void rem_key_ref (id_type node) { _p(node)->m_key.anchor.clear(); _rem_flags(node, KEYREF); } + void rem_val_ref (id_type node) { _p(node)->m_val.anchor.clear(); _rem_flags(node, VALREF); } + void rem_anchor_ref(id_type node) { _p(node)->m_key.anchor.clear(); _p(node)->m_val.anchor.clear(); _rem_flags(node, KEYANCH|VALANCH|KEYREF|VALREF); } + + /** @} */ + +public: + + /** @name tree modifiers */ + /** @{ */ + + /** reorder the tree in memory so that all the nodes are stored + * in a linear sequence when visited in depth-first order. + * This will invalidate existing ids, since the node id is its + * position in the tree's node array. */ + void reorder(); + + /** @} */ + +public: + + /** @name anchors and references/aliases */ + /** @{ */ + + /** Resolve references (aliases <- anchors), by forwarding to @ref + * ReferenceResolver::resolve(); refer to @ref + * ReferenceResolver::resolve() for further details. */ + void resolve(ReferenceResolver *C4_RESTRICT rr, bool clear_anchors=true); + + /** Resolve references (aliases <- anchors), by forwarding to @ref + * ReferenceResolver::resolve(); refer to @ref + * ReferenceResolver::resolve() for further details. This overload + * uses a throwaway resolver object. */ + void resolve(bool clear_anchors=true); + + /** @} */ + +public: + + /** @name tag directives */ + /** @{ */ + + void resolve_tags(); + void normalize_tags(); + void normalize_tags_long(); + + id_type num_tag_directives() const; + bool add_tag_directive(csubstr directive); + id_type add_tag_directive(TagDirective const& td); + void clear_tag_directives(); + + /** resolve the given tag, appearing at node_id. Write the result into output. + * @return the number of characters required for the resolved tag */ + size_t resolve_tag(substr output, csubstr tag, id_type node_id) const; + csubstr resolve_tag_sub(substr output, csubstr tag, id_type node_id) const + { + size_t needed = resolve_tag(output, tag, node_id); + return needed <= output.len ? output.first(needed) : output; + } + + TagDirective const* begin_tag_directives() const { return m_tag_directives; } + TagDirective const* end_tag_directives() const { return m_tag_directives + num_tag_directives(); } + c4::yml::TagDirectiveRange tag_directives() const { return c4::yml::TagDirectiveRange{begin_tag_directives(), end_tag_directives()}; } + + RYML_DEPRECATED("use c4::yml::tag_directive_const_iterator") typedef TagDirective const* tag_directive_const_iterator; + RYML_DEPRECATED("use c4::yml::TagDirectiveRange") typedef c4::yml::TagDirectiveRange TagDirectiveProxy; + + /** @} */ + +public: + + /** @name modifying hierarchy */ + /** @{ */ + + /** create and insert a new child of @p parent. insert after the (to-be) + * sibling @p after, which must be a child of @p parent. To insert as the + * first child, set after to NONE */ + C4_ALWAYS_INLINE id_type insert_child(id_type parent, id_type after) + { + _RYML_CB_ASSERT(m_callbacks, parent != NONE); + _RYML_CB_ASSERT(m_callbacks, is_container(parent) || is_root(parent)); + _RYML_CB_ASSERT(m_callbacks, after == NONE || (_p(after)->m_parent == parent)); + id_type child = _claim(); + _set_hierarchy(child, parent, after); + return child; + } + /** create and insert a node as the first child of @p parent */ + C4_ALWAYS_INLINE id_type prepend_child(id_type parent) { return insert_child(parent, NONE); } + /** create and insert a node as the last child of @p parent */ + C4_ALWAYS_INLINE id_type append_child(id_type parent) { return insert_child(parent, _p(parent)->m_last_child); } + C4_ALWAYS_INLINE id_type _append_child__unprotected(id_type parent) + { + id_type child = _claim(); + _set_hierarchy(child, parent, _p(parent)->m_last_child); + return child; + } + +public: + + #if defined(__clang__) + # pragma clang diagnostic push + # pragma clang diagnostic ignored "-Wnull-dereference" + #elif defined(__GNUC__) + # pragma GCC diagnostic push + # if __GNUC__ >= 6 + # pragma GCC diagnostic ignored "-Wnull-dereference" + # endif + #endif + + //! create and insert a new sibling of n. insert after "after" + C4_ALWAYS_INLINE id_type insert_sibling(id_type node, id_type after) + { + return insert_child(_p(node)->m_parent, after); + } + /** create and insert a node as the first node of @p parent */ + C4_ALWAYS_INLINE id_type prepend_sibling(id_type node) { return prepend_child(_p(node)->m_parent); } + C4_ALWAYS_INLINE id_type append_sibling(id_type node) { return append_child(_p(node)->m_parent); } + +public: + + /** remove an entire branch at once: ie remove the children and the node itself */ + void remove(id_type node) + { + remove_children(node); + _release(node); + } + + /** remove all the node's children, but keep the node itself */ + void remove_children(id_type node); + + /** change the @p type of the node to one of MAP, SEQ or VAL. @p + * type must have one and only one of MAP,SEQ,VAL; @p type may + * possibly have KEY, but if it does, then the @p node must also + * have KEY. Changing to the same type is a no-op. Otherwise, + * changing to a different type will initialize the node with an + * empty value of the desired type: changing to VAL will + * initialize with a null scalar (~), changing to MAP will + * initialize with an empty map ({}), and changing to SEQ will + * initialize with an empty seq ([]). */ + bool change_type(id_type node, NodeType type); + + bool change_type(id_type node, type_bits type) + { + return change_type(node, (NodeType)type); + } + + #if defined(__clang__) + # pragma clang diagnostic pop + #elif defined(__GNUC__) + # pragma GCC diagnostic pop + #endif + +public: + + /** change the node's position in the parent */ + void move(id_type node, id_type after); + + /** change the node's parent and position */ + void move(id_type node, id_type new_parent, id_type after); + + /** change the node's parent and position to a different tree + * @return the index of the new node in the destination tree */ + id_type move(Tree * src, id_type node, id_type new_parent, id_type after); + + /** ensure the first node is a stream. Eg, change this tree + * + * DOCMAP + * MAP + * KEYVAL + * KEYVAL + * SEQ + * VAL + * + * to + * + * STREAM + * DOCMAP + * MAP + * KEYVAL + * KEYVAL + * SEQ + * VAL + * + * If the root is already a stream, this is a no-op. + */ + void set_root_as_stream(); + +public: + + /** recursively duplicate a node from this tree into a new parent, + * placing it after one of its children + * @return the index of the copy */ + id_type duplicate(id_type node, id_type new_parent, id_type after); + /** recursively duplicate a node from a different tree into a new parent, + * placing it after one of its children + * @return the index of the copy */ + id_type duplicate(Tree const* src, id_type node, id_type new_parent, id_type after); + + /** recursively duplicate the node's children (but not the node) + * @return the index of the last duplicated child */ + id_type duplicate_children(id_type node, id_type parent, id_type after); + /** recursively duplicate the node's children (but not the node), where + * the node is from a different tree + * @return the index of the last duplicated child */ + id_type duplicate_children(Tree const* src, id_type node, id_type parent, id_type after); + + /** duplicate the node's children (but not the node) in a new parent, but + * omit repetitions where a duplicated node has the same key (in maps) or + * value (in seqs). If one of the duplicated children has the same key + * (in maps) or value (in seqs) as one of the parent's children, the one + * that is placed closest to the end will prevail. */ + id_type duplicate_children_no_rep(id_type node, id_type parent, id_type after); + id_type duplicate_children_no_rep(Tree const* src, id_type node, id_type parent, id_type after); + + void duplicate_contents(id_type node, id_type where); + void duplicate_contents(Tree const* src, id_type node, id_type where); + +public: + + void merge_with(Tree const* src, id_type src_node=NONE, id_type dst_root=NONE); + + /** @} */ + +public: + + /** @name locations */ + /** @{ */ + + /** Get the location of a node from the parse used to parse this tree. */ + Location location(Parser const& p, id_type node) const; + +private: + + bool _location_from_node(Parser const& p, id_type node, Location *C4_RESTRICT loc, id_type level) const; + bool _location_from_cont(Parser const& p, id_type node, Location *C4_RESTRICT loc) const; + + /** @} */ + +public: + + /** @name internal string arena */ + /** @{ */ + + /** get the current size of the tree's internal arena */ + RYML_DEPRECATED("use arena_size() instead") size_t arena_pos() const { return m_arena_pos; } + /** get the current size of the tree's internal arena */ + size_t arena_size() const { return m_arena_pos; } + /** get the current capacity of the tree's internal arena */ + size_t arena_capacity() const { return m_arena.len; } + /** get the current slack of the tree's internal arena */ + size_t arena_slack() const { _RYML_CB_ASSERT(m_callbacks, m_arena.len >= m_arena_pos); return m_arena.len - m_arena_pos; } + + /** get the current arena */ + csubstr arena() const { return m_arena.first(m_arena_pos); } + /** get the current arena */ + substr arena() { return m_arena.first(m_arena_pos); } // NOLINT(readability-make-member-function-const) + + /** return true if the given substring is part of the tree's string arena */ + bool in_arena(csubstr s) const + { + return m_arena.is_super(s); + } + + /** serialize the given floating-point variable to the tree's + * arena, growing it as needed to accomodate the serialization. + * + * @note Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual + * nodes, and thus cost O(numnodes)+O(arenasize). To avoid this + * cost, ensure that the arena is reserved to an appropriate size + * using @ref Tree::reserve_arena(). + * + * @see alloc_arena() */ + template + auto to_arena(T const& C4_RESTRICT a) + -> typename std::enable_if::value, csubstr>::type + { + substr rem(m_arena.sub(m_arena_pos)); + size_t num = to_chars_float(rem, a); + if(num > rem.len) + { + rem = _grow_arena(num); + num = to_chars_float(rem, a); + _RYML_CB_ASSERT(m_callbacks, num <= rem.len); + } + rem = _request_span(num); + return rem; + } + + /** serialize the given non-floating-point variable to the tree's + * arena, growing it as needed to accomodate the serialization. + * + * @note Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual + * nodes, and thus cost O(numnodes)+O(arenasize). To avoid this + * cost, ensure that the arena is reserved to an appropriate size + * using @ref Tree::reserve_arena(). + * + * @see alloc_arena() */ + template + auto to_arena(T const& C4_RESTRICT a) + -> typename std::enable_if::value, csubstr>::type + { + substr rem(m_arena.sub(m_arena_pos)); + size_t num = to_chars(rem, a); + if(num > rem.len) + { + rem = _grow_arena(num); + num = to_chars(rem, a); + _RYML_CB_ASSERT(m_callbacks, num <= rem.len); + } + rem = _request_span(num); + return rem; + } + + /** serialize the given csubstr to the tree's arena, growing the + * arena as needed to accomodate the serialization. + * + * @note Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual + * nodes, and thus cost O(numnodes)+O(arenasize). To avoid this + * cost, ensure that the arena is reserved to an appropriate size + * using @ref Tree::reserve_arena(). + * + * @see alloc_arena() */ + csubstr to_arena(csubstr a) + { + if(a.len > 0) + { + substr rem(m_arena.sub(m_arena_pos)); + size_t num = to_chars(rem, a); + if(num > rem.len) + { + rem = _grow_arena(num); + num = to_chars(rem, a); + _RYML_CB_ASSERT(m_callbacks, num <= rem.len); + } + return _request_span(num); + } + else + { + if(a.str == nullptr) + { + return csubstr{}; + } + else if(m_arena.str == nullptr) + { + // Arena is empty and we want to store a non-null + // zero-length string. + // Even though the string has zero length, we need + // some "memory" to store a non-nullptr string + _grow_arena(1); + } + return _request_span(0); + } + } + C4_ALWAYS_INLINE csubstr to_arena(const char *s) + { + return to_arena(to_csubstr(s)); + } + C4_ALWAYS_INLINE static csubstr to_arena(std::nullptr_t) + { + return csubstr{}; + } + + /** copy the given substr to the tree's arena, growing it by the + * required size + * + * @note Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual + * nodes, and thus cost O(numnodes)+O(arenasize). To avoid this + * cost, ensure that the arena is reserved to an appropriate size + * before using @ref Tree::reserve_arena() + * + * @see reserve_arena() + * @see alloc_arena() + */ + substr copy_to_arena(csubstr s) + { + substr cp = alloc_arena(s.len); + _RYML_CB_ASSERT(m_callbacks, cp.len == s.len); + _RYML_CB_ASSERT(m_callbacks, !s.overlaps(cp)); + #if (!defined(__clang__)) && (defined(__GNUC__) && __GNUC__ >= 10) + C4_SUPPRESS_WARNING_GCC_PUSH + C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow=") // no need for terminating \0 + C4_SUPPRESS_WARNING_GCC("-Wrestrict") // there's an assert to ensure no violation of restrict behavior + #endif + if(s.len) + memcpy(cp.str, s.str, s.len); + #if (!defined(__clang__)) && (defined(__GNUC__) && __GNUC__ >= 10) + C4_SUPPRESS_WARNING_GCC_POP + #endif + return cp; + } + + /** grow the tree's string arena by the given size and return a substr + * of the added portion + * + * @note Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual + * nodes, and thus cost O(numnodes)+O(arenasize). To avoid this + * cost, ensure that the arena is reserved to an appropriate size + * using .reserve_arena(). + * + * @see reserve_arena() */ + substr alloc_arena(size_t sz) + { + if(sz > arena_slack()) + _grow_arena(sz - arena_slack()); + substr s = _request_span(sz); + return s; + } + + /** ensure the tree's internal string arena is at least the given capacity + * @warning This operation may be expensive, with a potential complexity of O(numNodes)+O(arenasize). + * @warning Growing the arena may cause relocation of the entire + * existing arena, and thus change the contents of individual nodes. */ + void reserve_arena(size_t arena_cap) + { + if(arena_cap > m_arena.len) + { + substr buf; + buf.str = (char*) m_callbacks.m_allocate(arena_cap, m_arena.str, m_callbacks.m_user_data); + buf.len = arena_cap; + if(m_arena.str) + { + _RYML_CB_ASSERT(m_callbacks, m_arena.len >= 0); + _relocate(buf); // does a memcpy and changes nodes using the arena + m_callbacks.m_free(m_arena.str, m_arena.len, m_callbacks.m_user_data); + } + m_arena = buf; + } + } + + /** @} */ + +private: + + substr _grow_arena(size_t more) + { + size_t cap = m_arena.len + more; + cap = cap < 2 * m_arena.len ? 2 * m_arena.len : cap; + cap = cap < 64 ? 64 : cap; + reserve_arena(cap); + return m_arena.sub(m_arena_pos); + } + + substr _request_span(size_t sz) + { + _RYML_CB_ASSERT(m_callbacks, m_arena_pos + sz <= m_arena.len); + substr s; + s = m_arena.sub(m_arena_pos, sz); + m_arena_pos += sz; + return s; + } + + substr _relocated(csubstr s, substr next_arena) const + { + _RYML_CB_ASSERT(m_callbacks, m_arena.is_super(s)); + _RYML_CB_ASSERT(m_callbacks, m_arena.sub(0, m_arena_pos).is_super(s)); + auto pos = (s.str - m_arena.str); // this is larger than 0 based on the assertions above + substr r(next_arena.str + pos, s.len); + _RYML_CB_ASSERT(m_callbacks, r.str - next_arena.str == pos); + _RYML_CB_ASSERT(m_callbacks, next_arena.sub(0, m_arena_pos).is_super(r)); + return r; + } + +public: + + /** @name lookup */ + /** @{ */ + + struct lookup_result + { + id_type target; + id_type closest; + size_t path_pos; + csubstr path; + + operator bool() const { return target != NONE; } + + lookup_result() : target(NONE), closest(NONE), path_pos(0), path() {} + lookup_result(csubstr path_, id_type start) : target(NONE), closest(start), path_pos(0), path(path_) {} + + /** get the part ot the input path that was resolved */ + csubstr resolved() const; + /** get the part ot the input path that was unresolved */ + csubstr unresolved() const; + }; + + /** for example foo.bar[0].baz */ + lookup_result lookup_path(csubstr path, id_type start=NONE) const; + + /** defaulted lookup: lookup @p path; if the lookup fails, recursively modify + * the tree so that the corresponding lookup_path() would return the + * default value. + * @see lookup_path() */ + id_type lookup_path_or_modify(csubstr default_value, csubstr path, id_type start=NONE); + + /** defaulted lookup: lookup @p path; if the lookup fails, recursively modify + * the tree so that the corresponding lookup_path() would return the + * branch @p src_node (from the tree @p src). + * @see lookup_path() */ + id_type lookup_path_or_modify(Tree const *src, id_type src_node, csubstr path, id_type start=NONE); + + /** @} */ + +private: + + struct _lookup_path_token + { + csubstr value; + NodeType type; + _lookup_path_token() : value(), type() {} + _lookup_path_token(csubstr v, NodeType t) : value(v), type(t) {} + operator bool() const { return type != NOTYPE; } + bool is_index() const { return value.begins_with('[') && value.ends_with(']'); } + }; + + id_type _lookup_path_or_create(csubstr path, id_type start); + + void _lookup_path (lookup_result *r) const; + void _lookup_path_modify(lookup_result *r); + + id_type _next_node (lookup_result *r, _lookup_path_token *parent) const; + id_type _next_node_modify(lookup_result *r, _lookup_path_token *parent); + + static void _advance(lookup_result *r, size_t more); + + _lookup_path_token _next_token(lookup_result *r, _lookup_path_token const& parent) const; + +private: + + void _clear(); + void _free(); + void _copy(Tree const& that); + void _move(Tree & that) noexcept; + + void _relocate(substr next_arena); + +public: + + /** @cond dev*/ + + #if ! RYML_USE_ASSERT + C4_ALWAYS_INLINE void _check_next_flags(id_type, type_bits) {} + #else + void _check_next_flags(id_type node, type_bits f) + { + NodeData *n = _p(node); + type_bits o = n->m_type; // old + C4_UNUSED(o); + if(f & MAP) + { + RYML_ASSERT_MSG((f & SEQ) == 0, "cannot mark simultaneously as map and seq"); + RYML_ASSERT_MSG((f & VAL) == 0, "cannot mark simultaneously as map and val"); + RYML_ASSERT_MSG((o & SEQ) == 0, "cannot turn a seq into a map; clear first"); + RYML_ASSERT_MSG((o & VAL) == 0, "cannot turn a val into a map; clear first"); + } + else if(f & SEQ) + { + RYML_ASSERT_MSG((f & MAP) == 0, "cannot mark simultaneously as seq and map"); + RYML_ASSERT_MSG((f & VAL) == 0, "cannot mark simultaneously as seq and val"); + RYML_ASSERT_MSG((o & MAP) == 0, "cannot turn a map into a seq; clear first"); + RYML_ASSERT_MSG((o & VAL) == 0, "cannot turn a val into a seq; clear first"); + } + if(f & KEY) + { + _RYML_CB_ASSERT(m_callbacks, !is_root(node)); + auto pid = parent(node); C4_UNUSED(pid); + _RYML_CB_ASSERT(m_callbacks, is_map(pid)); + } + if((f & VAL) && !is_root(node)) + { + auto pid = parent(node); C4_UNUSED(pid); + _RYML_CB_ASSERT(m_callbacks, is_map(pid) || is_seq(pid)); + } + } + #endif + + void _set_flags(id_type node, NodeType_e f) { _check_next_flags(node, f); _p(node)->m_type = f; } + void _set_flags(id_type node, type_bits f) { _check_next_flags(node, f); _p(node)->m_type = f; } + + void _add_flags(id_type node, NodeType_e f) { NodeData *d = _p(node); type_bits fb = f | d->m_type; _check_next_flags(node, fb); d->m_type = (NodeType_e) fb; } + void _add_flags(id_type node, type_bits f) { NodeData *d = _p(node); f |= d->m_type; _check_next_flags(node, f); d->m_type = f; } + + void _rem_flags(id_type node, NodeType_e f) { NodeData *d = _p(node); type_bits fb = d->m_type & ~f; _check_next_flags(node, fb); d->m_type = (NodeType_e) fb; } + void _rem_flags(id_type node, type_bits f) { NodeData *d = _p(node); f = d->m_type & ~f; _check_next_flags(node, f); d->m_type = f; } + + void _set_key(id_type node, csubstr key, type_bits more_flags=0) + { + _p(node)->m_key.scalar = key; + _add_flags(node, KEY|more_flags); + } + void _set_key(id_type node, NodeScalar const& key, type_bits more_flags=0) + { + _p(node)->m_key = key; + _add_flags(node, KEY|more_flags); + } + + void _set_val(id_type node, csubstr val, type_bits more_flags=0) + { + _RYML_CB_ASSERT(m_callbacks, num_children(node) == 0); + _RYML_CB_ASSERT(m_callbacks, !is_seq(node) && !is_map(node)); + _p(node)->m_val.scalar = val; + _add_flags(node, VAL|more_flags); + } + void _set_val(id_type node, NodeScalar const& val, type_bits more_flags=0) + { + _RYML_CB_ASSERT(m_callbacks, num_children(node) == 0); + _RYML_CB_ASSERT(m_callbacks, ! is_container(node)); + _p(node)->m_val = val; + _add_flags(node, VAL|more_flags); + } + + void _set(id_type node, NodeInit const& i) + { + _RYML_CB_ASSERT(m_callbacks, i._check()); + NodeData *n = _p(node); + _RYML_CB_ASSERT(m_callbacks, n->m_key.scalar.empty() || i.key.scalar.empty() || i.key.scalar == n->m_key.scalar); + _add_flags(node, i.type); + if(n->m_key.scalar.empty()) + { + if( ! i.key.scalar.empty()) + { + _set_key(node, i.key.scalar); + } + } + n->m_key.tag = i.key.tag; + n->m_val = i.val; + } + + void _set_parent_as_container_if_needed(id_type in) + { + NodeData const* n = _p(in); + id_type ip = parent(in); + if(ip != NONE) + { + if( ! (is_seq(ip) || is_map(ip))) + { + if((in == first_child(ip)) && (in == last_child(ip))) + { + if( ! n->m_key.empty() || has_key(in)) + { + _add_flags(ip, MAP); + } + else + { + _add_flags(ip, SEQ); + } + } + } + } + } + + void _seq2map(id_type node) + { + _RYML_CB_ASSERT(m_callbacks, is_seq(node)); + for(id_type i = first_child(node); i != NONE; i = next_sibling(i)) + { + NodeData *C4_RESTRICT ch = _p(i); + if(ch->m_type.is_keyval()) + continue; + ch->m_type.add(KEY); + ch->m_key = ch->m_val; + } + auto *C4_RESTRICT n = _p(node); + n->m_type.rem(SEQ); + n->m_type.add(MAP); + } + + id_type _do_reorder(id_type *node, id_type count); + + void _swap(id_type n_, id_type m_); + void _swap_props(id_type n_, id_type m_); + void _swap_hierarchy(id_type n_, id_type m_); + void _copy_hierarchy(id_type dst_, id_type src_); + + void _copy_props(id_type dst_, id_type src_) + { + _copy_props(dst_, this, src_); + } + + void _copy_props_wo_key(id_type dst_, id_type src_) + { + _copy_props_wo_key(dst_, this, src_); + } + + void _copy_props(id_type dst_, Tree const* that_tree, id_type src_) + { + auto & C4_RESTRICT dst = *_p(dst_); + auto const& C4_RESTRICT src = *that_tree->_p(src_); + dst.m_type = src.m_type; + dst.m_key = src.m_key; + dst.m_val = src.m_val; + } + + void _copy_props(id_type dst_, Tree const* that_tree, id_type src_, type_bits src_mask) + { + auto & C4_RESTRICT dst = *_p(dst_); + auto const& C4_RESTRICT src = *that_tree->_p(src_); + dst.m_type = (src.m_type & src_mask) | (dst.m_type & ~src_mask); + dst.m_key = src.m_key; + dst.m_val = src.m_val; + } + + void _copy_props_wo_key(id_type dst_, Tree const* that_tree, id_type src_) + { + auto & C4_RESTRICT dst = *_p(dst_); + auto const& C4_RESTRICT src = *that_tree->_p(src_); + dst.m_type = (src.m_type & ~_KEYMASK) | (dst.m_type & _KEYMASK); + dst.m_val = src.m_val; + } + + void _copy_props_wo_key(id_type dst_, Tree const* that_tree, id_type src_, type_bits src_mask) + { + auto & C4_RESTRICT dst = *_p(dst_); + auto const& C4_RESTRICT src = *that_tree->_p(src_); + dst.m_type = (src.m_type & ((~_KEYMASK)|src_mask)) | (dst.m_type & (_KEYMASK|~src_mask)); + dst.m_val = src.m_val; + } + + void _clear_type(id_type node) + { + _p(node)->m_type = NOTYPE; + } + + void _clear(id_type node) + { + auto *C4_RESTRICT n = _p(node); + n->m_type = NOTYPE; + n->m_key.clear(); + n->m_val.clear(); + n->m_parent = NONE; + n->m_first_child = NONE; + n->m_last_child = NONE; + } + + void _clear_key(id_type node) + { + _p(node)->m_key.clear(); + _rem_flags(node, KEY); + } + + void _clear_val(id_type node) + { + _p(node)->m_val.clear(); + _rem_flags(node, VAL); + } + + /** @endcond */ + +private: + + void _clear_range(id_type first, id_type num); + +public: + id_type _claim(); +private: + void _claim_root(); + void _release(id_type node); + void _free_list_add(id_type node); + void _free_list_rem(id_type node); + + void _set_hierarchy(id_type node, id_type parent, id_type after_sibling); + void _rem_hierarchy(id_type node); + +public: + + // members are exposed, but you should NOT access them directly + + NodeData *m_buf; + id_type m_cap; + + id_type m_size; + + id_type m_free_head; + id_type m_free_tail; + + substr m_arena; + size_t m_arena_pos; + + Callbacks m_callbacks; + + TagDirective m_tag_directives[RYML_MAX_TAG_DIRECTIVES]; + +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +/** @defgroup doc_serialization_helpers Serialization helpers + * + * @{ + */ + + +// NON-ARITHMETIC ------------------------------------------------------------- + +/** convert the val of a scalar node to a particular non-arithmetic + * non-float type, by forwarding its val to @ref from_chars(). The + * full string is used. + * @return false if the conversion failed, or if the key was empty and unquoted */ +template +inline auto read(Tree const* C4_RESTRICT tree, id_type id, T *v) + -> typename std::enable_if::value, bool>::type +{ + return C4_LIKELY(!(tree->type(id) & VALNIL)) ? from_chars(tree->val(id), v) : false; +} + +/** convert the key of a node to a particular non-arithmetic + * non-float type, by forwarding its key to @ref from_chars(). The + * full string is used. + * @return false if the conversion failed, or if the key was empty and unquoted */ +template +inline auto readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) + -> typename std::enable_if::value, bool>::type +{ + return C4_LIKELY(!(tree->type(id) & KEYNIL)) ? from_chars(tree->key(id), v) : false; +} + + +// INTEGRAL, NOT FLOATING ------------------------------------------------------------- + +/** convert the val of a scalar node to a particular arithmetic + * integral non-float type, by forwarding its val to @ref + * from_chars(). The full string is used. + * + * @return false if the conversion failed */ +template +inline auto read(Tree const* C4_RESTRICT tree, id_type id, T *v) + -> typename std::enable_if::value && !std::is_floating_point::value, bool>::type +{ + using U = typename std::remove_cv::type; + enum { ischar = std::is_same::value || std::is_same::value || std::is_same::value }; + csubstr val = tree->val(id); + NodeType ty = tree->type(id); + if(C4_UNLIKELY((ty & VALNIL) || val.empty())) + return false; + // quote integral numbers if they have a leading 0 + // https://github.com/biojppm/rapidyaml/issues/291 + char first = val[0]; + if(ty.is_val_quoted() && (first != '0' && !ischar)) + return false; + else if(first == '+') + val = val.sub(1); + return from_chars(val, v); +} + +/** convert the key of a node to a particular arithmetic + * integral non-float type, by forwarding its val to @ref + * from_chars(). The full string is used. + * + * @return false if the conversion failed */ +template +inline auto readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) + -> typename std::enable_if::value && !std::is_floating_point::value, bool>::type +{ + using U = typename std::remove_cv::type; + enum { ischar = std::is_same::value || std::is_same::value || std::is_same::value }; + csubstr key = tree->key(id); + NodeType ty = tree->type(id); + if((ty & KEYNIL) || key.empty()) + return false; + // quote integral numbers if they have a leading 0 + // https://github.com/biojppm/rapidyaml/issues/291 + char first = key[0]; + if(ty.is_key_quoted() && (first != '0' && !ischar)) + return false; + else if(first == '+') + key = key.sub(1); + return from_chars(key, v); +} + + +// FLOATING ------------------------------------------------------------- + +/** encode a floating point value to a string. */ +template +size_t to_chars_float(substr buf, T val) +{ + static_assert(std::is_floating_point::value, "must be floating point"); + C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wfloat-equal"); + if(C4_UNLIKELY(std::isnan(val))) + return to_chars(buf, csubstr(".nan")); + else if(C4_UNLIKELY(val == std::numeric_limits::infinity())) + return to_chars(buf, csubstr(".inf")); + else if(C4_UNLIKELY(val == -std::numeric_limits::infinity())) + return to_chars(buf, csubstr("-.inf")); + return to_chars(buf, val); + C4_SUPPRESS_WARNING_GCC_CLANG_POP +} + + +/** decode a floating point from string. Accepts special values: .nan, + * .inf, -.inf */ +template +bool from_chars_float(csubstr buf, T *C4_RESTRICT val) +{ + static_assert(std::is_floating_point::value, "must be floating point"); + if(buf.begins_with('+')) + { + buf = buf.sub(1); + } + if(C4_LIKELY(from_chars(buf, val))) + { + return true; + } + else if(C4_UNLIKELY(buf == ".nan" || buf == ".NaN" || buf == ".NAN")) + { + *val = std::numeric_limits::quiet_NaN(); + return true; + } + else if(C4_UNLIKELY(buf == ".inf" || buf == ".Inf" || buf == ".INF")) + { + *val = std::numeric_limits::infinity(); + return true; + } + else if(C4_UNLIKELY(buf == "-.inf" || buf == "-.Inf" || buf == "-.INF")) + { + *val = -std::numeric_limits::infinity(); + return true; + } + else + { + return false; + } +} + +/** convert the val of a scalar node to a floating point type, by + * forwarding its val to @ref from_chars_float(). + * + * @return false if the conversion failed + * + * @warning Unlike non-floating types, only the leading part of the + * string that may constitute a number is processed. This happens + * because the float parsing is delegated to fast_float, which is + * implemented that way. Consequently, for example, all of `"34"`, + * `"34 "` `"34hg"` `"34 gh"` will be read as 34. If you are not sure + * about the contents of the data, you can use + * csubstr::first_real_span() to check before calling `>>`, for + * example like this: + * + * ```cpp + * csubstr val = node.val(); + * if(val.first_real_span() == val) + * node >> v; + * else + * ERROR("not a real") + * ``` + */ +template +typename std::enable_if::value, bool>::type +inline read(Tree const* C4_RESTRICT tree, id_type id, T *v) +{ + csubstr val = tree->val(id); + return C4_LIKELY(!val.empty()) ? from_chars_float(val, v) : false; +} + +/** convert the key of a scalar node to a floating point type, by + * forwarding its key to @ref from_chars_float(). + * + * @return false if the conversion failed + * + * @warning Unlike non-floating types, only the leading part of the + * string that may constitute a number is processed. This happens + * because the float parsing is delegated to fast_float, which is + * implemented that way. Consequently, for example, all of `"34"`, + * `"34 "` `"34hg"` `"34 gh"` will be read as 34. If you are not sure + * about the contents of the data, you can use + * csubstr::first_real_span() to check before calling `>>`, for + * example like this: + * + * ```cpp + * csubstr key = node.key(); + * if(key.first_real_span() == key) + * node >> v; + * else + * ERROR("not a real") + * ``` + */ +template +typename std::enable_if::value, bool>::type +inline readkey(Tree const* C4_RESTRICT tree, id_type id, T *v) +{ + csubstr key = tree->key(id); + return C4_LIKELY(!key.empty()) ? from_chars_float(key, v) : false; +} + +/** @} */ + +/** @} */ + + +} // namespace yml +} // namespace c4 + + +C4_SUPPRESS_WARNING_MSVC_POP +C4_SUPPRESS_WARNING_GCC_CLANG_POP + + +#endif /* _C4_YML_TREE_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/version.hpp b/3rdparty/rapidyaml/include/c4/yml/version.hpp new file mode 100644 index 0000000000..a6c3df310e --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/version.hpp @@ -0,0 +1,25 @@ +#ifndef _C4_YML_VERSION_HPP_ +#define _C4_YML_VERSION_HPP_ + +/** @file version.hpp */ + +#define RYML_VERSION "0.10.0" +#define RYML_VERSION_MAJOR 0 +#define RYML_VERSION_MINOR 10 +#define RYML_VERSION_PATCH 0 + +#include +#include + +namespace c4 { +namespace yml { + +RYML_EXPORT csubstr version(); +RYML_EXPORT int version_major(); +RYML_EXPORT int version_minor(); +RYML_EXPORT int version_patch(); + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_VERSION_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/writer.hpp b/3rdparty/rapidyaml/include/c4/yml/writer.hpp new file mode 100644 index 0000000000..3506f079a7 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/writer.hpp @@ -0,0 +1,195 @@ +#ifndef _C4_YML_WRITER_HPP_ +#define _C4_YML_WRITER_HPP_ + +#ifndef _C4_YML_COMMON_HPP_ +#include "./common.hpp" +#endif + +#include +#include // fwrite(), fputc() +#include // memcpy() + + +namespace c4 { +namespace yml { + +/** @addtogroup doc_emit + * @{ + */ + +/** @defgroup doc_writers Writer objects to use with an Emitter + * @see Emitter + * @{ + */ + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** A writer that outputs to a file. Defaults to stdout. */ +struct WriterFile +{ + FILE * m_file; + size_t m_pos; + + WriterFile(FILE *f = nullptr) : m_file(f ? f : stdout), m_pos(0) {} + + substr _get(bool /*error_on_excess*/) const + { + substr sp; + sp.str = nullptr; + sp.len = m_pos; + return sp; + } + + template + void _do_write(const char (&a)[N]) + { + (void)fwrite(a, sizeof(char), N - 1, m_file); + m_pos += N - 1; + } + + void _do_write(csubstr sp) + { + C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wsign-conversion") + if(sp.empty()) + return; + (void)fwrite(sp.str, sizeof(csubstr::char_type), sp.len, m_file); + m_pos += sp.len; + C4_SUPPRESS_WARNING_GCC_CLANG_POP + } + + void _do_write(const char c) + { + (void)fputc(c, m_file); + ++m_pos; + } + + void _do_write(const char c, size_t num_times) + { + for(size_t i = 0; i < num_times; ++i) + (void)fputc(c, m_file); + m_pos += num_times; + } +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** A writer that outputs to an STL-like ostream. */ +template +struct WriterOStream +{ + OStream* m_stream; + size_t m_pos; + + WriterOStream(OStream &s) : m_stream(&s), m_pos(0) {} + + substr _get(bool /*error_on_excess*/) const + { + substr sp; + sp.str = nullptr; + sp.len = m_pos; + return sp; + } + + template + void _do_write(const char (&a)[N]) + { + m_stream->write(a, N - 1); + m_pos += N - 1; + } + + void _do_write(csubstr sp) + { + C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wsign-conversion") + if(sp.empty()) + return; + m_stream->write(sp.str, sp.len); + m_pos += sp.len; + C4_SUPPRESS_WARNING_GCC_CLANG_POP + } + + void _do_write(const char c) + { + m_stream->put(c); + ++m_pos; + } + + void _do_write(const char c, size_t num_times) + { + for(size_t i = 0; i < num_times; ++i) + m_stream->put(c); + m_pos += num_times; + } +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +/** a writer to a substr */ +struct WriterBuf +{ + substr m_buf; + size_t m_pos; + + WriterBuf(substr sp) : m_buf(sp), m_pos(0) {} + + substr _get(bool error_on_excess) const + { + if(m_pos <= m_buf.len) + return m_buf.first(m_pos); + else if(error_on_excess) + c4::yml::error("not enough space in the given buffer"); + substr sp; + sp.str = nullptr; + sp.len = m_pos; + return sp; + } + + template + void _do_write(const char (&a)[N]) + { + RYML_ASSERT( ! m_buf.overlaps(a)); + if(m_pos + N-1 <= m_buf.len) + memcpy(&(m_buf[m_pos]), a, N-1); + m_pos += N-1; + } + + void _do_write(csubstr sp) + { + if(sp.empty()) + return; + RYML_ASSERT( ! sp.overlaps(m_buf)); + if(m_pos + sp.len <= m_buf.len) + memcpy(&(m_buf[m_pos]), sp.str, sp.len); + m_pos += sp.len; + } + + void _do_write(const char c) + { + if(m_pos + 1 <= m_buf.len) + m_buf[m_pos] = c; + ++m_pos; + } + + void _do_write(const char c, size_t num_times) + { + if(m_pos + num_times <= m_buf.len) + for(size_t i = 0; i < num_times; ++i) + m_buf[m_pos + i] = c; + m_pos += num_times; + } +}; + +/** @ } */ + +/** @ } */ + + +} // namespace yml +} // namespace c4 + +#endif /* _C4_YML_WRITER_HPP_ */ diff --git a/3rdparty/rapidyaml/include/c4/yml/yml.hpp b/3rdparty/rapidyaml/include/c4/yml/yml.hpp new file mode 100644 index 0000000000..0f997246b7 --- /dev/null +++ b/3rdparty/rapidyaml/include/c4/yml/yml.hpp @@ -0,0 +1,16 @@ +#ifndef _C4_YML_YML_HPP_ +#define _C4_YML_YML_HPP_ + +#include "c4/yml/version.hpp" +#include "c4/yml/tree.hpp" +#include "c4/yml/node.hpp" +#include "c4/yml/emit.hpp" +#include "c4/yml/event_handler_tree.hpp" +#include "c4/yml/parse_engine.hpp" +#include "c4/yml/filter_processor.hpp" +#include "c4/yml/parse.hpp" +#include "c4/yml/preprocess.hpp" +#include "c4/yml/reference_resolver.hpp" +#include "c4/yml/tag.hpp" + +#endif // _C4_YML_YML_HPP_ diff --git a/3rdparty/rapidyaml/include/ryml-gdbtypes.py b/3rdparty/rapidyaml/include/ryml-gdbtypes.py new file mode 100644 index 0000000000..2625a6ccde --- /dev/null +++ b/3rdparty/rapidyaml/include/ryml-gdbtypes.py @@ -0,0 +1,391 @@ +# To make this file known to Qt Creator using: +# Tools > Options > Debugger > Locals & Expressions > Extra Debugging Helpers +# Any contents here will be picked up by GDB, LLDB, and CDB based +# debugging in Qt Creator automatically. + + +# Example to display a simple type +# template struct MapNode +# { +# U key; +# V data; +# } +# +# def qdump__MapNode(d, value): +# d.putValue("This is the value column contents") +# d.putExpandable() +# if d.isExpanded(): +# with Children(d): +# # Compact simple case. +# d.putSubItem("key", value["key"]) +# # Same effect, with more customization possibilities. +# with SubItem(d, "data") +# d.putItem("data", value["data"]) + +# Check http://doc.qt.io/qtcreator/creator-debugging-helpers.html +# for more details or look at qttypes.py, stdtypes.py, boosttypes.py +# for more complex examples. + +# to try parsing: +# env PYTHONPATH=/usr/share/qtcreator/debugger/ python src/ryml-gdbtypes.py + + +import dumper +#from dumper import Dumper, Value, Children, SubItem +#from dumper import SubItem, Children +from dumper import * + +import sys +import os + + +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- + +# QtCreator makes it really hard to figure out problems in this code. +# So here are some debugging utilities. + + +# FIXME. this decorator is not working; find out why. +def dbglog(func): + """a decorator that logs entry and exit of functions""" + if not _DBG: + return func + def func_wrapper(*args, **kwargs): + _dbg_enter(func.__name__) + ret = func(*args, **kwargs) + _dbg_exit(func.__name__) + return ret + return func_wrapper + + +_DBG = False +_dbg_log = None +_dbg_stack = 0 +def _dbg(*args, **kwargs): + global _dbg_log, _dbg_stack + if not _DBG: + return + if _dbg_log is None: + filename = os.path.join(os.path.dirname(__file__), "dbg.txt") + _dbg_log = open(filename, "w") + kwargs['file'] = _dbg_log + kwargs['flush'] = True + print(" " * _dbg_stack, *args, **kwargs) + + +def _dbg_enter(name): + global _dbg_stack + _dbg(name, "- enter") + _dbg_stack += 1 + + +def _dbg_exit(name): + global _dbg_stack + _dbg_stack -= 1 + _dbg(name, "- exit!") + + + +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- + + +NPOS = 18446744073709551615 +MAX_SUBSTR_LEN_DISPLAY = 80 +MAX_SUBSTR_LEN_EXPAND = 1000 + + +def get_str_value(d, value, limit=0): + # adapted from dumper.py::Dumper::putCharArrayValue() + m_str = value["str"].pointer() + m_len = value["len"].integer() + if m_len == NPOS: + _dbg("getstr... 1", m_len) + m_str = "!!!!!!!!!!" + m_len = len(m_str) + return m_str, m_len + if limit == 0: + limit = d.displayStringLimit + elided, shown = d.computeLimit(m_len, limit) + mem = bytes(d.readRawMemory(m_str, shown)) + mem = mem.decode('utf8') + return mem, m_len + + +def __display_csubstr(d, value, limit=0): + m_str, m_len = get_str_value(d, value) + safe_len = min(m_len, MAX_SUBSTR_LEN_DISPLAY) + disp = m_str[0:safe_len] + # ensure the string escapes characters like \n\r\t etc + disp = disp.encode('unicode_escape').decode('utf8') + # WATCHOUT. quotes in the string will make qtcreator hang!!! + disp = disp.replace('"', '\\"') + disp = disp.replace('\'', '\\') + if m_len <= MAX_SUBSTR_LEN_DISPLAY: + d.putValue(f"[{m_len}] '{disp}'") + else: + d.putValue(f"[{m_len}] '{disp}'...") + return m_str, m_len + + +def qdump__c4__csubstr(d, value): + m_str, m_len = __display_csubstr(d, value) + d.putExpandable() + if d.isExpanded(): + with Children(d): + safe_len = min(m_len, MAX_SUBSTR_LEN_EXPAND) + for i in range(safe_len): + ct = d.createType('char') + d.putSubItem(safe_len, d.createValue(value["str"].pointer() + i, ct)) + d.putSubItem("len", value["len"]) + d.putPtrItem("str", value["str"].pointer()) + + +def qdump__c4__substr(d, value): + qdump__c4__csubstr(d, value) + + +def qdump__c4__basic_substring(d, value): + qdump__c4__csubstr(d, value) + + +def qdump__c4__yml__NodeScalar(d, value): + alen = value["anchor"]["len"].integer() + tlen = value["tag" ]["len"].integer() + m_str, m_len = get_str_value(d, value["scalar"]) + if alen == 0 and tlen == 0: + d.putValue(f'\'{m_str}\'') + elif alen == 0 and tlen > 0: + d.putValue(f'\'{m_str}\' [Ta]') + elif alen > 0 and tlen == 0: + d.putValue(f'\'{m_str}\' [tA]') + elif alen > 0 and tlen > 0: + d.putValue(f'\'{m_str}\' [TA]') + d.putExpandable() + if d.isExpanded(): + with Children(d): + d.putSubItem("[scalar]", value["scalar"]) + if tlen > 0: + d.putSubItem("[tag]", value["tag"]) + if alen > 0: + d.putSubItem("[anchor or ref]", value["anchor"]) + + +def _format_enum_value(int_value, enum_map): + str_value = enum_map.get(int_value, None) + display = f'{int_value}' if str_value is None else f'{str_value} ({int_value})' + return display + + +def _format_bitmask_value(int_value, enum_map): + str_value = enum_map.get(int_value, None) + if str_value: + return f'{str_value} ({int_value})' + else: + out = "" + orig = int_value + # do in reverse to get compound flags first + for k, v in reversed(enum_map.items()): + if (k != 0): + if (int_value & k) == k: + if len(out) > 0: + out += '|' + out += v + int_value &= ~k + else: + if len(out) == 0 and int_value == 0: + return v + if out == "": + return f'{int_value}' + return f"{out} ({orig})" + + +def _c4bit(*ints): + ret = 0 + for i in ints: + ret |= 1 << i + return ret + + +node_types = { + 0: "NOTYPE", + _c4bit(0): "VAL" , + _c4bit(1): "KEY" , + _c4bit(2): "MAP" , + _c4bit(3): "SEQ" , + _c4bit(4): "DOC" , + _c4bit(5,3): "STREAM", + _c4bit(6): "KEYREF" , + _c4bit(7): "VALREF" , + _c4bit(8): "KEYANCH" , + _c4bit(9): "VALANCH" , + _c4bit(10): "KEYTAG" , + _c4bit(11): "VALTAG" , + _c4bit(12): "VALQUO" , + _c4bit(13): "KEYQUO" , + _c4bit(1,0): "KEYVAL", + _c4bit(1,3): "KEYSEQ", + _c4bit(1,2): "KEYMAP", + _c4bit(4,2): "DOCMAP", + _c4bit(4,3): "DOCSEQ", + _c4bit(4,0): "DOCVAL", + # + _c4bit(14): "STYLE_FLOW_SL", + _c4bit(15): "STYLE_FLOW_ML", + _c4bit(16): "STYLE_BLOCK", + # + _c4bit(17): "KEY_LITERAL", + _c4bit(18): "VAL_LITERAL", + _c4bit(19): "KEY_FOLDED", + _c4bit(20): "VAL_FOLDED", + _c4bit(21): "KEY_SQUO", + _c4bit(22): "VAL_SQUO", + _c4bit(23): "KEY_DQUO", + _c4bit(24): "VAL_DQUO", + _c4bit(25): "KEY_PLAIN", + _c4bit(26): "VAL_PLAIN", +} +node_types_rev = {v: k for k, v in node_types.items()} + + +def _node_type_has_all(node_type_value, type_name): + exp = node_types_rev[type_name] + return (node_type_value & exp) == exp + + +def _node_type_has_any(node_type_value, type_name): + exp = node_types_rev[type_name] + return (node_type_value & exp) != 0 + + +def qdump__c4__yml__NodeType_e(d, value): + v = _format_bitmask_value(value.integer(), node_types) + d.putValue(v) + + +def qdump__c4__yml__NodeType(d, value): + qdump__c4__yml__NodeType_e(d, value["type"]) + + +def qdump__c4__yml__NodeData(d, value): + d.putValue("wtf") + ty = _format_bitmask_value(value.integer(), node_types) + t = value["m_type"]["type"].integer() + k = value["m_key"]["scalar"] + v = value["m_val"]["scalar"] + sk, lk = get_str_value(d, k) + sv, lv = get_str_value(d, v) + if _node_type_has_all(t, "KEYVAL"): + d.putValue(f"'{sk}': '{sv}' {ty}") + elif _node_type_has_any(t, "KEY"): + d.putValue(f"'{sk}': {ty}") + elif _node_type_has_any(t, "VAL"): + d.putValue(f"'{sv}' {ty}") + else: + d.putValue(f"{ty}") + d.putExpandable() + if d.isExpanded(): + with Children(d): + d.putSubItem("m_type", value["m_type"]) + # key + if _node_type_has_any(t, "KEY"): + d.putSubItem("m_key", value["m_key"]) + if _node_type_has_any(t, "KEYREF"): + with SubItem(d, "m_key.ref"): + s_, _ = get_str_value(d, value["m_key"]["anchor"]) + d.putValue(f"'{s_}'") + if _node_type_has_any(t, "KEYANCH"): + with SubItem(d, "m_key.anchor"): + s_, _ = get_str_value(d, value["m_key"]["anchor"]) + d.putValue(f"'{s_}'") + if _node_type_has_any(t, "KEYTAG"): + with SubItem(d, "m_key.tag"): + s_, _ = get_str_value(d, value["m_key"]["tag"]) + d.putValue(f"'{s_}'") + # val + if _node_type_has_any(t, "VAL"): + d.putSubItem("m_val", value["m_val"]) + if _node_type_has_any(t, "VALREF"): + with SubItem(d, "m_val.ref"): + s_, _ = get_str_value(d, value["m_val"]["anchor"]) + d.putValue(f"'{s_}'") + if _node_type_has_any(t, "VALANCH"): + with SubItem(d, "m_val.anchor"): + s_, _ = get_str_value(d, value["m_val"]["anchor"]) + d.putValue(f"'{s_}'") + if _node_type_has_any(t, "VALTAG"): + with SubItem(d, "m_val.tag"): + s_, _ = get_str_value(d, value["m_val"]["tag"]) + d.putValue(f"'{s_}'") + # hierarchy + _dump_node_index(d, "m_parent", value) + _dump_node_index(d, "m_first_child", value) + _dump_node_index(d, "m_last_child", value) + _dump_node_index(d, "m_next_sibling", value) + _dump_node_index(d, "m_prev_sibling", value) + + +def _dump_node_index(d, name, value): + if int(value[name].integer()) == NPOS: + pass + #with SubItem(d, name): + # d.putValue("-") + else: + d.putSubItem(name, value[name]) + + +# c4::yml::Tree +def qdump__c4__yml__Tree(d, value): + m_size = value["m_size"].integer() + m_cap = value["m_cap"].integer() + d.putExpandable() + if d.isExpanded(): + #d.putArrayData(value["m_buf"], m_size, value["m_buf"].dereference()) + with Children(d): + with SubItem(d, f"[nodes]"): + d.putItemCount(m_size) + d.putArrayData(value["m_buf"].pointer(), m_size, value["m_buf"].type.dereference()) + d.putPtrItem("m_buf", value["m_buf"].pointer()) + d.putIntItem("m_size", value["m_size"]) + d.putIntItem("m_cap (capacity)", value["m_cap"]) + d.putIntItem("[slack]", m_cap - m_size) + d.putIntItem("m_free_head", value["m_free_head"]) + d.putIntItem("m_free_tail", value["m_free_tail"]) + d.putSubItem("m_arena", value["m_arena"]) + + +def qdump__c4__yml__detail__stack(d, value): + T = value.type[0] + N = value.type[0] + m_size = value["m_size"].integer() + m_capacity = value["m_capacity"].integer() + d.putItemCount(m_size) + if d.isExpanded(): + with Children(d): + with SubItem(d, f"[nodes]"): + d.putItemCount(m_size) + d.putArrayData(value["m_stack"].pointer(), m_size, T) + d.putIntItem("m_size", value["m_size"]) + d.putIntItem("m_capacity", value["m_capacity"]) + #d.putIntItem("[small capacity]", N) + d.putIntItem("[is large]", value["m_buf"].address() == value["m_stack"].pointer()) + d.putPtrItem("m_stack", value["m_stack"].pointer()) + d.putPtrItem("m_buf", value["m_buf"].address()) + + +def qdump__c4__yml__detail__ReferenceResolver__refdata(d, value): + node = value["node"].integer() + ty = _format_bitmask_value(value["type"].integer(), node_types) + d.putValue(f'{node} {ty}') + d.putExpandable() + if d.isExpanded(): + with Children(d): + d.putSubItem("type", value["type"]) + d.putSubItem("node", value["node"]) + _dump_node_index(d, "prev_anchor", value) + _dump_node_index(d, "target", value) + _dump_node_index(d, "parent_ref", value) + _dump_node_index(d, "parent_ref_sibling", value) diff --git a/3rdparty/rapidyaml/include/ryml.hpp b/3rdparty/rapidyaml/include/ryml.hpp new file mode 100644 index 0000000000..c2b3e74b23 --- /dev/null +++ b/3rdparty/rapidyaml/include/ryml.hpp @@ -0,0 +1,11 @@ +#ifndef _RYML_HPP_ +#define _RYML_HPP_ + +#include "c4/yml/yml.hpp" + +namespace ryml { +using namespace c4::yml; +using namespace c4; +} + +#endif /* _RYML_HPP_ */ diff --git a/3rdparty/rapidyaml/include/ryml.natvis b/3rdparty/rapidyaml/include/ryml.natvis new file mode 100644 index 0000000000..cb0827546a --- /dev/null +++ b/3rdparty/rapidyaml/include/ryml.natvis @@ -0,0 +1,304 @@ + + + + + + + + {scalar.str,[scalar.len]} + {scalar.str,[scalar.len]} [T] + {scalar.str,[scalar.len]} [A] + {scalar.str,[scalar.len]} [T][A] + + scalar + tag + anchor + + + + + [KEYVAL] + [KEYSEQ] + [KEYMAP] + [DOCVAL] + [DOCSEQ] + [DOCMAP] + [VAL] + [KEY] + [SEQ] + [MAP] + [DOC] + [STREAM] + [NOTYPE] + + + + c4::yml::KEY + c4::yml::VAL + c4::yml::MAP + c4::yml::SEQ + c4::yml::DOC + c4::yml::STREAM + c4::yml::KEYREF + c4::yml::VALREF + c4::yml::KEYANCH + c4::yml::VALANCH + c4::yml::KEYTAG + c4::yml::VALTAG + + + + + c4::yml::_WIP_KEY_UNFILT + c4::yml::_WIP_VAL_UNFILT + c4::yml::_WIP_STYLE_FLOW + c4::yml::_WIP_STYLE_FLOW + c4::yml::_WIP_STYLE_BLOCK + c4::yml::_WIP_KEY_LITERAL + c4::yml::_WIP_VAL_LITERAL + c4::yml::_WIP_KEY_FOLDED + c4::yml::_WIP_VAL_FOLDED + c4::yml::_WIP_KEY_SQUO + c4::yml::_WIP_VAL_SQUO + c4::yml::_WIP_KEY_DQUO + c4::yml::_WIP_VAL_DQUO + c4::yml::_WIP_KEY_PLAIN + c4::yml::_WIP_VAL_PLAIN + + + + + + + [KEYVAL] {m_key.scalar.str,[m_key.scalar.len]}: {m_val.scalar.str,[m_val.scalar.len]} + [KEYSEQ] {m_key.scalar.str,[m_key.scalar.len]} + [KEYMAP] {m_key.scalar.str,[m_key.scalar.len]} + [DOCSEQ] + [DOCMAP] + [VAL] {m_val.scalar.str,[m_val.scalar.len]} + [KEY] {m_key.scalar.str,[m_key.scalar.len]} + [SEQ] + [MAP] + [DOC] + [STREAM] + [NOTYPE] + + m_type + m_key + m_val + c4::yml::KEYQUO + c4::yml::VALQUO + m_key.anchor + m_val.anchor + m_key.anchor + m_val.anchor + NONE + m_parent + m_first_child + m_last_child + m_prev_sibling + m_next_sibling + + + + + sz={m_size}, cap={m_cap} + + m_size + m_cap + + + + m_cap + m_buf + + + + m_free_head + m_arena + + + + + {value} ({type}) + + value + type + + + + + {path} -- target={target} closest={closest} + + target + closest + path_pos + path + + {path.str,[path_pos]} + + + {path.str+path_pos,[path.len-path_pos]} + + + + + + (void) + [INDEX SEED for] {*(m_tree->m_buf + m_id)} + [NAMED SEED for] {*(m_tree->m_buf + m_id)} + {*(m_tree->m_buf + m_id)} + + m_id + *(m_tree->m_buf + m_id) + m_tree + + + + + + + + buf + curr + curr = (buf + curr)->m_next_sibling + + + + + + + + + + (void) + {*(m_tree->m_buf + m_id)} + + m_id + *(m_tree->m_buf + m_id) + m_tree + + + + + + + + buf + curr + curr = (buf + curr)->m_next_sibling + + + + + + + + + + #refs={refs.m_size} #nodes={t->m_size} + + + + + + + t->m_buf + (refs.m_stack + curr)->node + curr = curr+1 + + + + + + + + + refs.m_size + refs.m_stack + + + + t + + + + + sz={m_size} cap={m_capacity} + + m_size + m_capacity + m_buf == m_stack + + + + m_size + m_stack + + + + + + + + src={src.str,[rpos]} dst={dst.str,[wpos]} + + src + dst + rpos + wpos + + src.str,[rpos] + + + rpos + src.str + + + + + + + + src={src.str,[rpos]} dst={src.str,[wpos]} + + rpos + wpos + wcap + + src.str,[wcap] + + + wcap + src.str + + + + src + + src.str+rpos,[src.len-rpos] + + src.len-rpossrc.str+rpos + + + + src.str,[rpos] + + rpossrc.str + + + + src.str,[wpos] + + wpossrc.str + + + + + + diff --git a/3rdparty/rapidyaml/include/ryml_std.hpp b/3rdparty/rapidyaml/include/ryml_std.hpp new file mode 100644 index 0000000000..5e81439ac6 --- /dev/null +++ b/3rdparty/rapidyaml/include/ryml_std.hpp @@ -0,0 +1,6 @@ +#ifndef _RYML_STD_HPP_ +#define _RYML_STD_HPP_ + +#include "./c4/yml/std/std.hpp" + +#endif /* _RYML_STD_HPP_ */ diff --git a/3rdparty/rapidyaml/rapidyaml.vcxproj b/3rdparty/rapidyaml/rapidyaml.vcxproj new file mode 100644 index 0000000000..502da175e3 --- /dev/null +++ b/3rdparty/rapidyaml/rapidyaml.vcxproj @@ -0,0 +1,139 @@ + + + + + + {DE9653B6-17DD-356A-9EE0-28A731772587} + Win32Proj + rapidyaml + + + + StaticLibrary + $(DefaultPlatformToolset) + ClangCL + MultiByte + true + true + false + + + + + + + + + + + + + + AllRules.ruleset + + + + %(PreprocessorDefinitions);C4_NO_DEBUG_BREAK + TurnOffAllWarnings + %(AdditionalIncludeDirectories);$(ProjectDir)src;$(ProjectDir)include;$(ProjectDir)..\fast_float\include + stdcpp17 + + + + + %(PreprocessorDefinitions);WIN32;_WINDOWS + + + + + %(PreprocessorDefinitions);WIN32;_WINDOWS;NDEBUG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/3rdparty/rapidyaml/src/c4/base64.cpp b/3rdparty/rapidyaml/src/c4/base64.cpp new file mode 100644 index 0000000000..b30ca64e7f --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/base64.cpp @@ -0,0 +1,226 @@ +#include "c4/base64.hpp" + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wchar-subscripts" // array subscript is of type 'char' +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wuseless-cast" +# pragma GCC diagnostic ignored "-Wchar-subscripts" +# pragma GCC diagnostic ignored "-Wtype-limits" +# pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + +// NOLINTBEGIN(bugprone-signed-char-misuse,cert-str34-c,hicpp-signed-bitwise) + +namespace c4 { + +namespace detail { + +constexpr static const char base64_sextet_to_char_[64] = { + /* 0/ 65*/ 'A', /* 1/ 66*/ 'B', /* 2/ 67*/ 'C', /* 3/ 68*/ 'D', + /* 4/ 69*/ 'E', /* 5/ 70*/ 'F', /* 6/ 71*/ 'G', /* 7/ 72*/ 'H', + /* 8/ 73*/ 'I', /* 9/ 74*/ 'J', /*10/ 75*/ 'K', /*11/ 74*/ 'L', + /*12/ 77*/ 'M', /*13/ 78*/ 'N', /*14/ 79*/ 'O', /*15/ 78*/ 'P', + /*16/ 81*/ 'Q', /*17/ 82*/ 'R', /*18/ 83*/ 'S', /*19/ 82*/ 'T', + /*20/ 85*/ 'U', /*21/ 86*/ 'V', /*22/ 87*/ 'W', /*23/ 88*/ 'X', + /*24/ 89*/ 'Y', /*25/ 90*/ 'Z', /*26/ 97*/ 'a', /*27/ 98*/ 'b', + /*28/ 99*/ 'c', /*29/100*/ 'd', /*30/101*/ 'e', /*31/102*/ 'f', + /*32/103*/ 'g', /*33/104*/ 'h', /*34/105*/ 'i', /*35/106*/ 'j', + /*36/107*/ 'k', /*37/108*/ 'l', /*38/109*/ 'm', /*39/110*/ 'n', + /*40/111*/ 'o', /*41/112*/ 'p', /*42/113*/ 'q', /*43/114*/ 'r', + /*44/115*/ 's', /*45/116*/ 't', /*46/117*/ 'u', /*47/118*/ 'v', + /*48/119*/ 'w', /*49/120*/ 'x', /*50/121*/ 'y', /*51/122*/ 'z', + /*52/ 48*/ '0', /*53/ 49*/ '1', /*54/ 50*/ '2', /*55/ 51*/ '3', + /*56/ 52*/ '4', /*57/ 53*/ '5', /*58/ 54*/ '6', /*59/ 55*/ '7', + /*60/ 56*/ '8', /*61/ 57*/ '9', /*62/ 43*/ '+', /*63/ 47*/ '/', +}; + +// https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html +constexpr static const char base64_char_to_sextet_[128] = { + #define __ char(-1) // undefined below + /* 0 NUL*/ __, /* 1 SOH*/ __, /* 2 STX*/ __, /* 3 ETX*/ __, + /* 4 EOT*/ __, /* 5 ENQ*/ __, /* 6 ACK*/ __, /* 7 BEL*/ __, + /* 8 BS */ __, /* 9 TAB*/ __, /* 10 LF */ __, /* 11 VT */ __, + /* 12 FF */ __, /* 13 CR */ __, /* 14 SO */ __, /* 15 SI */ __, + /* 16 DLE*/ __, /* 17 DC1*/ __, /* 18 DC2*/ __, /* 19 DC3*/ __, + /* 20 DC4*/ __, /* 21 NAK*/ __, /* 22 SYN*/ __, /* 23 ETB*/ __, + /* 24 CAN*/ __, /* 25 EM */ __, /* 26 SUB*/ __, /* 27 ESC*/ __, + /* 28 FS */ __, /* 29 GS */ __, /* 30 RS */ __, /* 31 US */ __, + /* 32 SPC*/ __, /* 33 ! */ __, /* 34 " */ __, /* 35 # */ __, + /* 36 $ */ __, /* 37 % */ __, /* 38 & */ __, /* 39 ' */ __, + /* 40 ( */ __, /* 41 ) */ __, /* 42 * */ __, /* 43 + */ 62, + /* 44 , */ __, /* 45 - */ __, /* 46 . */ __, /* 47 / */ 63, + /* 48 0 */ 52, /* 49 1 */ 53, /* 50 2 */ 54, /* 51 3 */ 55, + /* 52 4 */ 56, /* 53 5 */ 57, /* 54 6 */ 58, /* 55 7 */ 59, + /* 56 8 */ 60, /* 57 9 */ 61, /* 58 : */ __, /* 59 ; */ __, + /* 60 < */ __, /* 61 = */ __, /* 62 > */ __, /* 63 ? */ __, + /* 64 @ */ __, /* 65 A */ 0, /* 66 B */ 1, /* 67 C */ 2, + /* 68 D */ 3, /* 69 E */ 4, /* 70 F */ 5, /* 71 G */ 6, + /* 72 H */ 7, /* 73 I */ 8, /* 74 J */ 9, /* 75 K */ 10, + /* 76 L */ 11, /* 77 M */ 12, /* 78 N */ 13, /* 79 O */ 14, + /* 80 P */ 15, /* 81 Q */ 16, /* 82 R */ 17, /* 83 S */ 18, + /* 84 T */ 19, /* 85 U */ 20, /* 86 V */ 21, /* 87 W */ 22, + /* 88 X */ 23, /* 89 Y */ 24, /* 90 Z */ 25, /* 91 [ */ __, + /* 92 \ */ __, /* 93 ] */ __, /* 94 ^ */ __, /* 95 _ */ __, + /* 96 ` */ __, /* 97 a */ 26, /* 98 b */ 27, /* 99 c */ 28, + /*100 d */ 29, /*101 e */ 30, /*102 f */ 31, /*103 g */ 32, + /*104 h */ 33, /*105 i */ 34, /*106 j */ 35, /*107 k */ 36, + /*108 l */ 37, /*109 m */ 38, /*110 n */ 39, /*111 o */ 40, + /*112 p */ 41, /*113 q */ 42, /*114 r */ 43, /*115 s */ 44, + /*116 t */ 45, /*117 u */ 46, /*118 v */ 47, /*119 w */ 48, + /*120 x */ 49, /*121 y */ 50, /*122 z */ 51, /*123 { */ __, + /*124 | */ __, /*125 } */ __, /*126 ~ */ __, /*127 DEL*/ __, + #undef __ +}; + +#ifndef NDEBUG +void base64_test_tables() +{ + for(size_t i = 0; i < C4_COUNTOF(detail::base64_sextet_to_char_); ++i) + { + char s2c = base64_sextet_to_char_[i]; + char c2s = base64_char_to_sextet_[(unsigned)s2c]; + C4_CHECK((size_t)c2s == i); + } + for(size_t i = 0; i < C4_COUNTOF(detail::base64_char_to_sextet_); ++i) + { + char c2s = base64_char_to_sextet_[i]; + if(c2s == char(-1)) + continue; + char s2c = base64_sextet_to_char_[(unsigned)c2s]; + C4_CHECK((size_t)s2c == i); + } +} +#endif +} // namespace detail + + +bool base64_valid(csubstr encoded) +{ + if((encoded.len & size_t(3u)) != size_t(0)) // (encoded.len % 4u) + return false; + for(const char c : encoded) + { + if(c < 0/* || c >= 128*/) + return false; + if(c == '=') + continue; + if(detail::base64_char_to_sextet_[c] == char(-1)) + return false; + } + return true; +} + + +size_t base64_encode(substr buf, cblob data) +{ + #define c4append_(c) { if(pos < buf.len) { buf.str[pos] = (c); } ++pos; } + #define c4append_idx_(char_idx) \ + {\ + C4_XASSERT((char_idx) < sizeof(detail::base64_sextet_to_char_));\ + c4append_(detail::base64_sextet_to_char_[(char_idx)]);\ + } + size_t rem, pos = 0; + constexpr const uint32_t sextet_mask = uint32_t(1 << 6) - 1; + const unsigned char *C4_RESTRICT d = (const unsigned char *) data.buf; // cast to unsigned to avoid wrapping high-bits + for(rem = data.len; rem >= 3; rem -= 3, d += 3) + { + const uint32_t val = ((uint32_t(d[0]) << 16) | (uint32_t(d[1]) << 8) | (uint32_t(d[2]))); + c4append_idx_((val >> 18) & sextet_mask); + c4append_idx_((val >> 12) & sextet_mask); + c4append_idx_((val >> 6) & sextet_mask); + c4append_idx_((val ) & sextet_mask); + } + C4_ASSERT(rem < 3); + if(rem == 2) + { + const uint32_t val = ((uint32_t(d[0]) << 16) | (uint32_t(d[1]) << 8)); + c4append_idx_((val >> 18) & sextet_mask); + c4append_idx_((val >> 12) & sextet_mask); + c4append_idx_((val >> 6) & sextet_mask); + c4append_('='); + } + else if(rem == 1) + { + const uint32_t val = ((uint32_t(d[0]) << 16)); + c4append_idx_((val >> 18) & sextet_mask); + c4append_idx_((val >> 12) & sextet_mask); + c4append_('='); + c4append_('='); + } + return pos; + + #undef c4append_ + #undef c4append_idx_ +} + + +size_t base64_decode(csubstr encoded, blob data) +{ + #define c4append_(c) { if(wpos < data.len) { data.buf[wpos] = static_cast(c); } ++wpos; } + #define c4appendval_(c, shift)\ + {\ + C4_XASSERT((c) >= 0);\ + C4_XASSERT(size_t(c) < sizeof(detail::base64_char_to_sextet_));\ + val |= static_cast(detail::base64_char_to_sextet_[(c)]) << ((shift) * 6);\ + } + C4_ASSERT(base64_valid(encoded)); + C4_CHECK((encoded.len & 3u) == 0); + size_t wpos = 0; // the write position + const char *C4_RESTRICT d = encoded.str; + constexpr const uint32_t full_byte = 0xff; + // process every quartet of input 6 bits --> triplet of output bytes + for(size_t rpos = 0; rpos < encoded.len; rpos += 4, d += 4) + { + if(d[2] == '=' || d[3] == '=') // skip the last quartet if it is padded + { + C4_ASSERT(d + 4 == encoded.str + encoded.len); + break; + } + uint32_t val = 0; + c4appendval_(d[3], 0); + c4appendval_(d[2], 1); + c4appendval_(d[1], 2); + c4appendval_(d[0], 3); + c4append_((val >> (2 * 8)) & full_byte); + c4append_((val >> (1 * 8)) & full_byte); + c4append_((val ) & full_byte); + } + // deal with the last quartet when it is padded + if(d == encoded.str + encoded.len) + return wpos; + if(d[2] == '=') // 2 padding chars + { + C4_ASSERT(d + 4 == encoded.str + encoded.len); + C4_ASSERT(d[3] == '='); + uint32_t val = 0; + c4appendval_(d[1], 2); + c4appendval_(d[0], 3); + c4append_((val >> (2 * 8)) & full_byte); + } + else if(d[3] == '=') // 1 padding char + { + C4_ASSERT(d + 4 == encoded.str + encoded.len); + uint32_t val = 0; + c4appendval_(d[2], 1); + c4appendval_(d[1], 2); + c4appendval_(d[0], 3); + c4append_((val >> (2 * 8)) & full_byte); + c4append_((val >> (1 * 8)) & full_byte); + } + return wpos; + #undef c4append_ + #undef c4appendval_ +} + +} // namespace c4 + +// NOLINTEND(bugprone-signed-char-misuse,cert-str34-c,hicpp-signed-bitwise) + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/3rdparty/rapidyaml/src/c4/error.cpp b/3rdparty/rapidyaml/src/c4/error.cpp new file mode 100644 index 0000000000..94954ffedb --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/error.cpp @@ -0,0 +1,234 @@ +#include "c4/error.hpp" + +#include +#include +#include + +#define C4_LOGF_ERR(...) (void)fprintf(stderr, __VA_ARGS__); (void)fflush(stderr) +#define C4_LOGF_WARN(...) (void)fprintf(stderr, __VA_ARGS__); (void)fflush(stderr) +#define C4_LOGP(msg, ...) (void)printf(msg) + +#if defined(C4_XBOX) || (defined(C4_WIN) && defined(C4_MSVC)) +# include "c4/windows.hpp" +#elif defined(C4_PS4) +# include +#elif defined(C4_UNIX) || defined(C4_LINUX) +# include +# include +# include +#elif defined(C4_MACOS) || defined(C4_IOS) +# include +# include +# include +# include +#endif +// the amalgamation tool is dumb and was omitting this include under MACOS. +// So do it only once: +#if defined(C4_UNIX) || defined(C4_LINUX) || defined(C4_MACOS) || defined(C4_IOS) +# include +#endif + +#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION) +# include +#endif + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wformat-nonliteral" +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +# pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +// NOLINTBEGIN(*use-anonymous-namespace*,cert-dcl50-cpp) + + +//----------------------------------------------------------------------------- +namespace c4 { + +static error_flags s_error_flags = ON_ERROR_DEFAULTS; +static error_callback_type s_error_callback = nullptr; + + +//----------------------------------------------------------------------------- + +error_flags get_error_flags() +{ + return s_error_flags; +} +void set_error_flags(error_flags flags) +{ + s_error_flags = flags; +} + +error_callback_type get_error_callback() +{ + return s_error_callback; +} +/** Set the function which is called when an error occurs. */ +void set_error_callback(error_callback_type cb) +{ + s_error_callback = cb; +} + + +//----------------------------------------------------------------------------- + +void handle_error(srcloc where, const char *fmt, ...) +{ + char buf[1024]; + size_t msglen = 0; + if(s_error_flags & (ON_ERROR_LOG|ON_ERROR_CALLBACK)) + { + va_list args; + va_start(args, fmt); + int ilen = vsnprintf(buf, sizeof(buf), fmt, args); // NOLINT(clang-analyzer-valist.Uninitialized) + va_end(args); + msglen = ilen >= 0 && ilen < (int)sizeof(buf) ? static_cast(ilen) : sizeof(buf)-1; + } + + if(s_error_flags & ON_ERROR_LOG) + { + C4_LOGF_ERR("\n"); +#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC) + C4_LOGF_ERR("%s:%d: ERROR: %s\n", where.file, where.line, buf); + C4_LOGF_ERR("%s:%d: ERROR here: %s\n", where.file, where.line, where.func); +#elif defined(C4_ERROR_SHOWS_FILELINE) + C4_LOGF_ERR("%s:%d: ERROR: %s\n", where.file, where.line, buf); +#elif ! defined(C4_ERROR_SHOWS_FUNC) + C4_LOGF_ERR("ERROR: %s\n", buf); +#endif + } + + if(s_error_flags & ON_ERROR_CALLBACK) + { + if(s_error_callback) + { + s_error_callback(buf, msglen); + } + } + + if(s_error_flags & ON_ERROR_THROW) + { +#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION) + throw std::runtime_error(buf); +#endif + } + + if(s_error_flags & ON_ERROR_ABORT) + { + abort(); + } + + abort(); // abort anyway, in case nothing was set + C4_UNREACHABLE_AFTER_ERR(); +} + +//----------------------------------------------------------------------------- + +void handle_warning(srcloc where, const char *fmt, ...) +{ + va_list args; + char buf[1024]; + va_start(args, fmt); + int ret = vsnprintf(buf, sizeof(buf), fmt, args); // NOLINT(clang-analyzer-valist.Uninitialized) + if(ret+1 > (int)sizeof(buf)) + buf[sizeof(buf) - 1] = '\0'; // truncate + else if(ret < 0) + buf[0] = '\0'; // output/format error + va_end(args); + C4_LOGF_WARN("\n"); +#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC) + C4_LOGF_WARN("%s:%d: WARNING: %s\n", where.file, where.line, buf); + C4_LOGF_WARN("%s:%d: WARNING: here: %s\n", where.file, where.line, where.func); +#elif defined(C4_ERROR_SHOWS_FILELINE) + C4_LOGF_WARN("%s:%d: WARNING: %s\n", where.file, where.line, buf); +#elif ! defined(C4_ERROR_SHOWS_FUNC) + C4_LOGF_WARN("WARNING: %s\n", buf); +#endif +} + +//----------------------------------------------------------------------------- +bool is_debugger_attached() +{ +#if defined(C4_UNIX) || defined(C4_LINUX) + static bool first_call = true; + static bool first_call_result = false; + if(first_call) + { + first_call = false; + C4_SUPPRESS_WARNING_GCC_PUSH + #if defined(__GNUC__) && __GNUC__ > 9 + C4_SUPPRESS_WARNING_GCC("-Wanalyzer-fd-leak") + #endif + //! @see http://stackoverflow.com/questions/3596781/how-to-detect-if-the-current-process-is-being-run-by-gdb + //! (this answer: http://stackoverflow.com/a/24969863/3968589 ) + char buf[1024] = ""; + int status_fd = open("/proc/self/status", O_RDONLY); // NOLINT + if (status_fd == -1) + return false; + ssize_t num_read = ::read(status_fd, buf, sizeof(buf)); + if (num_read > 0) + { + static const char TracerPid[] = "TracerPid:"; + char *tracer_pid; + if(num_read < 1024) + buf[num_read] = 0; + tracer_pid = strstr(buf, TracerPid); + if(tracer_pid) + first_call_result = !!::atoi(tracer_pid + sizeof(TracerPid) - 1); // NOLINT + } + close(status_fd); + C4_SUPPRESS_WARNING_GCC_POP + } + return first_call_result; +#elif defined(C4_PS4) + return (sceDbgIsDebuggerAttached() != 0); +#elif defined(C4_XBOX) || (defined(C4_WIN) && defined(C4_MSVC)) + return IsDebuggerPresent() != 0; +#elif defined(C4_MACOS) || defined(C4_IOS) + // https://stackoverflow.com/questions/2200277/detecting-debugger-on-mac-os-x + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + int junk; + int mib[4]; + struct kinfo_proc info; + size_t size; + + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl. + + size = sizeof(info); + junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + assert(junk == 0); + (void)junk; + + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); +#else + return false; +#endif +} // is_debugger_attached() + +} // namespace c4 + +// NOLINTEND(*use-anonymous-namespace*,cert-dcl50-cpp) + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/3rdparty/rapidyaml/src/c4/format.cpp b/3rdparty/rapidyaml/src/c4/format.cpp new file mode 100644 index 0000000000..fc994781f4 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/format.cpp @@ -0,0 +1,64 @@ +#include "c4/format.hpp" + +#include // for std::align + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wformat-nonliteral" +# pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +# pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + +namespace c4 { + + +size_t to_chars(substr buf, fmt::const_raw_wrapper r) +{ + void * vptr = buf.str; + size_t space = buf.len; + char * ptr = (char*) std::align(r.alignment, r.len, vptr, space); + if(ptr == nullptr) + { + // if it was not possible to align, return a conservative estimate + // of the required space + return r.alignment + r.len; + } + C4_CHECK(ptr >= buf.begin() && ptr <= buf.end()); + size_t sz = static_cast(ptr - buf.str) + r.len; + if(sz <= buf.len) + { + memcpy(ptr, r.buf, r.len); + } + return sz; +} + + +bool from_chars(csubstr buf, fmt::raw_wrapper *r) +{ + C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wcast-qual") + void * vptr = (void*)buf.str; + C4_SUPPRESS_WARNING_GCC_POP + size_t space = buf.len; + char * ptr = (char*) std::align(r->alignment, r->len, vptr, space); + C4_CHECK(ptr != nullptr); + C4_CHECK(ptr >= buf.begin() && ptr <= buf.end()); + C4_SUPPRESS_WARNING_GCC_PUSH + #if defined(__GNUC__) && __GNUC__ > 9 + C4_SUPPRESS_WARNING_GCC("-Wanalyzer-null-argument") + #endif + memcpy(r->buf, ptr, r->len); + C4_SUPPRESS_WARNING_GCC_POP + return true; +} + + +} // namespace c4 + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/3rdparty/rapidyaml/src/c4/language.cpp b/3rdparty/rapidyaml/src/c4/language.cpp new file mode 100644 index 0000000000..d1b5697412 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/language.cpp @@ -0,0 +1,16 @@ +#include "c4/language.hpp" + +namespace c4 { +namespace detail { + +#ifndef __GNUC__ +void use_char_pointer(char const volatile* v) +{ + C4_UNUSED(v); +} +#else +void foo() {} // to avoid empty file warning from the linker +#endif + +} // namespace detail +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/memory_util.cpp b/3rdparty/rapidyaml/src/c4/memory_util.cpp new file mode 100644 index 0000000000..8e32330283 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/memory_util.cpp @@ -0,0 +1,32 @@ +#include "c4/memory_util.hpp" +#include "c4/error.hpp" + +namespace c4 { + + +/** Fills 'dest' with the first 'pattern_size' bytes at 'pattern', 'num_times'. */ +void mem_repeat(void* dest, void const* pattern, size_t pattern_size, size_t num_times) +{ + if(C4_UNLIKELY(num_times == 0)) + return; + C4_ASSERT( ! mem_overlaps(dest, pattern, num_times*pattern_size, pattern_size)); + char *begin = static_cast(dest); + char *end = begin + num_times * pattern_size; + // copy the pattern once + ::memcpy(begin, pattern, pattern_size); + // now copy from dest to itself, doubling up every time + size_t n = pattern_size; + while(begin + 2*n < end) + { + ::memcpy(begin + n, begin, n); + n <<= 1u; // double n + } + // copy the missing part + if(begin + n < end) + { + ::memcpy(begin + n, begin, static_cast(end - (begin + n))); + } +} + + +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/utf.cpp b/3rdparty/rapidyaml/src/c4/utf.cpp new file mode 100644 index 0000000000..797be3247c --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/utf.cpp @@ -0,0 +1,114 @@ +#include "c4/utf.hpp" +#include "c4/charconv.hpp" + +namespace c4 { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + +size_t decode_code_point(uint8_t *C4_RESTRICT buf, size_t buflen, const uint32_t code) +{ + C4_ASSERT(buf); + C4_ASSERT(buflen >= 4); + C4_UNUSED(buflen); + if (code <= UINT32_C(0x7f)) + { + buf[0] = (uint8_t)code; + return 1u; + } + else if(code <= UINT32_C(0x7ff)) + { + buf[0] = (uint8_t)(UINT32_C(0xc0) | (code >> 6u)); /* 110xxxxx */ + buf[1] = (uint8_t)(UINT32_C(0x80) | (code & UINT32_C(0x3f))); /* 10xxxxxx */ + return 2u; + } + else if(code <= UINT32_C(0xffff)) + { + buf[0] = (uint8_t)(UINT32_C(0xe0) | ((code >> 12u))); /* 1110xxxx */ + buf[1] = (uint8_t)(UINT32_C(0x80) | ((code >> 6u) & UINT32_C(0x3f))); /* 10xxxxxx */ + buf[2] = (uint8_t)(UINT32_C(0x80) | ((code ) & UINT32_C(0x3f))); /* 10xxxxxx */ + return 3u; + } + else if(code <= UINT32_C(0x10ffff)) + { + buf[0] = (uint8_t)(UINT32_C(0xf0) | ((code >> 18u))); /* 11110xxx */ + buf[1] = (uint8_t)(UINT32_C(0x80) | ((code >> 12u) & UINT32_C(0x3f))); /* 10xxxxxx */ + buf[2] = (uint8_t)(UINT32_C(0x80) | ((code >> 6u) & UINT32_C(0x3f))); /* 10xxxxxx */ + buf[3] = (uint8_t)(UINT32_C(0x80) | ((code ) & UINT32_C(0x3f))); /* 10xxxxxx */ + return 4u; + } + return 0; +} + +substr decode_code_point(substr out, csubstr code_point) +{ + C4_ASSERT(out.len >= 4); + C4_ASSERT(!code_point.begins_with("U+")); + C4_ASSERT(!code_point.begins_with("\\x")); + C4_ASSERT(!code_point.begins_with("\\u")); + C4_ASSERT(!code_point.begins_with("\\U")); + C4_ASSERT(!code_point.begins_with('0')); + C4_ASSERT(code_point.len <= 8); + C4_ASSERT(code_point.len > 0); + uint32_t code_point_val; + C4_CHECK(read_hex(code_point, &code_point_val)); + size_t ret = decode_code_point((uint8_t*)out.str, out.len, code_point_val); + C4_ASSERT(ret <= 4); + return out.first(ret); +} + +size_t first_non_bom(csubstr s) +{ + #define c4check2_(s, c0, c1) ((s).len >= 2) && (((s).str[0] == (c0)) && ((s).str[1] == (c1))) + #define c4check3_(s, c0, c1, c2) ((s).len >= 3) && (((s).str[0] == (c0)) && ((s).str[1] == (c1)) && ((s).str[2] == (c2))) + #define c4check4_(s, c0, c1, c2, c3) ((s).len >= 4) && (((s).str[0] == (c0)) && ((s).str[1] == (c1)) && ((s).str[2] == (c2)) && ((s).str[3] == (c3))) + // see https://en.wikipedia.org/wiki/Byte_order_mark#Byte-order_marks_by_encoding + if(s.len < 2u) + return false; + else if(c4check3_(s, '\xef', '\xbb', '\xbf')) // UTF-8 + return 3u; + else if(c4check4_(s, '\x00', '\x00', '\xfe', '\xff')) // UTF-32BE + return 4u; + else if(c4check4_(s, '\xff', '\xfe', '\x00', '\x00')) // UTF-32LE + return 4u; + else if(c4check2_(s, '\xfe', '\xff')) // UTF-16BE + return 2u; + else if(c4check2_(s, '\xff', '\xfe')) // UTF-16BE + return 2u; + else if(c4check3_(s, '\x2b', '\x2f', '\x76')) // UTF-7 + return 3u; + else if(c4check3_(s, '\xf7', '\x64', '\x4c')) // UTF-1 + return 3u; + else if(c4check4_(s, '\xdd', '\x73', '\x66', '\x73')) // UTF-EBCDIC + return 4u; + else if(c4check3_(s, '\x0e', '\xfe', '\xff')) // SCSU + return 3u; + else if(c4check3_(s, '\xfb', '\xee', '\x28')) // BOCU-1 + return 3u; + else if(c4check4_(s, '\x84', '\x31', '\x95', '\x33')) // GB18030 + return 4u; + return 0u; + #undef c4check2_ + #undef c4check3_ + #undef c4check4_ +} + +substr get_bom(substr s) +{ + return s.first(first_non_bom(s)); +} +csubstr get_bom(csubstr s) +{ + return s.first(first_non_bom(s)); +} +substr skip_bom(substr s) +{ + return s.sub(first_non_bom(s)); +} +csubstr skip_bom(csubstr s) +{ + return s.sub(first_non_bom(s)); +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/common.cpp b/3rdparty/rapidyaml/src/c4/yml/common.cpp new file mode 100644 index 0000000000..0c968a2e82 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/common.cpp @@ -0,0 +1,149 @@ +#include "c4/yml/common.hpp" + +#ifndef RYML_NO_DEFAULT_CALLBACKS +# include +# include +# ifdef RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS +# include +# endif +#endif // RYML_NO_DEFAULT_CALLBACKS + + +namespace c4 { +namespace yml { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") +C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4702/*unreachable code*/) // on the call to the unreachable macro + +namespace { +Callbacks s_default_callbacks; +} // anon namespace + +#ifndef RYML_NO_DEFAULT_CALLBACKS +void report_error_impl(const char* msg, size_t length, Location loc, FILE *f) +{ + if(!f) + f = stderr; + if(loc) + { + if(!loc.name.empty()) + { + // this is more portable than using fprintf("%.*s:") which + // is not available in some embedded platforms + fwrite(loc.name.str, 1, loc.name.len, f); // NOLINT + fputc(':', f); // NOLINT + } + fprintf(f, "%zu:", loc.line); // NOLINT + if(loc.col) + fprintf(f, "%zu:", loc.col); // NOLINT + if(loc.offset) + fprintf(f, " (%zuB):", loc.offset); // NOLINT + fputc(' ', f); // NOLINT + } + RYML_ASSERT(!csubstr(msg, length).ends_with('\0')); + fwrite(msg, 1, length, f); // NOLINT + fputc('\n', f); // NOLINT + fflush(f); // NOLINT +} + +[[noreturn]] void error_impl(const char* msg, size_t length, Location loc, void * /*user_data*/) +{ + RYML_ASSERT(!csubstr(msg, length).ends_with('\0')); + report_error_impl(msg, length, loc, nullptr); +#ifdef RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS + throw std::runtime_error(std::string(msg, length)); +#else + ::abort(); +#endif +} + +void* allocate_impl(size_t length, void * /*hint*/, void * /*user_data*/) +{ + void *mem = ::malloc(length); + if(mem == nullptr) + { + const char msg[] = "could not allocate memory"; + error_impl(msg, sizeof(msg)-1, {}, nullptr); + } + return mem; +} + +void free_impl(void *mem, size_t /*length*/, void * /*user_data*/) +{ + ::free(mem); +} +#endif // RYML_NO_DEFAULT_CALLBACKS + + + +Callbacks::Callbacks() noexcept + : + m_user_data(nullptr), + #ifndef RYML_NO_DEFAULT_CALLBACKS + m_allocate(allocate_impl), + m_free(free_impl), + m_error(error_impl) + #else + m_allocate(nullptr), + m_free(nullptr), + m_error(nullptr) + #endif +{ +} + +Callbacks::Callbacks(void *user_data, pfn_allocate alloc_, pfn_free free_, pfn_error error_) + : + m_user_data(user_data), + #ifndef RYML_NO_DEFAULT_CALLBACKS + m_allocate(alloc_ ? alloc_ : allocate_impl), + m_free(free_ ? free_ : free_impl), + m_error((error_ ? error_ : error_impl)) + #else + m_allocate(alloc_), + m_free(free_), + m_error(error_) + #endif +{ + RYML_CHECK(m_allocate); + RYML_CHECK(m_free); + RYML_CHECK(m_error); +} + + +void set_callbacks(Callbacks const& c) +{ + s_default_callbacks = c; +} + +Callbacks const& get_callbacks() +{ + return s_default_callbacks; +} + +void reset_callbacks() +{ + set_callbacks(Callbacks()); +} + +// the [[noreturn]] attribute needs to be here as well (UB otherwise) +// https://en.cppreference.com/w/cpp/language/attributes/noreturn +[[noreturn]] void error(Callbacks const& cb, const char *msg, size_t msg_len, Location loc) +{ + cb.m_error(msg, msg_len, loc, cb.m_user_data); + abort(); // call abort in case the error callback didn't interrupt execution + C4_UNREACHABLE(); +} + +// the [[noreturn]] attribute needs to be here as well (UB otherwise) +// see https://en.cppreference.com/w/cpp/language/attributes/noreturn +[[noreturn]] void error(const char *msg, size_t msg_len, Location loc) +{ + error(s_default_callbacks, msg, msg_len, loc); + C4_UNREACHABLE(); +} + +C4_SUPPRESS_WARNING_MSVC_POP +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/node.cpp b/3rdparty/rapidyaml/src/c4/yml/node.cpp new file mode 100644 index 0000000000..50c7a0b60b --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/node.cpp @@ -0,0 +1,30 @@ +#include "c4/yml/node.hpp" + +namespace c4 { +namespace yml { + + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +size_t NodeRef::set_key_serialized(c4::fmt::const_base64_wrapper w) +{ + _apply_seed(); + csubstr encoded = this->to_arena(w); + this->set_key(encoded); + return encoded.len; +} + +size_t NodeRef::set_val_serialized(c4::fmt::const_base64_wrapper w) +{ + _apply_seed(); + csubstr encoded = this->to_arena(w); + this->set_val(encoded); + return encoded.len; +} + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/node_type.cpp b/3rdparty/rapidyaml/src/c4/yml/node_type.cpp new file mode 100644 index 0000000000..c31fee9b3e --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/node_type.cpp @@ -0,0 +1,215 @@ +#include "c4/yml/node_type.hpp" + +namespace c4 { +namespace yml { + +const char* NodeType::type_str(NodeType_e ty) noexcept +{ + switch(ty & _TYMASK) + { + case KEYVAL: + return "KEYVAL"; + case KEY: + return "KEY"; + case VAL: + return "VAL"; + case MAP: + return "MAP"; + case SEQ: + return "SEQ"; + case KEYMAP: + return "KEYMAP"; + case KEYSEQ: + return "KEYSEQ"; + case DOCSEQ: + return "DOCSEQ"; + case DOCMAP: + return "DOCMAP"; + case DOCVAL: + return "DOCVAL"; + case DOC: + return "DOC"; + case STREAM: + return "STREAM"; + case NOTYPE: + return "NOTYPE"; + default: + if((ty & KEYVAL) == KEYVAL) + return "KEYVAL***"; + if((ty & KEYMAP) == KEYMAP) + return "KEYMAP***"; + if((ty & KEYSEQ) == KEYSEQ) + return "KEYSEQ***"; + if((ty & DOCSEQ) == DOCSEQ) + return "DOCSEQ***"; + if((ty & DOCMAP) == DOCMAP) + return "DOCMAP***"; + if((ty & DOCVAL) == DOCVAL) + return "DOCVAL***"; + if(ty & KEY) + return "KEY***"; + if(ty & VAL) + return "VAL***"; + if(ty & MAP) + return "MAP***"; + if(ty & SEQ) + return "SEQ***"; + if(ty & DOC) + return "DOC***"; + return "(unk)"; + } +} + +csubstr NodeType::type_str(substr buf, NodeType_e flags) noexcept +{ + size_t pos = 0; + bool gotone = false; + + #define _prflag(fl, txt) \ + do { \ + if((flags & (fl)) == (fl)) \ + { \ + if(gotone) \ + { \ + if(pos + 1 < buf.len) \ + buf[pos] = '|'; \ + ++pos; \ + } \ + csubstr fltxt = txt; \ + if(pos + fltxt.len <= buf.len) \ + memcpy(buf.str + pos, fltxt.str, fltxt.len); \ + pos += fltxt.len; \ + gotone = true; \ + flags = (flags & ~(fl)); /*remove the flag*/ \ + } \ + } while(0) + + _prflag(STREAM, "STREAM"); + _prflag(DOC, "DOC"); + // key properties + _prflag(KEY, "KEY"); + _prflag(KEYNIL, "KNIL"); + _prflag(KEYTAG, "KTAG"); + _prflag(KEYANCH, "KANCH"); + _prflag(KEYREF, "KREF"); + _prflag(KEY_LITERAL, "KLITERAL"); + _prflag(KEY_FOLDED, "KFOLDED"); + _prflag(KEY_SQUO, "KSQUO"); + _prflag(KEY_DQUO, "KDQUO"); + _prflag(KEY_PLAIN, "KPLAIN"); + _prflag(KEY_UNFILT, "KUNFILT"); + // val properties + _prflag(VAL, "VAL"); + _prflag(VALNIL, "VNIL"); + _prflag(VALTAG, "VTAG"); + _prflag(VALANCH, "VANCH"); + _prflag(VALREF, "VREF"); + _prflag(VAL_UNFILT, "VUNFILT"); + _prflag(VAL_LITERAL, "VLITERAL"); + _prflag(VAL_FOLDED, "VFOLDED"); + _prflag(VAL_SQUO, "VSQUO"); + _prflag(VAL_DQUO, "VDQUO"); + _prflag(VAL_PLAIN, "VPLAIN"); + _prflag(VAL_UNFILT, "VUNFILT"); + // container properties + _prflag(MAP, "MAP"); + _prflag(SEQ, "SEQ"); + _prflag(FLOW_SL, "FLOWSL"); + _prflag(FLOW_ML, "FLOWML"); + _prflag(BLOCK, "BLCK"); + if(pos == 0) + _prflag(NOTYPE, "NOTYPE"); + + #undef _prflag + + if(pos < buf.len) + { + buf[pos] = '\0'; + return buf.first(pos); + } + else + { + csubstr failed; + failed.len = pos + 1; + failed.str = nullptr; + return failed; + } +} + + +//----------------------------------------------------------------------------- + +// see https://www.yaml.info/learn/quote.html#noplain +bool scalar_style_query_squo(csubstr s) noexcept +{ + return ! s.first_of_any("\n ", "\n\t"); +} + +// see https://www.yaml.info/learn/quote.html#noplain +bool scalar_style_query_plain(csubstr s) noexcept +{ + if(s.begins_with("-.")) + { + if(s == "-.inf" || s == "-.INF") + return true; + else if(s.sub(2).is_number()) + return true; + } + else if(s.begins_with_any("0123456789.-+") && s.is_number()) + { + return true; + } + return s != ':' + && ( ! s.begins_with_any("-:?*&,'\"{}[]|>%#@`\r")) // @ and ` are reserved characters + && ( ! s.ends_with_any(":#")) + // make this check in the last place, as it has linear + // complexity, while the previous ones are + // constant-time + && (s.first_of("\n#:[]{},") == npos); +} + +NodeType_e scalar_style_choose(csubstr s) noexcept +{ + if(s.len) + { + if(s.begins_with_any(" \n\t") + || + s.ends_with_any(" \n\t")) + { + return SCALAR_DQUO; + } + else if( ! scalar_style_query_plain(s)) + { + return scalar_style_query_squo(s) ? SCALAR_SQUO : SCALAR_DQUO; + } + // nothing remarkable - use plain + return SCALAR_PLAIN; + } + return s.str ? SCALAR_SQUO : SCALAR_PLAIN; +} + +NodeType_e scalar_style_json_choose(csubstr s) noexcept +{ + // do not quote special cases + bool plain = ( + (s == "true" || s == "false" || s == "null") + || + ( + // do not quote numbers + s.is_number() + && + ( + // quote integral numbers if they have a leading 0 + // https://github.com/biojppm/rapidyaml/issues/291 + (!(s.len > 1 && s.begins_with('0'))) + // do not quote reals with leading 0 + // https://github.com/biojppm/rapidyaml/issues/313 + || (s.find('.') != csubstr::npos) + ) + ) + ); + return plain ? SCALAR_PLAIN : SCALAR_DQUO; +} + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/parse.cpp b/3rdparty/rapidyaml/src/c4/yml/parse.cpp new file mode 100644 index 0000000000..4f5622ff6f --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/parse.cpp @@ -0,0 +1,146 @@ +#include "c4/yml/parse.hpp" + +#ifndef _C4_YML_NODE_HPP_ +#include "c4/yml/node.hpp" +#endif +#ifndef _C4_YML_PARSE_ENGINE_HPP_ +#include "c4/yml/parse_engine.hpp" +#endif +#ifndef _C4_YML_PARSE_ENGINE_DEF_HPP_ +#include "c4/yml/parse_engine.def.hpp" +#endif +#ifndef _C4_YML_EVENT_HANDLER_TREE_HPP_ +#include "c4/yml/event_handler_tree.hpp" +#endif + + +//----------------------------------------------------------------------------- + +namespace c4 { +namespace yml { + +// instantiate the parser class +template class ParseEngine; + +namespace { +inline void _reset_tree_handler(Parser *parser, Tree *t, id_type node_id) +{ + RYML_ASSERT(parser); + RYML_ASSERT(t); + if(!parser->m_evt_handler) + _RYML_CB_ERR(t->m_callbacks, "event handler is not set"); + parser->m_evt_handler->reset(t, node_id); + RYML_ASSERT(parser->m_evt_handler->m_tree == t); +} +} // namespace + +void parse_in_place(Parser *parser, csubstr filename, substr yaml, Tree *t, id_type node_id) +{ + _reset_tree_handler(parser, t, node_id); + parser->parse_in_place_ev(filename, yaml); +} + +void parse_json_in_place(Parser *parser, csubstr filename, substr json, Tree *t, id_type node_id) +{ + _reset_tree_handler(parser, t, node_id); + parser->parse_json_in_place_ev(filename, json); +} + + +// this is vertically aligned to highlight the parameter differences. +void parse_in_place(Parser *parser, substr yaml, Tree *t, id_type node_id) { parse_in_place(parser, {}, yaml, t, node_id); } +void parse_in_place(Parser *parser, csubstr filename, substr yaml, Tree *t ) { RYML_CHECK(t); parse_in_place(parser, filename, yaml, t, t->root_id()); } +void parse_in_place(Parser *parser, substr yaml, Tree *t ) { RYML_CHECK(t); parse_in_place(parser, {} , yaml, t, t->root_id()); } +void parse_in_place(Parser *parser, csubstr filename, substr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); parse_in_place(parser, filename, yaml, node.tree(), node.id()); } +void parse_in_place(Parser *parser, substr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); parse_in_place(parser, {} , yaml, node.tree(), node.id()); } +Tree parse_in_place(Parser *parser, csubstr filename, substr yaml ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); parse_in_place(parser, filename, yaml, &tree, tree.root_id()); return tree; } +Tree parse_in_place(Parser *parser, substr yaml ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); parse_in_place(parser, {} , yaml, &tree, tree.root_id()); return tree; } + +// this is vertically aligned to highlight the parameter differences. +void parse_in_place(csubstr filename, substr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, filename, yaml, t, node_id); } +void parse_in_place( substr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, {} , yaml, t, node_id); } +void parse_in_place(csubstr filename, substr yaml, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, filename, yaml, t, t->root_id()); } +void parse_in_place( substr yaml, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, {} , yaml, t, t->root_id()); } +void parse_in_place(csubstr filename, substr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, filename, yaml, node.tree(), node.id()); } +void parse_in_place( substr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); parse_in_place(&parser, {} , yaml, node.tree(), node.id()); } +Tree parse_in_place(csubstr filename, substr yaml ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); parse_in_place(&parser, filename, yaml, &tree, tree.root_id()); return tree; } +Tree parse_in_place( substr yaml ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); parse_in_place(&parser, {} , yaml, &tree, tree.root_id()); return tree; } + + +// this is vertically aligned to highlight the parameter differences. +void parse_json_in_place(Parser *parser, substr json, Tree *t, id_type node_id) { parse_json_in_place(parser, {}, json, t, node_id); } +void parse_json_in_place(Parser *parser, csubstr filename, substr json, Tree *t ) { RYML_CHECK(t); parse_json_in_place(parser, filename, json, t, t->root_id()); } +void parse_json_in_place(Parser *parser, substr json, Tree *t ) { RYML_CHECK(t); parse_json_in_place(parser, {} , json, t, t->root_id()); } +void parse_json_in_place(Parser *parser, csubstr filename, substr json, NodeRef node ) { RYML_CHECK(!node.invalid()); parse_json_in_place(parser, filename, json, node.tree(), node.id()); } +void parse_json_in_place(Parser *parser, substr json, NodeRef node ) { RYML_CHECK(!node.invalid()); parse_json_in_place(parser, {} , json, node.tree(), node.id()); } +Tree parse_json_in_place(Parser *parser, csubstr filename, substr json ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); parse_json_in_place(parser, filename, json, &tree, tree.root_id()); return tree; } +Tree parse_json_in_place(Parser *parser, substr json ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); parse_json_in_place(parser, {} , json, &tree, tree.root_id()); return tree; } + +// this is vertically aligned to highlight the parameter differences. +void parse_json_in_place(csubstr filename, substr json, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, filename, json, t, node_id); } +void parse_json_in_place( substr json, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, {} , json, t, node_id); } +void parse_json_in_place(csubstr filename, substr json, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, filename, json, t, t->root_id()); } +void parse_json_in_place( substr json, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, {} , json, t, t->root_id()); } +void parse_json_in_place(csubstr filename, substr json, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, filename, json, node.tree(), node.id()); } +void parse_json_in_place( substr json, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); parse_json_in_place(&parser, {} , json, node.tree(), node.id()); } +Tree parse_json_in_place(csubstr filename, substr json ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); parse_json_in_place(&parser, filename, json, &tree, tree.root_id()); return tree; } +Tree parse_json_in_place( substr json ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); parse_json_in_place(&parser, {} , json, &tree, tree.root_id()); return tree; } + + +// this is vertically aligned to highlight the parameter differences. +void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); substr src = t->copy_to_arena(yaml); parse_in_place(parser, filename, src, t, node_id); } +void parse_in_arena(Parser *parser, csubstr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); substr src = t->copy_to_arena(yaml); parse_in_place(parser, {} , src, t, node_id); } +void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, Tree *t ) { RYML_CHECK(t); substr src = t->copy_to_arena(yaml); parse_in_place(parser, filename, src, t, t->root_id()); } +void parse_in_arena(Parser *parser, csubstr yaml, Tree *t ) { RYML_CHECK(t); substr src = t->copy_to_arena(yaml); parse_in_place(parser, {} , src, t, t->root_id()); } +void parse_in_arena(Parser *parser, csubstr filename, csubstr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); substr src = node.tree()->copy_to_arena(yaml); parse_in_place(parser, filename, src, node.tree(), node.id()); } +void parse_in_arena(Parser *parser, csubstr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); substr src = node.tree()->copy_to_arena(yaml); parse_in_place(parser, {} , src, node.tree(), node.id()); } +Tree parse_in_arena(Parser *parser, csubstr filename, csubstr yaml ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); substr src = tree.copy_to_arena(yaml); parse_in_place(parser, filename, src, &tree, tree.root_id()); return tree; } +Tree parse_in_arena(Parser *parser, csubstr yaml ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); substr src = tree.copy_to_arena(yaml); parse_in_place(parser, {} , src, &tree, tree.root_id()); return tree; } + +// this is vertically aligned to highlight the parameter differences. +void parse_in_arena(csubstr filename, csubstr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(yaml); parse_in_place(&parser, filename, src, t, node_id); } +void parse_in_arena( csubstr yaml, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(yaml); parse_in_place(&parser, {} , src, t, node_id); } +void parse_in_arena(csubstr filename, csubstr yaml, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(yaml); parse_in_place(&parser, filename, src, t, t->root_id()); } +void parse_in_arena( csubstr yaml, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(yaml); parse_in_place(&parser, {} , src, t, t->root_id()); } +void parse_in_arena(csubstr filename, csubstr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); substr src = node.tree()->copy_to_arena(yaml); parse_in_place(&parser, filename, src, node.tree(), node.id()); } +void parse_in_arena( csubstr yaml, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); substr src = node.tree()->copy_to_arena(yaml); parse_in_place(&parser, {} , src, node.tree(), node.id()); } +Tree parse_in_arena(csubstr filename, csubstr yaml ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); substr src = tree.copy_to_arena(yaml); parse_in_place(&parser, filename, src, &tree, tree.root_id()); return tree; } +Tree parse_in_arena( csubstr yaml ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); substr src = tree.copy_to_arena(yaml); parse_in_place(&parser, {} , src, &tree, tree.root_id()); return tree; } + + +// this is vertically aligned to highlight the parameter differences. +void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, Tree *t, id_type node_id) { RYML_CHECK(t); substr src = t->copy_to_arena(json); parse_json_in_place(parser, filename, src, t, node_id); } +void parse_json_in_arena(Parser *parser, csubstr json, Tree *t, id_type node_id) { RYML_CHECK(t); substr src = t->copy_to_arena(json); parse_json_in_place(parser, {} , src, t, node_id); } +void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, Tree *t ) { RYML_CHECK(t); substr src = t->copy_to_arena(json); parse_json_in_place(parser, filename, src, t, t->root_id()); } +void parse_json_in_arena(Parser *parser, csubstr json, Tree *t ) { RYML_CHECK(t); substr src = t->copy_to_arena(json); parse_json_in_place(parser, {} , src, t, t->root_id()); } +void parse_json_in_arena(Parser *parser, csubstr filename, csubstr json, NodeRef node ) { RYML_CHECK(!node.invalid()); substr src = node.tree()->copy_to_arena(json); parse_json_in_place(parser, filename, src, node.tree(), node.id()); } +void parse_json_in_arena(Parser *parser, csubstr json, NodeRef node ) { RYML_CHECK(!node.invalid()); substr src = node.tree()->copy_to_arena(json); parse_json_in_place(parser, {} , src, node.tree(), node.id()); } +Tree parse_json_in_arena(Parser *parser, csubstr filename, csubstr json ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); substr src = tree.copy_to_arena(json); parse_json_in_place(parser, filename, src, &tree, tree.root_id()); return tree; } +Tree parse_json_in_arena(Parser *parser, csubstr json ) { RYML_CHECK(parser); RYML_CHECK(parser->m_evt_handler); Tree tree(parser->callbacks()); substr src = tree.copy_to_arena(json); parse_json_in_place(parser, {} , src, &tree, tree.root_id()); return tree; } + +// this is vertically aligned to highlight the parameter differences. +void parse_json_in_arena(csubstr filename, csubstr json, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(json); parse_json_in_place(&parser, filename, src, t, node_id); } +void parse_json_in_arena( csubstr json, Tree *t, id_type node_id) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(json); parse_json_in_place(&parser, {} , src, t, node_id); } +void parse_json_in_arena(csubstr filename, csubstr json, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(json); parse_json_in_place(&parser, filename, src, t, t->root_id()); } +void parse_json_in_arena( csubstr json, Tree *t ) { RYML_CHECK(t); Parser::handler_type event_handler(t->callbacks()); Parser parser(&event_handler); substr src = t->copy_to_arena(json); parse_json_in_place(&parser, {} , src, t, t->root_id()); } +void parse_json_in_arena(csubstr filename, csubstr json, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); substr src = node.tree()->copy_to_arena(json); parse_json_in_place(&parser, filename, src, node.tree(), node.id()); } +void parse_json_in_arena( csubstr json, NodeRef node ) { RYML_CHECK(!node.invalid()); Parser::handler_type event_handler(node.tree()->callbacks()); Parser parser(&event_handler); substr src = node.tree()->copy_to_arena(json); parse_json_in_place(&parser, {} , src, node.tree(), node.id()); } +Tree parse_json_in_arena(csubstr filename, csubstr json ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); substr src = tree.copy_to_arena(json); parse_json_in_place(&parser, filename, src, &tree, tree.root_id()); return tree; } +Tree parse_json_in_arena( csubstr json ) { Parser::handler_type event_handler; Parser parser(&event_handler); Tree tree(parser.callbacks()); substr src = tree.copy_to_arena(json); parse_json_in_place(&parser, {} , src, &tree, tree.root_id()); return tree; } + + +//----------------------------------------------------------------------------- + +RYML_EXPORT id_type estimate_tree_capacity(csubstr src) +{ + id_type num_nodes = 1; // root + for(size_t i = 0; i < src.len; ++i) + { + const char c = src.str[i]; + num_nodes += (c == '\n') || (c == ',') || (c == '[') || (c == '{'); + } + return num_nodes; +} + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/preprocess.cpp b/3rdparty/rapidyaml/src/c4/yml/preprocess.cpp new file mode 100644 index 0000000000..e45abb8569 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/preprocess.cpp @@ -0,0 +1,112 @@ +#include "c4/yml/preprocess.hpp" +#include "c4/yml/detail/dbgprint.hpp" + +/** @file preprocess.hpp Functions for preprocessing YAML prior to parsing. */ + +namespace c4 { +namespace yml { + +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +namespace { +C4_ALWAYS_INLINE bool _is_idchar(char c) +{ + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || (c == '_' || c == '-' || c == '~' || c == '$'); +} + +enum _ppstate : int { kReadPending = 0, kKeyPending = 1, kValPending = 2 }; +C4_ALWAYS_INLINE _ppstate _next(_ppstate s) +{ + int n = (int)s + 1; + return (_ppstate)(n <= (int)kValPending ? n : 0); +} +} // empty namespace + + +//----------------------------------------------------------------------------- + +size_t preprocess_rxmap(csubstr s, substr buf) +{ + detail::_SubstrWriter writer(buf); + _ppstate state = kReadPending; + size_t last = 0; + + if(s.begins_with('{')) + { + RYML_CHECK(s.ends_with('}')); + s = s.offs(1, 1); + } + + writer.append('{'); + + for(size_t i = 0; i < s.len; ++i) + { + const char curr = s[i]; + const char next = i+1 < s.len ? s[i+1] : '\0'; + + if(curr == '\'' || curr == '"') + { + csubstr ss = s.sub(i).pair_range_esc(curr, '\\'); + i += static_cast(ss.end() - (s.str + i)); + state = _next(state); + } + else if(state == kReadPending && _is_idchar(curr)) + { + state = _next(state); + } + + switch(state) + { + case kKeyPending: + { + if(curr == ':' && next == ' ') + { + state = _next(state); + } + else if(curr == ',' && next == ' ') + { + writer.append(s.range(last, i)); + writer.append(": 1, "); + last = i + 2; + } + break; + } + case kValPending: + { + if(curr == '[' || curr == '{' || curr == '(') + { + csubstr ss = s.sub(i).pair_range_nested(curr, '\\'); + i += static_cast(ss.end() - (s.str + i)); + state = _next(state); + } + else if(curr == ',' && next == ' ') + { + state = _next(state); + } + break; + } + default: + // nothing to do + break; + } + } + + writer.append(s.sub(last)); + if(state == kKeyPending) + writer.append(": 1"); + writer.append('}'); + + return writer.pos; +} + +C4_SUPPRESS_WARNING_GCC_CLANG_POP + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/reference_resolver.cpp b/3rdparty/rapidyaml/src/c4/yml/reference_resolver.cpp new file mode 100644 index 0000000000..9ce9c57d70 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/reference_resolver.cpp @@ -0,0 +1,333 @@ +#include "c4/yml/reference_resolver.hpp" +#include "c4/yml/common.hpp" +#include "c4/yml/detail/dbgprint.hpp" +#ifdef RYML_DBG +#include "c4/yml/detail/print.hpp" +#else +#define _c4dbg_tree(...) +#define _c4dbg_node(...) +#endif + +namespace c4 { +namespace yml { + +/** @cond dev */ + +id_type ReferenceResolver::count_anchors_and_refs_(id_type n) +{ + id_type c = 0; + c += m_tree->has_key_anchor(n); + c += m_tree->has_val_anchor(n); + c += m_tree->is_key_ref(n); + c += m_tree->is_val_ref(n); + c += m_tree->has_key(n) && m_tree->key(n) == "<<"; + for(id_type ch = m_tree->first_child(n); ch != NONE; ch = m_tree->next_sibling(ch)) + c += count_anchors_and_refs_(ch); + return c; +} + +void ReferenceResolver::gather_anchors_and_refs__(id_type n) +{ + // insert key refs BEFORE inserting val refs + if(m_tree->has_key(n)) + { + if(!m_tree->is_key_quoted(n) && m_tree->key(n) == "<<") + { + _c4dbgpf("node[{}]: key is <<", n); + if(m_tree->has_val(n)) + { + if(m_tree->is_val_ref(n)) + { + _c4dbgpf("node[{}]: instance[{}]: val ref, inheriting! '{}'", n, m_refs.size(), m_tree->val_ref(n)); + m_refs.push({VALREF, n, NONE, NONE, NONE, NONE}); + //m_refs.push({KEYREF, n, NONE, NONE, NONE, NONE}); + } + else + { + _c4dbgpf("node[{}]: not ref!", n); + } + } + else if(m_tree->is_seq(n)) + { + // for merging multiple inheritance targets + // <<: [ *CENTER, *BIG ] + _c4dbgpf("node[{}]: is seq!", n); + for(id_type ich = m_tree->first_child(n); ich != NONE; ich = m_tree->next_sibling(ich)) + { + _c4dbgpf("node[{}]: instance [{}]: val ref, inheriting multiple: {} '{}'", n, m_refs.size(), ich, m_tree->val_ref(ich)); + if(m_tree->is_container(ich)) + { + detail::_report_err(m_tree->m_callbacks, "ERROR: node {} child {}: refs for << cannot be containers.'", n, ich); + C4_UNREACHABLE_AFTER_ERR(); + } + m_refs.push({VALREF, ich, NONE, NONE, n, m_tree->next_sibling(n)}); + } + return; // don't descend into the seq + } + else + { + detail::_report_err(m_tree->m_callbacks, "ERROR: node {}: refs for << must be either val or seq", n); + C4_UNREACHABLE_AFTER_ERR(); + } + } + else if(m_tree->is_key_ref(n)) + { + _c4dbgpf("node[{}]: instance[{}]: key ref: '{}', key='{}'", n, m_refs.size(), m_tree->key_ref(n), m_tree->has_key(n) ? m_tree->key(n) : csubstr{"-"}); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->key(n) != "<<"); + _RYML_CB_CHECK(m_tree->m_callbacks, (!m_tree->has_key(n)) || m_tree->key(n).ends_with(m_tree->key_ref(n))); + m_refs.push({KEYREF, n, NONE, NONE, NONE, NONE}); + } + } + // val ref + if(m_tree->is_val_ref(n) && (!m_tree->has_key(n) || m_tree->key(n) != "<<")) + { + _c4dbgpf("node[{}]: instance[{}]: val ref: '{}'", n, m_refs.size(), m_tree->val_ref(n)); + RYML_CHECK((!m_tree->has_val(n)) || m_tree->val(n).ends_with(m_tree->val_ref(n))); + m_refs.push({VALREF, n, NONE, NONE, NONE, NONE}); + } + // anchors + if(m_tree->has_key_anchor(n)) + { + _c4dbgpf("node[{}]: instance[{}]: key anchor: '{}'", n, m_refs.size(), m_tree->key_anchor(n)); + RYML_CHECK(m_tree->has_key(n)); + m_refs.push({KEYANCH, n, NONE, NONE, NONE, NONE}); + } + if(m_tree->has_val_anchor(n)) + { + _c4dbgpf("node[{}]: instance[{}]: val anchor: '{}'", n, m_refs.size(), m_tree->val_anchor(n)); + RYML_CHECK(m_tree->has_val(n) || m_tree->is_container(n)); + m_refs.push({VALANCH, n, NONE, NONE, NONE, NONE}); + } + // recurse + for(id_type ch = m_tree->first_child(n); ch != NONE; ch = m_tree->next_sibling(ch)) + gather_anchors_and_refs__(ch); +} + +void ReferenceResolver::gather_anchors_and_refs_() +{ + _c4dbgp("gathering anchors and refs..."); + + // minimize (re-)allocations by counting first + id_type num_anchors_and_refs = count_anchors_and_refs_(m_tree->root_id()); + if(!num_anchors_and_refs) + return; + m_refs.reserve(num_anchors_and_refs); + m_refs.clear(); + + // now descend through the hierarchy + gather_anchors_and_refs__(m_tree->root_id()); + + _c4dbgpf("found {} anchors/refs", m_refs.size()); + + // finally connect the reference list + id_type prev_anchor = NONE; + id_type count = 0; + for(auto &rd : m_refs) + { + rd.prev_anchor = prev_anchor; + if(rd.type.has_anchor()) + prev_anchor = count; + ++count; + } + _c4dbgp("gathering anchors and refs: finished"); +} + +id_type ReferenceResolver::lookup_(RefData const* C4_RESTRICT ra) +{ + #ifdef RYML_DBG + id_type instance = static_cast(ra-m_refs.m_stack); + id_type node = ra->node; + #endif + RYML_ASSERT(ra->type.is_key_ref() || ra->type.is_val_ref()); + RYML_ASSERT(ra->type.is_key_ref() != ra->type.is_val_ref()); + csubstr refname; + _c4dbgpf("instance[{}:node{}]: lookup from node={}...", instance, node, ra->node); + if(ra->type.is_val_ref()) + { + refname = m_tree->val_ref(ra->node); + _c4dbgpf("instance[{}:node{}]: valref: '{}'", instance, node, refname); + } + else + { + RYML_ASSERT(ra->type.is_key_ref()); + refname = m_tree->key_ref(ra->node); + _c4dbgpf("instance[{}:node{}]: keyref: '{}'", instance, node, refname); + } + while(ra->prev_anchor != NONE) + { + ra = &m_refs[ra->prev_anchor]; + _c4dbgpf("instance[{}:node{}]: lookup '{}' at [{}:node{}]: keyref='{}' valref='{}'", instance, node, refname, ra-m_refs.m_stack, ra->node, + (m_tree->has_key_anchor(ra->node) ? m_tree->key_anchor(ra->node) : csubstr("~")), + (m_tree->has_val_anchor(ra->node) ? m_tree->val_anchor(ra->node) : csubstr("~"))); + if(m_tree->has_anchor(ra->node, refname)) + { + _c4dbgpf("instance[{}:node{}]: got it at [{}:node{}]!", instance, node, ra-m_refs.m_stack, ra->node); + return ra->node; + } + } + detail::_report_err(m_tree->m_callbacks, "ERROR: anchor not found: '{}'", refname); + C4_UNREACHABLE_AFTER_ERR(); +} + +void ReferenceResolver::reset_(Tree *t_) +{ + if(t_->callbacks() != m_refs.m_callbacks) + { + m_refs.m_callbacks = t_->callbacks(); + } + m_tree = t_; + m_refs.clear(); +} + +void ReferenceResolver::resolve_() +{ + /* from the specs: "an alias node refers to the most recent + * node in the serialization having the specified anchor". So + * we need to start looking upward from ref nodes. + * + * @see http://yaml.org/spec/1.2/spec.html#id2765878 */ + _c4dbgp("matching anchors/refs..."); + for(id_type i = 0, e = m_refs.size(); i < e; ++i) + { + RefData &C4_RESTRICT refdata = m_refs.top(i); + if( ! refdata.type.is_ref()) + continue; + refdata.target = lookup_(&refdata); + } + _c4dbgp("matching anchors/refs: finished"); + + // insert the resolved references + _c4dbgp("modifying tree..."); + id_type prev_parent_ref = NONE; + id_type prev_parent_ref_after = NONE; + for(id_type i = 0, e = m_refs.size(); i < e; ++i) + { + RefData const& C4_RESTRICT refdata = m_refs[i]; + _c4dbgpf("instance[{}:node{}]: {}/{}...", i, refdata.node, i+1, e); + if( ! refdata.type.is_ref()) + continue; + _c4dbgpf("instance[{}:node{}]: is reference!", i, refdata.node); + if(refdata.parent_ref != NONE) + { + _c4dbgpf("instance[{}:node{}] has parent: {}", i, refdata.node, refdata.parent_ref); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->is_seq(refdata.parent_ref)); + const id_type p = m_tree->parent(refdata.parent_ref); + const id_type after = (prev_parent_ref != refdata.parent_ref) ? + refdata.parent_ref//prev_sibling(rd.parent_ref_sibling) + : + prev_parent_ref_after; + prev_parent_ref = refdata.parent_ref; + prev_parent_ref_after = m_tree->duplicate_children_no_rep(refdata.target, p, after); + m_tree->remove(refdata.node); + } + else + { + _c4dbgpf("instance[{}:node{}] has no parent", i, refdata.node, refdata.parent_ref); + if(m_tree->has_key(refdata.node) && m_tree->key(refdata.node) == "<<") + { + _c4dbgpf("instance[{}:node{}] is inheriting", i, refdata.node); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->is_keyval(refdata.node)); + const id_type p = m_tree->parent(refdata.node); + const id_type after = m_tree->prev_sibling(refdata.node); + _c4dbgpf("instance[{}:node{}] p={} after={}", i, refdata.node, p, after); + m_tree->duplicate_children_no_rep(refdata.target, p, after); + m_tree->remove(refdata.node); + } + else if(refdata.type.is_key_ref()) + { + _c4dbgpf("instance[{}:node{}] is key ref", i, refdata.node); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->is_key_ref(refdata.node)); + _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->has_key_anchor(refdata.target) || m_tree->has_val_anchor(refdata.target)); + if(m_tree->has_val_anchor(refdata.target) && m_tree->val_anchor(refdata.target) == m_tree->key_ref(refdata.node)) + { + _c4dbgpf("instance[{}:node{}] target.anchor==val.anchor=={}", i, refdata.node, m_tree->val_anchor(refdata.target)); + _RYML_CB_CHECK(m_tree->m_callbacks, !m_tree->is_container(refdata.target)); + _RYML_CB_CHECK(m_tree->m_callbacks, m_tree->has_val(refdata.target)); + const type_bits existing_style_flags = VAL_STYLE & m_tree->_p(refdata.target)->m_type.type; + static_assert((VAL_STYLE >> 1u) == (KEY_STYLE), "bad flags"); + m_tree->_p(refdata.node)->m_key.scalar = m_tree->val(refdata.target); + m_tree->_add_flags(refdata.node, KEY | (existing_style_flags >> 1u)); + } + else + { + _c4dbgpf("instance[{}:node{}] don't inherit container flags", i, refdata.node); + _RYML_CB_CHECK(m_tree->m_callbacks, m_tree->key_anchor(refdata.target) == m_tree->key_ref(refdata.node)); + m_tree->_p(refdata.node)->m_key.scalar = m_tree->key(refdata.target); + // keys cannot be containers, so don't inherit container flags + const type_bits existing_style_flags = KEY_STYLE & m_tree->_p(refdata.target)->m_type.type; + m_tree->_add_flags(refdata.node, KEY | existing_style_flags); + } + } + else // val ref + { + _c4dbgpf("instance[{}:node{}] is val ref", i, refdata.node); + _RYML_CB_ASSERT(m_tree->m_callbacks, refdata.type.is_val_ref()); + if(m_tree->has_key_anchor(refdata.target) && m_tree->key_anchor(refdata.target) == m_tree->val_ref(refdata.node)) + { + _c4dbgpf("instance[{}:node{}] target.anchor==key.anchor=={}", i, refdata.node, m_tree->key_anchor(refdata.target)); + _RYML_CB_CHECK(m_tree->m_callbacks, !m_tree->is_container(refdata.target)); + _RYML_CB_CHECK(m_tree->m_callbacks, m_tree->has_val(refdata.target)); + // keys cannot be containers, so don't inherit container flags + const type_bits existing_style_flags = (KEY_STYLE) & m_tree->_p(refdata.target)->m_type.type; + static_assert((KEY_STYLE << 1u) == (VAL_STYLE), "bad flags"); + m_tree->_p(refdata.node)->m_val.scalar = m_tree->key(refdata.target); + m_tree->_add_flags(refdata.node, VAL | (existing_style_flags << 1u)); + } + else + { + _c4dbgpf("instance[{}:node{}] duplicate contents", i, refdata.node); + m_tree->duplicate_contents(refdata.target, refdata.node); + } + } + } + _c4dbg_tree("after insertion", *m_tree); + } +} + +void ReferenceResolver::resolve(Tree *t_, bool clear_anchors) +{ + _c4dbgp("resolving references..."); + + reset_(t_); + + _c4dbg_tree("unresolved tree", *m_tree); + + gather_anchors_and_refs_(); + if(m_refs.empty()) + return; + resolve_(); + _c4dbg_tree("resolved tree", *m_tree); + + // clear anchors and refs + if(clear_anchors) + { + _c4dbgp("clearing anchors/refs"); + auto clear_ = [this]{ + for(auto const& C4_RESTRICT ar : m_refs) + { + m_tree->rem_anchor_ref(ar.node); + if(ar.parent_ref != NONE) + if(m_tree->type(ar.parent_ref) != NOTYPE) + m_tree->remove(ar.parent_ref); + } + }; + clear_(); + // some of the elements injected during the resolution may + // have nested anchors; these anchors will have been newly + // injected during the resolution; collect again, and clear + // again, to ensure those are also cleared: + gather_anchors_and_refs_(); + clear_(); + _c4dbgp("clearing anchors/refs: finished"); + } + + _c4dbg_tree("final resolved tree", *m_tree); + + m_tree = nullptr; + _c4dbgp("resolving references: finished"); +} + +/** @endcond */ + +} // namespace ryml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/tag.cpp b/3rdparty/rapidyaml/src/c4/yml/tag.cpp new file mode 100644 index 0000000000..ba5dbf14a4 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/tag.cpp @@ -0,0 +1,316 @@ +#include "c4/yml/tag.hpp" +#include "c4/yml/detail/dbgprint.hpp" + + +namespace c4 { +namespace yml { + +bool is_custom_tag(csubstr tag) +{ + if((tag.len > 2) && (tag.str[0] == '!')) + { + size_t pos = tag.find('!', 1); + return pos != npos && pos > 1 && tag.str[1] != '<'; + } + return false; +} + +csubstr normalize_tag(csubstr tag) +{ + YamlTag_e t = to_tag(tag); + if(t != TAG_NONE) + return from_tag(t); + if(tag.begins_with("!<")) + tag = tag.sub(1); + if(tag.begins_with("'; + result = output.first(len); + } + else + { + result.str = nullptr; + result.len = len; + } + } + return result; +} + +YamlTag_e to_tag(csubstr tag) +{ + if(tag.begins_with("!<")) + tag = tag.sub(1); + if(tag.begins_with("!!")) + tag = tag.sub(2); + else if(tag.begins_with('!')) + return TAG_NONE; + else if(tag.begins_with("tag:yaml.org,2002:")) + { + RYML_ASSERT(csubstr("tag:yaml.org,2002:").len == 18); + tag = tag.sub(18); + } + else if(tag.begins_with(""}; + case TAG_OMAP: + return {""}; + case TAG_PAIRS: + return {""}; + case TAG_SET: + return {""}; + case TAG_SEQ: + return {""}; + case TAG_BINARY: + return {""}; + case TAG_BOOL: + return {""}; + case TAG_FLOAT: + return {""}; + case TAG_INT: + return {""}; + case TAG_MERGE: + return {""}; + case TAG_NULL: + return {""}; + case TAG_STR: + return {""}; + case TAG_TIMESTAMP: + return {""}; + case TAG_VALUE: + return {""}; + case TAG_YAML: + return {""}; + case TAG_NONE: + default: + return {""}; + } +} + +csubstr from_tag(YamlTag_e tag) +{ + switch(tag) + { + case TAG_MAP: + return {"!!map"}; + case TAG_OMAP: + return {"!!omap"}; + case TAG_PAIRS: + return {"!!pairs"}; + case TAG_SET: + return {"!!set"}; + case TAG_SEQ: + return {"!!seq"}; + case TAG_BINARY: + return {"!!binary"}; + case TAG_BOOL: + return {"!!bool"}; + case TAG_FLOAT: + return {"!!float"}; + case TAG_INT: + return {"!!int"}; + case TAG_MERGE: + return {"!!merge"}; + case TAG_NULL: + return {"!!null"}; + case TAG_STR: + return {"!!str"}; + case TAG_TIMESTAMP: + return {"!!timestamp"}; + case TAG_VALUE: + return {"!!value"}; + case TAG_YAML: + return {"!!yaml"}; + case TAG_NONE: + default: + return {""}; + } +} + + +bool TagDirective::create_from_str(csubstr directive_) +{ + csubstr directive = directive_; + directive = directive.sub(4); + if(!directive.begins_with(' ')) + return false; + directive = directive.triml(' '); + size_t pos = directive.find(' '); + if(pos == npos) + return false; + handle = directive.first(pos); + directive = directive.sub(handle.len).triml(' '); + pos = directive.find(' '); + if(pos != npos) + directive = directive.first(pos); + prefix = directive; + next_node_id = NONE; + _c4dbgpf("%TAG: handle={} prefix={}", handle, prefix); + return true; +} + +size_t TagDirective::transform(csubstr tag, substr output, Callbacks const& callbacks, bool with_brackets) const +{ + _c4dbgpf("%TAG: handle={} prefix={} next_node={}. tag={}", handle, prefix, next_node_id, tag); + _RYML_CB_ASSERT(callbacks, tag.len >= handle.len); + csubstr rest = tag.sub(handle.len); + _c4dbgpf("%TAG: rest={}", rest); + if(rest.begins_with('<')) + { + _c4dbgpf("%TAG: begins with <. rest={}", rest); + if(C4_UNLIKELY(!rest.ends_with('>'))) + _RYML_CB_ERR(callbacks, "malformed tag"); + rest = rest.offs(1, 1); + if(rest.begins_with(prefix)) + { + _c4dbgpf("%TAG: already transformed! actual={}", rest.sub(prefix.len)); + return 0; // return 0 to signal that the tag is local and cannot be resolved + } + } + size_t len = prefix.len + rest.len; + if(with_brackets) + len += 2; + size_t numpc = rest.count('%'); + if(numpc == 0) + { + if(len <= output.len) + { + if(with_brackets) + { + output.str[0] = '<'; + memcpy(1u + output.str, prefix.str, prefix.len); + memcpy(1u + output.str + prefix.len, rest.str, rest.len); + output.str[1u + prefix.len + rest.len] = '>'; + } + else + { + memcpy(output.str, prefix.str, prefix.len); + memcpy(output.str + prefix.len, rest.str, rest.len); + } + } + } + else + { + // need to decode URI % sequences + size_t pos = rest.find('%'); + _RYML_CB_ASSERT(callbacks, pos != npos); + do { + size_t next = rest.first_not_of("0123456789abcdefABCDEF", pos+1); + if(next == npos) + next = rest.len; + _RYML_CB_CHECK(callbacks, pos+1 < next); + _RYML_CB_CHECK(callbacks, pos+1 + 2 <= next); + size_t delta = next - (pos+1); + len -= delta; + pos = rest.find('%', pos+1); + } while(pos != npos); + if(len <= output.len) + { + size_t prev = 0, wpos = 0; + auto appendstr = [&](csubstr s) { memcpy(output.str + wpos, s.str, s.len); wpos += s.len; }; + auto appendchar = [&](char c) { output.str[wpos++] = c; }; + if(with_brackets) + appendchar('<'); + appendstr(prefix); + pos = rest.find('%'); + _RYML_CB_ASSERT(callbacks, pos != npos); + do { + size_t next = rest.first_not_of("0123456789abcdefABCDEF", pos+1); + if(next == npos) + next = rest.len; + _RYML_CB_CHECK(callbacks, pos+1 < next); + _RYML_CB_CHECK(callbacks, pos+1 + 2 <= next); + uint8_t val; + if(C4_UNLIKELY(!read_hex(rest.range(pos+1, next), &val) || val > 127)) + _RYML_CB_ERR(callbacks, "invalid URI character"); + appendstr(rest.range(prev, pos)); + appendchar(static_cast(val)); + prev = next; + pos = rest.find('%', pos+1); + } while(pos != npos); + _RYML_CB_ASSERT(callbacks, pos == npos); + _RYML_CB_ASSERT(callbacks, prev > 0); + _RYML_CB_ASSERT(callbacks, rest.len >= prev); + appendstr(rest.sub(prev)); + if(with_brackets) + appendchar('>'); + _RYML_CB_ASSERT(callbacks, wpos == len); + } + } + return len; +} + +} // namespace yml +} // namespace c4 diff --git a/3rdparty/rapidyaml/src/c4/yml/tree.cpp b/3rdparty/rapidyaml/src/c4/yml/tree.cpp new file mode 100644 index 0000000000..fb2e58bcc2 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/tree.cpp @@ -0,0 +1,1978 @@ +#include "c4/yml/tree.hpp" +#include "c4/yml/detail/dbgprint.hpp" +#include "c4/yml/node.hpp" +#include "c4/yml/reference_resolver.hpp" + + +C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4296/*expression is always 'boolean_value'*/) +C4_SUPPRESS_WARNING_MSVC(4702/*unreachable code*/) +C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wold-style-cast") +C4_SUPPRESS_WARNING_GCC("-Wtype-limits") +C4_SUPPRESS_WARNING_GCC("-Wuseless-cast") + +namespace c4 { +namespace yml { + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +NodeRef Tree::rootref() +{ + return NodeRef(this, root_id()); +} +ConstNodeRef Tree::rootref() const +{ + return ConstNodeRef(this, root_id()); +} + +ConstNodeRef Tree::crootref() const +{ + return ConstNodeRef(this, root_id()); +} + +NodeRef Tree::ref(id_type id) +{ + _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap); + return NodeRef(this, id); +} +ConstNodeRef Tree::ref(id_type id) const +{ + _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap); + return ConstNodeRef(this, id); +} +ConstNodeRef Tree::cref(id_type id) const +{ + _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap); + return ConstNodeRef(this, id); +} + +NodeRef Tree::operator[] (csubstr key) +{ + return rootref()[key]; +} +ConstNodeRef Tree::operator[] (csubstr key) const +{ + return rootref()[key]; +} + +NodeRef Tree::operator[] (id_type i) +{ + return rootref()[i]; +} +ConstNodeRef Tree::operator[] (id_type i) const +{ + return rootref()[i]; +} + +NodeRef Tree::docref(id_type i) +{ + return ref(doc(i)); +} +ConstNodeRef Tree::docref(id_type i) const +{ + return cref(doc(i)); +} +ConstNodeRef Tree::cdocref(id_type i) const +{ + return cref(doc(i)); +} + + +//----------------------------------------------------------------------------- +Tree::Tree(Callbacks const& cb) + : m_buf(nullptr) + , m_cap(0) + , m_size(0) + , m_free_head(NONE) + , m_free_tail(NONE) + , m_arena() + , m_arena_pos(0) + , m_callbacks(cb) + , m_tag_directives() +{ +} + +Tree::Tree(id_type node_capacity, size_t arena_capacity, Callbacks const& cb) + : Tree(cb) +{ + reserve(node_capacity); + reserve_arena(arena_capacity); +} + +Tree::~Tree() +{ + _free(); +} + + +Tree::Tree(Tree const& that) : Tree(that.m_callbacks) +{ + _copy(that); +} + +Tree& Tree::operator= (Tree const& that) +{ + if(&that != this) + { + _free(); + m_callbacks = that.m_callbacks; + _copy(that); + } + return *this; +} + +Tree::Tree(Tree && that) noexcept : Tree(that.m_callbacks) +{ + _move(that); +} + +Tree& Tree::operator= (Tree && that) noexcept +{ + if(&that != this) + { + _free(); + m_callbacks = that.m_callbacks; + _move(that); + } + return *this; +} + +void Tree::_free() +{ + if(m_buf) + { + _RYML_CB_ASSERT(m_callbacks, m_cap > 0); + _RYML_CB_FREE(m_callbacks, m_buf, NodeData, (size_t)m_cap); + } + if(m_arena.str) + { + _RYML_CB_ASSERT(m_callbacks, m_arena.len > 0); + _RYML_CB_FREE(m_callbacks, m_arena.str, char, m_arena.len); + } + _clear(); +} + + +C4_SUPPRESS_WARNING_GCC_PUSH +#if defined(__GNUC__) && __GNUC__>= 8 + C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wclass-memaccess") // error: ‘void* memset(void*, int, size_t)’ clearing an object of type ‘class c4::yml::Tree’ with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +void Tree::_clear() +{ + m_buf = nullptr; + m_cap = 0; + m_size = 0; + m_free_head = 0; + m_free_tail = 0; + m_arena = {}; + m_arena_pos = 0; + for(id_type i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i) + m_tag_directives[i] = {}; +} + +void Tree::_copy(Tree const& that) +{ + _RYML_CB_ASSERT(m_callbacks, m_buf == nullptr); + _RYML_CB_ASSERT(m_callbacks, m_arena.str == nullptr); + _RYML_CB_ASSERT(m_callbacks, m_arena.len == 0); + if(that.m_cap) + { + m_buf = _RYML_CB_ALLOC_HINT(m_callbacks, NodeData, (size_t)that.m_cap, that.m_buf); + memcpy(m_buf, that.m_buf, (size_t)that.m_cap * sizeof(NodeData)); + } + m_cap = that.m_cap; + m_size = that.m_size; + m_free_head = that.m_free_head; + m_free_tail = that.m_free_tail; + m_arena_pos = that.m_arena_pos; + m_arena = that.m_arena; + if(that.m_arena.str) + { + _RYML_CB_ASSERT(m_callbacks, that.m_arena.len > 0); + substr arena; + arena.str = _RYML_CB_ALLOC_HINT(m_callbacks, char, that.m_arena.len, that.m_arena.str); + arena.len = that.m_arena.len; + _relocate(arena); // does a memcpy of the arena and updates nodes using the old arena + m_arena = arena; + } + for(id_type i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i) + m_tag_directives[i] = that.m_tag_directives[i]; +} + +void Tree::_move(Tree & that) noexcept +{ + _RYML_CB_ASSERT(m_callbacks, m_buf == nullptr); + _RYML_CB_ASSERT(m_callbacks, m_arena.str == nullptr); + _RYML_CB_ASSERT(m_callbacks, m_arena.len == 0); + m_buf = that.m_buf; + m_cap = that.m_cap; + m_size = that.m_size; + m_free_head = that.m_free_head; + m_free_tail = that.m_free_tail; + m_arena = that.m_arena; + m_arena_pos = that.m_arena_pos; + for(id_type i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i) + m_tag_directives[i] = that.m_tag_directives[i]; + that._clear(); +} + +void Tree::_relocate(substr next_arena) +{ + _RYML_CB_ASSERT(m_callbacks, next_arena.not_empty()); + _RYML_CB_ASSERT(m_callbacks, next_arena.len >= m_arena.len); + if(m_arena_pos) + memcpy(next_arena.str, m_arena.str, m_arena_pos); + for(NodeData *C4_RESTRICT n = m_buf, *e = m_buf + m_cap; n != e; ++n) + { + if(in_arena(n->m_key.scalar)) + n->m_key.scalar = _relocated(n->m_key.scalar, next_arena); + if(in_arena(n->m_key.tag)) + n->m_key.tag = _relocated(n->m_key.tag, next_arena); + if(in_arena(n->m_key.anchor)) + n->m_key.anchor = _relocated(n->m_key.anchor, next_arena); + if(in_arena(n->m_val.scalar)) + n->m_val.scalar = _relocated(n->m_val.scalar, next_arena); + if(in_arena(n->m_val.tag)) + n->m_val.tag = _relocated(n->m_val.tag, next_arena); + if(in_arena(n->m_val.anchor)) + n->m_val.anchor = _relocated(n->m_val.anchor, next_arena); + } + for(TagDirective &C4_RESTRICT td : m_tag_directives) + { + if(in_arena(td.prefix)) + td.prefix = _relocated(td.prefix, next_arena); + if(in_arena(td.handle)) + td.handle = _relocated(td.handle, next_arena); + } +} + + +//----------------------------------------------------------------------------- +void Tree::reserve(id_type cap) +{ + if(cap > m_cap) + { + NodeData *buf = _RYML_CB_ALLOC_HINT(m_callbacks, NodeData, (size_t)cap, m_buf); + if(m_buf) + { + memcpy(buf, m_buf, (size_t)m_cap * sizeof(NodeData)); + _RYML_CB_FREE(m_callbacks, m_buf, NodeData, (size_t)m_cap); + } + id_type first = m_cap, del = cap - m_cap; + m_cap = cap; + m_buf = buf; + _clear_range(first, del); + if(m_free_head != NONE) + { + _RYML_CB_ASSERT(m_callbacks, m_buf != nullptr); + _RYML_CB_ASSERT(m_callbacks, m_free_tail != NONE); + m_buf[m_free_tail].m_next_sibling = first; + m_buf[first].m_prev_sibling = m_free_tail; + m_free_tail = cap-1; + } + else + { + _RYML_CB_ASSERT(m_callbacks, m_free_tail == NONE); + m_free_head = first; + m_free_tail = cap-1; + } + _RYML_CB_ASSERT(m_callbacks, m_free_head == NONE || (m_free_head >= 0 && m_free_head < cap)); + _RYML_CB_ASSERT(m_callbacks, m_free_tail == NONE || (m_free_tail >= 0 && m_free_tail < cap)); + + if( ! m_size) + _claim_root(); + } +} + + +//----------------------------------------------------------------------------- +void Tree::clear() +{ + _clear_range(0, m_cap); + m_size = 0; + if(m_buf) + { + _RYML_CB_ASSERT(m_callbacks, m_cap >= 0); + m_free_head = 0; + m_free_tail = m_cap-1; + _claim_root(); + } + else + { + m_free_head = NONE; + m_free_tail = NONE; + } + for(id_type i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i) + m_tag_directives[i] = {}; +} + +void Tree::_claim_root() +{ + id_type r = _claim(); + _RYML_CB_ASSERT(m_callbacks, r == 0); + _set_hierarchy(r, NONE, NONE); +} + + +//----------------------------------------------------------------------------- +void Tree::_clear_range(id_type first, id_type num) +{ + if(num == 0) + return; // prevent overflow when subtracting + _RYML_CB_ASSERT(m_callbacks, first >= 0 && first + num <= m_cap); + memset(m_buf + first, 0, (size_t)num * sizeof(NodeData)); // TODO we should not need this + for(id_type i = first, e = first + num; i < e; ++i) + { + _clear(i); + NodeData *n = m_buf + i; + n->m_prev_sibling = i - 1; + n->m_next_sibling = i + 1; + } + m_buf[first + num - 1].m_next_sibling = NONE; +} + +C4_SUPPRESS_WARNING_GCC_POP + + +//----------------------------------------------------------------------------- +void Tree::_release(id_type i) +{ + _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap); + + _rem_hierarchy(i); + _free_list_add(i); + _clear(i); + + --m_size; +} + +//----------------------------------------------------------------------------- +// add to the front of the free list +void Tree::_free_list_add(id_type i) +{ + _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap); + NodeData &C4_RESTRICT w = m_buf[i]; + + w.m_parent = NONE; + w.m_next_sibling = m_free_head; + w.m_prev_sibling = NONE; + if(m_free_head != NONE) + m_buf[m_free_head].m_prev_sibling = i; + m_free_head = i; + if(m_free_tail == NONE) + m_free_tail = m_free_head; +} + +void Tree::_free_list_rem(id_type i) +{ + if(m_free_head == i) + m_free_head = _p(i)->m_next_sibling; + _rem_hierarchy(i); +} + +//----------------------------------------------------------------------------- +id_type Tree::_claim() +{ + if(m_free_head == NONE || m_buf == nullptr) + { + id_type sz = 2 * m_cap; + sz = sz ? sz : 16; + reserve(sz); + _RYML_CB_ASSERT(m_callbacks, m_free_head != NONE); + } + + _RYML_CB_ASSERT(m_callbacks, m_size < m_cap); + _RYML_CB_ASSERT(m_callbacks, m_free_head >= 0 && m_free_head < m_cap); + + id_type ichild = m_free_head; + NodeData *child = m_buf + ichild; + + ++m_size; + m_free_head = child->m_next_sibling; + if(m_free_head == NONE) + { + m_free_tail = NONE; + _RYML_CB_ASSERT(m_callbacks, m_size == m_cap); + } + + _clear(ichild); + + return ichild; +} + +//----------------------------------------------------------------------------- + +C4_SUPPRESS_WARNING_GCC_PUSH +C4_SUPPRESS_WARNING_CLANG_PUSH +C4_SUPPRESS_WARNING_CLANG("-Wnull-dereference") +#if defined(__GNUC__) +#if (__GNUC__ >= 6) +C4_SUPPRESS_WARNING_GCC("-Wnull-dereference") +#endif +#if (__GNUC__ > 9) +C4_SUPPRESS_WARNING_GCC("-Wanalyzer-fd-leak") +#endif +#endif + +void Tree::_set_hierarchy(id_type ichild, id_type iparent, id_type iprev_sibling) +{ + _RYML_CB_ASSERT(m_callbacks, ichild >= 0 && ichild < m_cap); + _RYML_CB_ASSERT(m_callbacks, iparent == NONE || (iparent >= 0 && iparent < m_cap)); + _RYML_CB_ASSERT(m_callbacks, iprev_sibling == NONE || (iprev_sibling >= 0 && iprev_sibling < m_cap)); + + NodeData *C4_RESTRICT child = _p(ichild); + + child->m_parent = iparent; + child->m_prev_sibling = NONE; + child->m_next_sibling = NONE; + + if(iparent == NONE) + { + _RYML_CB_ASSERT(m_callbacks, ichild == 0); + _RYML_CB_ASSERT(m_callbacks, iprev_sibling == NONE); + } + + if(iparent == NONE) + return; + + id_type inext_sibling = iprev_sibling != NONE ? next_sibling(iprev_sibling) : first_child(iparent); + NodeData *C4_RESTRICT parent = get(iparent); + NodeData *C4_RESTRICT psib = get(iprev_sibling); + NodeData *C4_RESTRICT nsib = get(inext_sibling); + + if(psib) + { + _RYML_CB_ASSERT(m_callbacks, next_sibling(iprev_sibling) == id(nsib)); + child->m_prev_sibling = id(psib); + psib->m_next_sibling = id(child); + _RYML_CB_ASSERT(m_callbacks, psib->m_prev_sibling != psib->m_next_sibling || psib->m_prev_sibling == NONE); + } + + if(nsib) + { + _RYML_CB_ASSERT(m_callbacks, prev_sibling(inext_sibling) == id(psib)); + child->m_next_sibling = id(nsib); + nsib->m_prev_sibling = id(child); + _RYML_CB_ASSERT(m_callbacks, nsib->m_prev_sibling != nsib->m_next_sibling || nsib->m_prev_sibling == NONE); + } + + if(parent->m_first_child == NONE) + { + _RYML_CB_ASSERT(m_callbacks, parent->m_last_child == NONE); + parent->m_first_child = id(child); + parent->m_last_child = id(child); + } + else + { + if(child->m_next_sibling == parent->m_first_child) + parent->m_first_child = id(child); + + if(child->m_prev_sibling == parent->m_last_child) + parent->m_last_child = id(child); + } +} + +C4_SUPPRESS_WARNING_GCC_POP +C4_SUPPRESS_WARNING_CLANG_POP + + +//----------------------------------------------------------------------------- +void Tree::_rem_hierarchy(id_type i) +{ + _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap); + + NodeData &C4_RESTRICT w = m_buf[i]; + + // remove from the parent + if(w.m_parent != NONE) + { + NodeData &C4_RESTRICT p = m_buf[w.m_parent]; + if(p.m_first_child == i) + { + p.m_first_child = w.m_next_sibling; + } + if(p.m_last_child == i) + { + p.m_last_child = w.m_prev_sibling; + } + } + + // remove from the used list + if(w.m_prev_sibling != NONE) + { + NodeData *C4_RESTRICT prev = get(w.m_prev_sibling); + prev->m_next_sibling = w.m_next_sibling; + } + if(w.m_next_sibling != NONE) + { + NodeData *C4_RESTRICT next = get(w.m_next_sibling); + next->m_prev_sibling = w.m_prev_sibling; + } +} + +//----------------------------------------------------------------------------- +/** @cond dev */ +id_type Tree::_do_reorder(id_type *node, id_type count) +{ + // swap this node if it's not in place + if(*node != count) + { + _swap(*node, count); + *node = count; + } + ++count; // bump the count from this node + + // now descend in the hierarchy + for(id_type i = first_child(*node); i != NONE; i = next_sibling(i)) + { + // this child may have been relocated to a different index, + // so get an updated version + count = _do_reorder(&i, count); + } + return count; +} +/** @endcond */ + +void Tree::reorder() +{ + id_type r = root_id(); + _do_reorder(&r, 0); +} + + +//----------------------------------------------------------------------------- +/** @cond dev */ +void Tree::_swap(id_type n_, id_type m_) +{ + _RYML_CB_ASSERT(m_callbacks, (parent(n_) != NONE) || type(n_) == NOTYPE); + _RYML_CB_ASSERT(m_callbacks, (parent(m_) != NONE) || type(m_) == NOTYPE); + NodeType tn = type(n_); + NodeType tm = type(m_); + if(tn != NOTYPE && tm != NOTYPE) + { + _swap_props(n_, m_); + _swap_hierarchy(n_, m_); + } + else if(tn == NOTYPE && tm != NOTYPE) + { + _copy_props(n_, m_); + _free_list_rem(n_); + _copy_hierarchy(n_, m_); + _clear(m_); + _free_list_add(m_); + } + else if(tn != NOTYPE && tm == NOTYPE) + { + _copy_props(m_, n_); + _free_list_rem(m_); + _copy_hierarchy(m_, n_); + _clear(n_); + _free_list_add(n_); + } + else + { + C4_NEVER_REACH(); + } +} + +//----------------------------------------------------------------------------- +void Tree::_swap_hierarchy(id_type ia, id_type ib) +{ + if(ia == ib) return; + + for(id_type i = first_child(ia); i != NONE; i = next_sibling(i)) + { + if(i == ib || i == ia) + continue; + _p(i)->m_parent = ib; + } + + for(id_type i = first_child(ib); i != NONE; i = next_sibling(i)) + { + if(i == ib || i == ia) + continue; + _p(i)->m_parent = ia; + } + + auto & C4_RESTRICT a = *_p(ia); + auto & C4_RESTRICT b = *_p(ib); + auto & C4_RESTRICT pa = *_p(a.m_parent); + auto & C4_RESTRICT pb = *_p(b.m_parent); + + if(&pa == &pb) + { + if((pa.m_first_child == ib && pa.m_last_child == ia) + || + (pa.m_first_child == ia && pa.m_last_child == ib)) + { + std::swap(pa.m_first_child, pa.m_last_child); + } + else + { + bool changed = false; + if(pa.m_first_child == ia) + { + pa.m_first_child = ib; + changed = true; + } + if(pa.m_last_child == ia) + { + pa.m_last_child = ib; + changed = true; + } + if(pb.m_first_child == ib && !changed) + { + pb.m_first_child = ia; + } + if(pb.m_last_child == ib && !changed) + { + pb.m_last_child = ia; + } + } + } + else + { + if(pa.m_first_child == ia) + pa.m_first_child = ib; + if(pa.m_last_child == ia) + pa.m_last_child = ib; + if(pb.m_first_child == ib) + pb.m_first_child = ia; + if(pb.m_last_child == ib) + pb.m_last_child = ia; + } + std::swap(a.m_first_child , b.m_first_child); + std::swap(a.m_last_child , b.m_last_child); + + if(a.m_prev_sibling != ib && b.m_prev_sibling != ia && + a.m_next_sibling != ib && b.m_next_sibling != ia) + { + if(a.m_prev_sibling != NONE && a.m_prev_sibling != ib) + _p(a.m_prev_sibling)->m_next_sibling = ib; + if(a.m_next_sibling != NONE && a.m_next_sibling != ib) + _p(a.m_next_sibling)->m_prev_sibling = ib; + if(b.m_prev_sibling != NONE && b.m_prev_sibling != ia) + _p(b.m_prev_sibling)->m_next_sibling = ia; + if(b.m_next_sibling != NONE && b.m_next_sibling != ia) + _p(b.m_next_sibling)->m_prev_sibling = ia; + std::swap(a.m_prev_sibling, b.m_prev_sibling); + std::swap(a.m_next_sibling, b.m_next_sibling); + } + else + { + if(a.m_next_sibling == ib) // n will go after m + { + _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling == ia); + if(a.m_prev_sibling != NONE) + { + _RYML_CB_ASSERT(m_callbacks, a.m_prev_sibling != ib); + _p(a.m_prev_sibling)->m_next_sibling = ib; + } + if(b.m_next_sibling != NONE) + { + _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling != ia); + _p(b.m_next_sibling)->m_prev_sibling = ia; + } + id_type ns = b.m_next_sibling; + b.m_prev_sibling = a.m_prev_sibling; + b.m_next_sibling = ia; + a.m_prev_sibling = ib; + a.m_next_sibling = ns; + } + else if(a.m_prev_sibling == ib) // m will go after n + { + _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling == ia); + if(b.m_prev_sibling != NONE) + { + _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling != ia); + _p(b.m_prev_sibling)->m_next_sibling = ia; + } + if(a.m_next_sibling != NONE) + { + _RYML_CB_ASSERT(m_callbacks, a.m_next_sibling != ib); + _p(a.m_next_sibling)->m_prev_sibling = ib; + } + id_type ns = b.m_prev_sibling; + a.m_prev_sibling = b.m_prev_sibling; + a.m_next_sibling = ib; + b.m_prev_sibling = ia; + b.m_next_sibling = ns; + } + else + { + C4_NEVER_REACH(); + } + } + _RYML_CB_ASSERT(m_callbacks, a.m_next_sibling != ia); + _RYML_CB_ASSERT(m_callbacks, a.m_prev_sibling != ia); + _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling != ib); + _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling != ib); + + if(a.m_parent != ib && b.m_parent != ia) + { + std::swap(a.m_parent, b.m_parent); + } + else + { + if(a.m_parent == ib && b.m_parent != ia) + { + a.m_parent = b.m_parent; + b.m_parent = ia; + } + else if(a.m_parent != ib && b.m_parent == ia) + { + b.m_parent = a.m_parent; + a.m_parent = ib; + } + else + { + C4_NEVER_REACH(); + } + } +} + +//----------------------------------------------------------------------------- +void Tree::_copy_hierarchy(id_type dst_, id_type src_) +{ + auto const& C4_RESTRICT src = *_p(src_); + auto & C4_RESTRICT dst = *_p(dst_); + auto & C4_RESTRICT prt = *_p(src.m_parent); + for(id_type i = src.m_first_child; i != NONE; i = next_sibling(i)) + { + _p(i)->m_parent = dst_; + } + if(src.m_prev_sibling != NONE) + { + _p(src.m_prev_sibling)->m_next_sibling = dst_; + } + if(src.m_next_sibling != NONE) + { + _p(src.m_next_sibling)->m_prev_sibling = dst_; + } + if(prt.m_first_child == src_) + { + prt.m_first_child = dst_; + } + if(prt.m_last_child == src_) + { + prt.m_last_child = dst_; + } + dst.m_parent = src.m_parent; + dst.m_first_child = src.m_first_child; + dst.m_last_child = src.m_last_child; + dst.m_prev_sibling = src.m_prev_sibling; + dst.m_next_sibling = src.m_next_sibling; +} + +//----------------------------------------------------------------------------- +void Tree::_swap_props(id_type n_, id_type m_) +{ + NodeData &C4_RESTRICT n = *_p(n_); + NodeData &C4_RESTRICT m = *_p(m_); + std::swap(n.m_type, m.m_type); + std::swap(n.m_key, m.m_key); + std::swap(n.m_val, m.m_val); +} +/** @endcond */ + +//----------------------------------------------------------------------------- +void Tree::move(id_type node, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, node != after); + _RYML_CB_ASSERT(m_callbacks, ! is_root(node)); + _RYML_CB_ASSERT(m_callbacks, (after == NONE) || (has_sibling(node, after) && has_sibling(after, node))); + + _rem_hierarchy(node); + _set_hierarchy(node, parent(node), after); +} + +//----------------------------------------------------------------------------- + +void Tree::move(id_type node, id_type new_parent, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, node != after); + _RYML_CB_ASSERT(m_callbacks, new_parent != NONE); + _RYML_CB_ASSERT(m_callbacks, new_parent != node); + _RYML_CB_ASSERT(m_callbacks, new_parent != after); + _RYML_CB_ASSERT(m_callbacks, ! is_root(node)); + + _rem_hierarchy(node); + _set_hierarchy(node, new_parent, after); +} + +id_type Tree::move(Tree *src, id_type node, id_type new_parent, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, src != nullptr); + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, new_parent != NONE); + _RYML_CB_ASSERT(m_callbacks, new_parent != after); + + id_type dup = duplicate(src, node, new_parent, after); + src->remove(node); + return dup; +} + +void Tree::set_root_as_stream() +{ + id_type root = root_id(); + if(is_stream(root)) + return; + // don't use _add_flags() because it's checked and will fail + if(!has_children(root)) + { + if(is_val(root)) + { + _p(root)->m_type.add(SEQ); + id_type next_doc = append_child(root); + _copy_props_wo_key(next_doc, root); + _p(next_doc)->m_type.add(DOC); + _p(next_doc)->m_type.rem(SEQ); + } + _p(root)->m_type = STREAM; + return; + } + _RYML_CB_ASSERT(m_callbacks, !has_key(root)); + id_type next_doc = append_child(root); + _copy_props_wo_key(next_doc, root); + _add_flags(next_doc, DOC); + for(id_type prev = NONE, ch = first_child(root), next = next_sibling(ch); ch != NONE; ) + { + if(ch == next_doc) + break; + move(ch, next_doc, prev); + prev = ch; + ch = next; + next = next_sibling(next); + } + _p(root)->m_type = STREAM; +} + + +//----------------------------------------------------------------------------- +void Tree::remove_children(id_type node) +{ + _RYML_CB_ASSERT(m_callbacks, get(node) != nullptr); + #if __GNUC__ >= 6 + C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wnull-dereference") + #endif + id_type ich = get(node)->m_first_child; + #if __GNUC__ >= 6 + C4_SUPPRESS_WARNING_GCC_POP + #endif + while(ich != NONE) + { + remove_children(ich); + _RYML_CB_ASSERT(m_callbacks, get(ich) != nullptr); + id_type next = get(ich)->m_next_sibling; + _release(ich); + if(ich == get(node)->m_last_child) + break; + ich = next; + } +} + +bool Tree::change_type(id_type node, NodeType type) +{ + _RYML_CB_ASSERT(m_callbacks, type.is_val() || type.is_map() || type.is_seq()); + _RYML_CB_ASSERT(m_callbacks, type.is_val() + type.is_map() + type.is_seq() == 1); + _RYML_CB_ASSERT(m_callbacks, type.has_key() == has_key(node) || (has_key(node) && !type.has_key())); + NodeData *d = _p(node); + if(type.is_map() && is_map(node)) + return false; + else if(type.is_seq() && is_seq(node)) + return false; + else if(type.is_val() && is_val(node)) + return false; + d->m_type = (d->m_type & (~(MAP|SEQ|VAL))) | type; + remove_children(node); + return true; +} + + +//----------------------------------------------------------------------------- +id_type Tree::duplicate(id_type node, id_type parent, id_type after) +{ + return duplicate(this, node, parent, after); +} + +id_type Tree::duplicate(Tree const* src, id_type node, id_type parent, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, src != nullptr); + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, parent != NONE); + _RYML_CB_ASSERT(m_callbacks, ! src->is_root(node)); + + id_type copy = _claim(); + + _copy_props(copy, src, node); + _set_hierarchy(copy, parent, after); + duplicate_children(src, node, copy, NONE); + + return copy; +} + +//----------------------------------------------------------------------------- +id_type Tree::duplicate_children(id_type node, id_type parent, id_type after) +{ + return duplicate_children(this, node, parent, after); +} + +id_type Tree::duplicate_children(Tree const* src, id_type node, id_type parent, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, src != nullptr); + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, parent != NONE); + _RYML_CB_ASSERT(m_callbacks, after == NONE || has_child(parent, after)); + + id_type prev = after; + for(id_type i = src->first_child(node); i != NONE; i = src->next_sibling(i)) + { + prev = duplicate(src, i, parent, prev); + } + + return prev; +} + +//----------------------------------------------------------------------------- +void Tree::duplicate_contents(id_type node, id_type where) +{ + duplicate_contents(this, node, where); +} + +void Tree::duplicate_contents(Tree const *src, id_type node, id_type where) +{ + _RYML_CB_ASSERT(m_callbacks, src != nullptr); + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, where != NONE); + _copy_props_wo_key(where, src, node); + duplicate_children(src, node, where, last_child(where)); +} + +//----------------------------------------------------------------------------- +id_type Tree::duplicate_children_no_rep(id_type node, id_type parent, id_type after) +{ + return duplicate_children_no_rep(this, node, parent, after); +} + +id_type Tree::duplicate_children_no_rep(Tree const *src, id_type node, id_type parent, id_type after) +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, parent != NONE); + _RYML_CB_ASSERT(m_callbacks, after == NONE || has_child(parent, after)); + + // don't loop using pointers as there may be a relocation + + // find the position where "after" is + id_type after_pos = NONE; + if(after != NONE) + { + for(id_type i = first_child(parent), icount = 0; i != NONE; ++icount, i = next_sibling(i)) + { + if(i == after) + { + after_pos = icount; + break; + } + } + _RYML_CB_ASSERT(m_callbacks, after_pos != NONE); + } + + // for each child to be duplicated... + id_type prev = after; + for(id_type i = src->first_child(node); i != NONE; i = src->next_sibling(i)) + { + _c4dbgpf("duplicate_no_rep: {} -> {}/{}", i, parent, prev); + _RYML_CB_CHECK(m_callbacks, this != src || (parent != i && !is_ancestor(parent, i))); + if(is_seq(parent)) + { + _c4dbgpf("duplicate_no_rep: {} is seq", parent); + prev = duplicate(src, i, parent, prev); + } + else + { + _c4dbgpf("duplicate_no_rep: {} is map", parent); + _RYML_CB_ASSERT(m_callbacks, is_map(parent)); + // does the parent already have a node with key equal to that of the current duplicate? + id_type dstnode_dup = NONE, dstnode_dup_pos = NONE; + { + csubstr srckey = src->key(i); + for(id_type j = first_child(parent), jcount = 0; j != NONE; ++jcount, j = next_sibling(j)) + { + if(key(j) == srckey) + { + _c4dbgpf("duplicate_no_rep: found matching key '{}' src={}/{} dst={}/{}", srckey, node, i, parent, j); + dstnode_dup = j; + dstnode_dup_pos = jcount; + break; + } + } + } + _c4dbgpf("duplicate_no_rep: dstnode_dup={} dstnode_dup_pos={} after_pos={}", dstnode_dup, dstnode_dup_pos, after_pos); + if(dstnode_dup == NONE) // there is no repetition; just duplicate + { + _c4dbgpf("duplicate_no_rep: no repetition, just duplicate i={} parent={} prev={}", i, parent, prev); + prev = duplicate(src, i, parent, prev); + } + else // yes, there is a repetition + { + if(after_pos != NONE && dstnode_dup_pos <= after_pos) + { + // the dst duplicate is located before the node which will be inserted, + // and will be overridden by the duplicate. So replace it. + _c4dbgpf("duplicate_no_dstnode_dup: replace {}/{} with {}/{}", parent, dstnode_dup, node, i); + if(prev == dstnode_dup) + prev = prev_sibling(dstnode_dup); + remove(dstnode_dup); + prev = duplicate(src, i, parent, prev); + } + else if(prev == NONE) + { + _c4dbgpf("duplicate_no_dstnode_dup: {}=prev <- {}", prev, dstnode_dup); + // first iteration with prev = after = NONE and dstnode_dupetition + prev = dstnode_dup; + } + else if(dstnode_dup != prev) + { + // dstnode_dup is located after the node which will be inserted + // and overrides it. So move the dstnode_dup into this node's place. + _c4dbgpf("duplicate_no_dstnode_dup: move({}, {})", dstnode_dup, prev); + move(dstnode_dup, prev); + prev = dstnode_dup; + } + } // there's a dstnode_dupetition + } + } + + return prev; +} + + +//----------------------------------------------------------------------------- + +void Tree::merge_with(Tree const *src, id_type src_node, id_type dst_node) +{ + _RYML_CB_ASSERT(m_callbacks, src != nullptr); + if(src_node == NONE) + src_node = src->root_id(); + if(dst_node == NONE) + dst_node = root_id(); + _RYML_CB_ASSERT(m_callbacks, src->has_val(src_node) || src->is_seq(src_node) || src->is_map(src_node)); + if(src->has_val(src_node)) + { + type_bits mask_src = ~STYLE; // keep the existing style if it is already a val + if( ! has_val(dst_node)) + { + if(has_children(dst_node)) + remove_children(dst_node); + mask_src |= VAL_STYLE; // copy the src style + } + if(src->is_keyval(src_node)) + { + _copy_props(dst_node, src, src_node, mask_src); + } + else + { + _RYML_CB_ASSERT(m_callbacks, src->is_val(src_node)); + _copy_props_wo_key(dst_node, src, src_node, mask_src); + } + } + else if(src->is_seq(src_node)) + { + if( ! is_seq(dst_node)) + { + if(has_children(dst_node)) + remove_children(dst_node); + _clear_type(dst_node); + if(src->has_key(src_node)) + to_seq(dst_node, src->key(src_node)); + else + to_seq(dst_node); + _p(dst_node)->m_type = src->_p(src_node)->m_type; + } + for(id_type sch = src->first_child(src_node); sch != NONE; sch = src->next_sibling(sch)) + { + id_type dch = append_child(dst_node); + _copy_props_wo_key(dch, src, sch); + merge_with(src, sch, dch); + } + } + else + { + _RYML_CB_ASSERT(m_callbacks, src->is_map(src_node)); + if( ! is_map(dst_node)) + { + if(has_children(dst_node)) + remove_children(dst_node); + _clear_type(dst_node); + if(src->has_key(src_node)) + to_map(dst_node, src->key(src_node)); + else + to_map(dst_node); + _p(dst_node)->m_type = src->_p(src_node)->m_type; + } + for(id_type sch = src->first_child(src_node); sch != NONE; sch = src->next_sibling(sch)) + { + id_type dch = find_child(dst_node, src->key(sch)); + if(dch == NONE) + { + dch = append_child(dst_node); + _copy_props(dch, src, sch); + } + merge_with(src, sch, dch); + } + } +} + + +//----------------------------------------------------------------------------- + +void Tree::resolve(bool clear_anchors) +{ + if(m_size == 0) + return; + ReferenceResolver rr; + resolve(&rr, clear_anchors); +} + +void Tree::resolve(ReferenceResolver *C4_RESTRICT rr, bool clear_anchors) +{ + if(m_size == 0) + return; + rr->resolve(this, clear_anchors); +} + + +//----------------------------------------------------------------------------- + +id_type Tree::num_children(id_type node) const +{ + id_type count = 0; + for(id_type i = first_child(node); i != NONE; i = next_sibling(i)) + ++count; + return count; +} + +id_type Tree::child(id_type node, id_type pos) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + id_type count = 0; + for(id_type i = first_child(node); i != NONE; i = next_sibling(i)) + { + if(count++ == pos) + return i; + } + return NONE; +} + +id_type Tree::child_pos(id_type node, id_type ch) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + id_type count = 0; + for(id_type i = first_child(node); i != NONE; i = next_sibling(i)) + { + if(i == ch) + return count; + ++count; + } + return NONE; +} + +#if defined(__clang__) +# pragma clang diagnostic push +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# if __GNUC__ >= 6 +# pragma GCC diagnostic ignored "-Wnull-dereference" +# endif +# if __GNUC__ > 9 +# pragma GCC diagnostic ignored "-Wanalyzer-null-dereference" +# endif +#endif + +id_type Tree::find_child(id_type node, csubstr const& name) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + _RYML_CB_ASSERT(m_callbacks, is_map(node)); + if(get(node)->m_first_child == NONE) + { + _RYML_CB_ASSERT(m_callbacks, _p(node)->m_last_child == NONE); + return NONE; + } + else + { + _RYML_CB_ASSERT(m_callbacks, _p(node)->m_last_child != NONE); + } + for(id_type i = first_child(node); i != NONE; i = next_sibling(i)) + { + if(_p(i)->m_key.scalar == name) + { + return i; + } + } + return NONE; +} + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +namespace { +id_type depth_desc_(Tree const& C4_RESTRICT t, id_type id, id_type currdepth=0, id_type maxdepth=0) +{ + maxdepth = currdepth > maxdepth ? currdepth : maxdepth; + for(id_type child = t.first_child(id); child != NONE; child = t.next_sibling(child)) + { + const id_type d = depth_desc_(t, child, currdepth+1, maxdepth); + maxdepth = d > maxdepth ? d : maxdepth; + } + return maxdepth; +} +} + +id_type Tree::depth_desc(id_type node) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + return depth_desc_(*this, node); +} + +id_type Tree::depth_asc(id_type node) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + id_type depth = 0; + while(!is_root(node)) + { + ++depth; + node = parent(node); + } + return depth; +} + +bool Tree::is_ancestor(id_type node, id_type ancestor) const +{ + _RYML_CB_ASSERT(m_callbacks, node != NONE); + id_type p = parent(node); + while(p != NONE) + { + if(p == ancestor) + return true; + p = parent(p); + } + return false; +} + + +//----------------------------------------------------------------------------- + +void Tree::to_val(id_type node, csubstr val, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || ! parent_is_map(node)); + _set_flags(node, VAL|more_flags); + _p(node)->m_key.clear(); + _p(node)->m_val = val; +} + +void Tree::to_keyval(id_type node, csubstr key, csubstr val, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node)); + _set_flags(node, KEYVAL|more_flags); + _p(node)->m_key = key; + _p(node)->m_val = val; +} + +void Tree::to_map(id_type node, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || ! parent_is_map(node)); // parent must not have children with keys + _set_flags(node, MAP|more_flags); + _p(node)->m_key.clear(); + _p(node)->m_val.clear(); +} + +void Tree::to_map(id_type node, csubstr key, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node)); + _set_flags(node, KEY|MAP|more_flags); + _p(node)->m_key = key; + _p(node)->m_val.clear(); +} + +void Tree::to_seq(id_type node, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_seq(node)); + _set_flags(node, SEQ|more_flags); + _p(node)->m_key.clear(); + _p(node)->m_val.clear(); +} + +void Tree::to_seq(id_type node, csubstr key, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node)); + _set_flags(node, KEY|SEQ|more_flags); + _p(node)->m_key = key; + _p(node)->m_val.clear(); +} + +void Tree::to_doc(id_type node, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _set_flags(node, DOC|more_flags); + _p(node)->m_key.clear(); + _p(node)->m_val.clear(); +} + +void Tree::to_stream(id_type node, type_bits more_flags) +{ + _RYML_CB_ASSERT(m_callbacks, ! has_children(node)); + _set_flags(node, STREAM|more_flags); + _p(node)->m_key.clear(); + _p(node)->m_val.clear(); +} + + +//----------------------------------------------------------------------------- + +void Tree::clear_style(id_type node, bool recurse) +{ + NodeData *C4_RESTRICT d = _p(node); + d->m_type.clear_style(); + if(!recurse) + return; + for(id_type child = d->m_first_child; child != NONE; child = next_sibling(child)) + clear_style(child, recurse); +} + +void Tree::set_style_conditionally(id_type node, + NodeType type_mask, + NodeType rem_style_flags, + NodeType add_style_flags, + bool recurse) +{ + NodeData *C4_RESTRICT d = _p(node); + if((d->m_type & type_mask) == type_mask) + { + d->m_type &= ~(NodeType)rem_style_flags; + d->m_type |= (NodeType)add_style_flags; + } + if(!recurse) + return; + for(id_type child = d->m_first_child; child != NONE; child = next_sibling(child)) + set_style_conditionally(child, type_mask, rem_style_flags, add_style_flags, recurse); +} + + +//----------------------------------------------------------------------------- +id_type Tree::num_tag_directives() const +{ + // this assumes we have a very small number of tag directives + for(id_type i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i) + if(m_tag_directives[i].handle.empty()) + return i; + return RYML_MAX_TAG_DIRECTIVES; +} + +void Tree::clear_tag_directives() +{ + for(TagDirective &td : m_tag_directives) + td = {}; +} + +id_type Tree::add_tag_directive(TagDirective const& td) +{ + _RYML_CB_CHECK(m_callbacks, !td.handle.empty()); + _RYML_CB_CHECK(m_callbacks, !td.prefix.empty()); + _RYML_CB_CHECK(m_callbacks, td.handle.begins_with('!')); + _RYML_CB_CHECK(m_callbacks, td.handle.ends_with('!')); + // https://yaml.org/spec/1.2.2/#rule-ns-word-char + _RYML_CB_CHECK(m_callbacks, td.handle == '!' || td.handle == "!!" || td.handle.trim('!').first_not_of("01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-") == npos); + id_type pos = num_tag_directives(); + _RYML_CB_CHECK(m_callbacks, pos < RYML_MAX_TAG_DIRECTIVES); + m_tag_directives[pos] = td; + return pos; +} + +namespace { +bool _create_tag_directive_from_str(csubstr directive_, TagDirective *td, Tree *tree) +{ + _RYML_CB_CHECK(tree->callbacks(), directive_.begins_with("%TAG ")); + if(!td->create_from_str(directive_)) + { + _RYML_CB_ERR(tree->callbacks(), "invalid tag directive"); + } + td->next_node_id = tree->size(); + if(!tree->empty()) + { + const id_type prev = tree->size() - 1; + if(tree->is_root(prev) && tree->type(prev) != NOTYPE && !tree->is_stream(prev)) + ++td->next_node_id; + } + _c4dbgpf("%TAG: handle={} prefix={} next_node={}", td->handle, td->prefix, td->next_node_id); + return true; +} +} // namespace + +bool Tree::add_tag_directive(csubstr directive_) +{ + TagDirective td; + if(_create_tag_directive_from_str(directive_, &td, this)) + { + add_tag_directive(td); + return true; + } + return false; +} + +size_t Tree::resolve_tag(substr output, csubstr tag, id_type node_id) const +{ + // lookup from the end. We want to find the first directive that + // matches the tag and has a target node id leq than the given + // node_id. + for(id_type i = RYML_MAX_TAG_DIRECTIVES-1; i != (id_type)-1; --i) + { + auto const& td = m_tag_directives[i]; + if(td.handle.empty()) + continue; + if(tag.begins_with(td.handle) && td.next_node_id <= node_id) + return td.transform(tag, output, m_callbacks); + } + if(tag.begins_with('!')) + { + if(is_custom_tag(tag)) + { + _RYML_CB_ERR(m_callbacks, "tag directive not found"); + } + } + return 0; // return 0 to signal that the tag is local and cannot be resolved +} + +namespace { +csubstr _transform_tag(Tree *t, csubstr tag, id_type node) +{ + _c4dbgpf("[{}] resolving tag ~~~{}~~~", node, tag); + size_t required_size = t->resolve_tag(substr{}, tag, node); + if(!required_size) + { + if(tag.begins_with("!<")) + tag = tag.sub(1); + _c4dbgpf("[{}] resolved tag: ~~~{}~~~", node, tag); + return tag; + } + const char *prev_arena = t->arena().str;(void)prev_arena; + substr buf = t->alloc_arena(required_size); + _RYML_CB_ASSERT(t->m_callbacks, t->arena().str == prev_arena); + size_t actual_size = t->resolve_tag(buf, tag, node); + _RYML_CB_ASSERT(t->m_callbacks, actual_size <= required_size); + _c4dbgpf("[{}] resolved tag: ~~~{}~~~", node, buf.first(actual_size)); + return buf.first(actual_size); +} +void _resolve_tags(Tree *t, id_type node) +{ + NodeData *C4_RESTRICT d = t->_p(node); + if(d->m_type & KEYTAG) + d->m_key.tag = _transform_tag(t, d->m_key.tag, node); + if(d->m_type & VALTAG) + d->m_val.tag = _transform_tag(t, d->m_val.tag, node); + for(id_type child = t->first_child(node); child != NONE; child = t->next_sibling(child)) + _resolve_tags(t, child); +} +size_t _count_resolved_tags_size(Tree const* t, id_type node) +{ + size_t sz = 0; + NodeData const* C4_RESTRICT d = t->_p(node); + if(d->m_type & KEYTAG) + sz += t->resolve_tag(substr{}, d->m_key.tag, node); + if(d->m_type & VALTAG) + sz += t->resolve_tag(substr{}, d->m_val.tag, node); + for(id_type child = t->first_child(node); child != NONE; child = t->next_sibling(child)) + sz += _count_resolved_tags_size(t, child); + return sz; +} +void _normalize_tags(Tree *t, id_type node) +{ + NodeData *C4_RESTRICT d = t->_p(node); + if(d->m_type & KEYTAG) + d->m_key.tag = normalize_tag(d->m_key.tag); + if(d->m_type & VALTAG) + d->m_val.tag = normalize_tag(d->m_val.tag); + for(id_type child = t->first_child(node); child != NONE; child = t->next_sibling(child)) + _normalize_tags(t, child); +} +void _normalize_tags_long(Tree *t, id_type node) +{ + NodeData *C4_RESTRICT d = t->_p(node); + if(d->m_type & KEYTAG) + d->m_key.tag = normalize_tag_long(d->m_key.tag); + if(d->m_type & VALTAG) + d->m_val.tag = normalize_tag_long(d->m_val.tag); + for(id_type child = t->first_child(node); child != NONE; child = t->next_sibling(child)) + _normalize_tags_long(t, child); +} +} // namespace + +void Tree::resolve_tags() +{ + if(empty()) + return; + size_t needed_size = _count_resolved_tags_size(this, root_id()); + if(needed_size) + reserve_arena(arena_size() + needed_size); + _resolve_tags(this, root_id()); +} + +void Tree::normalize_tags() +{ + if(empty()) + return; + _normalize_tags(this, root_id()); +} + +void Tree::normalize_tags_long() +{ + if(empty()) + return; + _normalize_tags_long(this, root_id()); +} + + +//----------------------------------------------------------------------------- + +csubstr Tree::lookup_result::resolved() const +{ + csubstr p = path.first(path_pos); + if(p.ends_with('.')) + p = p.first(p.len-1); + return p; +} + +csubstr Tree::lookup_result::unresolved() const +{ + return path.sub(path_pos); +} + +void Tree::_advance(lookup_result *r, size_t more) +{ + r->path_pos += more; + if(r->path.sub(r->path_pos).begins_with('.')) + ++r->path_pos; +} + +Tree::lookup_result Tree::lookup_path(csubstr path, id_type start) const +{ + if(start == NONE) + start = root_id(); + lookup_result r(path, start); + if(path.empty()) + return r; + _lookup_path(&r); + if(r.target == NONE && r.closest == start) + r.closest = NONE; + return r; +} + +id_type Tree::lookup_path_or_modify(csubstr default_value, csubstr path, id_type start) +{ + id_type target = _lookup_path_or_create(path, start); + if(parent_is_map(target)) + to_keyval(target, key(target), default_value); + else + to_val(target, default_value); + return target; +} + +id_type Tree::lookup_path_or_modify(Tree const *src, id_type src_node, csubstr path, id_type start) +{ + id_type target = _lookup_path_or_create(path, start); + merge_with(src, src_node, target); + return target; +} + +id_type Tree::_lookup_path_or_create(csubstr path, id_type start) +{ + if(start == NONE) + start = root_id(); + lookup_result r(path, start); + _lookup_path(&r); + if(r.target != NONE) + { + C4_ASSERT(r.unresolved().empty()); + return r.target; + } + _lookup_path_modify(&r); + return r.target; +} + +void Tree::_lookup_path(lookup_result *r) const +{ + C4_ASSERT( ! r->unresolved().empty()); + _lookup_path_token parent{"", type(r->closest)}; + id_type node; + do + { + node = _next_node(r, &parent); + if(node != NONE) + r->closest = node; + if(r->unresolved().empty()) + { + r->target = node; + return; + } + } while(node != NONE); +} + +void Tree::_lookup_path_modify(lookup_result *r) +{ + C4_ASSERT( ! r->unresolved().empty()); + _lookup_path_token parent{"", type(r->closest)}; + id_type node; + do + { + node = _next_node_modify(r, &parent); + if(node != NONE) + r->closest = node; + if(r->unresolved().empty()) + { + r->target = node; + return; + } + } while(node != NONE); +} + +id_type Tree::_next_node(lookup_result * r, _lookup_path_token *parent) const +{ + _lookup_path_token token = _next_token(r, *parent); + if( ! token) + return NONE; + + id_type node = NONE; + csubstr prev = token.value; + if(token.type == MAP || token.type == SEQ) + { + _RYML_CB_ASSERT(m_callbacks, !token.value.begins_with('[')); + //_RYML_CB_ASSERT(m_callbacks, is_container(r->closest) || r->closest == NONE); + _RYML_CB_ASSERT(m_callbacks, is_map(r->closest)); + node = find_child(r->closest, token.value); + } + else if(token.type == KEYVAL) + { + _RYML_CB_ASSERT(m_callbacks, r->unresolved().empty()); + if(is_map(r->closest)) + node = find_child(r->closest, token.value); + } + else if(token.type == KEY) + { + _RYML_CB_ASSERT(m_callbacks, token.value.begins_with('[') && token.value.ends_with(']')); + token.value = token.value.offs(1, 1).trim(' '); + id_type idx = 0; + _RYML_CB_CHECK(m_callbacks, from_chars(token.value, &idx)); + node = child(r->closest, idx); + } + else + { + C4_NEVER_REACH(); + } + + if(node != NONE) + { + *parent = token; + } + else + { + csubstr p = r->path.sub(r->path_pos > 0 ? r->path_pos - 1 : r->path_pos); + r->path_pos -= prev.len; + if(p.begins_with('.')) + r->path_pos -= 1u; + } + + return node; +} + +id_type Tree::_next_node_modify(lookup_result * r, _lookup_path_token *parent) +{ + _lookup_path_token token = _next_token(r, *parent); + if( ! token) + return NONE; + + id_type node = NONE; + if(token.type == MAP || token.type == SEQ) + { + _RYML_CB_ASSERT(m_callbacks, !token.value.begins_with('[')); + //_RYML_CB_ASSERT(m_callbacks, is_container(r->closest) || r->closest == NONE); + if( ! is_container(r->closest)) + { + if(has_key(r->closest)) + to_map(r->closest, key(r->closest)); + else + to_map(r->closest); + } + else + { + if(is_map(r->closest)) + node = find_child(r->closest, token.value); + else + { + id_type pos = NONE; + _RYML_CB_CHECK(m_callbacks, c4::atox(token.value, &pos)); + _RYML_CB_ASSERT(m_callbacks, pos != NONE); + node = child(r->closest, pos); + } + } + if(node == NONE) + { + _RYML_CB_ASSERT(m_callbacks, is_map(r->closest)); + node = append_child(r->closest); + NodeData *n = _p(node); + n->m_key.scalar = token.value; + n->m_type.add(KEY); + } + } + else if(token.type == KEYVAL) + { + _RYML_CB_ASSERT(m_callbacks, r->unresolved().empty()); + if(is_map(r->closest)) + { + node = find_child(r->closest, token.value); + if(node == NONE) + node = append_child(r->closest); + } + else + { + _RYML_CB_ASSERT(m_callbacks, !is_seq(r->closest)); + _add_flags(r->closest, MAP); + node = append_child(r->closest); + } + NodeData *n = _p(node); + n->m_key.scalar = token.value; + n->m_val.scalar = ""; + n->m_type.add(KEYVAL); + } + else if(token.type == KEY) + { + _RYML_CB_ASSERT(m_callbacks, token.value.begins_with('[') && token.value.ends_with(']')); + token.value = token.value.offs(1, 1).trim(' '); + id_type idx; + if( ! from_chars(token.value, &idx)) + return NONE; + if( ! is_container(r->closest)) + { + if(has_key(r->closest)) + { + csubstr k = key(r->closest); + _clear_type(r->closest); + to_seq(r->closest, k); + } + else + { + _clear_type(r->closest); + to_seq(r->closest); + } + } + _RYML_CB_ASSERT(m_callbacks, is_container(r->closest)); + node = child(r->closest, idx); + if(node == NONE) + { + _RYML_CB_ASSERT(m_callbacks, num_children(r->closest) <= idx); + for(id_type i = num_children(r->closest); i <= idx; ++i) + { + node = append_child(r->closest); + if(i < idx) + { + if(is_map(r->closest)) + to_keyval(node, /*"~"*/{}, /*"~"*/{}); + else if(is_seq(r->closest)) + to_val(node, /*"~"*/{}); + } + } + } + } + else + { + C4_NEVER_REACH(); + } + + _RYML_CB_ASSERT(m_callbacks, node != NONE); + *parent = token; + return node; +} + +/* types of tokens: + * - seeing "map." ---> "map"/MAP + * - finishing "scalar" ---> "scalar"/KEYVAL + * - seeing "seq[n]" ---> "seq"/SEQ (--> "[n]"/KEY) + * - seeing "[n]" ---> "[n]"/KEY + */ +Tree::_lookup_path_token Tree::_next_token(lookup_result *r, _lookup_path_token const& parent) const +{ + csubstr unres = r->unresolved(); + if(unres.empty()) + return {}; + + // is it an indexation like [0], [1], etc? + if(unres.begins_with('[')) + { + size_t pos = unres.find(']'); + if(pos == csubstr::npos) + return {}; + csubstr idx = unres.first(pos + 1); + _advance(r, pos + 1); + return {idx, KEY}; + } + + // no. so it must be a name + size_t pos = unres.first_of(".["); + if(pos == csubstr::npos) + { + _advance(r, unres.len); + NodeType t; + if(( ! parent) || parent.type.is_seq()) + return {unres, VAL}; + return {unres, KEYVAL}; + } + + // it's either a map or a seq + _RYML_CB_ASSERT(m_callbacks, unres[pos] == '.' || unres[pos] == '['); + if(unres[pos] == '.') + { + _RYML_CB_ASSERT(m_callbacks, pos != 0); + _advance(r, pos + 1); + return {unres.first(pos), MAP}; + } + + _RYML_CB_ASSERT(m_callbacks, unres[pos] == '['); + _advance(r, pos); + return {unres.first(pos), SEQ}; +} + + +} // namespace yml +} // namespace c4 + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#include "c4/yml/event_handler_tree.hpp" +#include "c4/yml/parse_engine.def.hpp" +#include "c4/yml/parse.hpp" + +namespace c4 { +namespace yml { + +Location Tree::location(Parser const& parser, id_type node) const +{ + // try hard to avoid getting the location from a null string. + Location loc; + if(_location_from_node(parser, node, &loc, 0)) + return loc; + return parser.val_location(parser.source().str); +} + +bool Tree::_location_from_node(Parser const& parser, id_type node, Location *C4_RESTRICT loc, id_type level) const +{ + if(has_key(node)) + { + csubstr k = key(node); + if(C4_LIKELY(k.str != nullptr)) + { + _RYML_CB_ASSERT(m_callbacks, k.is_sub(parser.source())); + _RYML_CB_ASSERT(m_callbacks, parser.source().is_super(k)); + *loc = parser.val_location(k.str); + return true; + } + } + + if(has_val(node)) + { + csubstr v = val(node); + if(C4_LIKELY(v.str != nullptr)) + { + _RYML_CB_ASSERT(m_callbacks, v.is_sub(parser.source())); + _RYML_CB_ASSERT(m_callbacks, parser.source().is_super(v)); + *loc = parser.val_location(v.str); + return true; + } + } + + if(is_container(node)) + { + if(_location_from_cont(parser, node, loc)) + return true; + } + + if(type(node) != NOTYPE && level == 0) + { + // try the prev sibling + { + const id_type prev = prev_sibling(node); + if(prev != NONE) + { + if(_location_from_node(parser, prev, loc, level+1)) + return true; + } + } + // try the next sibling + { + const id_type next = next_sibling(node); + if(next != NONE) + { + if(_location_from_node(parser, next, loc, level+1)) + return true; + } + } + // try the parent + { + const id_type parent = this->parent(node); + if(parent != NONE) + { + if(_location_from_node(parser, parent, loc, level+1)) + return true; + } + } + } + return false; +} + +bool Tree::_location_from_cont(Parser const& parser, id_type node, Location *C4_RESTRICT loc) const +{ + _RYML_CB_ASSERT(m_callbacks, is_container(node)); + if(!is_stream(node)) + { + const char *node_start = _p(node)->m_val.scalar.str; // this was stored in the container + if(has_children(node)) + { + id_type child = first_child(node); + if(has_key(child)) + { + // when a map starts, the container was set after the key + csubstr k = key(child); + if(k.str && node_start > k.str) + node_start = k.str; + } + } + *loc = parser.val_location(node_start); + return true; + } + else // it's a stream + { + *loc = parser.val_location(parser.source().str); // just return the front of the buffer + } + return true; +} + +} // namespace yml +} // namespace c4 + + +C4_SUPPRESS_WARNING_GCC_CLANG_POP +C4_SUPPRESS_WARNING_MSVC_POP diff --git a/3rdparty/rapidyaml/src/c4/yml/version.cpp b/3rdparty/rapidyaml/src/c4/yml/version.cpp new file mode 100644 index 0000000000..c54c1f4fd0 --- /dev/null +++ b/3rdparty/rapidyaml/src/c4/yml/version.cpp @@ -0,0 +1,27 @@ +#include "c4/yml/version.hpp" + +namespace c4 { +namespace yml { + +csubstr version() +{ + return RYML_VERSION; +} + +int version_major() +{ + return RYML_VERSION_MAJOR; +} + +int version_minor() +{ + return RYML_VERSION_MINOR; +} + +int version_patch() +{ + return RYML_VERSION_PATCH; +} + +} // namespace yml +} // namespace c4 diff --git a/cmake/SearchForStuff.cmake b/cmake/SearchForStuff.cmake index 963a6f88f3..08ef98df70 100644 --- a/cmake/SearchForStuff.cmake +++ b/cmake/SearchForStuff.cmake @@ -21,7 +21,6 @@ find_package(SDL3 3.2.6 REQUIRED) find_package(Freetype 2.10 REQUIRED) # 2.10 is the first with COLRv0 support, which we need for rendering emoji find_package(plutovg 1.1.0 REQUIRED) find_package(plutosvg 0.0.7 REQUIRED) -find_package(ryml REQUIRED) if (WIN32) find_package(DirectX-Headers 1.618.1 REQUIRED) endif() @@ -84,6 +83,9 @@ endif() set(CMAKE_FIND_FRAMEWORK ${FIND_FRAMEWORK_BACKUP}) add_subdirectory(3rdparty/fast_float EXCLUDE_FROM_ALL) +# rapidyaml re-vendored in-tree (fork-local; upstream 0beb18c9e un-bundled it in +# favour of a system ryml). Keeps the build self-contained for handheld/cross builds. +add_subdirectory(3rdparty/rapidyaml EXCLUDE_FROM_ALL) add_subdirectory(3rdparty/lzma EXCLUDE_FROM_ALL) add_subdirectory(3rdparty/libchdr EXCLUDE_FROM_ALL) disable_compiler_warnings_for_target(libchdr) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index bb22d7ec52..6ca5f0bb7b 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -217,7 +217,7 @@ target_link_libraries(common PRIVATE target_link_libraries(common PUBLIC fmt::fmt fast_float - ryml::ryml + rapidyaml::rapidyaml ) fixup_file_properties(common) diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 1d0e010043..17cdb5f8d8 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1369,7 +1369,7 @@ function(setup_main_executable target) # Copy dependency libraries. set(DEPS_BINDIR "${CMAKE_SOURCE_DIR}/deps/bin") - set(DEPS_TO_COPY freetype.dll harfbuzz.dll jpeg62.dll libpng16.dll libsharpyuv.dll libwebp.dll libwebpdemux.dll libwebpmux.dll lz4.dll SDL3.dll shaderc_shared.dll z.dll zstd.dll plutovg.dll plutosvg.dll ryml.dll) + set(DEPS_TO_COPY freetype.dll harfbuzz.dll jpeg62.dll libpng16.dll libsharpyuv.dll libwebp.dll libwebpdemux.dll libwebpmux.dll lz4.dll SDL3.dll shaderc_shared.dll z.dll zstd.dll plutovg.dll plutosvg.dll) if(ENABLE_QT_DEBUGGER) set(DEPS_TO_COPY $,kddockwidgets-qt6d.dll,kddockwidgets-qt6.dll> From 474e6c324ddcf8474cd7f07911127d7d22a6f24a Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Mon, 22 Jun 2026 14:09:37 -0700 Subject: [PATCH 016/292] pcsx2-vurunner: cross-arch JIT-state dump + --vu-clamp-mode + cycle diff Tooling to localize arch-specific VU codegen bugs by comparing arm64-rec against the mature x86-rec over the same vu_capture corpus. - --dump-jit-state[-raw]: per-capture FNV digest of the JIT architectural post-state (VF/VI/ACC/[VU1 xgkick]/VU-mem), basename-keyed so cross-machine path prefixes don't pollute a `diff`. Both microVU backends share a byte-identical cycle model, so the same capture+budget stops at the same guest insn on either arch -- post-states are directly comparable even for budget-truncated programs, the class JIT-vs-interp is structurally blind to. - --vu-clamp-mode N: force EmuConfig VU clamp mode 0..3 (mirrors GameDatabase.cpp vuClampMode). The harness otherwise stays at default mode 1 (vu0Overflow only), which never exercises the extra-overflow per-op operand clamp real games request via gamedb (e.g. SoulCalibur III vuClampMode:2). - RunDiff also reports consumed-cycle divergence (gated on both engines hitting the E-bit) -- the EE<->VU0 timing quantity the reg/mem diff can't see. - VuReplay records jit_cycles/interp_cycles deltas. - x86 portability: guard the arm64-only progcache/persist includes+calls so the same source builds a native x86 vurunner for the cross-arch reference. Co-Authored-By: Claude Opus 4.8 --- pcsx2-vurunner/Main.cpp | 209 +++++++++++++++++- .../harness/RecompilerTestEnvironment.cpp | 4 + .../core/recompilers/harness/VuReplay.cpp | 4 + .../ctest/core/recompilers/harness/VuReplay.h | 13 ++ 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/pcsx2-vurunner/Main.cpp b/pcsx2-vurunner/Main.cpp index e7ec420f8f..4e4ff5cc87 100644 --- a/pcsx2-vurunner/Main.cpp +++ b/pcsx2-vurunner/Main.cpp @@ -26,8 +26,10 @@ #include "VUmicro.h" #include "Gif_Unit.h" #include "microVU_Divtrace.h" +#if defined(_M_ARM64) || defined(__aarch64__) #include "arm64/microVU_Persist-arm64.h" #include "arm64/microVU_ProgCache-arm64.h" +#endif #include "DebugTools/Debug.h" #include "common/FPControl.h" @@ -55,12 +57,15 @@ struct Options bool bench = false; bool dump_asm = false; bool dump_microcode = false; + bool dump_jit_state = false; // cross-arch JIT post-state digest + bool dump_jit_state_raw = false; // + full per-field raw dump bool divtrace = false; bool bench_no_reprime = false; bool print_bases = false; bool no_progcache = false; // determinism gate: force program cache + recording off u32 dump_count = 64; u32 cycle_override = 0; // 0 = use captured budget + int vu_clamp_mode = -1; // -1 = leave EmuConfig default (mode 1); 0..3 = force VU clamp mode std::string cache_dir; // empty = persisted-JIT program cache off std::vector files; }; @@ -143,6 +148,15 @@ bool ParseArgs(int argc, char** argv, Options& opts) { opts.dump_microcode = true; } + else if (a == "--dump-jit-state") + { + opts.dump_jit_state = true; + } + else if (a == "--dump-jit-state-raw") + { + opts.dump_jit_state = true; + opts.dump_jit_state_raw = true; + } else if (a == "--divtrace") { opts.divtrace = true; @@ -218,6 +232,21 @@ bool ParseArgs(int argc, char** argv, Options& opts) } opts.iters = static_cast(n); } + else if (a == "--vu-clamp-mode") + { + if (i + 1 >= argc) + { + std::fprintf(stderr, "vurunner: --vu-clamp-mode requires an argument (0..3)\n"); + return false; + } + const long n = std::strtol(argv[++i], nullptr, 10); + if (n < 0 || n > 3) + { + std::fprintf(stderr, "vurunner: --vu-clamp-mode must be in [0, 3]\n"); + return false; + } + opts.vu_clamp_mode = static_cast(n); + } else { std::fprintf(stderr, "vurunner: unknown option '%s'\n", a.c_str()); @@ -230,7 +259,8 @@ bool ParseArgs(int argc, char** argv, Options& opts) return false; } if (!opts.diff && !opts.bench && !opts.dump_asm - && !opts.dump_microcode && !opts.divtrace && !opts.print_bases) + && !opts.dump_microcode && !opts.divtrace && !opts.print_bases + && !opts.dump_jit_state) opts.diff = true; return true; } @@ -353,6 +383,128 @@ void PrintPathDiff(const std::vector& jit, const std::vector& interp) } } +// ---- Cross-arch JIT post-state dump -------------------------------------- +// +// Runs the JIT (only) against each capture and emits a stable digest of its +// architectural post-state (VF / VI / ACC / [VU1 xgkick] / VU data memory), +// plus consumed cycles and E-bit termination. The point is a cross-ARCH +// JIT-vs-JIT comparison: replay the SAME capture corpus with the SAME cycle +// budget on arm64 and x86. Both microVU backends share a byte-identical cycle +// model (mVUincCycles/mVUtestCycles), so they execute the same guest-insn +// count and stop at the same point — making their post-states directly +// comparable even for budget-truncated (looping) programs. `diff` the two +// outputs: any differing line is a capture where the two codegen backends +// produce a different architectural result for identical input (an +// arch-specific codegen bug). Identical output across the whole corpus rules +// the per-capture VU program replay OUT as the divergence source, pointing at +// carried pipeline state / dispatcher / COP2-sync integration that +// vu_capture doesn't capture. + +bool IsFullWidthVi(int i) +{ + return i == REG_R || i == REG_I || i == REG_Q || i == REG_P + || i == REG_STATUS_FLAG || i == REG_MAC_FLAG || i == REG_CLIP_FLAG + || i == REG_TPC || i == REG_FBRST || i == REG_VPU_STAT; +} + +void HashArchSurface(const recompiler_tests::VuSnapshot& s, u64& reg_hash, u64& mem_hash) +{ + auto mix = [](u64 h, u32 v) { + for (int b = 0; b < 4; ++b) + { + h ^= (v >> (b * 8)) & 0xFFu; + h *= 0x100000001b3ull; // FNV-1a 64 prime + } + return h; + }; + const VURegs& g = s.regs; + u64 r = 0xcbf29ce484222325ull; + for (int i = 0; i < 32; ++i) + { + r = mix(r, g.VF[i].i.x); r = mix(r, g.VF[i].i.y); + r = mix(r, g.VF[i].i.z); r = mix(r, g.VF[i].i.w); + } + for (int i = 0; i < 32; ++i) + r = mix(r, g.VI[i].UL & (IsFullWidthVi(i) ? 0xFFFFFFFFu : 0xFFFFu)); + r = mix(r, g.ACC.i.x); r = mix(r, g.ACC.i.y); + r = mix(r, g.ACC.i.z); r = mix(r, g.ACC.i.w); + if (s.index == 1) + { + r = mix(r, g.xgkickaddr); r = mix(r, g.xgkickdiff); + r = mix(r, g.xgkicksizeremaining); r = mix(r, g.xgkickcyclecount); + r = mix(r, g.xgkickenable); r = mix(r, g.xgkickendpacket); + } + reg_hash = r; + u64 m = 0xcbf29ce484222325ull; + for (const auto& w : s.mem_windows) + { + m = mix(m, w.addr); + for (u8 b : w.bytes) { m ^= b; m *= 0x100000001b3ull; } + } + mem_hash = m; +} + +void DumpArchRaw(const recompiler_tests::VuSnapshot& s) +{ + const VURegs& g = s.regs; + for (int i = 0; i < 32; ++i) + std::printf(" VF%02d %08x %08x %08x %08x\n", i, + g.VF[i].i.x, g.VF[i].i.y, g.VF[i].i.z, g.VF[i].i.w); + std::printf(" ACC %08x %08x %08x %08x\n", g.ACC.i.x, g.ACC.i.y, g.ACC.i.z, g.ACC.i.w); + for (int i = 0; i < 32; ++i) + std::printf(" VI%02d %08x\n", i, g.VI[i].UL & (IsFullWidthVi(i) ? 0xFFFFFFFFu : 0xFFFFu)); + if (s.index == 1) + std::printf(" XGK addr=%08x diff=%08x size=%08x cyc=%08x en=%08x endp=%08x\n", + g.xgkickaddr, g.xgkickdiff, g.xgkicksizeremaining, g.xgkickcyclecount, + g.xgkickenable, g.xgkickendpacket); + for (size_t wi = 0; wi < s.mem_windows.size(); ++wi) + { + const auto& w = s.mem_windows[wi]; + for (size_t off = 0; off < w.bytes.size(); off += 16) + { + std::printf(" M%zu@%04zx", wi, off); + for (size_t k = 0; k < 16 && off + k < w.bytes.size(); ++k) + std::printf(" %02x", w.bytes[off + k]); + std::printf("\n"); + } + } +} + +int RunDumpJitState(const std::vector& records, + const std::vector& names, + u32 cycle_override, + bool raw) +{ + for (size_t fi = 0; fi < records.size(); ++fi) + { + const auto& rec = records[fi]; + const auto r = recompiler_tests::ReplayCapture(rec, + recompiler_tests::VuDiffMode::PipelinePermissive, cycle_override); + // Basename only, so cross-machine path prefixes don't pollute the diff. + std::string base = names[fi]; + const auto slash = base.find_last_of('/'); + if (slash != std::string::npos) + base = base.substr(slash + 1); + if (!r.ok) + { + std::printf("%s replay-failed\n", base.c_str()); + continue; + } + u64 rh = 0, mh = 0; + HashArchSurface(r.jit_snapshot, rh, mh); + std::printf("%s vu%u pc=%08x ebit=%d jitcyc=%llu reghash=%016llx memhash=%016llx\n", + base.c_str(), rec.vu_index, rec.start_pc, r.jit_ebit ? 1 : 0, + (unsigned long long)r.jit_cycles, + (unsigned long long)rh, (unsigned long long)mh); + if (raw) + { + std::printf("--- %s raw ---\n", base.c_str()); + DumpArchRaw(r.jit_snapshot); + } + } + return 0; +} + int RunDiff(const std::vector& records, const std::vector& names, u32 iters, @@ -360,6 +512,7 @@ int RunDiff(const std::vector& records, { int diverged_files = 0; int path_diverged_files = 0; + int cycle_diverged_files = 0; for (size_t fi = 0; fi < records.size(); ++fi) { const auto& rec = records[fi]; @@ -368,6 +521,8 @@ int RunDiff(const std::vector& records, bool any_diverged = false; bool path_diverged = false; const char* term = "unknown"; + u64 jit_cycles = 0, interp_cycles = 0; + bool both_ebit = false; for (u32 i = 0; i < iters; ++i) { const auto r = recompiler_tests::ReplayCapture(rec, @@ -381,7 +536,12 @@ int RunDiff(const std::vector& records, // Termination signal (interp = oracle): ebit = program ran to its // E-bit; budget = truncated by cycle budget (loop noise, not a bug). if (i == 0) + { term = r.interp_ebit ? "ebit" : "budget"; + jit_cycles = r.jit_cycles; + interp_cycles = r.interp_cycles; + both_ebit = r.jit_ebit && r.interp_ebit; + } const bool path_diff = (r.path1_packets_jit != r.path1_packets_interp); if (r.diverged || path_diff) { @@ -397,6 +557,22 @@ int RunDiff(const std::vector& records, } } std::printf(" term=%s\n", term); + // Consumed-cycle comparison (JIT vs interp). This is the quantity the + // register/memory diff is blind to and that the mVU dispatcher reports + // back to the EE as VU0 runtime (drives EE<->VU0 timing / animation + // phase). Only meaningful when both engines ran the whole program + // (both_ebit) — a budget-truncated program legitimately overshoots to a + // block boundary under JIT vs stops mid-op under interp. + if (jit_cycles != interp_cycles) + { + const bool flag = both_ebit; + std::printf(" cycles jit=%llu interp=%llu (delta=%lld)%s\n", + (unsigned long long)jit_cycles, (unsigned long long)interp_cycles, + (long long)jit_cycles - (long long)interp_cycles, + flag ? " <<< CYCLE DIVERGENCE (both E-bit)" : " (budget-truncated, expected)"); + if (flag) + ++cycle_diverged_files; + } if (!any_diverged) std::printf(" ok (%u iters)\n", iters); else @@ -409,7 +585,10 @@ int RunDiff(const std::vector& records, if (diverged_files) std::printf("[diff] %d of %zu captures diverged (%d with PATH1 byte diff)\n", diverged_files, records.size(), path_diverged_files); - return diverged_files == 0 ? 0 : 2; + if (cycle_diverged_files) + std::printf("[diff] %d of %zu captures had a CYCLE divergence with both engines at E-bit\n", + cycle_diverged_files, records.size()); + return (diverged_files == 0 && cycle_diverged_files == 0) ? 0 : 2; } int RunBench(const std::vector& records, @@ -1009,6 +1188,7 @@ int main(int argc, char** argv) return 1; } +#if defined(_M_ARM64) || defined(__aarch64__) if (opts.no_progcache) mVUPersist::SetProcessDisable(true); @@ -1033,6 +1213,7 @@ int main(int argc, char** argv) mVUPersist::SetRecordingEnabled(true); } } +#endif if (!recompiler_tests::RecompilerTestEnvironment::Initialize()) { @@ -1040,6 +1221,26 @@ int main(int argc, char** argv) return 3; } + // Force a VU clamp mode (mirrors GameDatabase.cpp vuClampMode mapping). The + // harness otherwise leaves EmuConfig at default mode 1 (vu0Overflow only) — + // which never exercises the extra-overflow per-op operand clamp (mVUclamp3) + // that real games request via gamedb (e.g. SoulCalibur III vuClampMode:2). + // Blocks recompile per-capture (PrimeFromCapture resets the cache), so this + // takes effect on the next replay. -1 = leave default. + if (opts.vu_clamp_mode >= 0) + { + const int m = opts.vu_clamp_mode; + EmuConfig.Cpu.Recompiler.vu0Overflow = (m >= 1); + EmuConfig.Cpu.Recompiler.vu1Overflow = (m >= 1); + EmuConfig.Cpu.Recompiler.vu0ExtraOverflow = (m >= 2); + EmuConfig.Cpu.Recompiler.vu1ExtraOverflow = (m >= 2); + EmuConfig.Cpu.Recompiler.vu0SignOverflow = (m >= 3); + EmuConfig.Cpu.Recompiler.vu1SignOverflow = (m >= 3); + std::fprintf(stderr, "vurunner: forced VU clamp mode = %d " + "(overflow=%d extra=%d sign=%d)\n", + m, (m >= 1), (m >= 2), (m >= 3)); + } + if (opts.print_bases) { const int rc = RunPrintBases(); @@ -1071,6 +1272,8 @@ int main(int argc, char** argv) int exit_code = 0; if (opts.dump_microcode) exit_code |= RunDumpMicrocode(records, names, opts.dump_count); + if (opts.dump_jit_state) + exit_code |= RunDumpJitState(records, names, opts.cycle_override, opts.dump_jit_state_raw); if (opts.diff) exit_code |= RunDiff(records, names, opts.iters, opts.cycle_override); if (opts.bench) @@ -1080,6 +1283,7 @@ int main(int argc, char** argv) if (opts.divtrace) exit_code |= RunDivTrace(records, names, opts.cycle_override); +#if defined(_M_ARM64) || defined(__aarch64__) if (!opts.cache_dir.empty()) { // Flush still-live programs to disk so their saves show in the @@ -1109,6 +1313,7 @@ int main(int argc, char** argv) (unsigned long long)mVUPersist::GetBlockCompileCount(vu)); } } +#endif recompiler_tests::RecompilerTestEnvironment::Shutdown(); return exit_code; diff --git a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp index c57b3ab385..a343602541 100644 --- a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp +++ b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp @@ -13,7 +13,9 @@ #include "R5900.h" #include "VMManager.h" #include "VUmicro.h" +#if defined(_M_ARM64) || defined(__aarch64__) #include "arm64/microVU_Persist-arm64.h" +#endif #include "common/FPControl.h" #include "cpuinfo.h" @@ -110,7 +112,9 @@ bool RecompilerTestEnvironment::Initialize() // it from EmuConfig — the persist/abi/disk tests set recording (and the // on-disk cache) explicitly. Must precede the Reserve calls below, since // CpuMicroVU0.Reserve runs mVUinit (which calls SyncRecordingFromConfig). +#if defined(_M_ARM64) || defined(__aarch64__) mVUPersist::SetTestManualRecording(true); +#endif // microVU JITs. mVUinit allocates the per-VU regAlloc + sets cache // pointers; the corresponding Reset call below emits dispatchers. diff --git a/tests/ctest/core/recompilers/harness/VuReplay.cpp b/tests/ctest/core/recompilers/harness/VuReplay.cpp index d609604400..a62dabe98b 100644 --- a/tests/ctest/core/recompilers/harness/VuReplay.cpp +++ b/tests/ctest/core/recompilers/harness/VuReplay.cpp @@ -204,7 +204,9 @@ VuReplayResult ReplayCapture(const vu_capture::CaptureRecord& rec, PrimeFromCapture(rec); const VuSnapshot pre = VuSnapshot::Capture(idx, windows); path1_buf.clear(); + const u64 jit_cycle_before = Regs(idx).cycle; RunJitFromSeeded(idx, cycles); + out.jit_cycles = Regs(idx).cycle - jit_cycle_before; out.jit_snapshot = VuSnapshot::Capture(idx, windows); out.path1_packets_jit = path1_buf; // Authoritative running bit lives in vuRegs[0] for both VUs (see header). @@ -224,7 +226,9 @@ VuReplayResult ReplayCapture(const vu_capture::CaptureRecord& rec, JitCpu(idx)->SetStartPC(rec.start_pc); path1_buf.clear(); + const u64 interp_cycle_before = Regs(idx).cycle; RunInterpFromSeeded(idx, cycles); + out.interp_cycles = Regs(idx).cycle - interp_cycle_before; out.interp_snapshot = VuSnapshot::Capture(idx, windows); out.path1_packets_interp = path1_buf; out.interp_ebit = (vuRegs[0].VI[REG_VPU_STAT].UL & RunningBit(idx)) == 0; diff --git a/tests/ctest/core/recompilers/harness/VuReplay.h b/tests/ctest/core/recompilers/harness/VuReplay.h index ed301b908c..c8bbf0c3f7 100644 --- a/tests/ctest/core/recompilers/harness/VuReplay.h +++ b/tests/ctest/core/recompilers/harness/VuReplay.h @@ -50,6 +50,19 @@ struct VuReplayResult // the divergence (it's loop/budget noise, not a mis-emitted op). bool jit_ebit = false; bool interp_ebit = false; + + // Cycles consumed by each engine for this program+entry-state+budget. + // PrimeFromCapture zeroes vu.cycle before each pass, so this is the delta + // the engine added to VURegs::cycle. The mVU dispatcher reports this back + // to the EE as how long VU0 ran (VU0.cpp _vu0run: cpuRegs.cycle += delta), + // so a JIT-vs-interp mismatch drives EE<->VU0 timing drift even when the + // architectural post-state is bit-identical — the one quantity the + // register/memory diff is structurally blind to. Compare apples-to-apples + // only when BOTH jit_ebit && interp_ebit (whole program ran in both); a + // budget-truncated program legitimately overshoots to a block boundary + // under JIT while interp stops mid-op at the exact budget. + u64 jit_cycles = 0; + u64 interp_cycles = 0; }; // Drives the JIT and interpreter against one captured program. Restores the From 4de53e5cc2ed4a80e62974d076147c813e49b650 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Mon, 22 Jun 2026 14:09:45 -0700 Subject: [PATCH 017/292] arm64: wire mVUdivSet into doUpperOp (micro-mode FDIV flag -> STATUS) The I (invalid, 0x10) and D (divide-by-zero, 0x20) bits that VDIV/VSQRT/VRSQRT produce into mVU.divFlag must be folded into the architectural STATUS flag by mVUdivSet() on the instruction downstream of the FDIV flag latency. arm64 micro mode dropped this entirely: doUpperOp() ran mVUopU() but never called mVUdivSet(), so the bits reached STATUS only via COP2 macro mode. Match x86 microVU_Compile.inl doUpperOp(). Adds 4 RED-before / GREEN-after regression tests (Vu0Qpipe.*InJitStatus, VU0 + VU1) that assert the bit on the JIT side directly, padding past the 7-cycle flag latency so mVUdivSet has a downstream instruction to fold the flag. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_Compile-arm64.inl | 2 +- .../core/recompilers/vu0_q_pipeline_tests.cpp | 108 +++++++++++++++++- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/pcsx2/arm64/microVU_Compile-arm64.inl b/pcsx2/arm64/microVU_Compile-arm64.inl index 16b53d9f96..2ff4933984 100644 --- a/pcsx2/arm64/microVU_Compile-arm64.inl +++ b/pcsx2/arm64/microVU_Compile-arm64.inl @@ -543,7 +543,7 @@ static void mvuPreloadRegisters(microVU& mVU, u32 endCount) mVU.code = orig_code; } -__ri void doUpperOp(mV) { mVUopU(mVU, 1); } +__ri void doUpperOp(mV) { mVUopU(mVU, 1); mVUdivSet(mVU); } __ri void doLowerOp(mV) { incPC(-1); mVUopL(mVU, 1); incPC(1); } __ri void flushRegs(mV) { if (!doRegAlloc) mVU.regAlloc->flushAll(); } diff --git a/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp b/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp index f5cb0bd5f1..49447025b8 100644 --- a/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp +++ b/tests/ctest/core/recompilers/vu0_q_pipeline_tests.cpp @@ -10,11 +10,20 @@ // exercised — both the architectural REG_STATUS_FLAG and the magic q // payload values 0x7F7FFFFF / 0xFF7FFFFF the VU emits on /0 with sign. // -// Known JIT divergence: on short standalone programs the divFlag → STATUS -// propagation in the FDIV unit's pipeline isn't drained at end-of-program, -// so STATUS bits 0x10/0x20 land in interp but not in JIT. Tests that probe -// this opt out of REG_STATUS_FLAG via IgnoreViInDiff and route their -// architectural status asserts through the interp side. +// FDIV flag → STATUS transfer (micro mode): the I (invalid, 0x10) and D +// (divide-by-zero, 0x20) bits are produced by VDIV/VSQRT/VRSQRT into +// mVU.divFlag, then folded into the architectural STATUS flag by mVUdivSet() +// on the instruction 7 cycles downstream (the FDIV flag latency). For a long +// time arm64 micro-mode dropped this entirely — doUpperOp() never called +// mVUdivSet(), so the bits reached STATUS only in COP2 macro mode. That +// surfaced as Soul Calibur 3 character-model jitter and was misfiled as a +// "short standalone program" / test-side limitation (the older tests below +// opt REG_STATUS_FLAG out of the diff and assert via the interp side). The +// fix wires mVUdivSet() into doUpperOp(), matching x86; the +// *InJitStatus regression tests assert the bit on the JIT side directly and +// are RED without it. A genuinely-too-short program (FDIV that E-bits within +// the 7-cycle latency) still can't observe the flag — that's shared with x86, +// not an arm64 bug — so pad past the latency when asserting the JIT side. #include "harness/VuTestHarness.h" @@ -128,6 +137,95 @@ TEST(Vu0Qpipe, VdivZeroOverZeroSetsBit10InvalidOp) EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10, 0u); } +// -------- FDIV flags reach the JIT STATUS flag (micro-mode regression) -------- +// +// These assert the I/D bits on the JIT side directly (the older tests above +// route through interp because the JIT used to drop them). They pad past the +// 7-cycle FDIV flag latency so mVUdivSet() — now called from doUpperOp() — has +// a downstream instruction to fold mVU.divFlag into STATUS. RED before the +// doUpperOp()→mVUdivSet() fix; the Soul Calibur 3 VU0-micro jitter regression. + +// Eight NOP pairs (inlined per call site) span the 7-cycle FDIV flag latency. + +TEST(Vu0Qpipe, VdivByZeroSetsDBitInJitStatus) +{ + // 1.0 / 0.0 → D (divide-by-zero, 0x20) must reach the JIT STATUS flag. + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); // sticky/Z/S corners differ on tiny progs; assert the D bit directly + h.SetVf(1, 1.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + NopPair(), NopPair(), NopPair(), NopPair(), + NopPair(), NopPair(), NopPair(), NopPair(), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_NE(h.GetViJit(REG_STATUS_FLAG) & 0x20u, 0u) + << "JIT dropped the FDIV divide-by-zero (D) flag — mVUdivSet missing from doUpperOp?"; + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x20u, 0u); // oracle +} + +TEST(Vu0Qpipe, VdivZeroOverZeroSetsIBitInJitStatus) +{ + // 0.0 / 0.0 → I (invalid-op, 0x10) must reach the JIT STATUS flag. + // This is the exact shape behind the Soul Calibur 3 VU0-micro jitter. + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 0.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + NopPair(), NopPair(), NopPair(), NopPair(), + NopPair(), NopPair(), NopPair(), NopPair(), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_NE(h.GetViJit(REG_STATUS_FLAG) & 0x10u, 0u) + << "JIT dropped the FDIV invalid-op (I) flag (0/0)"; + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10u, 0u); +} + +TEST(Vu0Qpipe, VsqrtNegativeSetsIBitInJitStatus) +{ + // sqrt(-25) → I (invalid-op, 0x10) must reach the JIT STATUS flag. + VuTestHarness h(0); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 0, 0, -25.0f, 0); + h.LoadProgram({ + LowerOnly(VSQRT_L(vf::vf1, 2)), + NopPair(), NopPair(), NopPair(), NopPair(), + NopPair(), NopPair(), NopPair(), NopPair(), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_NE(h.GetViJit(REG_STATUS_FLAG) & 0x10u, 0u) + << "JIT dropped the VSQRT invalid-op (I) flag (negative operand)"; + EXPECT_NE(h.GetViInterp(REG_STATUS_FLAG) & 0x10u, 0u); +} + +TEST(Vu0Qpipe, VdivByZeroSetsDBitInJitStatusOnVu1) +{ + // Same transfer on VU1's micro path. + VuTestHarness h(1); + h.IgnoreViInDiff(REG_STATUS_FLAG); + h.SetVf(1, 1.0f, 0, 0, 0); + h.SetVf(2, 0, 0, 0, 0.0f); + h.LoadProgram({ + LowerOnly(VDIV_L(vf::vf1, 0, vf::vf2, 3)), + NopPair(), NopPair(), NopPair(), NopPair(), + NopPair(), NopPair(), NopPair(), NopPair(), + WaitQPair(), + EBitNopPair(), + }); + h.Run(); + EXPECT_NE(h.GetViJit(REG_STATUS_FLAG) & 0x20u, 0u) + << "VU1 JIT dropped the FDIV divide-by-zero (D) flag"; +} + // -------- VSQRT -------- TEST(Vu0Qpipe, VsqrtPositive) From f0da4e9885ee02e50e4fef26931dee46c1241697 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Mon, 22 Jun 2026 14:09:56 -0700 Subject: [PATCH 018/292] arm64: preserve sibling lanes in mVUclamp1 single-lane clamp (SoulCalibur III SPS) The single-component case (xyzw in {1,2,4,8}) clamped via a 32-bit scalar FMINNM/FMAXNM written back to the full V-register, which zeroes lanes 1-3 -- unlike x86 MIN.SS/MAX.SS, which leave them intact. Single-scalar FMAC ops rotate the live lane to lane 0 (shuffleSSto0), clamp+operate on lane 0, then rotate the siblings back (shuffleSSfrom0), so the values parked in lanes 1-3 must survive the clamp. Zeroing them clobbered carried state (e.g. the ACC accumulator across single-component MADD chains) -> garbage vertices -> SoulCalibur III (SLUS-21216) trembling character geometry (SPS). Only bites at vuClampMode:2 (SC3's gamedb mode) -- the per-op extra-overflow operand clamp; default mode 1 never emits this path, which is why the harness, the cross-arch corpus and shadow-diff all missed it for months (all ran mode 1). Fix: compute the clamped scalar in RQSCRATCH3 and INS it back into lane 0 only, mirroring the x86 SS path. Reference lineage: an early reference ARM64 PS2 implementation had the same scalar-S lane-zeroing -- ours inherited it; a later reference build fixed it by clamping the full V4S. Our fix takes the x86 shape (lane 0 + Ins). Proven via the cross-arch JIT-vs-JIT corpus at --vu-clamp-mode 2 (arm64-rec vs x86-rec: 3153 diffs -> 0; mode 1 stays 0) and live-verified in-game. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_Clamp-arm64.inl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pcsx2/arm64/microVU_Clamp-arm64.inl b/pcsx2/arm64/microVU_Clamp-arm64.inl index 2e6e3e12ee..3ee517ec82 100644 --- a/pcsx2/arm64/microVU_Clamp-arm64.inl +++ b/pcsx2/arm64/microVU_Clamp-arm64.inl @@ -20,12 +20,25 @@ void mVUclamp1(microVU& mVU, const a64::VRegister& reg, const a64::VRegister& re { case 1: case 2: case 4: case 8: { + // Clamp ONLY lane 0 to [minFloat, maxFloat], PRESERVING lanes 1-3. + // A 32-bit scalar NEON FP op zeroes the dest V-reg's upper lanes, + // unlike x86 MIN.SS/MAX.SS which leave them intact. Single-scalar + // FMAC ops rotate the live lane to lane 0 (shuffleSSto0), clamp + + // operate on lane 0, then rotate the siblings back (shuffleSSfrom0) + // — so the siblings parked in lanes 1-3 MUST survive the clamp. + // Zeroing them clobbered carried state (e.g. the ACC accumulator + // across single-component MADD chains), which is SoulCalibur III's + // vuClampMode:2 SPS / trembling geometry. Compute the clamped + // scalar in RQSCRATCH3 and INS it back into lane 0 only, mirroring + // the x86 mVUclamp1 SS path. armAsm->Ldr(a64::VRegister(RQSCRATCH3.GetCode(), 32), mVUglobMem(&mVUglob.maxvals[0])); - armAsm->Fminnm(a64::VRegister(reg.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), + armAsm->Fminnm(a64::VRegister(RQSCRATCH3.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), a64::VRegister(RQSCRATCH3.GetCode(), 32)); + armAsm->Ins(reg.V4S(), 0, RQSCRATCH3.V4S(), 0); armAsm->Ldr(a64::VRegister(RQSCRATCH3.GetCode(), 32), mVUglobMem(&mVUglob.minvals[0])); - armAsm->Fmaxnm(a64::VRegister(reg.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), + armAsm->Fmaxnm(a64::VRegister(RQSCRATCH3.GetCode(), 32), a64::VRegister(reg.GetCode(), 32), a64::VRegister(RQSCRATCH3.GetCode(), 32)); + armAsm->Ins(reg.V4S(), 0, RQSCRATCH3.V4S(), 0); break; } default: From c375e394e2f9ee7750cc6691cda9bdf087669709 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:02:22 -0700 Subject: [PATCH 019/292] arm64: map full 4 MB ROM1 in EE rec LUT (upstream 8fbb1e556) recResetRaw() only mapped ROM1 over pages 0x1e00..0x1e04 (256 KB), but recROM1 is already reserved for the full Ps2MemSize::Rom1 (4 MB / 64 pages) and both the x86 EE rec (iR5900.cpp:573) and our arm64 IOP rec map 0x1e00..0x1e40. EROM lives inside ROM1 at a variable offset, so EE code executing from ROM1 pages above 0x1e04 dispatched to unmapped LUT entries. Mirror the upstream fix. Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/iR5900-arm64.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp index 2c8fe222a8..784ffcf765 100644 --- a/pcsx2/arm64/iR5900-arm64.cpp +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -1336,8 +1336,11 @@ static void recResetRaw() recLUT_SetPage(recLUT, hwLUT, recROM, 0xa000, i, i - 0x1fc0); } - // Map ROM1 - for (int i = 0x1e00; i < 0x1e04; i++) + // Map ROM1 (full 4 MB / 64 pages). EROM lives inside ROM1 at a variable + // offset, so games using EROM above page 0x1e04 must dispatch to mapped JIT + // pages. recROM1 is already reserved for Rom1/4 entries above; matches x86 + // iR5900.cpp:573 and the arm64 IOP twin. Upstream fix 8fbb1e556. + for (int i = 0x1e00; i < 0x1e40; i++) { recLUT_SetPage(recLUT, hwLUT, recROM1, 0x0000, i, i - 0x1e00); recLUT_SetPage(recLUT, hwLUT, recROM1, 0x8000, i, i - 0x1e00); From 15cc18c98f35155e9ed8b1830b2b623a751f643e Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:02:30 -0700 Subject: [PATCH 020/292] arm64: mVU XGKICK read VU1.cycle as u64 (match x86 ptr64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VU1.cycle was widened to u64 upstream (f0723a6ec); our 8b327c354 had narrowed the XgKickHack load to w9 back when the field was u32. The xgkicklastcycle store is 32-bit (matching x86 ptr32), so the stored result is identical either way — this only reads the full u64 to match x86's ptr64 idiom and corrects the now-false "VU1.cycle is u32" comment. Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_Lower-arm64.inl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pcsx2/arm64/microVU_Lower-arm64.inl b/pcsx2/arm64/microVU_Lower-arm64.inl index 8bf8327ddc..819e4927c6 100644 --- a/pcsx2/arm64/microVU_Lower-arm64.inl +++ b/pcsx2/arm64/microVU_Lower-arm64.inl @@ -1964,9 +1964,10 @@ mVUop(mVU_XGKICK) armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); armAsm->Sub(gprT2.W(), gprT2.W(), a64::w9); armMoveAddressToReg(a64::x8, &VU1.cycle); - // VU1.cycle is u32 — narrow load, otherwise we splice 4 bytes of - // neighbouring VU1 state into the carry path of the 64-bit add. - armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8)); + // VU1.cycle is u64 (upstream f0723a6ec widened it). Read the full + // value to match x86's ptr64. The xgkicklastcycle store below is + // 32-bit (matching x86 ptr32), so only the low 32 bits are retained. + armAsm->Ldr(a64::x9, a64::MemOperand(a64::x8)); armAsm->Add(gprT2q, gprT2q, a64::x9); armMoveAddressToReg(a64::x8, &VU1.xgkicklastcycle); armAsm->Str(gprT2.W(), a64::MemOperand(a64::x8)); From 7c8d53842a0c65f5ad1471ec81c508365d9cb1cd Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:02:37 -0700 Subject: [PATCH 021/292] arm64: silence vtlb backpatch log spam (upstream fe0b401ed) DevCon.WriteLn fired on every fastmem-fault backpatch. Wrap in #if 0 to match upstream x86 recVTLB.cpp (keeps the diagnostic re-enableable; the params stay referenced). Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/RecStubs.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pcsx2/arm64/RecStubs.cpp b/pcsx2/arm64/RecStubs.cpp index 4accf0254d..d41f337a2f 100644 --- a/pcsx2/arm64/RecStubs.cpp +++ b/pcsx2/arm64/RecStubs.cpp @@ -20,9 +20,11 @@ void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc, u32 gpr_bitmask, u32 fpr_bitmask, u8 address_register, u8 data_register, u8 size_in_bits, bool is_signed, bool is_load, bool is_fpr) { +#if 0 DevCon.WriteLn("Backpatching %s at %p[%u] (pc %08X vaddr %08X): GPR %08X FPR %08X Addr %u Data %u Size %u Flags %02X %02X", is_load ? "load" : "store", (void*)code_address, code_size, guest_pc, guest_addr, gpr_bitmask, fpr_bitmask, address_register, data_register, size_in_bits, is_signed, is_load); +#endif u8* thunk = recBeginThunk(); From 7b328521eaaf82978885935394e4132db26c4b1a Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:08:03 -0700 Subject: [PATCH 022/292] arm64: preserve sign in FPU NEG.S clamp (upstream 4ffbe0bbf) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recNEG_S_xmm clamped the result with fpuClampResult (Fminnm/Fmaxnm), which folds every NaN to +fMax — so NEG.S of a poisoned +NaN produced a -NaN intermediate that clamped to +fMax (sign stripped). Switch to the sign-preserving fpuClampCompareOperand (Smin/Umin, the arm64 fpuFloat3) so a -NaN/-Inf intermediate clamps to -fMax. Mirrors x86's switch from ClampValues to fpuFloat3. Only manifests on poisoned (raw Inf/NaN bits via LWC1/MTC1/MOV.S) inputs; the single-precision interp NEG.S does no clamp at all, so the test asserts the JIT result directly via RunJitNoDiff. Test: EeRecFpu.NegSPreservesSignOnPoisonedNan Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/iFPU-arm64.cpp | 5 +++- .../core/recompilers/ee_rec_fpu_tests.cpp | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pcsx2/arm64/iFPU-arm64.cpp b/pcsx2/arm64/iFPU-arm64.cpp index c1a08c6d96..127bab6487 100644 --- a/pcsx2/arm64/iFPU-arm64.cpp +++ b/pcsx2/arm64/iFPU-arm64.cpp @@ -453,7 +453,10 @@ void recABS_S() static void recNEG_S_xmm(int info) { armAsm->Fneg(armSRegister(EEREC_D), armSRegister(EEREC_S)); - fpuClampResult(armSRegister(EEREC_D)); + // Sign-preserving clamp: NEG.S of a poisoned +NaN/-Inf must keep the sign + // (-> -fMax), but fpuClampResult (Fminnm/Fmaxnm) folds every NaN to +fMax. + // Mirrors x86's switch from ClampValues to fpuFloat3 (upstream 4ffbe0bbf). + fpuClampCompareOperand(armSRegister(EEREC_D)); } void recNEG_S() diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp index a6605130bc..a2849fa644 100644 --- a/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp @@ -290,6 +290,29 @@ TEST(EeRecFpu, NegSFlipsSignBit) h.ExpectFpr(2, FloatBits(-3.5f)); } +// NEG.S must preserve the sign when clamping a poisoned (raw Inf/NaN bits) +// operand. NEG_S of a +NaN produces a -NaN intermediate (Fneg = sign flip), +// which the result clamp must fold to -FLT_MAX (sign preserved), not +FLT_MAX. +// The arm64 rec used fpuClampResult (Fminnm/Fmaxnm), which folds every NaN to +// +fMax (sign lost); the fix uses fpuClampCompareOperand (Smin/Umin, sign- +// preserving), mirroring x86's switch from ClampValues to fpuFloat3 (upstream +// 4ffbe0bbf). +// +// JIT-only: the single-precision interp NEG_S (FPU.cpp:334) just XORs the sign +// bit with no clamp at all (-> raw -NaN), so neither the pre- nor post-fix rec +// matches it. Assert GetFprBitsJit() directly via RunJitNoDiff(). +TEST(EeRecFpu, NegSPreservesSignOnPoisonedNan) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(1, 0x7FC00000u); // +NaN raw bits (poisoned fpr) + h.LoadProgram({ + ee::NEG_S(2, 1), + }); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), 0xFF7FFFFFu); // -FLT_MAX (sign preserved) +} + TEST(EeRecFpu, AbsSClearsSignBit) { EeRecTestHarness h; From d184353b989c159279101368bc0334717d9117fb Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:25:07 -0700 Subject: [PATCH 023/292] arm64: mVU back up viWriteReg on uncached clone-write (upstream 265afcec7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit allocGPR's uncached path only reloaded viWriteReg's pre-write value for write-only allocs (viLoadReg < 0) before writeVIBackup. For an uncached clone-write (viLoadReg >= 0 && viLoadReg != viWriteReg) it backed up viLoadReg's value instead of viWriteReg's — so an IBxx branch whose condition VI was clone-written in its delay slot evaluated against the wrong register. Restructure to back up viWriteReg from memory BEFORE loading the clone source into idx (matching x86 allocGPR order); the same-reg RMW case still backs up the loaded pre-write value afterward. Upstream 265afcec7 fixes a Gitaroo Man hang. The bug only fires when BOTH the clone source and the write target VI are uncached at the clone-write (register pressure) while the branch still consumes the backup — a minimal VU program can't force that (the branch's read keeps the target cached), so there's no deterministic unit repro. VibeqCloneWriteViBackupInDelaySlot guards the adjacent, always-correct cached path. Full suite (1010) green. Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_IR-arm64.h | 35 ++++++++------- .../recompilers/vu0_branch_delay_tests.cpp | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/pcsx2/arm64/microVU_IR-arm64.h b/pcsx2/arm64/microVU_IR-arm64.h index 0c7a71306e..2e9f3d1476 100644 --- a/pcsx2/arm64/microVU_IR-arm64.h +++ b/pcsx2/arm64/microVU_IR-arm64.h @@ -550,6 +550,24 @@ public: gprMap[idx].count = counter; gprMap[idx].isNeeded = true; + // Back up viWriteReg's pre-write value BEFORE loading the clone source + // into idx (matches x86 allocGPR order). The freshly-allocated idx does + // not yet hold viWriteReg's old value, and the viLoadReg load below would + // overwrite it — so write-only (viLoadReg < 0) and clone-write (viLoadReg + // != viWriteReg) allocs must back up viWriteReg from memory here, then + // re-load idx with the clone source. The same-reg RMW case is handled + // after the load, where idx already holds viWriteReg's pre-write value. + // Upstream 265afcec7 ("Fix incorrect VI being backed up when uncached", + // fixes a Gitaroo Man hang) — the clone-write case previously backed up + // viLoadReg's value instead of viWriteReg's. + if (backup && viWriteReg > 0 && viLoadReg != viWriteReg) + { + armAsm->Ldrh(armWRegister(idx), + mVUstateMem(offsetof(VURegs, VI) + viWriteReg * sizeof(REG_VI))); + writeVIBackup(armWRegister(idx)); + backup = false; + } + if (viLoadReg >= 0 && viLoadReg != 0) { // Load VI from memory (16-bit zero-extended) @@ -566,23 +584,10 @@ public: gprMap[idx].VIreg = (viWriteReg >= 0) ? viWriteReg : ((viLoadReg >= 0) ? viLoadReg : -1); gprMap[idx].dirty = (viWriteReg >= 0); + // Same-reg RMW (viLoadReg == viWriteReg): idx holds the loaded pre-write + // value, so back it up directly. if (backup) - { - // viWriteReg wasn't already in any GPR slot (so unbindAny didn't - // back it up). For write-only allocations (viLoadReg < 0, e.g. - // MTIR), the freshly-allocated `idx` register is uninitialised at - // this point — backing it up would store garbage to mVU.VIbackup - // and the following IBxxx branch would read garbage. - // - // Load viWriteReg's CURRENT (pre-write) value from VI memory first, - // then back it up. Mirrors x86 allocGPR. - if (viLoadReg < 0 && viWriteReg > 0) - { - armAsm->Ldrh(armWRegister(idx), - mVUstateMem(offsetof(VURegs, VI) + viWriteReg * sizeof(REG_VI))); - } writeVIBackup(armWRegister(idx)); - } return armWRegister(idx); } diff --git a/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp b/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp index 2282621265..40c56f7cb9 100644 --- a/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp +++ b/tests/ctest/core/recompilers/vu0_branch_delay_tests.cpp @@ -225,6 +225,51 @@ TEST(Vu0BranchDelay, VibeqNotTakenWhenUnequal) EXPECT_EQ(h.GetViJit(vi::vi5), h.GetViInterp(vi::vi5)); } +// Regression: mVU VI-backup on the UNCACHED clone-write path. When a branch +// delay-slot integer op writes the branch's condition VI via a clone-write +// (load from a DIFFERENT, uncached VI), the backup must save the WRITE +// target's pre-write value — not the load source. The arm64 allocGPR uncached +// path only handled write-only allocs (viLoadReg < 0); for a clone-write it +// backed up the load source instead, so the branch evaluated against the wrong +// VI. Upstream fix 265afcec7 ("Fix incorrect VI being backed up when +// uncached") — fixes a Gitaroo Man hang. +// +// Clone-write VI backup in a branch delay slot: the IBEQ's condition reg vi1 is +// clone-written by its delay slot (IAND vi1, vi3, vi4), so the branch must +// compare vi1's PRE-delay-slot value (42). The backup must save vi1's old value +// (42) -> branch taken; a backup of the clone SOURCE vi3 (99) would make +// IBEQ(99,42) not taken. This exercises the *cached* allocGPR clone-write +// backup path (vi1 is cached by the branch's own read of it), which has always +// been correct. +// +// NOTE: the *uncached* clone-write backup bug (upstream 265afcec7, "Fix +// incorrect VI being backed up when uncached" — a Gitaroo Man hang, fixed in +// microVU_IR-arm64.h allocGPR) needs BOTH the clone source and the write target +// VI to be uncached at the clone-write while the branch still consumes the +// backup. That is a register-pressure coincidence not reproducible in a minimal +// VU program (the branch's read keeps the target cached), so it has no +// deterministic unit repro; this test guards the adjacent cached path. +TEST(Vu0BranchDelay, VibeqCloneWriteViBackupInDelaySlot) +{ + VuTestHarness h(0); + h.SetVi(vi::vi1, 42); // condition reg, PRE-delay-slot value — equals vi2 + h.SetVi(vi::vi2, 42); + h.SetVi(vi::vi3, 99); // clone-write source (uncached); != 42 + h.SetVi(vi::vi4, 0xFFFF); // IAND mask + h.LoadProgram({ + LowerOnly(VIBEQ_L(vi::vi1, vi::vi2, +2)), // pair 0: taken iff backup(vi1)==vi2 → pair 3 + LowerOnly(VIAND_L(vi::vi1, vi::vi3, vi::vi4)), // pair 1: delay slot clone-write vi1 (backup) + LowerOnly(VIADDIU_L(vi::vi6, vi::vi1, 0)), // pair 2: skipped iff taken; reads vi1 (=used) + LoadViImm(vi::vi7, 0x333), // pair 3: target + EBitNopPair(), + }); + h.Run(); + // Correct: branch taken (vi1_old 42 == vi2 42) → pair 2 skipped → vi6 stays 0. + EXPECT_EQ(h.GetViJit(vi::vi6), 0u); + EXPECT_EQ(h.GetViJit(vi::vi6), h.GetViInterp(vi::vi6)); + EXPECT_EQ(h.GetViJit(vi::vi7), 0x333u); +} + TEST(Vu0BranchDelay, VibneTakenWhenUnequal) { VuTestHarness h(0); From b599ca4fbdc264a6901e6496873437f158c732fd Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:27:44 -0700 Subject: [PATCH 024/292] arm64: apply GameDB DynamicPatches in EE rec (upstream 7ee62b822) recompileNextInstruction never called Patch::ApplyDynamicPatches(pc), so per-instruction GameDB DynamicPatch entries never took effect under the arm64 EE recompiler. Mirror x86: apply them at the top of recompileNextInstruction, gated on EmuConfig.EnablePatches. No recompiler-test harness for GameDB dynamic patches, so this ships without a unit test (the call is a gated, direct mirror of x86). Full suite (1010) green. Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/iR5900-arm64.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp index 784ffcf765..e9c27b32d4 100644 --- a/pcsx2/arm64/iR5900-arm64.cpp +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -19,6 +19,7 @@ #include "R5900OpcodeTables.h" #include "Common.h" #include "VMManager.h" +#include "Patch.h" #include "Config.h" #include "vtlb.h" #include "Dmac.h" @@ -960,6 +961,13 @@ void LoadBranchState() void recompileNextInstruction(bool delayslot, bool swapped_delay_slot) { + // Apply GameDB DynamicPatch pattern-matches during recompilation, matching + // x86 recompileNextInstruction. Without this, per-instruction dynamic + // patches from the game database never take effect under the arm64 EE rec. + // Upstream 7ee62b822. + if (EmuConfig.EnablePatches) + Patch::ApplyDynamicPatches(pc); + const u32 old_code = cpuRegs.code; EEINST* old_inst_info = g_pCurInstInfo; From 2ee8d3057411eb98dd5ad1700ffea78ea9cb442e Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 17:40:10 -0700 Subject: [PATCH 025/292] arm64: CTC2 writes integer VIs as 16-bit (upstream a7af3cd48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recCOP2_CTC2's default branch stored the full 32-bit GPR to integer VIs (1-15), but those registers are physically 16-bit and the micro JIT reads/writes them 16-bit — so a CTC2 of a value > 0xFFFF left stale upper bits that a later CFC2 (.UL) read back. Write only the low 16 bits for fs < REG_STATUS_FLAG, matching x86 recCTC2; control regs (>= 16) keep the 32-bit path. Hardware-correct, deliberate JIT-vs-interp divergence: the shared interp CTC2 stores the full 32 bits, so the test asserts the JIT post-state via RunJitNoDiff (CTC2 0xDEADBEEF -> VI1, CFC2 VI1 == 0xBEEF). Per-finding audit decision: adopt (VU integer VIs are 16-bit; the interp is the imperfect one here). Default-order suite (1011) green. A pre-existing COP2-macro VOPMULA/VOPMSUB shuffle-order flakiness is unrelated (reproduces on baseline without this change). Test: EeVu0Ctc2.WritesPlainViTruncatesUpperBitsAcrossCfc2 Correctness audit against reference material. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/iCOP2-arm64.cpp | 12 +++++++++- .../recompilers/ee_vu0_cfc2_ctc2_tests.cpp | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp index 422d6abe09..3e21a187fd 100644 --- a/pcsx2/arm64/iCOP2-arm64.cpp +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -750,9 +750,19 @@ void recCOP2_CTC2() armAsm->Dup(RQSCRATCH.V4S(), RWSCRATCH); armAsm->Str(RQSCRATCH, armVU0Mem(&VU0.micro_statusflags)); } + else if (fs < REG_STATUS_FLAG) + { + // Integer VIs (1-15) are physically 16-bit; the micro JIT reads/writes + // them as 16-bit, so a 32-bit store would leave stale upper bits that a + // later CFC2 (.UL) reads back. Store only the low 16 bits, matching x86 + // recCTC2 (upstream a7af3cd48). NOTE: this is a deliberate, hardware- + // correct JIT-vs-interp divergence — the shared interp CTC2 stores the + // full 32 bits. + armAsm->Strh(RWSCRATCH, armVU0Mem(&VU0.VI[fs])); + } else { - // Default: write 32-bit value to VI register + // Control VIs (>= REG_STATUS_FLAG) reaching the default: full 32-bit. armAsm->Str(RWSCRATCH, armVU0Mem(&VU0.VI[fs])); } } diff --git a/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp index 3059172d0c..2bb199a6ab 100644 --- a/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp +++ b/tests/ctest/core/recompilers/ee_vu0_cfc2_ctc2_tests.cpp @@ -158,6 +158,30 @@ TEST(EeVu0Ctc2, WritesPlainViLow16Bits) EXPECT_EQ(h.GetVu0ViJit(1), h.GetVu0ViInterp(1)); } +// CTC2 to a plain integer VI (1-15) must store only the low 16 bits — those +// registers are physically 16-bit and the micro JIT reads/writes them 16-bit, +// so a 32-bit store leaves stale upper bits that a later CFC2 (.UL) reads back. +// CTC2(0xDEADBEEF -> VI1) then CFC2(VI1) must yield 0xBEEF, not 0xDEADBEEF. +// +// HARDWARE-CORRECT JIT-vs-interp divergence (adopted from upstream a7af3cd48): +// the shared interpreter CTC2 stores the full 32 bits, so its CFC2 reads back +// the sign-extended 0xDEADBEEF. The VU only has 16-bit integer VIs, so the JIT +// (and real PS2) are right; assert the JIT post-state directly via RunJitNoDiff. +TEST(EeVu0Ctc2, WritesPlainViTruncatesUpperBitsAcrossCfc2) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vi(1, 0); // clear VI[1] upper bits (deterministic across tests) + h.SetGpr64(r_t0, 0xDEADBEEFu); + h.LoadProgram({ + CTC2(r_t0, 1), // VU0.VI[1] = 0xDEADBEEF -> must keep only 0xBEEF + CFC2(r_t1, 1), // read VI[1].UL back (bit 31 clear -> zero-extended) + }); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetGpr64Jit(r_t1), 0xBEEFu); +} + TEST(EeVu0Ctc2, WritesRegStatusFlagMasksStickyFieldAndDenormalizesToMicroStatusflags) { // CTC2 to REG_STATUS_FLAG is microVU-aware: only the 0xFC0 "sticky" field From cc1f35450db9388a148ff95de1bbd8a797784e82 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 18:25:05 -0700 Subject: [PATCH 026/292] arm64: bound mVU register preload to the block end (isEOB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mVUcompile passes endCount = whole-micro-memory size (microMemSize/8) to mvuPreloadRegisters, not the block length. The preload loop walks until it runs out of free host registers, breaking only on doXGKICK / lOp.branch — never on the E-bit. An E-bit-terminated block (no branch) therefore runs the preload PAST its own end, and since mVUreset does not clear mVU.prog.IRinfo.info[], it reads VF/VI usage left by a PRIOR compile and preloads registers the current program never touches. Runtime behaviour is unchanged (it is an unused-register load), but the emitted bytes then depend on compile history — non-deterministic codegen. In a cold cache the stale entries read zero (reg 0 -> skipped), so the cold shape is unaffected; this only suppresses the spurious warm-state preloads. That is exactly what the persisted-JIT ABI digest guards, so the drift surfaced as order-dependent failures in MvuAbiDigest under --gtest_shuffle. Fix: break the preload at info->isEOB, mirroring the analysis loop's own termination. Diverges from x86 (same latent over-read there, but no on-disk cache that needs deterministic emit). Same spirit as the flagInfo "clear each compile" fix in mVUinitFirstPass. Adds MvuAbiDigest.EmittedShapeIndependentOfPriorCompile — a deterministic guard (compile a probe after two polluters that seed different VI reads past the probe's end; the probe digest must match). Red before this change, green after. The existing pin test only caught it under --gtest_shuffle. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_Compile-arm64.inl | 17 ++++++ .../core/recompilers/mvu_abi_digest_tests.cpp | 59 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/pcsx2/arm64/microVU_Compile-arm64.inl b/pcsx2/arm64/microVU_Compile-arm64.inl index 2ff4933984..676f2045bf 100644 --- a/pcsx2/arm64/microVU_Compile-arm64.inl +++ b/pcsx2/arm64/microVU_Compile-arm64.inl @@ -537,6 +537,23 @@ static void mvuPreloadRegisters(microVU& mVU, u32 endCount) if (info->lOp.branch) break; + + // Stop at the block's true end. endCount is the whole micro-memory size + // (microMemSize/8), not the block length — the analysis loop above only + // clears + populates IRinfo.info[] for the block's own instructions and + // breaks at isEOB. Without the matching isEOB break here, an E-bit- + // terminated block (no lOp.branch) walks the preload past its own end + // into info[] entries left over from a PRIOR compile, preloading VF/VI + // the program never touches. Harmless at runtime (an unused reg load), + // but it makes the emitted shape depend on compile history — non- + // deterministic codegen that the persisted-JIT ABI digest must not see. + // In a cold cache those stale entries read zero (reg 0 → skipped), so + // this only suppresses the spurious warm-state preloads; the cold shape + // (what the digest pins) is unchanged. Diverges from x86, which has the + // same latent over-read but no on-disk cache that needs deterministic + // emit. Mirrors the flagInfo "clear each compile" fix in mVUinitFirstPass. + if (info->isEOB) + break; } iPC = orig_pc; diff --git a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp index cf45f5468e..6b66da444d 100644 --- a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp +++ b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp @@ -166,4 +166,63 @@ TEST(MvuAbiDigest, EmittedShapePinnedPerAbiVersion) << explain("indirectJump", actual.indirectJump, pin->digests.indirectJump); } +// A program's emitted shape must depend ONLY on the program — never on what +// compiled before it. mVUcompile passes endCount = whole-micro-memory size to +// mvuPreloadRegisters; the preload walks until it runs out of free registers, +// not until the block ends. mVUreset does NOT clear mVU.prog.IRinfo.info[], so +// an E-bit-terminated block whose preload over-runs its own end reads stale +// VF/VI usage left by a PRIOR compile and preloads registers the program never +// touches. The runtime result is identical (an unused reg load), but the +// emitted bytes drift with compile history — which corrupts the persisted-JIT +// ABI digest's "same emitter ⇒ same shape" contract. The mvuPreloadRegisters +// isEOB break is the fix; this is its deterministic regression guard (the main +// pin test only catches it under --gtest_shuffle, which CI may not run). +TEST(MvuAbiDigest, EmittedShapeIndependentOfPriorCompile) +{ + ASSERT_TRUE(RecompilerTestEnvironment::IsReady()); + mVUPersist::SetRecordingEnabled(true); + + // The probe: a short pure-FMAC, E-bit-terminated block (no branch — so the + // only thing that can bound its preload is the isEOB break). + const auto probe = { + UpperOnly(VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + UpperOnly(bits::E | VMUL_U(mask::xyzw, vf::vf4, vf::vf3, vf::vf2)), + }; + + // Two "polluter" programs, longer than the probe, that leave DIFFERENT VI + // read-usage in IRinfo.info[] at indices PAST the probe's own end. The probe + // (2 ops + E-bit delay) clears info[0..~3]; the leading NOPs push the + // VI-reading VIADDs out to indices >= 4 so they survive the probe's analysis. + // VIADD reads two source VIs. If the probe's preload over-runs its block end, + // it picks up these (differing) VIs and the two digests diverge. + const auto polluteViLow = { + NopPair(), NopPair(), NopPair(), NopPair(), + LowerOnly(VIADD_L(vi::vi3, vi::vi5, vi::vi6)), + LowerOnly(VIADD_L(vi::vi3, vi::vi5, vi::vi6)), + LowerOnly(VIADD_L(vi::vi3, vi::vi5, vi::vi6)), + UpperOnly(bits::E | VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + }; + const auto polluteViHigh = { + NopPair(), NopPair(), NopPair(), NopPair(), + LowerOnly(VIADD_L(vi::vi3, vi::vi9, vi::vi10)), + LowerOnly(VIADD_L(vi::vi3, vi::vi9, vi::vi10)), + LowerOnly(VIADD_L(vi::vi3, vi::vi9, vi::vi10)), + UpperOnly(bits::E | VADD_U(mask::xyzw, vf::vf3, vf::vf1, vf::vf2)), + }; + + (void)CompileAndDigest(polluteViLow); + const u64 afterLow = CompileAndDigest(probe); + (void)CompileAndDigest(polluteViHigh); + const u64 afterHigh = CompileAndDigest(probe); + + mVUPersist::SetRecordingEnabled(false); + + ASSERT_NE(afterLow, 0u); + EXPECT_EQ(afterLow, afterHigh) + << "Probe digest changed with the preceding compile (0x" << std::hex + << afterLow << " after VIADD vi5,vi6 vs 0x" << afterHigh + << " after VIADD vi9,vi10) — mvuPreloadRegisters over-ran the block end " + "into stale IRinfo.info[]. Emitted shape must be history-independent."; +} + } // namespace recompiler_tests From 477aaf6d498d830e64c497fdb2ce34419f547918 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 18:25:19 -0700 Subject: [PATCH 027/292] test: give every VU0-capturing EE-rec test a clean VU0 baseline EnableVu0Capture only reset VF[0]/VI[0]. vuRegs[0] is a global, so the MAC/STATUS/CLIP flags, ACC and other VF/VI a previous test left behind survived into this test's pre-snapshot, which Run() feeds to BOTH the JIT and interp passes. Most state leaks harmlessly (it lands equally on both sides), but the flags do not: masked FMACs (OPMULA/OPMSUB touch only xyz) PRESERVE the untouched lane's MAC bit in the interp (VU_MAC_UPDATE keys off the per-op shift; VU_STAT_UPDATE folds macflag&0x000F into STATUS), whereas the JIT recomputes the full flag word. With a stale non-zero w-lane MAC bit incoming the two diverge, surfacing as order-dependent failures in EeVu0Cop2Macro.Vopm* under --gtest_shuffle (baseline seeds 2/6/15/23/25). Zero the architectural register + flag file in EnableVu0Capture so test order can't leak. Infra (Mem/Micro/idx/cycle) and the control registers at VI[24..31] are left intact; tests seed what they need afterwards. recompiler_tests is now shuffle-clean across seeds 1-80. Co-Authored-By: Claude Opus 4.8 --- .../recompilers/harness/EeRecTestHarness.cpp | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp index 23afd7359f..e11f1be76e 100644 --- a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp @@ -457,15 +457,51 @@ void EeRecTestHarness::ExpectAcc(u32 bits) const void EeRecTestHarness::EnableVu0Capture() { capture_vu0_ = true; + + // Give every VU0-capturing test a clean architectural baseline. + // + // vuRegs[0] is a global: whatever flags/ACC/VF a previous test left behind + // survive into this test's pre-snapshot, which Run() then feeds to BOTH the + // JIT and interp passes. Most state leaks harmlessly (it lands equally on + // both sides). The MAC/STATUS flags do not: masked FMACs (e.g. OPMULA/OPMSUB + // only touch xyz) PRESERVE the untouched lane's MAC bit in the interp + // (VUflags.cpp VU_MAC_UPDATE keys off the per-op shift; VU_STAT_UPDATE then + // folds macflag&0x000F into STATUS), whereas the JIT recomputes the full + // flag word. With a stale non-zero w-lane MAC bit incoming, the two diverge + // — surfacing as order-dependent failures in EeVu0Cop2Macro.Vopm* under + // --gtest_shuffle. Zero the register/flag file here so test order can't leak. + // + // Only architectural register + flag state is cleared. Infra the VU0 engine + // needs (Mem/Micro pointers, idx, cycle) and the control registers at + // VI[24..31] (TPC/CMSAR0/FBRST/VPU_STAT/CMSAR1) are left intact; tests that + // care seed them explicitly after this call. + VURegs& vu = vuRegs[0]; + for (int i = 1; i < 32; i++) + vu.VF[i].UD[0] = vu.VF[i].UD[1] = 0; + for (int i = 1; i <= REG_P; i++) // VI[1..15] integer + VI[16..23] flags/R/I/Q/P + vu.VI[i].UL = 0; + vu.ACC.UD[0] = vu.ACC.UD[1] = 0; + vu.q.UL = 0; + vu.p.UL = 0; + vu.macflag = 0; + vu.statusflag = 0; + vu.clipflag = 0; + for (int i = 0; i < 4; i++) + { + vu.micro_macflags[i] = 0; + vu.micro_statusflags[i] = 0; + vu.micro_clipflags[i] = 0; + } + // Mirror the VuTestHarness's first-touch invariant: VF[0] must read as // (0,0,0,1.0). _vu0Exec asserts on drift via DbgCon.Error. The interp // run will trigger that assertion on whatever stale state the previous // test left behind unless this is reset here. - vuRegs[0].VF[0].f.x = 0.0f; - vuRegs[0].VF[0].f.y = 0.0f; - vuRegs[0].VF[0].f.z = 0.0f; - vuRegs[0].VF[0].f.w = 1.0f; - vuRegs[0].VI[0].UL = 0; + vu.VF[0].f.x = 0.0f; + vu.VF[0].f.y = 0.0f; + vu.VF[0].f.z = 0.0f; + vu.VF[0].f.w = 1.0f; + vu.VI[0].UL = 0; } void EeRecTestHarness::SeedVu0Vf(u32 reg_idx, float x, float y, float z, float w) From 3a1785854de625d5491b70ac7d48022104df11b7 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 18:39:35 -0700 Subject: [PATCH 028/292] arm64/microVU: fold vi00-relative loadstore addresses (upstream 6018936dc) When a LQ/SQ/ILW/ISW base VI is vi00 (always reads 0), the access address is a compile-time constant. Port Stenzek's x86 mVUoptimizeConstantAddr: instead of emitting moveVIToGPR + imm-add + mVUaddrFix (mask/shift) + Mem-load + base-add, collapse to a single Mem-pointer load plus at most one immediate add, then run the existing load/store. Typical vi00 loadstore drops from ~6 emitted insns to 2 (VU0 in-range offsets fold the byte offset into a single add immediate). The VU0 cross-VU-register window (addr & 0x400) and the IbitHack runtime-reconstruct path keep the existing runtime mVUaddrFix path. This postdates the reference ARM64 PS2 implementations we track, so it's adopted under the "newer correct upstream pattern" rule; the helper mirrors x86 1:1 in semantics (arm64 returns the folded base in a register rather than an x86 memory operand). Bumps kMvuCompilerAbiVersion 3 -> 4 (+ versioning mirror) since mVU codegen changed; evicts on-disk VU program caches recorded with the pre-fold shape. The ABI-digest probes use no LQ/SQ/ILW/ISW, so their pinned digests are bit-identical (re-pinned to abi 4 unchanged). Adds Vu1AluLower.{Lq,Sq,Ilw,Isw}Vi00*FoldsToConstAddr covering all four folded ops at a non-zero quad offset; Run() auto-diffs the folded JIT against the interpreter. 1016 recompiler_tests pass, shuffle-clean seeds 1-40. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU-arm64.h | 2 +- pcsx2/arm64/microVU_Lower-arm64.inl | 176 ++++++++++-------- pcsx2/arm64/microVU_Misc-arm64.inl | 33 ++++ .../core/recompilers/mvu_abi_digest_tests.cpp | 6 +- .../mvu_progcache_versioning_tests.cpp | 2 +- .../core/recompilers/vu1_alu_lower_tests.cpp | 72 +++++++ 6 files changed, 206 insertions(+), 85 deletions(-) diff --git a/pcsx2/arm64/microVU-arm64.h b/pcsx2/arm64/microVU-arm64.h index acc604cce9..e6341648b4 100644 --- a/pcsx2/arm64/microVU-arm64.h +++ b/pcsx2/arm64/microVU-arm64.h @@ -42,7 +42,7 @@ // invalidates atomically when the helper ABI shape changes. // 3 — dropped helperTableLayoutHash from the options sentinel // (sentinel layout shrank; every contentHash changes). -static constexpr u32 kMvuCompilerAbiVersion = 3; +static constexpr u32 kMvuCompilerAbiVersion = 4; // Hash/equality functors for XXH128_hash_t — let std::unordered_map // work without a wrapping struct. low64 already carries the well-mixed half of diff --git a/pcsx2/arm64/microVU_Lower-arm64.inl b/pcsx2/arm64/microVU_Lower-arm64.inl index 819e4927c6..01117a3788 100644 --- a/pcsx2/arm64/microVU_Lower-arm64.inl +++ b/pcsx2/arm64/microVU_Lower-arm64.inl @@ -1339,35 +1339,38 @@ mVUop(mVU_ILW) pass2 { // Compute address: (VI[Is] + Imm11) wrapped, then byte offset - mVU.regAlloc->moveVIToGPR(gprT1, _Is_); - if (!EmuConfig.Gamefixes.IbitHack) + if (!mVUoptimizeConstantAddr(mVU, _Is_, _Imm11_, offsetSS, gprT1q)) { - if (_Imm11_ != 0) + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) { - s32 imm = _Imm11_; - if (imm >= 0) - armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); - else - armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } } - } - else - { - // IbitHack: reconstruct signed Imm11 from the live opcode word at - // runtime via sbfx+bfxil. - armLoadPtr(RWSCRATCH, &curI); - armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); - armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); - armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); - } - mVUaddrFix(mVU, gprT1); + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at + // runtime via sbfx+bfxil. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); - // Add lane offset for the selected component - armAsm->Add(gprT1.W(), gprT1.W(), offsetSS); + // Add lane offset for the selected component + armAsm->Add(gprT1.W(), gprT1.W(), offsetSS); - // Add VU memory base - armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); - armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + // Add VU memory base + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + } // Load 16-bit value from memory const a64::Register& regT = mVU.regAlloc->allocGPR(-1, _It_, mVUlow.backupVI); @@ -1427,30 +1430,33 @@ mVUop(mVU_ISW) pass2 { // Compute address - mVU.regAlloc->moveVIToGPR(gprT1, _Is_); - if (!EmuConfig.Gamefixes.IbitHack) + if (!mVUoptimizeConstantAddr(mVU, _Is_, _Imm11_, 0, gprT1q)) { - if (_Imm11_ != 0) + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) { - s32 imm = _Imm11_; - if (imm >= 0) - armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); - else - armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } } - } - else - { - // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. - armLoadPtr(RWSCRATCH, &curI); - armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); - armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); - armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); - } - mVUaddrFix(mVU, gprT1); + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); - armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); - armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); + } // Load VI[It] value (zero-extended to 32-bit) and store to selected lanes const a64::Register& regT = mVU.regAlloc->allocGPR(_It_, -1, false, true); @@ -1508,29 +1514,32 @@ mVUop(mVU_LQ) pass2 { // Compute address: (VI[Is] + Imm11) wrapped - mVU.regAlloc->moveVIToGPR(gprT1, _Is_); - if (!EmuConfig.Gamefixes.IbitHack) + if (!mVUoptimizeConstantAddr(mVU, _Is_, _Imm11_, 0, gprT1q)) { - if (_Imm11_ != 0) + mVU.regAlloc->moveVIToGPR(gprT1, _Is_); + if (!EmuConfig.Gamefixes.IbitHack) { - s32 imm = _Imm11_; - if (imm >= 0) - armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); - else - armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); } - else - { - // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. - armLoadPtr(RWSCRATCH, &curI); - armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); - armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); - armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); - } - mVUaddrFix(mVU, gprT1); - armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); - armAsm->Add(gprT1q, gprT2q, gprT1q.X()); const a64::VRegister& Ft = mVU.regAlloc->allocReg(-1, _Ft_, _X_Y_Z_W); mVUloadMem(Ft, gprT1q, _X_Y_Z_W); @@ -1617,29 +1626,32 @@ mVUop(mVU_SQ) pass2 { // Compute address from VI[It] + Imm11 - mVU.regAlloc->moveVIToGPR(gprT1, _It_); - if (!EmuConfig.Gamefixes.IbitHack) + if (!mVUoptimizeConstantAddr(mVU, _It_, _Imm11_, 0, gprT1q)) { - if (_Imm11_ != 0) + mVU.regAlloc->moveVIToGPR(gprT1, _It_); + if (!EmuConfig.Gamefixes.IbitHack) { - s32 imm = _Imm11_; - if (imm >= 0) - armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); - else - armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + if (_Imm11_ != 0) + { + s32 imm = _Imm11_; + if (imm >= 0) + armAsm->Add(gprT1.W(), gprT1.W(), (u32)imm); + else + armAsm->Sub(gprT1.W(), gprT1.W(), (u32)(-imm)); + } } + else + { + // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. + armLoadPtr(RWSCRATCH, &curI); + armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); + armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); + armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); + } + mVUaddrFix(mVU, gprT1); + armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); + armAsm->Add(gprT1q, gprT2q, gprT1q.X()); } - else - { - // IbitHack: reconstruct signed Imm11 from the live opcode word at runtime. - armLoadPtr(RWSCRATCH, &curI); - armAsm->Sbfx(gprT2.W(), RWSCRATCH, 10, 1); - armAsm->Bfxil(gprT2.W(), RWSCRATCH, 0, 10); - armAsm->Add(gprT1.W(), gprT1.W(), gprT2.W()); - } - mVUaddrFix(mVU, gprT1); - armAsm->Ldr(gprT2q, mVUstateMem(offsetof(VURegs, Mem))); - armAsm->Add(gprT1q, gprT2q, gprT1q.X()); const a64::VRegister& Fs = mVU.regAlloc->allocReg(_Fs_, -1, _X_Y_Z_W); if (_X_Y_Z_W == 0xf) diff --git a/pcsx2/arm64/microVU_Misc-arm64.inl b/pcsx2/arm64/microVU_Misc-arm64.inl index 7a8cb3c87c..0b0941ecdd 100644 --- a/pcsx2/arm64/microVU_Misc-arm64.inl +++ b/pcsx2/arm64/microVU_Misc-arm64.inl @@ -354,3 +354,36 @@ __fi void mVUaddrFix(mV, const a64::Register& gprReg) armAsm->Lsl(gprReg.X(), gprReg.X(), 4); } } + +// Constant-address fold for loadstores whose base VI is vi00 (always 0). With a +// constant base the whole address is known at compile time, so the runtime +// moveVIToGPR + imm-add + mVUaddrFix (mask/shift) + base-add chain collapses to +// a single Mem-pointer load plus (at most) one immediate add. On a return of +// true gprOutQ holds &VU.Mem[const], ready for the load/store; on false the +// caller emits the normal runtime path. Mirrors x86 mVUoptimizeConstantAddr +// (microVU_Misc.inl). The VU0 cross-VU-register window (addr & 0x400) and the +// IbitHack runtime-reconstruct path are deliberately left to mVUaddrFix. +// Ported 2026-06-23 from upstream 6018936dc (postdates the leak/4248; correct +// and ABI-neutral, so adopted per the "newer-upstream-pattern" rule). +__fi bool mVUoptimizeConstantAddr(mV, u32 srcreg, s32 offset, s32 offsetSS_, const a64::Register& gprOutQ) +{ + if (srcreg != 0 || EmuConfig.Gamefixes.IbitHack) + return false; + + s32 byteAddr; + if (isVU1) + { + byteAddr = ((offset & 0x3ff) << 4) + offsetSS_; + } + else + { + if (offset & 0x400) + return false; // cross-VU-register access — runtime path handles it + byteAddr = ((offset & 0xff) << 4) + offsetSS_; + } + + armAsm->Ldr(gprOutQ, mVUstateMem(offsetof(VURegs, Mem))); + if (byteAddr != 0) + armAsm->Add(gprOutQ, gprOutQ, byteAddr); + return true; +} diff --git a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp index 6b66da444d..529429aaf8 100644 --- a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp +++ b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp @@ -83,7 +83,11 @@ struct AbiPin // === THE PIN TABLE === (see header comment for the update protocol) constexpr AbiPin kPins[] = { - {3, {0x4c3b6e1330199619, 0xd6f530cc13f0d0aa, 0xfcead342cc0b7df8}}, + // abi 4: vi00 const-addr loadstore fold (6018936dc). The probes below use no + // LQ/SQ/ILW/ISW, so the folded ops leave their emitted shape unchanged — the + // digests are bit-identical to abi 3; the bump is to evict on-disk caches + // recorded with the pre-fold loadstore shape. + {4, {0x4c3b6e1330199619, 0xd6f530cc13f0d0aa, 0xfcead342cc0b7df8}}, }; u64 CompileAndDigest(std::initializer_list pairs) diff --git a/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp b/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp index 1a3e5c6866..293cf7f20a 100644 --- a/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp +++ b/tests/ctest/core/recompilers/mvu_progcache_versioning_tests.cpp @@ -59,7 +59,7 @@ // round-trip tests. namespace pcsx2_test { - static constexpr u32 kMvuCompilerAbiVersionMirror = 3; + static constexpr u32 kMvuCompilerAbiVersionMirror = 4; } namespace diff --git a/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp b/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp index 52943ec59d..80ba054c3a 100644 --- a/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp +++ b/tests/ctest/core/recompilers/vu1_alu_lower_tests.cpp @@ -472,4 +472,76 @@ TEST(Vu1AluLower, VlqYLoadsOnlyYLane) EXPECT_EQ(h.GetVfBitsJit(20, 'w'), 0x44u); } +// ========================================================================= +// vi00 constant-address loadstore fold (upstream 6018936dc) +// ========================================================================= +// +// When the base VI is vi00 (always reads 0) the loadstore address is a +// compile-time constant, so microVU folds it (mVUoptimizeConstantAddr) into a +// Mem-pointer load + one immediate add instead of the runtime +// moveVI + imm-add + mask/shift + base-add chain. These cover all four folded +// ops (LQ/SQ/ILW/ISW) at a NON-ZERO quad offset, exercising the VU1 0x3FF mask +// and the byte-offset add. h.Run() auto-diffs the folded JIT against the +// interpreter; the explicit checks pin the resolved address + value. + +TEST(Vu1AluLower, LqVi00NonZeroOffsetFoldsToConstAddr) +{ + VuTestHarness h(1); + h.WriteMemU128(5 * 16, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u); + h.SetVfBits(10, 0, 0, 0, 0); + h.LoadProgram({ + LowerOnly(VLQ_L(mask::xyzw, vf::vf10, vi::vi0, 5)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetVfBitsJit(10, 'x'), 0x11111111u); + EXPECT_EQ(h.GetVfBitsJit(10, 'y'), 0x22222222u); + EXPECT_EQ(h.GetVfBitsJit(10, 'z'), 0x33333333u); + EXPECT_EQ(h.GetVfBitsJit(10, 'w'), 0x44444444u); +} + +TEST(Vu1AluLower, SqVi00NonZeroOffsetFoldsToConstAddr) +{ + VuTestHarness h(1); + h.WriteMemU128(7 * 16, 0, 0, 0, 0); + h.SetVfBits(11, 0xAAAAAAAAu, 0xBBBBBBBBu, 0xCCCCCCCCu, 0xDDDDDDDDu); + h.LoadProgram({ + LowerOnly(VSQ_L(mask::xyzw, vf::vf11, vi::vi0, 7)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(7 * 16 + 0), 0xAAAAAAAAu); + EXPECT_EQ(h.GetMemU32Jit(7 * 16 + 4), 0xBBBBBBBBu); + EXPECT_EQ(h.GetMemU32Jit(7 * 16 + 8), 0xCCCCCCCCu); + EXPECT_EQ(h.GetMemU32Jit(7 * 16 + 12), 0xDDDDDDDDu); +} + +TEST(Vu1AluLower, IlwVi00OffsetLaneFoldsToConstAddr) +{ + VuTestHarness h(1); + // mem[3].z = 0x1234; ILW.z reads the z lane (offsetSS=8) into vi5. + h.WriteMemU128(3 * 16, 0, 0, 0x1234u, 0); + h.SetVi(5, 0); + h.LoadProgram({ + LowerOnly(VILW_L(mask::z, vi::vi5, vi::vi0, 3)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetViJit(5) & 0xFFFFu, 0x1234u); +} + +TEST(Vu1AluLower, IswVi00OffsetFoldsToConstAddr) +{ + VuTestHarness h(1); + h.WriteMemU128(9 * 16, 0, 0, 0, 0); + h.SetVi(6, 0x5678); + h.LoadProgram({ + LowerOnly(VISW_L(mask::xyzw, vi::vi6, vi::vi0, 9)), + EBitNopPair(), + }); + h.Run(); + EXPECT_EQ(h.GetMemU32Jit(9 * 16 + 0), 0x5678u); + EXPECT_EQ(h.GetMemU32Jit(9 * 16 + 12), 0x5678u); +} + } // namespace recompiler_tests From d7439fadfc3d6b321dae25cc7d33f956568b6465 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 19:53:34 -0700 Subject: [PATCH 029/292] arm64: run VU0 sync ahead on small non-interlocked COP2 blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of upstream 6dc5087cb ("VU: Run sync ahead on small blocks") to the arm64 COP2 path. The shared machinery (CalculateMinRunCycles, the ExecuteBlockJIT interlocked param, s_nBlockInterlocked) was already merged from upstream, but arm64's recompiled COP2 ops sync VU0 via cop2EmitConditionalSync -> vu0Sync, not via ExecuteBlockJIT — so the 16-cycle floor never fired and every non-interlocked catch-up dispatched VU0 by the exact (often tiny) delta, paying the full mVU dispatch envelope to run as little as 3 cycles. vu0Sync gains a run-ahead sibling (vu0SyncRunAhead) that floors the sync-only catch-up at 16 cycles; the non-interlock branch of cop2EmitConditionalSync emits a call to it. Overshoot is bounded and self-limiting — the next sync sees a negative delta and no-ops until the EE catches up — so several round-trips collapse into one. This directly trims VU0 dispatch-envelope / exit-thunk cost, two of our top-three remaining JIT overheads, on exactly the workload the upstream commit names (Ratchet & Clank's intro). An interlocked COP2 op anywhere in the block sets s_nBlockInterlocked (reset per block in recRecompile, beside s_nBlockCycles), which forces the exact vu0Sync for the rest of the block so a later interlock can't be overshot — mirroring upstream's block-level flag. No new test: the run-ahead only diverges from the exact path mid-program (final VU0 state converges deterministically), so it's invisible to the JIT-vs-interp harness by construction; all 1016 recompiler_tests pass unchanged. Magnitude is workload-dependent and validated on hardware. Co-Authored-By: Claude Opus 4.8 --- pcsx2/VU0.cpp | 23 ++++++++++++++++++----- pcsx2/arm64/iCOP2-arm64.cpp | 12 +++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pcsx2/VU0.cpp b/pcsx2/VU0.cpp index edcc836625..c8d89b44da 100644 --- a/pcsx2/VU0.cpp +++ b/pcsx2/VU0.cpp @@ -64,7 +64,7 @@ void COP2_Unknown() //**************************************************************************** -__fi void _vu0run(bool breakOnMbit, bool addCycles, bool sync_only) { +__fi void _vu0run(bool breakOnMbit, bool addCycles, bool sync_only, bool runAhead) { if (!(VU0.VI[REG_VPU_STAT].UL & 1)) return; @@ -87,6 +87,18 @@ __fi void _vu0run(bool breakOnMbit, bool addCycles, bool sync_only) { if (runCycles < 0) return; + + // Run-ahead (non-interlocked COP2 sync only): dispatching a tiny VU0 + // catch-up (e.g. 3 cycles) pays the full mVU dispatch envelope to run + // almost nothing. When the sync isn't interlocked it's fine to overshoot + // the EE by a few cycles — the next sync sees a negative delta and + // no-ops until the EE catches back up, so several round-trips collapse + // into one. Mirrors upstream CalculateMinRunCycles(delta, /*accurate*/ + // false) (commit 6dc5087cb "VU: Run sync ahead on small blocks"); the + // arm64 COP2 path syncs via vu0Sync rather than ExecuteBlockJIT, so the + // floor lives here instead. + if (runAhead && runCycles < 16) + runCycles = 16; } do { // Run VU until it finishes or M-Bit @@ -105,10 +117,11 @@ __fi void _vu0run(bool breakOnMbit, bool addCycles, bool sync_only) { } } -void _vu0WaitMicro() { _vu0run(1, 1, 0); } // Runs VU0 Micro Until E-bit or M-Bit End -void _vu0FinishMicro() { _vu0run(0, 1, 0); } // Runs VU0 Micro Until E-Bit End -void vu0Finish() { _vu0run(0, 0, 0); } // Runs VU0 Micro Until E-Bit End (doesn't stall EE) -void vu0Sync() { _vu0run(0, 0, 1); } // Runs VU0 until it catches up +void _vu0WaitMicro() { _vu0run(1, 1, 0, 0); } // Runs VU0 Micro Until E-bit or M-Bit End +void _vu0FinishMicro() { _vu0run(0, 1, 0, 0); } // Runs VU0 Micro Until E-Bit End +void vu0Finish() { _vu0run(0, 0, 0, 0); } // Runs VU0 Micro Until E-Bit End (doesn't stall EE) +void vu0Sync() { _vu0run(0, 0, 1, 0); } // Runs VU0 until it catches up (exact) +void vu0SyncRunAhead() { _vu0run(0, 0, 1, 1); } // Catches up, but runs a 16-cycle minimum (non-interlocked) namespace R5900 { namespace Interpreter{ diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp index 3e21a187fd..b094c669fd 100644 --- a/pcsx2/arm64/iCOP2-arm64.cpp +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -525,6 +525,7 @@ void endMacroOp_arm64(int mode) // Sync is skipped in the common case where VU0 micro isn't executing. extern void vu0Sync(); +extern void vu0SyncRunAhead(); extern void _vu0FinishMicro(); extern void _vu0WaitMicro(); @@ -536,6 +537,12 @@ void cop2EmitConditionalSync(bool interlock, void (*finishFunc)()) // Handle interlock (bit 0 set): COP2_Interlock pattern if (interlock) { + // An interlocked COP2 op anywhere in the block means VU0 timing must be + // exact: forbid the non-interlock run-ahead for the rest of the block so + // a later sync can't overshoot the cycle this interlock waits on. Mirrors + // upstream's block-level s_nBlockInterlocked (set in COP2_Interlock). + s_nBlockInterlocked = true; + // Interlock requires sync — check if analysis says VU0 could be running if (g_pCurInstInfo->info & EEINST_COP2_SYNC_VU0) { @@ -594,7 +601,10 @@ void cop2EmitConditionalSync(bool interlock, void (*finishFunc)()) armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle)); if (needsSync) - armEmitCall((void*)vu0Sync); + // Non-interlocked catch-up: run a 16-cycle minimum to amortize the mVU + // dispatch envelope over small blocks (6dc5087cb). If the block also + // contains an interlocked op, fall back to the exact sync. + armEmitCall((void*)(s_nBlockInterlocked ? vu0Sync : vu0SyncRunAhead)); else armEmitCall((void*)_vu0FinishMicro); From 23a8acc0e35e270491725be438d9b20567c18c04 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 20:14:05 -0700 Subject: [PATCH 030/292] arm64: skip COP2 macro status-flag denormalize/normalize when dead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the perf half of upstream bc3729c93 ("iR5900: Skip reloading COP2 flags register when it's not used") to the arm64 hand-rolled COP2 macro path. The correctness half (the _Funct_ >= 074 guard in the COP2FlagHackPass) already ships in the shared x86/iR5900Analysis.cpp the arm64 build compiles. setupMacroOp_arm64/endMacroOp_arm64 emitted cop2EmitDenormalizeStatusFlag (~11 insns) and cop2EmitNormalizeStatusFlag (~10 insns) unconditionally for every status-updating COP2 FMAC. With vuFlagHack on (the production default) the shared COP2FlagHackPass already tags every status write that reaches a CFC2 with EEINST_COP2_STATUS_FLAG — sticky reach included, since the tag is applied to all writes before the next CFC2 (apc < m_cfc2_pc). A write that reaches no CFC2 is dead, so the denormalize/normalize dance is pure dead emitted code. Gate both on a shared cop2StatusFlagLive() predicate (same EEINST_COP2_STATUS_FLAG gate the adjacent mVUmacroSetupCOP2State already uses for the mVU-reuse path); setup and teardown skip in lockstep so a denormalize never lands without its normalize. arm64 re-seeds s_cop2DenormStatusFlag from VU0.VI[REG_STATUS_FLAG] every op (no x86 gprF0 cross-op persistence), so this is the dead-status *subset* of the upstream change and needs none of that persistence: the live path still normalizes to the architectural register every time, and a dead op's leftover scratch scribble is overwritten by the next live op's denormalize. The x86 "first denormalizer" marker EEINST_COP2_DENORMALIZE_STATUS_FLAG is therefore intentionally not in the gate. Since vuFlagHack defaults on, this trims emitted code volume on COP2-macro FMACs in real gameplay. Test infrastructure for the flaghack path: - RecompilerTestEnvironment pins vuFlagHack OFF for JIT-vs-interp determinism (it lets the JIT skip status updates the always-accurate interpreter still performs — the same determinism rationale as the existing vuThread/ vu1Instant pins). This was an oversight; flaghack defaulted on in tests. - mvu_abi_digest_tests forces vuFlagHack back ON inside CompileAndDigest so the ABI digest keeps pinning the production-cached mVU shape (digests are bit-identical — the only change was the harness default). - EeRecTestHarness::RunJitNoDiff now captures the VU0/VU1 snapshots (it only captured EE state, so GetVu0Vf* read a stale prior-test snapshot — an order-dependence the new dead-status test surfaced under --gtest_shuffle). - Two EeVu0Cop2MacroFlagHack tests opt back into flaghack: status-live across a CFC2 (auto-diff: the gate must keep the normalize, JIT == interp) and status-dead standalone (JIT-only: the skip must not disturb the arithmetic). 1018 recompiler_tests pass, shuffle-clean seeds 1-40. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/iCOP2-arm64.cpp | 49 ++++++++++++--- .../recompilers/ee_vu0_cop2_macro_tests.cpp | 63 +++++++++++++++++++ .../recompilers/harness/EeRecTestHarness.cpp | 10 +++ .../harness/RecompilerTestEnvironment.cpp | 9 ++- .../core/recompilers/mvu_abi_digest_tests.cpp | 12 ++++ 5 files changed, 132 insertions(+), 11 deletions(-) diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp index b094c669fd..d2a1639624 100644 --- a/pcsx2/arm64/iCOP2-arm64.cpp +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -295,6 +295,20 @@ static void cop2EmitIntegerMin(int fsReg) // thread-local slot that only ever has one instance. static u32 s_cop2DenormStatusFlag; +// Status-flag liveness for the hand-rolled COP2 macro path (bc3729c93). With +// vuFlagHack on, the denormalize (setup) and normalize (teardown) are emitted +// only when the status output is actually consumed by a later CFC2; with the +// hack off, or when analysis info is missing, they're always emitted. setup and +// teardown MUST call this with the same instruction in flight so they skip in +// lockstep — a denormalize without its matching normalize (or vice versa) +// corrupts VU0.VI[REG_STATUS_FLAG]. +static bool cop2StatusFlagLive() +{ + // CHECK_VU_FLAGHACK (microVU_Misc-arm64.h) expands to this; inlined here to + // avoid pulling a microVU header into the COP2 codegen TU. + return !EmuConfig.Speedhacks.vuFlagHack || !g_pCurInstInfo || (g_pCurInstInfo->info & EEINST_COP2_STATUS_FLAG); +} + // Emit code to denormalize status flag from VU0.VI[REG_STATUS_FLAG] // into s_cop2DenormStatusFlag (mVUallocSFLAGd). // Denormalized = ((norm >> 3) & 0x18) | ((norm << 11) & 0x1800) | ((norm << 14) & 0x3cf0000) @@ -468,8 +482,20 @@ void setupMacroOp_arm64(int mode) if (mode & 0x10) // Status/MAC flags will be updated { - // Always denormalize the status flag; no liveness-based skip is applied. - cop2EmitDenormalizeStatusFlag(); + // Denormalize VU0's status flag into s_cop2DenormStatusFlag, but skip it + // when the status output is dead. With vuFlagHack (on by default) the + // shared COP2FlagHackPass tags every status write that reaches a CFC2 + // with EEINST_COP2_STATUS_FLAG — sticky reach included, since the tag is + // applied to all writes before the next CFC2 (apc < m_cfc2_pc). A write + // that reaches no CFC2 is dead, so the ~11-instruction denormalize plus + // the matching normalize in endMacroOp are pure dead emitted code. + // Mirrors upstream bc3729c93 and the EEINST_COP2_STATUS_FLAG gate already + // used by mVUmacroSetupCOP2State for the mVU-reuse path. arm64 re-seeds + // s_cop2DenormStatusFlag from VU0.VI[REG_STATUS_FLAG] every op (no x86 + // gprF0 persistence across ops), so EEINST_COP2_DENORMALIZE_STATUS_FLAG — + // the x86 "first denormalizer" marker — is intentionally not in the gate. + if (cop2StatusFlagLive()) + cop2EmitDenormalizeStatusFlag(); } if (mode & 0x01) // Q register will be read — load into RQSCRATCH3 @@ -494,13 +520,18 @@ void endMacroOp_arm64(int mode) if (mode & 0x10) // Status/MAC flags were updated { - // Always normalize status flag back to VU0.VI[REG_STATUS_FLAG]. - // Each COP2 macro instruction is self-contained, so the normalized - // flag must be written every time. The vuFlagHack optimization - // (skipping normalization when no one reads the flag) requires - // correct denormalized flag persistence across instructions, - // which is not yet supported. - cop2EmitNormalizeStatusFlag(); + // Normalize the status flag back to VU0.VI[REG_STATUS_FLAG], under the + // same liveness gate as the setup denormalize (they must skip together — + // see cop2StatusFlagLive). When status is dead the whole denormalize -> + // update -> normalize chain is elided: the body's cop2EmitFlagUpdate + // still scribbles s_cop2DenormStatusFlag, but that value is dead (never + // normalized out) and the next live op re-seeds the scratch from + // VU0.VI[REG_STATUS_FLAG], so nothing leaks. This is the dead-status + // subset of bc3729c93; it needs no cross-op denormalized persistence + // (which arm64 doesn't implement) because the live path still normalizes + // to the architectural register every time. + if (cop2StatusFlagLive()) + cop2EmitNormalizeStatusFlag(); } // microVU0 state teardown — flushPartialForCOP2 + cop2=0 + regAlloc reset. diff --git a/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp index de2e867a38..ce5c8d8ae0 100644 --- a/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp +++ b/tests/ctest/core/recompilers/ee_vu0_cop2_macro_tests.cpp @@ -21,6 +21,7 @@ #include "harness/EeRecTestHarness.h" #include "VU.h" +#include "Config.h" #include @@ -1210,4 +1211,66 @@ TEST(EeVu0Cop2Macro, Bc2tlNotTakenSquashesDelaySlot) h.ExpectGpr64(reg::t0, 7ull); // delay slot squashed } +// ========================================================================= +// vuFlagHack status-liveness skip (bc3729c93) +// +// setupMacroOp_arm64/endMacroOp_arm64 skip the status-flag denormalize/ +// normalize when vuFlagHack is on AND the COP2FlagHackPass marks the op's +// status output dead (no CFC2 consumes it). The recompiler test environment +// pins vuFlagHack OFF for JIT-vs-interp determinism (a dead-flag skip would +// otherwise diverge from the always-accurate interpreter), so these two +// tests opt back into the production default to cover the gate directly. +// ========================================================================= + +namespace { +struct ScopedFlagHack +{ + bool saved; + explicit ScopedFlagHack(bool on) : saved(EmuConfig.Speedhacks.vuFlagHack) { EmuConfig.Speedhacks.vuFlagHack = on; } + ~ScopedFlagHack() { EmuConfig.Speedhacks.vuFlagHack = saved; } +}; +} // namespace + +TEST(EeVu0Cop2MacroFlagHack, StatusLiveAcrossCfc2NotSkipped) +{ + // VADD result (-4, 0, 4, 5): lane x sets the sign bit, lane y the zero bit. + // The following CFC2 reads VI[REG_STATUS_FLAG], so COP2FlagHackPass marks the + // VADD EEINST_COP2_STATUS_FLAG -> cop2StatusFlagLive() keeps the normalize and + // the JIT-emitted status must match the interpreter. + EeRecTestHarness h; + ScopedFlagHack flagHack(true); + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, -5.0f, 0.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 1.0f, 0.0f, 1.0f, 1.0f); + h.LoadProgram({ + VADD_C2(mask_xyzw, /*fd*/3, /*fs*/1, /*ft*/2), + CFC2(reg::t0, REG_STATUS_FLAG), + }); + h.Run(); // auto-diffs JIT vs interp, including the CFC2-read status in t0 + EXPECT_EQ(h.GetGpr64Jit(reg::t0), h.GetGpr64Interp(reg::t0)); + EXPECT_NE(h.GetGpr64Jit(reg::t0), 0u); // status was actually populated + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), -4.0f); +} + +TEST(EeVu0Cop2MacroFlagHack, StatusDeadStandaloneSkipsButKeepsResult) +{ + // No CFC2 reads the status, so EEINST_COP2_STATUS_FLAG stays clear and the + // denormalize/normalize dance is skipped. The arithmetic must be unaffected. + // Status is dead, so the interpreter (which always updates it) legitimately + // diverges — run the JIT in isolation and assert only the live result. + EeRecTestHarness h; + ScopedFlagHack flagHack(true); + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0Vf(1, -5.0f, 0.0f, 3.0f, 4.0f); + h.SeedVu0Vf(2, 1.0f, 0.0f, 1.0f, 1.0f); + h.LoadProgram({VADD_C2(mask_xyzw, /*fd*/3, /*fs*/1, /*ft*/2)}); + h.RunJitNoDiff(); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'x'), -4.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'y'), 0.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'z'), 4.0f); + EXPECT_FLOAT_EQ(h.GetVu0VfJit(3, 'w'), 5.0f); +} + } // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp index e11f1be76e..87bc17c0af 100644 --- a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp @@ -394,9 +394,19 @@ void EeRecTestHarness::RunJitNoDiff(RunMode mode) recEeExecuteBlock(kCycleBudget, kParkingPc); FPControlRegister::SetCurrent(saved_fpcr); jit_snapshot_ = EeSnapshot::Capture(mem_windows_); + if (capture_vu0_) + vu0_jit_snapshot_ = VuSnapshot::Capture(0, {}); + if (capture_vu1_) + vu1_jit_snapshot_ = VuSnapshot::Capture(1, {}); // Mirror the JIT post-state into the interp snapshot so accessors that read // either side return the JIT value (there is no interp double-mode oracle). + // The VU snapshots must be mirrored too — otherwise GetVu0Vf*/GetVu1Vf* + // read a stale snapshot left by a prior Run()-based test (order-dependent). interp_snapshot_ = jit_snapshot_; + if (capture_vu0_) + vu0_interp_snapshot_ = vu0_jit_snapshot_; + if (capture_vu1_) + vu1_interp_snapshot_ = vu1_jit_snapshot_; has_run_ = true; } diff --git a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp index a343602541..5a4097d59b 100644 --- a/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp +++ b/tests/ctest/core/recompilers/harness/RecompilerTestEnvironment.cpp @@ -164,10 +164,15 @@ bool RecompilerTestEnvironment::Initialize() // Pin VU-related EmuConfig flags off for determinism. THREAD_VU1 forks // VU1 dispatch into a separate thread (parallel-universe code paths); // vu1Instant short-circuits VU1 cycle accounting; XgKickHack alters - // XGKICK cycle accumulation per-game. None of these belong in a unit - // test where determinism is the contract. + // XGKICK cycle accumulation per-game. vuFlagHack (on by default!) lets the + // JIT skip COP2 status-flag updates the interpreter still performs, so the + // JIT-vs-interp diff would see legitimately-dead flag divergences. None of + // these belong in a unit test where determinism is the contract — tests + // that exercise the flaghack path opt in by setting it true locally and + // reading only live values (RunJitNoDiff). EmuConfig.Speedhacks.vuThread = false; EmuConfig.Speedhacks.vu1Instant = false; + EmuConfig.Speedhacks.vuFlagHack = false; EmuConfig.Gamefixes.XgKickHack = false; // 9. Parking lot for test programs' `jr ra` sentinel + EE exception diff --git a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp index 529429aaf8..8238949bbf 100644 --- a/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp +++ b/tests/ctest/core/recompilers/mvu_abi_digest_tests.cpp @@ -40,6 +40,7 @@ #include "VU.h" #include "VUmicro.h" +#include "Config.h" #include "arm64/microVU_Persist-arm64.h" #include "arm64/microVU_ProgCache-arm64.h" @@ -92,6 +93,15 @@ constexpr AbiPin kPins[] = { u64 CompileAndDigest(std::initializer_list pairs) { + // The ABI digest pins the emitted shape that lands in the on-disk cache in + // PRODUCTION, where vuFlagHack defaults on (and the options sentinel keeps + // flaghack-on/off caches separate). The recompiler test environment now pins + // vuFlagHack off for JIT-vs-interp determinism, so force it back on here — + // otherwise the pins would track the non-production flaghack-off shape and + // drift with the harness default rather than with real emitter changes. + const bool savedFlagHack = EmuConfig.Speedhacks.vuFlagHack; + EmuConfig.Speedhacks.vuFlagHack = true; + VuTestHarness h(0); h.SetVf(1, 1.5f, -2.25f, 3.0f, 0.0625f); h.SetVf(2, 4.0f, 0.5f, -1.0f, 8.0f); @@ -102,6 +112,8 @@ u64 CompileAndDigest(std::initializer_list pairs) u64 digest = 0; EXPECT_TRUE(mVUPersist::TestComputeEmitDigest(0, digest)); RecompilerTestEnvironment::ResetVuBlockCache(0); + + EmuConfig.Speedhacks.vuFlagHack = savedFlagHack; return digest; } From f016d603d2ccad3c71661baaa466aa4f1224ec35 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 23 Jun 2026 20:18:41 -0700 Subject: [PATCH 031/292] arm64: register only the program-entry VU block with perf jitdump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the start_pc == startPC guard from x86 microVU_Compile.inl (a5ed24ca8) to mVUcompile. arm64 already registers compiled VU blocks via Perf::vu0/vu1.RegisterPC, but did so unconditionally — every branch-target sub-block compiled at a mid-program PC got its own jitdump symbol. The guard restricts registration to the program-entry compile (start_pc is re-pointed only on the dispatch / indirect-jump path, not for blockFetch sub-blocks), matching x86: one clean symbol per VU program instead of one per linked sub-block. Profiling-only; no effect on emitted code or emulation. 1018 recompiler_tests pass. Co-Authored-By: Claude Opus 4.8 --- pcsx2/arm64/microVU_Compile-arm64.inl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pcsx2/arm64/microVU_Compile-arm64.inl b/pcsx2/arm64/microVU_Compile-arm64.inl index 676f2045bf..62e9820e23 100644 --- a/pcsx2/arm64/microVU_Compile-arm64.inl +++ b/pcsx2/arm64/microVU_Compile-arm64.inl @@ -1178,6 +1178,12 @@ void* mVUcompile(microVU& mVU, u32 startPC, uptr pState) mVUendProgram(mVU, &mFC, 1); perf_and_return: + // Register the program-entry compile only, not every continuation block + // compiled at a mid-program PC (start_pc stays the program entry for + // branch-target sub-blocks; it's only re-pointed on the dispatch/indirect- + // jump path). Mirrors x86 microVU_Compile.inl (a5ed24ca8) — one clean + // jitdump symbol per VU program rather than a symbol per linked sub-block. + if (mVU.regs().start_pc == startPC) { u8* endPtr = armGetCurrentCodePointer(); if (mVU.index) From a187fdc0b62e294f1d386f5a7229850348c4e25d Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Wed, 24 Jun 2026 12:26:14 -0700 Subject: [PATCH 032/292] tools/perf: M2-first CPU profiling rig + runner --perf-jitdump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 0 of the `neither` cherry-pick funnel: a repeatable, attributable, wallclock-anchored CPU bottleneck baseline for our own ARM64 port, since the RK3562-era numbers are stale and the target has shifted to Snapdragon 865. Built and validated on M2 Max / Asahi first; the same scripts run on SD865 with a new devices/