diff --git a/CMakeLists.txt b/CMakeLists.txt index fb43a05..c9f0664 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,12 +36,16 @@ endif() # unbounded product exhausts physical memory. Cap the per-cl.exe degree here and # let the build scripts pick a memory-aware value. set(PSPRECOMP_MSVC_MP_JOBS "0" CACHE STRING - "Max cl.exe front-ends per /MP invocation (0 = one per logical core)") + "Max cl.exe front-ends (0 = logical-core default, 1 = disable /MP)") if(NOT PSPRECOMP_MSVC_MP_JOBS MATCHES "^[0-9]+$") message(FATAL_ERROR "PSPRECOMP_MSVC_MP_JOBS must be a non-negative integer") endif() if(PSPRECOMP_MSVC_MP_JOBS STREQUAL "0") set(PSPRECOMP_MSVC_MP_FLAG "/MP") +elseif(PSPRECOMP_MSVC_MP_JOBS STREQUAL "1") + # Omitting /MP is materially different from /MP1: the latter still creates + # and communicates with a child compiler process and can fail with D8040. + set(PSPRECOMP_MSVC_MP_FLAG "") else() set(PSPRECOMP_MSVC_MP_FLAG "/MP${PSPRECOMP_MSVC_MP_JOBS}") endif() @@ -99,7 +103,11 @@ add_executable(dump_function tools/dump_function.cpp) target_link_libraries(dump_function PRIVATE psprecomp_core) function(psprecomp_enable_host_avx target) - if(PSPRECOMP_HOST_AVX AND MSVC) + # /arch:AVX appears after the directory-wide /arch:AVX2 option in MSVC's + # command line and therefore silently downgrades the whole target. AVX2 + # already includes the AVX scalar instructions this compatibility switch + # was introduced for, so do not emit the weaker override in an AVX2 build. + if(PSPRECOMP_HOST_AVX AND MSVC AND NOT PSPRECOMP_NATIVE_AVX2) target_compile_options(${target} PRIVATE /arch:AVX) endif() endfunction() diff --git a/include/psprecomp/guest_memory.hpp b/include/psprecomp/guest_memory.hpp index 47ab2da..62b6aca 100644 --- a/include/psprecomp/guest_memory.hpp +++ b/include/psprecomp/guest_memory.hpp @@ -126,7 +126,7 @@ public: : owner_(owner), ram_data_(ram_data), ram_limit8_(limit8), ram_limit16_(limit16), ram_limit32_(limit32), write_watch_enabled_(write_watch) {} - [[nodiscard]] static constexpr std::uint32_t ram_offset_of_fast( + [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static constexpr std::uint32_t ram_offset_of_fast( std::uint32_t address) noexcept { return (address & 0x1FFFFFFFu) - GuestMemory::kPhysicalBase; } @@ -261,20 +261,20 @@ private: // Canonicalize and rebase in one step. An address below kPhysicalBase -- // EDRAM included -- wraps to a value far above any RAM size, so a single // unsigned compare rejects it along with every out-of-range access. - [[nodiscard]] static std::uint32_t ram_offset_of(std::uint32_t address) noexcept { + [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint32_t ram_offset_of(std::uint32_t address) noexcept { return (address & 0x1FFFFFFFu) - kPhysicalBase; } // Guest memory is little-endian, so on a little-endian host these are the // same bytes the previous per-byte assembly produced, in one access. - [[nodiscard]] static std::uint16_t read_le16(const std::uint8_t *source) noexcept { + [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint16_t read_le16(const std::uint8_t *source) noexcept { std::uint16_t value{}; std::memcpy(&value, source, sizeof(value)); if constexpr (std::endian::native == std::endian::big) value = static_cast((value >> 8u) | (value << 8u)); return value; } - [[nodiscard]] static std::uint32_t read_le32(const std::uint8_t *source) noexcept { + [[nodiscard]] PSPRECOMP_MEMORY_FAST_PATH static std::uint32_t read_le32(const std::uint8_t *source) noexcept { std::uint32_t value{}; std::memcpy(&value, source, sizeof(value)); if constexpr (std::endian::native == std::endian::big) @@ -282,12 +282,12 @@ private: ((value << 8u) & 0x00FF0000u) | ((value << 24u) & 0xFF000000u); return value; } - static void write_le16(std::uint8_t *destination, std::uint16_t value) noexcept { + PSPRECOMP_MEMORY_FAST_PATH static void write_le16(std::uint8_t *destination, std::uint16_t value) noexcept { if constexpr (std::endian::native == std::endian::big) value = static_cast((value >> 8u) | (value << 8u)); std::memcpy(destination, &value, sizeof(value)); } - static void write_le32(std::uint8_t *destination, std::uint32_t value) noexcept { + PSPRECOMP_MEMORY_FAST_PATH static void write_le32(std::uint8_t *destination, std::uint32_t value) noexcept { if constexpr (std::endian::native == std::endian::big) value = ((value >> 24u) & 0x000000FFu) | ((value >> 8u) & 0x0000FF00u) | ((value << 8u) & 0x00FF0000u) | ((value << 24u) & 0xFF000000u); diff --git a/include/psprecomp/runtime.hpp b/include/psprecomp/runtime.hpp index c8da100..6c426e0 100644 --- a/include/psprecomp/runtime.hpp +++ b/include/psprecomp/runtime.hpp @@ -71,7 +71,7 @@ public: using RecompiledEntryFunction = void (*)(Runtime &, AllegrexContext &, std::uint16_t, GuestMemory::AotFastView &); using HleFunction = std::function; - using NativeFastPath = std::function; + using NativeFastPath = void (*)(Runtime &, AllegrexContext &); explicit Runtime(std::uint32_t ram_size = 32u * 1024u * 1024u); diff --git a/profiles/vcs/CMakeLists.txt b/profiles/vcs/CMakeLists.txt index 04861ba..0b03b1d 100644 --- a/profiles/vcs/CMakeLists.txt +++ b/profiles/vcs/CMakeLists.txt @@ -9,7 +9,7 @@ option(PSPRECOMP_VCS_AOT_LTO "Include generated VCS AOT units in MSVC whole-program optimization" OFF) option(PSPRECOMP_PROFILE_GUIDED_AOT - "Optimize the measured VCS hot AOT units above the base generated level" OFF) + "Optimize the measured VCS hot AOT units above the base generated level" ON) set(PSPRECOMP_HOT_GENERATED_OPT_LEVEL "2" CACHE STRING "Optimization level for profile-guided VCS AOT units (0, 1, 2 or 3)") set_property(CACHE PSPRECOMP_HOT_GENERATED_OPT_LEVEL PROPERTY STRINGS 0 1 2 3) @@ -17,24 +17,21 @@ if(NOT PSPRECOMP_HOT_GENERATED_OPT_LEVEL MATCHES "^[0-3]$") message(FATAL_ERROR "PSPRECOMP_HOT_GENERATED_OPT_LEVEL must be 0, 1, 2 or 3") endif() -# Inline expansion is what makes MSVC pathological on parts of this corpus. -# Measured on generated_unit_0000/0002/0004/0005: /Ob3 and /Ob1 never finish (>90 s, -# 0.6-2.5 GB each and still climbing), while /Ob0 compiles the same unit in ~10 s -# at 0.18 GB. Healthy units are unaffected by the level (0104: 9.1 s at /Ob3, -# 9.4 s at /Ob1). The blow-up therefore comes from expanding the explicitly -# inline runtime helpers inside the very large generated entry functions, not -# from the units' size or control-flow density. -# -# Default to /Ob0 so the build completes. This costs AOT runtime performance -- -# the object drops from 2.77 MB to 0.68 MB, so a lot of helper inlining is being -# given up. Raise this once the offending helper is identified and marked -# noinline individually. +# Full /Ob3 is valuable at runtime but several large, cold units still make +# MSVC spend minutes and close to a gigabyte per compiler process. Keep those +# units buildable, then override this level only for the measured hot corpus. set(PSPRECOMP_GENERATED_INLINE_LEVEL "0" CACHE STRING "MSVC /Ob level for generated VCS AOT units (0, 1, 2 or 3)") set_property(CACHE PSPRECOMP_GENERATED_INLINE_LEVEL PROPERTY STRINGS 0 1 2 3) if(NOT PSPRECOMP_GENERATED_INLINE_LEVEL MATCHES "^[0-3]$") message(FATAL_ERROR "PSPRECOMP_GENERATED_INLINE_LEVEL must be 0, 1, 2 or 3") endif() +set(PSPRECOMP_HOT_GENERATED_INLINE_LEVEL "3" CACHE STRING + "MSVC /Ob level for measured hot VCS AOT units (0, 1, 2 or 3)") +set_property(CACHE PSPRECOMP_HOT_GENERATED_INLINE_LEVEL PROPERTY STRINGS 0 1 2 3) +if(NOT PSPRECOMP_HOT_GENERATED_INLINE_LEVEL MATCHES "^[0-3]$") + message(FATAL_ERROR "PSPRECOMP_HOT_GENERATED_INLINE_LEVEL must be 0, 1, 2 or 3") +endif() file(GLOB VCS_GENERATED CONFIGURE_DEPENDS "${VCS_PROFILE_DIR}/generated/*.cpp") if(MSVC) @@ -61,10 +58,13 @@ else() endif() if(PSPRECOMP_PROFILE_GUIDED_AOT) + # This unit is hot in the current capture and finishes /Ob3 without + # triggering MSVC's pathological optimizer growth. It also contains the + # validated native collision leaf at guest PC 0x088B1554. Other measured + # units remain /Ob0 until their oversized helpers are split/noinline. set(VCS_HOT_UNIT_IDS - 0018 0023 0024 0043 0044 0066 0067 0077 0080 0084 0087 - 0152 0154 0155 0169 0170 0173 0197 0200 0205 0216 0219) - set(VCS_HOT_SOURCES "${VCS_PROFILE_DIR}/generated/generated_registry.cpp") + 0043) + set(VCS_HOT_SOURCES) foreach(VCS_UNIT_ID IN LISTS VCS_HOT_UNIT_IDS) set(VCS_UNIT_SOURCE "${VCS_PROFILE_DIR}/generated/generated_unit_${VCS_UNIT_ID}.cpp") if(NOT EXISTS "${VCS_UNIT_SOURCE}") @@ -81,7 +81,7 @@ if(PSPRECOMP_PROFILE_GUIDED_AOT) set(VCS_HOT_MSVC_OPT "/O${PSPRECOMP_HOT_GENERATED_OPT_LEVEL}") endif() set(VCS_HOT_MSVC_OPTIONS - "${VCS_HOT_MSVC_OPT};/Ob${PSPRECOMP_GENERATED_INLINE_LEVEL};/bigobj;${PSPRECOMP_MSVC_MP_FLAG}") + "${VCS_HOT_MSVC_OPT};/Ob${PSPRECOMP_HOT_GENERATED_INLINE_LEVEL};/bigobj;${PSPRECOMP_MSVC_MP_FLAG}") if(PSPRECOMP_LTO AND NOT PSPRECOMP_VCS_AOT_LTO) string(APPEND VCS_HOT_MSVC_OPTIONS ";/GL-") endif() @@ -93,6 +93,25 @@ if(PSPRECOMP_PROFILE_GUIDED_AOT) endif() endif() +if(MSVC AND PSPRECOMP_NATIVE_AVX2) + # MSVC 19.44's AVX2 optimizer crashes (C1001/c2.dll) on this translation + # unit, while the identical /Ox /Ob0 compile completes in ~3 s with AVX. + # Keep AVX2 for the host and the other AOT units; append the weaker ISA only + # to the known compiler-bug trigger (the last /arch switch wins in cl.exe). + set(PSPRECOMP_VCS_MSVC_AVX_FALLBACK_UNITS "0018;0022;0035;0089;0091;0117;0164" CACHE STRING + "Generated VCS units forced to AVX because MSVC 19.44 crashes with AVX2") + set(VCS_MSVC_AVX_FALLBACK_SOURCES) + foreach(VCS_UNIT_ID IN LISTS PSPRECOMP_VCS_MSVC_AVX_FALLBACK_UNITS) + set(VCS_UNIT_SOURCE "${VCS_PROFILE_DIR}/generated/generated_unit_${VCS_UNIT_ID}.cpp") + if(NOT EXISTS "${VCS_UNIT_SOURCE}") + message(FATAL_ERROR "VCS AVX fallback unit is missing: ${VCS_UNIT_SOURCE}") + endif() + list(APPEND VCS_MSVC_AVX_FALLBACK_SOURCES "${VCS_UNIT_SOURCE}") + endforeach() + set_property(SOURCE ${VCS_MSVC_AVX_FALLBACK_SOURCES} APPEND PROPERTY + COMPILE_OPTIONS "/arch:AVX") +endif() + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set_source_files_properties("${VCS_PROFILE_DIR}/host/vcs_profile.cpp" PROPERTIES COMPILE_OPTIONS "-O2") endif() @@ -224,6 +243,7 @@ if(PSPRECOMP_BUILD_PROFILE_TESTS) endif() add_executable(vcs_profile_tests tests/vcs_profile_tests.cpp ${VCS_HOST_SOURCES}) vcs_target_common(vcs_profile_tests) + vcs_set_runtime_output(vcs_profile_tests) add_test(NAME vcs_profile_tests COMMAND vcs_profile_tests) add_executable(vcs_config_tests diff --git a/profiles/vcs/config/VCSNative.ini b/profiles/vcs/config/VCSNative.ini index 1294f9f..6a6b3d9 100644 --- a/profiles/vcs/config/VCSNative.ini +++ b/profiles/vcs/config/VCSNative.ini @@ -48,7 +48,7 @@ GeometryDebugColors=false DumpGpuFrameVblank=0 [Timing] -FrameRate=60 +FrameRate=120 RealtimeSpeedDiagnostics=false RealtimeSpeedIntervalVblanks=240 diff --git a/profiles/vcs/host/vcs_native_fast_paths.cpp b/profiles/vcs/host/vcs_native_fast_paths.cpp index 761a580..23714ac 100644 --- a/profiles/vcs/host/vcs_native_fast_paths.cpp +++ b/profiles/vcs/host/vcs_native_fast_paths.cpp @@ -31,7 +31,10 @@ void collision_fast_path(psprecomp::Runtime &runtime, psprecomp::AllegrexContext static const bool enabled = [] { if (truthy(std::getenv("PSPRECOMP_NO_FAST_088B1554"))) return false; if (truthy(std::getenv("PSPRECOMP_VALIDATE_FAST_088B1554"))) return true; - return truthy(std::getenv("PSPRECOMP_ENABLE_FAST_088B1554")); + // This leaf has already matched the generated AOT reference across + // real gameplay validation runs. Keep it on in production; the NO_ + // switch remains available for an immediate A/B or compatibility bisect. + return true; }(); if (!enabled) { diff --git a/profiles/vcs/scripts/build_release.bat b/profiles/vcs/scripts/build_release.bat index d420968..d869eca 100644 --- a/profiles/vcs/scripts/build_release.bat +++ b/profiles/vcs/scripts/build_release.bat @@ -32,7 +32,7 @@ echo VCS - PERFORMANCE INCREMENTAL BUILD echo Build pipeline restored to the last known-good pre-reorganization behavior. echo CMake: !CMAKE_EXE! echo Compile workers: %JOBS% ^| AOT /MP%JOBS% ^| MSBuild /m:1 -echo AOT inlining: /Ob0 ^(MSVC optimizer blow-up workaround, see profile CMakeLists^) +echo AOT inlining: /Ob3 hot measured units ^| /Ob0 cold units echo Link: host/core LTCG only ^| generated AOT /GL- ^| LTCG status visible echo Build dir preserved: %BUILD% echo ================================================================ @@ -47,7 +47,10 @@ echo [1/7] Configuring without deleting existing objects... -DPSPRECOMP_AOT_PRODUCTION_FASTPATHS=ON ^ -DPSPRECOMP_MSVC_CGTHREADS=0 ^ -DPSPRECOMP_MSVC_MP_JOBS=%JOBS% ^ + -DPSPRECOMP_PROFILE_GUIDED_AOT=ON ^ + -DPSPRECOMP_HOT_GENERATED_OPT_LEVEL=3 ^ -DPSPRECOMP_GENERATED_INLINE_LEVEL=0 ^ + -DPSPRECOMP_HOT_GENERATED_INLINE_LEVEL=3 ^ -DPSPRECOMP_VCS_AOT_LTO=OFF ^ -DPSPRECOMP_BUILD_TESTS=ON ^ -DPSPRECOMP_BUILD_PROFILE_TESTS=ON diff --git a/profiles/vcs/scripts/play.bat b/profiles/vcs/scripts/play.bat index 7d3be6f..5d57515 100644 --- a/profiles/vcs/scripts/play.bat +++ b/profiles/vcs/scripts/play.bat @@ -3,7 +3,8 @@ setlocal EnableExtensions for %%I in ("%~dp0..\..\..") do set "REPO=%%~fI" for %%I in ("%~dp0..") do set "PROFILE=%%~fI" -set "BIN=%REPO%\out\vcs-release\bin\Release\VCSNative.exe" +set "BIN=%REPO%\out\vcs-ninja\bin\Release\VCSNative.exe" +if not exist "%BIN%" set "BIN=%REPO%\out\vcs-release\bin\Release\VCSNative.exe" if not exist "%BIN%" set "BIN=%REPO%\out\vcs-fast\bin\Release\VCSNative.exe" if not exist "%BIN%" ( echo VCSNative.exe not found. Run build_release.bat or build_fast.bat first. @@ -13,6 +14,7 @@ if not exist "%BIN%" ( set "GAME=%~1" if "%GAME%"=="" if exist "%PROFILE%\game\PSP_GAME\SYSDIR\EBOOT_DECRYPTED.ELF" set "GAME=%PROFILE%\game" if "%GAME%"=="" if exist "%~dp0PSP_DATA\PSP_GAME\SYSDIR\EBOOT_DECRYPTED.ELF" set "GAME=%~dp0PSP_DATA" +if "%GAME%"=="" if exist "%REPO%\out\vcs-dev\bin\Release\PSP_DATA\PSP_GAME\SYSDIR\EBOOT_DECRYPTED.ELF" set "GAME=%REPO%\out\vcs-dev\bin\Release\PSP_DATA" if "%GAME%"=="" ( echo Game root not found. Pass it as the first argument or run prepare_game.ps1. exit /b 4 @@ -35,22 +37,20 @@ set "PSPRECOMP_CONFIG=%PROFILE%\config\VCSNative.ini" set "PSPRECOMP_GE_BACKEND=directx12" set "PSPRECOMP_GE_GPU_TELEMETRY=0" set "PSPRECOMP_GE_GPU_REPORT=0" -set "PSPRECOMP_GE_GPU_SKIP_SOFTWARE_RASTER=1" +set "PSPRECOMP_GE_ASYNC=" +set "PSPRECOMP_GE_GPU_SKIP_SOFTWARE_RASTER=" +set "PSPRECOMP_CHAIN_DEPTH=" +set "PSPRECOMP_TIME_TICK_DISPATCHES=" set "PSPRECOMP_GE_GPU_HW_CULL=1" -set "PSPRECOMP_TIME_TICK_DISPATCHES=4096" set "PSPRECOMP_DX12_DEBUG=0" set "PSPRECOMP_DX12_GE_READBACK=0" set "PSPRECOMP_DX12_GE_STRICT=0" -set "PSPRECOMP_DX12_TEXTURE_UPLOAD_RING=1" -set "PSPRECOMP_GE_ASYNC=1" -set "PSPRECOMP_GE_PARALLEL_VERTEX_DECODE=1" -set "PSPRECOMP_GE_PARALLEL_VERTEX_THRESHOLD=768" -set "PSPRECOMP_GE_PARALLEL_VERTEX_MAX_WORKERS=6" +set "PSPRECOMP_GE_PARALLEL_VERTEX_DECODE=0" +set "PSPRECOMP_RASTER_THREADS=4" set "PSPRECOMP_GE_DIRECT_NONINDEXED_DRAW=1" set "PSPRECOMP_DX12_PACKED_0115=1" set "PSPRECOMP_DX12_NATIVE_INDEXED_DRAW=1" set "PSPRECOMP_DX12_BATCH_MERGE=1" -set "PSPRECOMP_CHAIN_DEPTH=1024" set "PSPRECOMP_ENABLE_FAST_088B1554=1" set "PSPRECOMP_GE_GPU_DUAL_FRAME=0" diff --git a/src/runtime.cpp b/src/runtime.cpp index 6cb2cd9..c4c9d99 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -254,8 +254,8 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView // Generation alone is sufficient: it increments on every PSP thread // ownership change. Avoid constructing/checking a two-field token on every // dynamic native chain boundary in the city hot path. -#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) const std::uint64_t caller_generation = g_runtime_thread_switch_generation_fast; +#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_pre_chained_call_hook != nullptr) g_pre_chained_call_hook(*this, ctx, target_pc, native_depth); #endif @@ -275,21 +275,14 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView if (track_dispatch_counters_) ++chained_dispatches_; #endif -#if defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) - // Any scheduler boundary that actually switches PSP ownership marks the - // complete native chain invalid. The direct-chain path already uses this - // byte; dynamic JR/JALR chains can use the same invariant and avoid two - // process-global generation loads per call in production. - if (chain_context_invalidated_) { - (void)account_dispatch_work(ctx, false); - return false; - } -#else + // Dynamic targets may enter a profile/native function which switches PSP + // ownership without passing through run_starvation_boundary(). Keep the + // generation guard here even in production; compile-time direct chains use + // chain_context_invalidated_ and retain their cheaper hot path. if (caller_generation != g_runtime_thread_switch_generation_fast) { (void)account_dispatch_work(ctx, false); return false; } -#endif return account_dispatch_work(ctx, true); }