fix build memory blow-up and boot crash by removing AOT register caches

This commit is contained in:
Jessica_Natalia
2026-08-12 19:13:55 -03:00
parent 37e5469cbd
commit 97be32d89e
246 changed files with 810821 additions and 1098850 deletions
+24
View File
@@ -3,6 +3,30 @@
/build/
CMakeUserPresets.json
# Build directories created outside /out/ (per-stage trees use names like b457_perf)
b*_perf/
cmake-build-*/
# Binary build artifacts, wherever they land
*.exe
*.dll
*.lib
*.obj
*.exp
*.iobj
*.ipdb
*.res
*.tlog
# Copyrighted game data. Never publish: the PSP executable, its decrypted ELF
# and save data are the user's own dumped media, not part of this source tree.
PSP_DATA/
SAVEDATA/
*.ELF
*.elf
*.iso
*.cso
# User-owned game data and reverse-engineering workspace
/profiles/*/game/
/profiles/*/analysis/
+17 -1
View File
@@ -30,6 +30,22 @@ if(NOT PSPRECOMP_MSVC_CGTHREADS MATCHES "^[0-9]+$")
message(FATAL_ERROR "PSPRECOMP_MSVC_CGTHREADS must be a non-negative integer")
endif()
# Bare /MP lets cl.exe spawn one front-end per logical core, and MSBuild /m
# multiplies that by the number of projects it builds in parallel. On the VCS
# profile each generated AOT unit costs well over a gigabyte at /Ox /Ob3, so the
# 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)")
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")
else()
set(PSPRECOMP_MSVC_MP_FLAG "/MP${PSPRECOMP_MSVC_MP_JOBS}")
endif()
if(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH)
add_compile_definitions(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH=1)
endif()
@@ -67,7 +83,7 @@ target_include_directories(psprecomp_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/inc
target_compile_definitions(psprecomp_core PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:_CRT_SECURE_NO_WARNINGS>)
if(MSVC)
target_compile_options(psprecomp_core PRIVATE /W4 /permissive- /EHsc /MP
target_compile_options(psprecomp_core PRIVATE /W4 /permissive- /EHsc ${PSPRECOMP_MSVC_MP_FLAG}
$<$<CONFIG:Release>:/Ob3> $<$<CONFIG:RelWithDebInfo>:/Ob3>)
else()
target_compile_options(psprecomp_core PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wshadow)
+21 -61
View File
@@ -40,43 +40,13 @@ extern std::uint64_t g_runtime_thread_switch_generation_fast;
#define PSPRECOMP_RESTRICT
#endif
// Cross-unit hot-register cache. Seven Allegrex GPRs plus six scalar FPRs account
// for the majority of the remaining generated GPR/FPR context traffic after the
// per-basic-block cache. A native direct chain shares one cache
// across generated units and materializes it back to AllegrexContext only at
// scheduler/HLE/outer-dispatch visibility boundaries.
struct AotHotRegisterCache {
std::uint32_t g2{};
std::uint32_t g4{};
std::uint32_t g5{};
std::uint32_t g6{};
std::uint32_t g7{};
std::uint32_t g29{};
std::uint32_t g31{};
float f12{};
float f13{};
float f14{};
float f15{};
float f20{};
float f22{};
AotHotRegisterCache() = default;
explicit PSPRECOMP_RUNTIME_FORCEINLINE AotHotRegisterCache(const AllegrexContext &ctx) noexcept { reload_from(ctx); }
PSPRECOMP_RUNTIME_FORCEINLINE void reload_from(const AllegrexContext &ctx) noexcept {
g2 = ctx.gpr[2]; g4 = ctx.gpr[4]; g5 = ctx.gpr[5]; g6 = ctx.gpr[6];
g7 = ctx.gpr[7]; g29 = ctx.gpr[29]; g31 = ctx.gpr[31];
f12 = ctx.fpr[12]; f13 = ctx.fpr[13]; f14 = ctx.fpr[14];
f15 = ctx.fpr[15]; f20 = ctx.fpr[20]; f22 = ctx.fpr[22];
}
PSPRECOMP_RUNTIME_FORCEINLINE void flush_to(AllegrexContext &ctx) const noexcept {
ctx.gpr[2] = g2; ctx.gpr[4] = g4; ctx.gpr[5] = g5; ctx.gpr[6] = g6;
ctx.gpr[7] = g7; ctx.gpr[29] = g29; ctx.gpr[31] = g31;
ctx.fpr[12] = f12; ctx.fpr[13] = f13; ctx.fpr[14] = f14;
ctx.fpr[15] = f15; ctx.fpr[20] = f20; ctx.fpr[22] = f22;
}
};
// The cross-unit hot-register cache (AotHotRegisterCache) was removed here. It
// kept seven GPRs and six scalar FPRs live in a second object alongside
// AllegrexContext for the whole duration of a generated unit. Inside the
// ~10,000-line single functions this corpus emits, that pushed MSVC's optimizer
// past the point where it converges: affected units never finished compiling and
// grew past 2 GB each, which exhausted system memory during a normal build. The
// last configuration observed booting on hardware (Stage 45.7) does not have it.
struct RuntimeExecutionContextToken {
std::int32_t thread_uid{-1};
std::uint64_t switch_generation{};
@@ -99,7 +69,7 @@ class Runtime {
public:
using RecompiledFunction = void (*)(Runtime &, AllegrexContext &);
using RecompiledEntryFunction = void (*)(Runtime &, AllegrexContext &, std::uint16_t,
GuestMemory::AotFastView &, AotHotRegisterCache &);
GuestMemory::AotFastView &);
using HleFunction = std::function<void(Runtime &, AllegrexContext &)>;
using NativeFastPath = std::function<void(Runtime &, AllegrexContext &)>;
@@ -141,8 +111,7 @@ public:
// Generated profile code can call this API without putting game-specific
// addresses or implementations in the reusable runtime.
void register_native_fast_path(std::uint32_t address, NativeFastPath function);
void invoke_native_fast_path(std::uint32_t address, AllegrexContext &ctx,
AotHotRegisterCache *shared_hot_regs = nullptr);
void invoke_native_fast_path(std::uint32_t address, AllegrexContext &ctx);
// Bounded cross-unit call chaining.
//
@@ -162,15 +131,13 @@ public:
// at the same address. Depth is bounded so guest recursion cannot exhaust
// the native stack. PSPRECOMP_NO_CHAIN=1 disables it for A/B checks.
[[nodiscard]] bool invoke_chained_call(AllegrexContext &ctx,
GuestMemory::AotFastView *shared_aot_mem = nullptr,
AotHotRegisterCache *shared_hot_regs = nullptr);
GuestMemory::AotFastView *shared_aot_mem = nullptr);
// Fast path for compile-time-known cross-unit targets. Automatic AOT knows
// the 16 KiB unit index and can avoid the large guest-PC dispatch table.
// Units containing an import/HLE/host override fall back to the exact
// per-PC chainability path at runtime.
[[nodiscard]] bool invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index,
GuestMemory::AotFastView *shared_aot_mem = nullptr,
AotHotRegisterCache *shared_hot_regs = nullptr);
GuestMemory::AotFastView *shared_aot_mem = nullptr);
// compile-time unit chain. Automatic AOT knows both the target
// function symbol and bucket, so the normal path becomes a direct native
@@ -184,8 +151,7 @@ public:
template <auto Function, std::uint32_t UnitIndex, std::uint16_t DirectEntryId = 0u,
std::uint32_t DirectTargetPc = 0u>
[[nodiscard]] PSPRECOMP_RUNTIME_FORCEINLINE bool invoke_chained_direct(
AllegrexContext &ctx, GuestMemory::AotFastView *shared_aot_mem = nullptr,
AotHotRegisterCache *shared_hot_regs = nullptr) {
AllegrexContext &ctx, GuestMemory::AotFastView *shared_aot_mem = nullptr) {
#if defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
if (UnitIndex >= kGeneratedUnitFastCapacity || !generated_unit_layout_valid_) {
#else
@@ -194,7 +160,7 @@ public:
!generated_unit_layout_valid_) {
#endif
if constexpr (DirectTargetPc != 0u) ctx.pc = DirectTargetPc;
return invoke_chained_unit(ctx, UnitIndex, shared_aot_mem, shared_hot_regs);
return invoke_chained_unit(ctx, UnitIndex, shared_aot_mem);
}
if (generated_unit_disabled_[UnitIndex] != 0u) {
// A unit can be poisoned because it contains PSP import stubs while
@@ -205,7 +171,7 @@ public:
// the mixed bucket. Host/HLE overrides are still non-chainable there.
if constexpr (DirectTargetPc != 0u) {
ctx.pc = DirectTargetPc;
return invoke_chained_call(ctx, shared_aot_mem, shared_hot_regs);
return invoke_chained_call(ctx, shared_aot_mem);
} else {
return false;
}
@@ -237,14 +203,12 @@ public:
} guard(chain_depth_);
if constexpr (DirectEntryId != 0u &&
std::is_invocable_v<decltype(Function), Runtime &, AllegrexContext &, std::uint16_t,
GuestMemory::AotFastView &, AotHotRegisterCache &>) {
if (shared_aot_mem != nullptr && shared_hot_regs != nullptr) {
Function(*this, ctx, DirectEntryId, *shared_aot_mem, *shared_hot_regs);
GuestMemory::AotFastView &>) {
if (shared_aot_mem != nullptr) {
Function(*this, ctx, DirectEntryId, *shared_aot_mem);
} else {
auto local_aot_mem = shared_aot_mem != nullptr ? *shared_aot_mem : memory_.aot_fast_view();
AotHotRegisterCache local_hot_regs(ctx);
Function(*this, ctx, DirectEntryId, local_aot_mem, local_hot_regs);
local_hot_regs.flush_to(ctx);
auto local_aot_mem = memory_.aot_fast_view();
Function(*this, ctx, DirectEntryId, local_aot_mem);
}
} else if constexpr (DirectEntryId != 0u &&
std::is_invocable_v<decltype(Function), Runtime &, AllegrexContext &, std::uint16_t>) {
@@ -271,10 +235,7 @@ public:
const std::uint64_t starvation_interval = g_runtime_starvation_interval_fast;
if (starvation_interval == 0u) return true;
if (++dispatches_since_import_ < starvation_interval) return true;
if (shared_hot_regs != nullptr) shared_hot_regs->flush_to(ctx);
const bool same_context = run_starvation_boundary(ctx);
if (shared_hot_regs != nullptr) shared_hot_regs->reload_from(ctx);
return same_context;
return run_starvation_boundary(ctx);
}
void register_generated_unit(std::uint32_t unit_index, std::uint32_t unit_address,
@@ -315,8 +276,7 @@ private:
[[nodiscard]] const FunctionEntry *lookup_entry(std::uint32_t address) const noexcept;
// Returns false only when the starvation/preemption hook changed the PSP
// execution context at this safe boundary.
[[nodiscard]] bool account_dispatch_work(AllegrexContext &ctx, bool allow_preemption,
AotHotRegisterCache *shared_hot_regs = nullptr);
[[nodiscard]] bool account_dispatch_work(AllegrexContext &ctx, bool allow_preemption);
// Called only once per configured scheduler interval by the header-inline
// direct-chain fast path. Keeping hook/context-token work here leaves the
// other ~4095 boundaries as a counter increment + predictable compare.
+32 -4
View File
@@ -17,6 +17,25 @@ 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.
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()
file(GLOB VCS_GENERATED CONFIGURE_DEPENDS "${VCS_PROFILE_DIR}/generated/*.cpp")
if(MSVC)
if(PSPRECOMP_GENERATED_OPT_LEVEL STREQUAL "0")
@@ -27,7 +46,7 @@ if(MSVC)
set(VCS_GENERATED_MSVC_OPT "/O${PSPRECOMP_GENERATED_OPT_LEVEL}")
endif()
set(VCS_GENERATED_MSVC_OPTIONS
"${VCS_GENERATED_MSVC_OPT};/Ob3;/bigobj;/MP")
"${VCS_GENERATED_MSVC_OPT};/Ob${PSPRECOMP_GENERATED_INLINE_LEVEL};/bigobj;${PSPRECOMP_MSVC_MP_FLAG}")
if(PSPRECOMP_LTO AND NOT PSPRECOMP_VCS_AOT_LTO)
# CMAKE_INTERPROCEDURAL_OPTIMIZATION adds /GL at target scope. /GL- on
# these sources overrides it so the linker receives native .obj code
@@ -61,7 +80,8 @@ if(PSPRECOMP_PROFILE_GUIDED_AOT)
else()
set(VCS_HOT_MSVC_OPT "/O${PSPRECOMP_HOT_GENERATED_OPT_LEVEL}")
endif()
set(VCS_HOT_MSVC_OPTIONS "${VCS_HOT_MSVC_OPT};/Ob3;/bigobj;/MP")
set(VCS_HOT_MSVC_OPTIONS
"${VCS_HOT_MSVC_OPT};/Ob${PSPRECOMP_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()
@@ -152,8 +172,16 @@ add_executable(VCSNative
vcs_target_common(VCSNative)
vcs_set_runtime_output(VCSNative)
if(MSVC)
target_compile_options(VCSNative PRIVATE /MP
$<$<CONFIG:Release>:/Ob3> $<$<CONFIG:RelWithDebInfo>:/Ob3>)
# /Ob3 must stay off the target: target_compile_options lands in the vcxproj
# AdditionalOptions, which cl.exe sees *after* the per-source
# InlineFunctionExpansion, so a target-wide /Ob3 silently overrides the
# /Ob${PSPRECOMP_GENERATED_INLINE_LEVEL} set on the generated corpus above
# (MSVC reports this as "D9025: overriding '/Ob0' with '/Ob3'"). Apply it to
# the host sources only.
target_compile_options(VCSNative PRIVATE ${PSPRECOMP_MSVC_MP_FLAG})
set_source_files_properties(host/main.cpp ${VCS_HOST_SOURCES}
DIRECTORY "${VCS_PROFILE_DIR}" PROPERTIES COMPILE_OPTIONS
"$<$<CONFIG:Release>:/Ob3>;$<$<CONFIG:RelWithDebInfo>:/Ob3>")
target_link_options(VCSNative PRIVATE /STACK:67108864)
if(PSPRECOMP_LTO)
# Make the otherwise silent LTCG phase visible in the console.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More