diff --git a/include/psprecomp/runtime.hpp b/include/psprecomp/runtime.hpp index ca673d9..a17a7fe 100644 --- a/include/psprecomp/runtime.hpp +++ b/include/psprecomp/runtime.hpp @@ -77,6 +77,45 @@ extern bool g_unit_profile_enabled; extern std::uint64_t g_unit_profile_counts[kUnitProfileCapacity]; void report_unit_profile(std::size_t limit = 40u); +// Low-overhead guest/AOT hotspot sampler used by the VCS performance build. +// Unit entry counts are exact while wall-clock timing is sampled sparsely. +// Sampled durations are inclusive of nested native AOT calls; this is +// deliberate, because the next Tier-2 pass needs to identify expensive trace +// roots before instrumenting individual basic blocks in only those units. +inline constexpr std::size_t kGuestHotspotPcCapacity = 4096u; +extern bool g_guest_hotspot_profile_enabled; +extern std::uint32_t g_guest_hotspot_sample_mask; +extern std::uint64_t g_guest_hotspot_unit_calls[kUnitProfileCapacity]; + +struct GuestHotspotUnitEntry { + std::uint32_t unit{}; + std::uint64_t calls{}; + std::uint64_t samples{}; + std::uint64_t inclusive_sample_ns{}; +}; + +struct GuestHotspotPcEntry { + std::uint32_t unit{}; + std::uint32_t pc{}; + std::uint64_t samples{}; + std::uint64_t inclusive_sample_ns{}; +}; + +struct GuestHotspotSnapshot { + std::uint32_t sample_stride{1u}; + std::uint64_t total_unit_calls{}; + std::uint64_t total_samples{}; + std::vector units; + std::vector pcs; +}; + +void set_guest_hotspot_profile(bool enabled, std::uint32_t sample_shift = 8u) noexcept; +[[nodiscard]] std::uint64_t guest_hotspot_clock_ns() noexcept; +void guest_hotspot_record_sample(std::uint32_t unit, std::uint32_t pc, + std::uint64_t elapsed_ns) noexcept; +[[nodiscard]] GuestHotspotSnapshot consume_guest_hotspot_profile( + std::size_t unit_limit = 16u, std::size_t pc_limit = 24u); + class Runtime { public: using RecompiledFunction = void (*)(Runtime &, AllegrexContext &); @@ -208,6 +247,17 @@ public: // invalidation flag. All active direct ancestors see that same hot // byte and unwind. This replaces two process-global 64-bit loads on // every fixed cross-unit transfer with one normally-false local load. + bool guest_hotspot_sample = false; + std::uint64_t guest_hotspot_start_ns = 0u; + if (g_guest_hotspot_profile_enabled) { + if constexpr (UnitIndex < kUnitProfileCapacity) + ++g_guest_hotspot_unit_calls[UnitIndex]; + const std::uint64_t ticket = ++guest_hotspot_ticket_; + guest_hotspot_sample = + (ticket & static_cast(g_guest_hotspot_sample_mask)) == 0u; + if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns(); + } + struct DepthGuard { std::uint32_t &depth; explicit DepthGuard(std::uint32_t &value) : depth(value) { ++depth; } @@ -228,6 +278,12 @@ public: } else { Function(*this, ctx); } + if (guest_hotspot_sample) { + const std::uint64_t end_ns = guest_hotspot_clock_ns(); + guest_hotspot_record_sample(UnitIndex, DirectTargetPc, + end_ns >= guest_hotspot_start_ns + ? end_ns - guest_hotspot_start_ns : 0u); + } if (g_unit_profile_enabled) ++g_unit_profile_counts[UnitIndex]; #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (track_dispatch_counters_) { @@ -251,6 +307,72 @@ public: return run_starvation_boundary(ctx); } + // Tier-2 hot-leaf lowering keeps the scheduler accounting that an ordinary + // cross-unit generated call would have performed, while allowing trivial + // leaf accessors to be emitted directly in their measured caller. No HLE + // or PSP ownership switch can occur inside those leaf bodies, so only the + // starvation safe-point cadence needs to be preserved here. + [[nodiscard]] PSPRECOMP_RUNTIME_FORCEINLINE bool account_inlined_generated_leaf( + AllegrexContext &ctx) { + 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; + return run_starvation_boundary(ctx); + } + + // Tier-2 profile-guided superblocks can fuse a cross-unit edge into a local + // C++ goto. The guest-visible control flow is unchanged, but the ordinary + // invoke_chained_direct() native frame no longer exists. These helpers keep + // the two pieces of runtime state owned by that removed frame exact: + // chain-depth limiting and execution-driven scheduler accounting. + // + // Enter is intentionally separate from completion. A fused J/JAL may run + // through many local blocks before the logical chain frame unwinds, matching + // the old nested native-call behavior rather than moving starvation safe + // points into the middle of the guest trace. + template + [[nodiscard]] PSPRECOMP_RUNTIME_FORCEINLINE bool tier2_enter_fused_transfer( + AllegrexContext &ctx) { + if (chain_depth_ >= chain_depth_limit_) { + ctx.pc = TargetPc; + return false; + } + ++chain_depth_; + if (g_unit_profile_enabled && UnitIndex < kUnitProfileCapacity) + ++g_unit_profile_counts[UnitIndex]; + if (g_guest_hotspot_profile_enabled && UnitIndex < kUnitProfileCapacity) + ++g_guest_hotspot_unit_calls[UnitIndex]; + return true; + } + + // Complete N logical invoke_chained_direct() frames in unwind order. This + // deliberately performs scheduler accounting once per removed frame. After + // the first PSP context switch, remaining ancestors mirror the normal direct + // chain path: they advance dispatch work without running another scheduler + // boundary and unwind immediately. + [[nodiscard]] PSPRECOMP_RUNTIME_FORCEINLINE bool tier2_complete_fused_transfers( + AllegrexContext &ctx, std::uint32_t count) { + bool same_context = true; + while (count-- != 0u) { +#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) + if (track_dispatch_counters_) { + ++chained_dispatches_; + ++dispatch_work_count_; + } +#endif + const std::uint64_t interval = g_runtime_starvation_interval_fast; + if (chain_context_invalidated_) { + if (interval != 0u) ++dispatches_since_import_; + same_context = false; + } else if (interval != 0u) { + if (++dispatches_since_import_ >= interval && !run_starvation_boundary(ctx)) + same_context = false; + } + if (chain_depth_ != 0u) --chain_depth_; + } + return same_context; + } + void register_generated_unit(std::uint32_t unit_index, std::uint32_t unit_address, std::uint32_t unit_span, RecompiledFunction function, RecompiledEntryFunction entry_function = nullptr); @@ -327,6 +449,9 @@ private: // ownership while native AOT frames may still be nested. Cleared at the // beginning of each outer Runtime dispatch. bool chain_context_invalidated_{}; + // Per-runtime sampler ticket. Only touched when hotspot profiling is on; + // keeping it local avoids contending on a process-global counter. + std::uint64_t guest_hotspot_ticket_{}; std::uint64_t dispatches_since_import_{}; std::uint64_t chained_dispatches_{}; std::uint64_t dispatch_work_count_{}; diff --git a/profiles/vcs/BUILD_VCS_NINJA.bat b/profiles/vcs/BUILD_VCS_NINJA.bat index ff38d6c..d956f5a 100644 --- a/profiles/vcs/BUILD_VCS_NINJA.bat +++ b/profiles/vcs/BUILD_VCS_NINJA.bat @@ -4,6 +4,17 @@ for %%I in ("%~dp0..\..") do set "REPO=%%~fI" set "BUILD=%REPO%\out\vcs-release-ninja" set "MARKER95=%BUILD%\.v9_5_host_texture_objects_done" set "MARKER96=%BUILD%\.v9_6_savedata_cancel_objects_done" +set "MARKERSAFE=%BUILD%\.tier2_hotspot_hotfix_safe_done" + +if exist "%BUILD%" if not exist "%MARKERSAFE%" ( + echo [0c-safe] HOTSPOT OPT1 rollback - invalidating only affected objects once... + for %%U in (0154 0155 0085 0158 0084 0157 0129 0044 0043 0086 0179 0152 0035 0023) do ( + for /r "%BUILD%" %%F in (generated_unit_%%U.cpp.obj) do if exist "%%F" del /f /q "%%F" >nul 2>nul + ) + for %%N in (main.cpp.obj vcs_config.cpp.obj vcs_profile.cpp.obj vcs_runtime_log.cpp.obj) do ( + for /r "%BUILD%" %%F in (%%N) do if exist "%%F" del /f /q "%%F" >nul 2>nul + ) +) call "%~dp0FORCE_V9_5_HOST_TEXTURE_OBJECTS.bat" if errorlevel 1 exit /b %errorlevel% @@ -16,5 +27,6 @@ if "%RC%"=="0" ( if not exist "%BUILD%" mkdir "%BUILD%" >nul 2>nul >"%MARKER95%" echo V9.5 host-texture isolation successfully rebuilt critical objects. >"%MARKER96%" echo V9.6 mode-aware savedata cancellation successfully rebuilt vcs_profile.cpp. + >"%MARKERSAFE%" echo HOTSPOT OPT1 semantic rollback rebuilt successfully. ) exit /b %RC% diff --git a/profiles/vcs/CMakeLists.txt b/profiles/vcs/CMakeLists.txt index 6f86e6b..fa99dd0 100644 --- a/profiles/vcs/CMakeLists.txt +++ b/profiles/vcs/CMakeLists.txt @@ -151,6 +151,18 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set_source_files_properties("${VCS_PROFILE_DIR}/host/vcs_profile.cpp" PROPERTIES COMPILE_OPTIONS "-O2") endif() +# Tier-2 V1 is deliberately a compact hot-region superblock (~1.1k lines), +# not a full-unit mega-TU. Give this host-side trace normal aggressive host +# optimization while keeping the 234 generated AOT units on their measured +# per-source policy above. +if(MSVC) + set_source_files_properties("${VCS_PROFILE_DIR}/host/vcs_tier2_superblocks.cpp" PROPERTIES + COMPILE_OPTIONS "/O2;/Ob3;/bigobj") +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set_source_files_properties("${VCS_PROFILE_DIR}/host/vcs_tier2_superblocks.cpp" PROPERTIES + COMPILE_OPTIONS "-O3;-g0") +endif() + set(VCS_GPU_BACKEND_SOURCE host/ge_gpu_backend_dx12.cpp) set(VCS_HDR_POST_SOURCE host/vcs_hdr_post_dx12_stub.cpp) set(VCS_HOST_SOURCES @@ -222,6 +234,7 @@ endif() add_executable(VCSNative ${VCS_APP_RESOURCES} host/main.cpp + host/vcs_tier2_superblocks.cpp ${VCS_HOST_SOURCES} ${VCS_GENERATED} ) @@ -235,7 +248,7 @@ if(MSVC) # (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} + set_source_files_properties(host/main.cpp host/vcs_tier2_superblocks.cpp ${VCS_HOST_SOURCES} DIRECTORY "${VCS_PROFILE_DIR}" PROPERTIES COMPILE_OPTIONS "$<$:/Ob3>;$<$:/Ob3>") target_link_options(VCSNative PRIVATE /STACK:67108864) @@ -311,7 +324,7 @@ if(PSPRECOMP_BUILD_PROFILE_TESTS) set_property(TARGET vfpu_tier2_tests PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO FALSE) set_property(TARGET vfpu_tier2_tests PROPERTY INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL FALSE) if(MSVC) - target_compile_options(vfpu_tier2_tests PRIVATE /GL-) + target_compile_options(vfpu_tier2_tests PRIVATE /Od /Ob0 /GL-) endif() add_test(NAME vfpu_tier2_tests COMMAND vfpu_tier2_tests) diff --git a/profiles/vcs/VCS_TIER2_LEAF_AB_2026-08-16.md b/profiles/vcs/VCS_TIER2_LEAF_AB_2026-08-16.md new file mode 100644 index 0000000..217cf4a --- /dev/null +++ b/profiles/vcs/VCS_TIER2_LEAF_AB_2026-08-16.md @@ -0,0 +1,77 @@ +# VCS Tier-2 LEAF A/B Handoff — 2026-08-16 + +## Goal +Isolate the leaf-call inlining part of the failed HOTSPOT OPT1 experiment while keeping the known-good HOTFIX SAFE corpus everywhere else. + +## Baseline +Built directly from `PSPRecomp-VCS-TIER2-HOTSPOT-HOTFIX-SAFE-2026-08-16`, the last source tree confirmed by the user to boot and reach gameplay. + +## What changed +Only two generated caller units are semantically changed: +- `generated_unit_0155.cpp` +- `generated_unit_0085.cpp` + +18 static cross-unit callsites are replaced with exact bodies for eight tiny generated leaf entries: +- unit 0152 entries 432/433 (`0x08A65EA0`, `0x08A65EB4`) +- unit 0153 entries 100..104 (`0x08A68CBC` .. `0x08A68CDC`) +- unit 0044 entry 665 (`0x088B7AD8`) + +No GPR block cache, no FPR cache, no global tail-chain change, no local-transfer cap increase, no scheduler cadence change, and no new VFPU hotspot expansion are included in this A/B. + +## Critical semantic correction versus HOTSPOT OPT1 +The failed OPT1 leaf transform called `account_inlined_generated_leaf(ctx)` while `ctx.pc` could still contain a stale native-frame PC. If the starvation boundary fired at that exact call, the scheduler could observe/save the wrong guest PC. + +The A/B transform now reproduces the generated callee's return semantics first: + +```cpp +ctx.pc = ; +if (!rt.account_inlined_generated_leaf(ctx)) return; +goto ; +``` + +This uses the existing Runtime helper from the known-good SAFE tree. `runtime.hpp` is unchanged, avoiding a full generated-corpus rebuild. + +## Build behavior +`BUILD_VCS_NINJA.bat` uses marker `.tier2_leaf_ab_20260816_v1` and, on the first build only, invalidates: +- `generated_unit_0085.cpp.obj` +- `generated_unit_0155.cpp.obj` +- `vcs_runtime_log.cpp.obj` + +Ninja then handles normal dependencies. It should not intentionally invalidate all generated units. + +## Runtime log identity +Expected header: + +```text +stage=tier2-leaf-ab-2026-08-16 +perf_telemetry=1 +guest_hotspot=0 +``` + +## Validation completed +- Transform application: 2 files, 18 static callsites. +- Transform idempotence: PASS (second run reports 0 changes). +- Generated corpus audit: only units 0085 and 0155 differ from HOTFIX SAFE. +- `generated_unit_0085.cpp`: `g++ -fsyntax-only` PASS. +- `generated_unit_0155.cpp`: `g++ -fsyntax-only` PASS. +- `psprecomp_tests`: PASS. +- `vcs_profile_tests`: PASS. +- `vfpu_tier2_tests`: PASS. +- Full Linux `VCSNative` build was started and reached generated-unit compilation without source errors, but was not allowed to finish within the execution-call windows. Windows/DX12 runtime boot must be confirmed by the user. + +## Test protocol +1. Overlay the LEAF A/B package on top of the current HOTFIX SAFE tree. +2. Run `profiles\\vcs\\BUILD_VCS_NINJA.bat`. +3. Confirm `[0c-leaf]` appears on the first build. +4. Launch normally. +5. First priority: confirm the loading screens and gameplay still boot. +6. If it boots, run the same heavy-city route for 3–5 minutes and send `VCSNative.log`. +7. Compare `guest_cpu_us_avg` and FPS distribution against HOTFIX SAFE. + +## Rollback +If it closes before loading, re-overlay `PSPRecomp-VCS-TIER2-HOTSPOT-HOTFIX-SAFE-OVERLAY-2026-08-16.zip` and rebuild. Because the experiment modifies only two generated units, this cleanly isolates leaf-inline as the cause. + +## Estimated project progress +- Overall project: ~82% +- Performance diagnosis: 100% +- Tier-2 implementation: ~69% diff --git a/profiles/vcs/VCS_TIER2_SUPERBLOCK_V1_PYFIX_2026-08-16.md b/profiles/vcs/VCS_TIER2_SUPERBLOCK_V1_PYFIX_2026-08-16.md new file mode 100644 index 0000000..469f74e --- /dev/null +++ b/profiles/vcs/VCS_TIER2_SUPERBLOCK_V1_PYFIX_2026-08-16.md @@ -0,0 +1,25 @@ +# VCS Tier-2 Superblock V1 — Python launcher fix (2026-08-16) + +## Problem +`profiles/vcs/scripts/build_release_ninja.bat` invoked the superblock generator with the hard-coded command `python`. On Windows systems where Python is installed through the standard `py` launcher but `python.exe` resolves only to the Microsoft Store app-execution alias, build step `[0b2/7]` failed before CMake/Ninja. + +The preceding Tier-2 transform step already used `py -3` successfully, so no new dependency is required. + +## Fix +The build script now probes Python 3 in this order: +1. `py -3` +2. `python` + +Each candidate is executed with a real Python 3 version check. The Microsoft Store alias therefore cannot be mistaken for a usable interpreter. + +The selected command is printed before running `tools/build_tier2_superblocks.py`. + +## Scope +No C++, generated AOT corpus, superblock CFG, scheduler accounting, CMake flags, or runtime behavior changed. This is build-script-only. + +## Expected output +``` +[0b2/7] Building profile-guided Tier-2 superblock cluster 0154+0155... + Python 3: py -3 +... +``` diff --git a/profiles/vcs/generated/generated_unit_0154.cpp b/profiles/vcs/generated/generated_unit_0154.cpp index 999a625..0dbd1fe 100644 --- a/profiles/vcs/generated/generated_unit_0154.cpp +++ b/profiles/vcs/generated/generated_unit_0154.cpp @@ -1,5 +1,6 @@ #include "psprecomp/runtime.hpp" #include "generated_units.hpp" +#include "vcs_tier2_superblocks.hpp" #include #include #include @@ -6791,6 +6792,12 @@ L_08A6E828: aot_mem.aot_store32(ctx.gpr[29] + static_cast(1584), ctx.gpr[6]); goto L_08A6E894; L_08A6E894: +// TIER2_SUPERBLOCK_V1_HOOK_BEGIN + if (vcs::tier2_superblocks_enabled()) { + vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x08A6E894u); + return; + } +// TIER2_SUPERBLOCK_V1_HOOK_END ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1600))); ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); { const bool branch_taken = ctx.gpr[4] == 0u; @@ -6801,6 +6808,12 @@ L_08A6E894: goto L_08A6E8A4; } L_08A6E8A4: +// TIER2_SUPERBLOCK_V1_HOOK_BEGIN + if (vcs::tier2_superblocks_enabled()) { + vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x08A6E8A4u); + return; + } +// TIER2_SUPERBLOCK_V1_HOOK_END ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1576))); ctx.gpr[17] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(8))); diff --git a/profiles/vcs/generated/generated_unit_0155.cpp b/profiles/vcs/generated/generated_unit_0155.cpp index 7d1f3c8..b9e5fe2 100644 --- a/profiles/vcs/generated/generated_unit_0155.cpp +++ b/profiles/vcs/generated/generated_unit_0155.cpp @@ -1,5 +1,6 @@ #include "psprecomp/runtime.hpp" #include "generated_units.hpp" +#include "vcs_tier2_superblocks.hpp" #include #include #include @@ -3416,6 +3417,12 @@ L_08A710F8: goto L_08A71100; } L_08A71100: +// TIER2_SUPERBLOCK_V1_HOOK_BEGIN + if (vcs::tier2_superblocks_enabled()) { + vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x08A71100u); + return; + } +// TIER2_SUPERBLOCK_V1_HOOK_END { const bool branch_taken = ctx.gpr[4] != 0u; // nop if (branch_taken) { @@ -3589,6 +3596,12 @@ L_08A711D4: if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 104u, 0x08A68CDCu>(ctx, &aot_mem) && ctx.pc == 0x08A711E0u) goto L_08A711E0; return; L_08A711E0: +// TIER2_SUPERBLOCK_V1_HOOK_BEGIN + if (vcs::tier2_superblocks_enabled()) { + vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x08A711E0u); + return; + } +// TIER2_SUPERBLOCK_V1_HOOK_END ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1576))); { const bool branch_taken = ctx.gpr[4] != 0u; // nop @@ -3598,6 +3611,12 @@ L_08A711E0: goto L_08A711EC; } L_08A711EC: +// TIER2_SUPERBLOCK_V1_HOOK_BEGIN + if (vcs::tier2_superblocks_enabled()) { + vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x08A711ECu); + return; + } +// TIER2_SUPERBLOCK_V1_HOOK_END ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1600))); ctx.gpr[4] = (ctx.gpr[4] + static_cast(4)); ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1596))); diff --git a/profiles/vcs/host/ge_gpu_backend.hpp b/profiles/vcs/host/ge_gpu_backend.hpp index 6c654a7..0731aca 100644 --- a/profiles/vcs/host/ge_gpu_backend.hpp +++ b/profiles/vcs/host/ge_gpu_backend.hpp @@ -369,6 +369,12 @@ struct GeGpuBackendReport { // same-target feedback snapshots the pre-draw image on the GPU to avoid // the D3D12 RTV+SRV hazard without a CPU readback. std::uint64_t dx12_native_framebuffer_targets{}; + std::uint64_t dx12_framebuffer_target_hits{}; + std::uint64_t dx12_framebuffer_target_creates{}; + std::uint64_t dx12_batch_appends{}; + std::uint64_t dx12_batch_merges{}; + std::uint64_t dx12_gpu_draw_calls{}; + std::uint64_t dx12_srv_high_water{}; std::uint64_t dx12_gpu_feedback_draws{}; std::uint64_t dx12_self_feedback_snapshots{}; // Stage 44.6 Direct3D 12 release-candidate diagnostics. diff --git a/profiles/vcs/host/ge_gpu_backend_dx12.cpp b/profiles/vcs/host/ge_gpu_backend_dx12.cpp index d033615..11d81a0 100644 --- a/profiles/vcs/host/ge_gpu_backend_dx12.cpp +++ b/profiles/vcs/host/ge_gpu_backend_dx12.cpp @@ -1091,10 +1091,12 @@ bool append_or_merge_batch(Dx12GeState &s, Dx12Batch batch) { } previous.vertex_count += batch.vertex_count; previous.logical_draw_count += batch.logical_draw_count; + ++s.report.dx12_batch_merges; return true; } } s.batches.push_back(std::move(batch)); + ++s.report.dx12_batch_appends; return false; } @@ -1649,7 +1651,10 @@ void note_framebuffer_logical_extent(Dx12GeState &s, std::uint32_t address, bool ensure_framebuffer_target(Dx12GeState &s, std::uint32_t address, std::string &error) noexcept { address &= 0x001FFFF0u; - if (auto *existing = find_framebuffer_target(s, address)) return existing->color != nullptr; + if (auto *existing = find_framebuffer_target(s, address)) { + ++s.report.dx12_framebuffer_target_hits; + return existing->color != nullptr; + } if (!s.device || !s.rtv_heap || !s.dsv_heap || !s.srv_heap || s.next_rtv >= kFramebufferTargetCapacity || s.next_dsv >= kFramebufferTargetCapacity || s.next_srv >= kSrvCapacity) { @@ -1740,8 +1745,10 @@ bool ensure_framebuffer_target(Dx12GeState &s, std::uint32_t address, target.color_state = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; s.frame_targets.emplace(address, std::move(target)); s.known_frame_targets.insert(address); + ++s.report.dx12_framebuffer_target_creates; s.report.framebuffer_targets_observed = s.known_frame_targets.size(); s.report.dx12_native_framebuffer_targets = s.frame_targets.size(); + s.report.dx12_srv_high_water = std::max(s.report.dx12_srv_high_water, s.next_srv); { std::ostringstream log; const auto created = s.frame_targets.find(address); @@ -4326,6 +4333,7 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept { if (current_target != nullptr) resolve_target_for_sampling(s, *current_target, false); + s.report.dx12_gpu_draw_calls += executed_batches; if (executed_batches == 0u) { hr = s.list->Close(); if (SUCCEEDED(hr)) { @@ -4476,7 +4484,11 @@ bool ge_gpu_backend_copy_offscreen_rgba(std::span destination) noexce return ge_gpu_backend_copy_game_frame_rgba(destination); } void ge_gpu_backend_mark_window_presented() noexcept { state().report.gpu_frame_presented_to_window = true; } -GeGpuBackendReport ge_gpu_backend_report() { return state().report; } +GeGpuBackendReport ge_gpu_backend_report() { + auto &s = state(); + s.report.dx12_srv_high_water = std::max(s.report.dx12_srv_high_water, s.next_srv); + return s.report; +} #else diff --git a/profiles/vcs/host/main.cpp b/profiles/vcs/host/main.cpp index fd78937..73cdf62 100644 --- a/profiles/vcs/host/main.cpp +++ b/profiles/vcs/host/main.cpp @@ -177,6 +177,9 @@ int main(int argc, char **argv) { psprecomp::Elf32Image elf = psprecomp::Elf32Image::from_file(executable); psprecomp::Runtime runtime(32u * 1024u * 1024u); + // The heavy GUESTHOT sampler is opt-in. The rolling PERF telemetry stays on, + // but normal gameplay does not pay a census/timestamp branch per cross-unit edge. + psprecomp::set_guest_hotspot_profile(configuration.diagnostics.guest_hotspot_profile, 8u); runtime.set_game_root(root); const auto relocations = elf.load_and_relocate(runtime.memory(), psprecomp::kDefaultPspUserLoadBase); std::uint64_t image_end = 0u; diff --git a/profiles/vcs/host/vcs_config.cpp b/profiles/vcs/host/vcs_config.cpp index 7ad3f4d..bb3723d 100644 --- a/profiles/vcs/host/vcs_config.cpp +++ b/profiles/vcs/host/vcs_config.cpp @@ -581,6 +581,21 @@ void apply_diagnostics_key(VcsConfiguration &config, const std::string &key, warning(config, line, "Diagnostics.FlushEveryLine expects true/false"); return; } + if (key == "perftelemetry" || key == "performancetelemetry") { + if (!parse_bool(value, config.diagnostics.perf_telemetry)) + warning(config, line, "Diagnostics.PerfTelemetry expects true/false"); + return; + } + if (key == "perftelemetryintervalvblanks" || key == "telemetryintervalvblanks") { + if (!parse_u64(value, 10u, 36000u, config.diagnostics.perf_telemetry_interval_vblanks)) + warning(config, line, "Diagnostics.PerfTelemetryIntervalVblanks must be between 10 and 36000"); + return; + } + if (key == "guesthotspotprofile" || key == "guesthotspot") { + if (!parse_bool(value, config.diagnostics.guest_hotspot_profile)) + warning(config, line, "Diagnostics.GuestHotspotProfile expects true/false"); + return; + } warning(config, line, "unknown [Diagnostics] key '" + key + "'"); } @@ -801,6 +816,21 @@ void initialize_vcs_configuration(const std::filesystem::path &executable_direct std::getenv("PSPRECOMP_REALTIME_SPEED_DIAG") == nullptr) { set_environment_value("PSPRECOMP_REALTIME_SPEED_DIAG", "1"); } + if (config.diagnostics.perf_telemetry && + std::getenv("PSPRECOMP_PERF_TELEMETRY") == nullptr) { + set_environment_value("PSPRECOMP_PERF_TELEMETRY", "1"); + } + if (std::getenv("PSPRECOMP_PERF_TELEMETRY_INTERVAL") == nullptr) { + set_environment_value("PSPRECOMP_PERF_TELEMETRY_INTERVAL", + std::to_string(config.diagnostics.perf_telemetry_interval_vblanks)); + } + // GPU timing counters are sampled only when requested. PERF TELEMETRY uses + // them for queue/fence/present attribution, but still logs only once per + // aggregation window. + if (config.diagnostics.perf_telemetry && + std::getenv("PSPRECOMP_GPU_TIMING_DIAG") == nullptr) { + set_environment_value("PSPRECOMP_GPU_TIMING_DIAG", "1"); + } if (std::getenv("PSPRECOMP_REALTIME_SPEED_INTERVAL") == nullptr) { set_environment_value("PSPRECOMP_REALTIME_SPEED_INTERVAL", std::to_string(config.timing.realtime_speed_interval_vblanks)); diff --git a/profiles/vcs/host/vcs_config.hpp b/profiles/vcs/host/vcs_config.hpp index 122d448..db19438 100644 --- a/profiles/vcs/host/vcs_config.hpp +++ b/profiles/vcs/host/vcs_config.hpp @@ -158,6 +158,13 @@ struct DiagnosticsConfiguration { bool log_to_file{false}; std::string log_file{"VCSNative.log"}; bool flush_every_line{true}; + // Low-overhead rolling performance census. Unlike the legacy per-frame + // stderr diagnostics this aggregates many vblanks and emits one log line, + // so the profiler does not become the bottleneck it is trying to measure. + bool perf_telemetry{true}; + std::uint64_t perf_telemetry_interval_vblanks{60u}; + // Heavy cross-unit hotspot sampler is opt-in. Keep disabled in the safe benchmark build. + bool guest_hotspot_profile{false}; }; // Widescreen / ultrawide frustum, after ThirteenAG's WidescreenFixesPack diff --git a/profiles/vcs/host/vcs_profile.cpp b/profiles/vcs/host/vcs_profile.cpp index 11f0347..e7c7243 100644 --- a/profiles/vcs/host/vcs_profile.cpp +++ b/profiles/vcs/host/vcs_profile.cpp @@ -13,6 +13,7 @@ #include "vcs_draw_distance_patch.hpp" #include "savedata_utility_ui.hpp" #include "vcs_texture_replacement.hpp" +#include "vcs_runtime_log.hpp" #include "psprecomp/common.hpp" #include "psprecomp/deflate.hpp" @@ -4679,6 +4680,74 @@ bool frame_time_diag_enabled() { return enabled; } +bool perf_telemetry_enabled() { + static const bool enabled = [] { + const char *text = std::getenv("PSPRECOMP_PERF_TELEMETRY"); + return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0 && + std::strcmp(text, "false") != 0 && std::strcmp(text, "FALSE") != 0; + }(); + return enabled; +} + +std::uint64_t perf_telemetry_interval() { + static const std::uint64_t value = std::max(10u, + parse_environment_u64("PSPRECOMP_PERF_TELEMETRY_INTERVAL", 60u)); + return value; +} + +bool perf_timing_enabled() { return frame_time_diag_enabled() || perf_telemetry_enabled(); } + +struct PerfTelemetryAccumulator { + std::uint64_t frames{}; + std::uint64_t frame_us_sum{}, frame_us_min{UINT64_MAX}, frame_us_max{}; + std::uint64_t guest_cpu_us_sum{}, ge_us_sum{}, ge_wait_us_sum{}, present_us_sum{}, io_us_sum{}; + std::uint64_t ge_calls_sum{}; + GeGpuBackendReport previous_report{}; + bool report_started{}; +}; +PerfTelemetryAccumulator perf_telemetry; +std::uint32_t guest_hotspot_perf_windows{}; + +void report_guest_hotspot_window(std::uint64_t vblank) { + const psprecomp::GuestHotspotSnapshot snap = psprecomp::consume_guest_hotspot_profile(16u, 24u); + std::ostringstream summary; + summary << "GUESTHOT summary vblank=" << vblank + << " stride=" << snap.sample_stride + << " unit_calls=" << snap.total_unit_calls + << " samples=" << snap.total_samples + << " units=" << snap.units.size() + << " pcs=" << snap.pcs.size(); + runtime_log_line(summary.str()); + + std::size_t rank = 0u; + for (const auto &e : snap.units) { + const std::uint64_t avg_ns = e.samples ? e.inclusive_sample_ns / e.samples : 0u; + const std::uint64_t est_us = (e.inclusive_sample_ns * snap.sample_stride) / 1000u; + std::ostringstream line; + line << "GUESTHOT_UNIT rank=" << (++rank) + << " unit=" << std::setw(4) << std::setfill('0') << e.unit << std::setfill(' ') + << " calls=" << e.calls + << " samples=" << e.samples + << " avg_inclusive_ns=" << avg_ns + << " est_inclusive_us=" << est_us; + runtime_log_line(line.str()); + } + + rank = 0u; + for (const auto &e : snap.pcs) { + const std::uint64_t avg_ns = e.samples ? e.inclusive_sample_ns / e.samples : 0u; + const std::uint64_t est_us = (e.inclusive_sample_ns * snap.sample_stride) / 1000u; + std::ostringstream line; + line << "GUESTHOT_PC rank=" << (++rank) + << " unit=" << std::setw(4) << std::setfill('0') << e.unit << std::setfill(' ') + << " pc=" << psprecomp::hex32(e.pc) + << " samples=" << e.samples + << " avg_inclusive_ns=" << avg_ns + << " est_inclusive_us=" << est_us; + runtime_log_line(line.str()); + } +} + bool ge_phase_diag_line_enabled() { static const bool enabled = std::getenv("PSPRECOMP_GE_PHASE_DIAG") != nullptr; return enabled; @@ -4689,7 +4758,10 @@ bool gpu_timing_diag_line_enabled() { const char *text = std::getenv("PSPRECOMP_GPU_TIMING_DIAG"); return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0; }(); - return enabled; + // PERF TELEMETRY uses the same backend timing counters, but intentionally + // suppresses the legacy per-vblank stderr line: logging the profiler every + // frame was itself measured at >1 ms/frame on Windows. + return enabled && !perf_telemetry_enabled(); } struct GpuTimingCensus { @@ -4783,7 +4855,7 @@ bool execute_ge_list(psprecomp::Runtime &runtime, GeListRecord &list, std::vector &callbacks, const std::atomic *async_stall = nullptr) { constexpr std::uint64_t kMaximumCommandsPerRun = 4'000'000u; - const bool time_ge = frame_time_diag_enabled(); + const bool time_ge = perf_timing_enabled(); const bool ge_histogram = ge_histogram_diag_enabled(); const bool count_ge_commands = ge_phase_diag_line_enabled(); const auto ge_entry_time = time_ge @@ -7450,7 +7522,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start // injects Start during a movie. vcs::audio_output_advance(virtual_time_us); report_realtime_speed_if_requested(); - if (frame_time_diag_enabled()) { + if (perf_timing_enabled()) { const auto now = std::chrono::steady_clock::now(); if (frame_time_stats.started) { const auto frame = now - frame_time_stats.last_vblank; @@ -7489,7 +7561,73 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start << " guest_us=" << (virtual_time_us - frame_time_stats.last_guest_time) << " ge_calls=" << frame_time_stats.ge_calls << " fps=" << (frame_us > 0 ? 1000000 / frame_us : 0) << "\n"; - write_diag_line(frame_line); + if (frame_time_diag_enabled()) write_diag_line(frame_line); + + const auto guest_cpu_us = static_cast( + frame_us > accounted_non_guest ? frame_us - accounted_non_guest : 0); + if (perf_telemetry_enabled()) { + auto &a = perf_telemetry; + ++a.frames; + a.frame_us_sum += static_cast(std::max(0, frame_us)); + a.frame_us_min = std::min(a.frame_us_min, static_cast(std::max(0, frame_us))); + a.frame_us_max = std::max(a.frame_us_max, static_cast(std::max(0, frame_us))); + a.guest_cpu_us_sum += guest_cpu_us; + a.ge_us_sum += static_cast(std::max(0, ge_us)); + a.ge_wait_us_sum += static_cast(std::max(0, ge_async_wait_us)); + a.present_us_sum += static_cast(std::max(0, present_us)); + a.io_us_sum += static_cast(std::max(0, io_us)); + a.ge_calls_sum += frame_time_stats.ge_calls; + if (a.frames >= perf_telemetry_interval()) { + const GeGpuBackendReport r = ge_gpu_backend_report(); + const GeGpuBackendReport &o = a.previous_report; + const auto d = [](std::uint64_t n, std::uint64_t p) { return n >= p ? n - p : 0u; }; + const std::uint64_t n = a.frames; + std::ostringstream t; + t << "PERF window=" << n + << " vblank=" << display_vblank_index + << " fps_avg=" << (a.frame_us_sum ? (1000000.0 * n / a.frame_us_sum) : 0.0) + << " fps_min=" << (a.frame_us_max ? (1000000.0 / a.frame_us_max) : 0.0) + << " fps_max=" << (a.frame_us_min && a.frame_us_min != UINT64_MAX ? (1000000.0 / a.frame_us_min) : 0.0) + << " frame_us_avg=" << (a.frame_us_sum / n) + << " guest_cpu_us_avg=" << (a.guest_cpu_us_sum / n) + << " ge_us_avg=" << (a.ge_us_sum / n) + << " ge_wait_us_avg=" << (a.ge_wait_us_sum / n) + << " present_us_avg=" << (a.present_us_sum / n) + << " io_us_avg=" << (a.io_us_sum / n) + << " ge_calls_avg=" << (a.ge_calls_sum / n) + << " game_draws=" << d(r.game_draw_calls, o.game_draw_calls) + << " gpu_draws=" << d(r.dx12_gpu_draw_calls, o.dx12_gpu_draw_calls) + << " batch_appends=" << d(r.dx12_batch_appends, o.dx12_batch_appends) + << " batch_merges=" << d(r.dx12_batch_merges, o.dx12_batch_merges) + << " tex_req=" << d(r.texture_decode_requests, o.texture_decode_requests) + << " tex_hits=" << d(r.texture_cache_hits, o.texture_cache_hits) + << " tex_uploads=" << d(r.decoded_texture_uploads, o.decoded_texture_uploads) + << " tex_upload_bytes=" << d(r.decoded_texture_bytes, o.decoded_texture_bytes) + << " tex_evict=" << d(r.evicted_textures, o.evicted_textures) + << " srv_recycled=" << d(r.recycled_texture_descriptor_sets, o.recycled_texture_descriptor_sets) + << " transfer_submits=" << d(r.transfer_submissions, o.transfer_submissions) + << " transfer_bytes=" << d(r.transfer_bytes, o.transfer_bytes) + << " fb_hits=" << d(r.dx12_framebuffer_target_hits, o.dx12_framebuffer_target_hits) + << " fb_creates=" << d(r.dx12_framebuffer_target_creates, o.dx12_framebuffer_target_creates) + << " fb_live=" << r.dx12_native_framebuffer_targets + << " fb_feedback=" << d(r.dx12_gpu_feedback_draws, o.dx12_gpu_feedback_draws) + << " fb_selfsnap=" << d(r.dx12_self_feedback_snapshots, o.dx12_self_feedback_snapshots) + << " srv_high=" << r.dx12_srv_high_water + << " submit_us=" << (d(r.perf_queue_submit_ns, o.perf_queue_submit_ns) / 1000u) + << " presentq_us=" << (d(r.perf_queue_present_ns, o.perf_queue_present_ns) / 1000u) + << " fence_us=" << (d(r.perf_wait_for_frame_ns, o.perf_wait_for_frame_ns) / 1000u) + << " finish_us=" << (d(r.perf_finish_frame_ns, o.perf_finish_frame_ns) / 1000u); + runtime_log_line(t.str()); + if (vcs_configuration().diagnostics.guest_hotspot_profile && + ++guest_hotspot_perf_windows >= 5u) { + report_guest_hotspot_window(display_vblank_index); + guest_hotspot_perf_windows = 0u; + } + a = {}; + a.previous_report = r; + a.report_started = true; + } + } // Splits ge_us into the per-fragment pixel loop and everything // else, which is per-triangle geometry. Says directly which of // the two a heavy frame is actually spent on. @@ -7597,7 +7735,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start display_window_set_aspect_lock( !movie_output_buffers.empty() && movie_output_buffers.count(normalize_ram_address(display_state.frame_buffer)) != 0u); - const auto present_entry = frame_time_diag_enabled() + const auto present_entry = perf_timing_enabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // PSP firmware-owned savedata utility: render its HLE surface into the // same GE target before the frame is finalized. No desktop/Win32 chooser. @@ -7650,7 +7788,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start ++swapchain_presents; } if (gpu_frame_ready) dump_gpu_internal_frame_if_requested(display_vblank_index); - if (frame_time_diag_enabled()) + if (perf_timing_enabled()) frame_time_stats.present_time += std::chrono::steady_clock::now() - present_entry; limit_frame_rate(); // limit_frame_rate() may advance virtual_time_us when the host misses the @@ -9918,7 +10056,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start ctx.set_gpr(2, 0x80010009u); return; } - const bool time_io = frame_time_diag_enabled(); + const bool time_io = perf_timing_enabled(); const auto io_entry = time_io ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const std::size_t read = read_virtual_disc( @@ -9982,7 +10120,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start ctx.set_gpr(2, 0x80010009u); return; } - const bool time_io = frame_time_diag_enabled(); + const bool time_io = perf_timing_enabled(); const auto io_entry = time_io ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; it->second.read(reinterpret_cast(guest_destination), diff --git a/profiles/vcs/host/vcs_runtime_log.cpp b/profiles/vcs/host/vcs_runtime_log.cpp index e91ca28..6aa97e1 100644 --- a/profiles/vcs/host/vcs_runtime_log.cpp +++ b/profiles/vcs/host/vcs_runtime_log.cpp @@ -62,9 +62,15 @@ void runtime_log_initialize(const VcsConfiguration &configuration) { return; } s.file << "VCSNative runtime log\n"; - s.file << "stage=tier2-extreme-2026-08-16\n"; + s.file << "stage=tier2-superblock-v1-2026-08-16\n"; s.file << "config=" << configuration.source_path.string() << '\n'; - s.file << "started=" << timestamp_now() << "\n\n"; + s.file << "started=" << timestamp_now() << '\n'; + s.file << "perf_telemetry=" << (configuration.diagnostics.perf_telemetry ? 1 : 0) + << " interval_vblanks=" << configuration.diagnostics.perf_telemetry_interval_vblanks + << "\n"; + s.file << "guest_hotspot=" << (configuration.diagnostics.guest_hotspot_profile ? 1 : 0) + << " sample_stride=256 interval_vblanks=300\n"; + s.file << "tier2_superblocks=1 cluster=0154+0155 hot_region_edges=8 cold_exits=3\n\n"; if (s.flush_every_line) s.file.flush(); } diff --git a/profiles/vcs/host/vcs_tier2_superblocks.cpp b/profiles/vcs/host/vcs_tier2_superblocks.cpp new file mode 100644 index 0000000..c2cc669 --- /dev/null +++ b/profiles/vcs/host/vcs_tier2_superblocks.cpp @@ -0,0 +1,1144 @@ +// AUTO-GENERATED by profiles/vcs/tools/build_tier2_superblocks.py. +// Profile-guided second-layer superblock: 0154/0155 dominant entity loop. +#include "vcs_tier2_superblocks.hpp" +#include "vcs_runtime_log.hpp" +#include "psprecomp/runtime.hpp" +#include "generated_units.hpp" + +#include +#include + +namespace vcs { + +// Generated AOT call targets are declared in namespace psprecomp. The copied +// region text intentionally stays byte-close to the original units, so make +// those names visible here instead of rewriting every external helper target. +using namespace psprecomp; + +bool tier2_superblocks_enabled() noexcept { + static const bool enabled = [] { + const char *value = std::getenv("PSPRECOMP_TIER2_SUPERBLOCKS"); + return value == nullptr || std::strcmp(value, "0") != 0; + }(); + return enabled; +} + +void tier2_superblock_154_155(psprecomp::Runtime &rt, + psprecomp::AllegrexContext &ctx, + psprecomp::GuestMemory::AotFastView &aot_mem, + std::uint32_t entry_pc) { + std::uint32_t tier2_pending_transfers = 0u; +#define TIER2_SB_RETURN() do { \ + if (tier2_pending_transfers != 0u) { \ + (void)rt.tier2_complete_fused_transfers(ctx, tier2_pending_transfers); \ + tier2_pending_transfers = 0u; \ + } \ + return; \ + } while (false) + + switch (entry_pc) { + case 0x08A6E894u: goto SB_L_08A6E894; + case 0x08A6E8A4u: goto SB_L_08A6E8A4; + case 0x08A71100u: goto SB_L_08A71100; + case 0x08A711E0u: goto SB_L_08A711E0; + case 0x08A711ECu: goto SB_L_08A711EC; + default: ctx.pc = entry_pc; TIER2_SB_RETURN(); + } + +SB_L_08A6E894: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1600))); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); + { const bool branch_taken = ctx.gpr[4] == 0u; + aot_mem.aot_store32(ctx.gpr[29] + static_cast(1576), ctx.gpr[4]); + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A711ECu>(ctx)) { + ctx.pc = 0x08A711ECu; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A711EC; + } + goto SB_L_08A6E8A4; + } +SB_L_08A6E8A4: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1576))); + ctx.gpr[17] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(8))); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(8428))); + aot_mem.aot_store32(ctx.gpr[29] + static_cast(1576), ctx.gpr[4]); + ctx.gpr[5] = (0u | 3u); + ctx.gpr[4] = (48972u << 16u); + ctx.gpr[4] = (ctx.gpr[4] | 52429u); + ctx.fpr[20] = std::bit_cast(ctx.gpr[4]); + ctx.gpr[8] = (0u | 58u); + ctx.gpr[21] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1588))); + ctx.gpr[9] = (0u + static_cast(-2)); + ctx.gpr[10] = (0u | 311u); + ctx.gpr[4] = (16168u << 16u); + ctx.gpr[4] = (ctx.gpr[4] | 62915u); + { const bool branch_taken = ctx.gpr[19] == ctx.gpr[6]; + ctx.fpr[12] = std::bit_cast(ctx.gpr[4]); + if (branch_taken) { + goto SB_L_08A6E8F4; + } + goto SB_L_08A6E8E8; + } +SB_L_08A6E8E8: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(8428))); + { const bool branch_taken = ctx.gpr[17] != ctx.gpr[4]; + ctx.gpr[23] = (0u | 1u); + if (branch_taken) { + goto SB_L_08A6E8FC; + } + goto SB_L_08A6E8F4; + } +SB_L_08A6E8F4: + { const bool branch_taken = 0u == 0u; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A711E0u>(ctx)) { + ctx.pc = 0x08A711E0u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A711E0; + } + goto SB_L_08A6E8FC; + } +SB_L_08A6E8FC: + ctx.gpr[4] = (ctx.gpr[23] | 0u); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[6] = (ctx.gpr[6] & 512u); + ctx.gpr[6] = (0u < ctx.gpr[6] ? 1u : 0u); + ctx.gpr[6] = (ctx.gpr[6] & 255u); + { const bool branch_taken = ctx.gpr[6] == 0u; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx)) { + ctx.pc = 0x08A71100u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A71100; + } + goto SB_L_08A6E918; + } +SB_L_08A6E918: + ctx.gpr[6] = (aot_mem.aot_load16(ctx.gpr[17] + static_cast(84))); + ctx.gpr[7] = (aot_mem.aot_load16(ctx.gpr[28] + static_cast(-25492))); + { const bool branch_taken = ctx.gpr[6] == ctx.gpr[7]; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx)) { + ctx.pc = 0x08A71100u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A71100; + } + goto SB_L_08A6E928; + } +SB_L_08A6E928: + { const bool branch_taken = ctx.gpr[17] == ctx.gpr[19]; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx)) { + ctx.pc = 0x08A71100u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A71100; + } + goto SB_L_08A6E930; + } +SB_L_08A6E930: + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load16(ctx.gpr[17] + static_cast(86)))))); + ctx.gpr[4] = (ctx.gpr[4] << 2u); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(24))); + ctx.gpr[4] = (ctx.gpr[6] + ctx.gpr[4]); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(20))); + { const std::uint32_t vfpu_address = ctx.gpr[17] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<0u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[17] + static_cast(16); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<1u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[17] + static_cast(32); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<2u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[17] + static_cast(48); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<3u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<4u, 4u>(vfpu_value); } + { float vfpu_value[4]{}; + ctx.write_vfpu_vector_with_destination_prefix_ct<35u, 3u>(vfpu_value); } + { float vfpu_value[4]{1.0f, 1.0f, 1.0f, 1.0f}; + ctx.write_vfpu_vector_with_destination_prefix_ct<99u, 1u>(vfpu_value); } + ctx.gpr[4] = (ctx.gpr[29] + static_cast(32)); + { const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<8u, 4u>(vfpu_value); } + { float vfpu_matrix[16]{}, vfpu_target_raw[4]{}, vfpu_target[4]{}, vfpu_result[4]{}; + ctx.read_vfpu_matrix_ct<32u, 4u>(vfpu_matrix); + ctx.read_vfpu_vector_ct<4u, 4u>(vfpu_target_raw); + constexpr std::uint32_t vfpu_side = 4u; + constexpr std::uint32_t vfpu_input_length = 3u; + for (std::uint32_t i = 0; i < 4u; ++i) vfpu_target[i] = i < vfpu_input_length ? vfpu_target_raw[i] : 0.0f; + if (vfpu_side - 1u >= vfpu_input_length) vfpu_target[vfpu_side - 1u] = 1.0f; + for (std::uint32_t row = 0; row + 1u < vfpu_side; ++row) { + float sum = 0.0f; + for (std::uint32_t column = 0; column < vfpu_side; ++column) sum += vfpu_matrix[row * 4u + column] * vfpu_target[column]; + vfpu_result[row] = sum; + } + float vfpu_final_row[4]{vfpu_matrix[(vfpu_side - 1u) * 4u + 0u], vfpu_matrix[(vfpu_side - 1u) * 4u + 1u], + vfpu_matrix[(vfpu_side - 1u) * 4u + 2u], vfpu_matrix[(vfpu_side - 1u) * 4u + 3u]}; + ctx.apply_vfpu_source_prefix_ct<4u, 0u>(vfpu_final_row); + ctx.apply_vfpu_source_prefix_ct<4u, 1u>(vfpu_target); + for (std::uint32_t column = 0; column < 4u; ++column) vfpu_result[vfpu_side - 1u] += vfpu_final_row[column] * vfpu_target[column]; + const std::uint32_t vfpu_destination_prefix = ctx.vfpu_ctrl[2]; + const std::uint32_t vfpu_last_lane = vfpu_side - 1u; + ctx.vfpu_ctrl[2] = ((vfpu_destination_prefix & (1u << 8u)) << vfpu_last_lane) | + ((vfpu_destination_prefix & 3u) << (vfpu_last_lane * 2u)); + ctx.write_vfpu_vector_with_destination_prefix_ct<5u, 4u>(vfpu_result); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<100u, 1u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<104u, 1u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 1u; ++i) vfpu_d[i] = vfpu_s[i] + vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<100u, 1u>(vfpu_d); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<100u, 1u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<100u, 1u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 1u; ++i) vfpu_d[i] = vfpu_s[i] * vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<100u, 1u>(vfpu_d); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<8u, 3u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<5u, 3u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 3u; ++i) vfpu_d[i] = vfpu_s[i] - vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<5u, 3u>(vfpu_d); } + ctx.execute_vfpu_vdot_ct<4u, 5u, 5u, 3u>(); + ctx.execute_vfpu_vcmp_ct<4u, 100u, 1u, 7u>(); + ctx.gpr[4] = (0u | 0u); + { const bool branch_taken = ((ctx.vfpu_ctrl[3] >> 0u) & 1u) != 0u; + // vflush: architectural no-op that retains VFPU prefixes + if (branch_taken) { + goto SB_L_08A6E998; + } + goto SB_L_08A6E990; + } +SB_L_08A6E990: + ctx.gpr[4] = (0u + static_cast(1)); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + goto SB_L_08A6E998; +SB_L_08A6E998: + ctx.gpr[6] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[6] == 0u; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx)) { + ctx.pc = 0x08A71100u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A71100; + } + goto SB_L_08A6E9A4; + } +SB_L_08A6E9A4: + ctx.gpr[4] = (0u + static_cast(-4097)); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(236))); + ctx.gpr[4] = (ctx.gpr[6] & ctx.gpr[4]); + aot_mem.aot_store32(ctx.gpr[19] + static_cast(236), ctx.gpr[4]); + ctx.gpr[18] = (0u | 0u); + ctx.gpr[20] = (0u | 0u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 2u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6E9E0; + } + goto SB_L_08A6E9D8; + } +SB_L_08A6E9D8: + { const bool branch_taken = 0u == 0u; + ctx.gpr[18] = (0u | 0u); + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 388u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6E9E0; + } +SB_L_08A6E9E0: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 8u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EA78; + } + goto SB_L_08A6E9FC; + } +SB_L_08A6E9FC: + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load8(ctx.gpr[19] + static_cast(483)))))); + ctx.gpr[4] = (ctx.gpr[4] & 2u); + ctx.gpr[4] = (0u < ctx.gpr[4] ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EA78; + } + goto SB_L_08A6EA14; + } +SB_L_08A6EA14: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 4u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + if (ctx.gpr[4] != 0u) { + ctx.fpr[13] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[19] + static_cast(40))); + goto SB_L_08A6EA50; + } + goto SB_L_08A6EA30; +SB_L_08A6EA30: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 6u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EA78; + } + goto SB_L_08A6EA4C; + } +SB_L_08A6EA4C: + ctx.fpr[13] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[19] + static_cast(40))); + goto SB_L_08A6EA50; +SB_L_08A6EA50: + ctx.fcr31 = (ctx.fcr31 & ~0x00800000u) | (((ctx.fpr[13] < ctx.fpr[12])) ? 0x00800000u : 0u); + // nop + { const bool branch_taken = !((ctx.fcr31 & 0x00800000u) != 0u); + // nop + if (branch_taken) { + goto SB_L_08A6EA78; + } + goto SB_L_08A6EA60; + } +SB_L_08A6EA60: + ctx.gpr[18] = (ctx.gpr[23] | 0u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(236))); + ctx.gpr[4] = (ctx.gpr[4] | 4096u); + aot_mem.aot_store32(ctx.gpr[19] + static_cast(236), ctx.gpr[4]); + { const bool branch_taken = 0u == 0u; + aot_mem.aot_store32(ctx.gpr[19] + static_cast(444), ctx.gpr[17]); + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 388u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6EA78; + } +SB_L_08A6EA78: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 8u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EB10; + } + goto SB_L_08A6EA94; + } +SB_L_08A6EA94: + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load8(ctx.gpr[17] + static_cast(483)))))); + ctx.gpr[4] = (ctx.gpr[4] & 2u); + ctx.gpr[4] = (0u < ctx.gpr[4] ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EB10; + } + goto SB_L_08A6EAAC; + } +SB_L_08A6EAAC: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 4u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + if (ctx.gpr[4] != 0u) { + ctx.fpr[13] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[17] + static_cast(40))); + goto SB_L_08A6EAE8; + } + goto SB_L_08A6EAC8; +SB_L_08A6EAC8: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 6u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EB10; + } + goto SB_L_08A6EAE4; + } +SB_L_08A6EAE4: + ctx.fpr[13] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[17] + static_cast(40))); + goto SB_L_08A6EAE8; +SB_L_08A6EAE8: + ctx.fcr31 = (ctx.fcr31 & ~0x00800000u) | (((ctx.fpr[13] < ctx.fpr[12])) ? 0x00800000u : 0u); + // nop + { const bool branch_taken = !((ctx.fcr31 & 0x00800000u) != 0u); + // nop + if (branch_taken) { + goto SB_L_08A6EB10; + } + goto SB_L_08A6EAF8; + } +SB_L_08A6EAF8: + ctx.gpr[18] = (ctx.gpr[23] | 0u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(236))); + ctx.gpr[4] = (ctx.gpr[4] | 4096u); + aot_mem.aot_store32(ctx.gpr[19] + static_cast(236), ctx.gpr[4]); + { const bool branch_taken = 0u == 0u; + aot_mem.aot_store32(ctx.gpr[17] + static_cast(444), ctx.gpr[19]); + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 388u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6EB10; + } +SB_L_08A6EB10: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 8u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EB78; + } + goto SB_L_08A6EB2C; + } +SB_L_08A6EB2C: + ctx.gpr[6] = (aot_mem.aot_load8(ctx.gpr[19] + static_cast(474))); + ctx.gpr[4] = (0u | 4u); + { const bool branch_taken = ctx.gpr[6] != ctx.gpr[4]; + // nop + if (branch_taken) { + goto SB_L_08A6EB78; + } + goto SB_L_08A6EB3C; + } +SB_L_08A6EB3C: + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[6] = (ctx.gpr[6] & 14u); + ctx.gpr[6] = (ctx.gpr[6] ^ 8u); + ctx.gpr[6] = (ctx.gpr[6] < static_cast(1) ? 1u : 0u); + ctx.gpr[6] = (ctx.gpr[6] & 255u); + { const bool branch_taken = ctx.gpr[6] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6EB78; + } + goto SB_L_08A6EB58; + } +SB_L_08A6EB58: + ctx.gpr[6] = (aot_mem.aot_load8(ctx.gpr[17] + static_cast(474))); + { const bool branch_taken = ctx.gpr[6] != ctx.gpr[4]; + // nop + if (branch_taken) { + goto SB_L_08A6EB78; + } + goto SB_L_08A6EB64; + } +SB_L_08A6EB64: + ctx.gpr[18] = (ctx.gpr[23] | 0u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(236))); + ctx.gpr[4] = (ctx.gpr[4] | 4096u); + { const bool branch_taken = 0u == 0u; + aot_mem.aot_store32(ctx.gpr[19] + static_cast(236), ctx.gpr[4]); + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 388u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6EB78; + } +SB_L_08A6EB78: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[19] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 8u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 301u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6EB94; + } +SB_L_08A6EB94: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[17] + static_cast(72))); + ctx.gpr[4] = (ctx.gpr[4] & 14u); + ctx.gpr[4] = (ctx.gpr[4] ^ 4u); + ctx.gpr[4] = (ctx.gpr[4] < static_cast(1) ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 301u, aot_mem); TIER2_SB_RETURN(); + } + goto SB_L_08A6EBB0; + } +SB_L_08A6EBB0: + ctx.gpr[16] = (ctx.gpr[19] | 0u); + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load16(ctx.gpr[16] + static_cast(86)))))); + { const bool branch_taken = ctx.gpr[4] != ctx.gpr[10]; + // nop + if (branch_taken) { + goto SB_L_08A6EBC8; + } + goto SB_L_08A6EBC0; + } +SB_L_08A6EBC0: + { const bool branch_taken = 0u == 0u; + ctx.gpr[18] = (ctx.gpr[23] | 0u); + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6EBC8; + } +SB_L_08A6EBC8: + ctx.gpr[4] = (aot_mem.aot_load8(ctx.gpr[16] + static_cast(476))); + { const bool branch_taken = ctx.gpr[4] == ctx.gpr[5]; + // nop + if (branch_taken) { + goto SB_L_08A6EC1C; + } + goto SB_L_08A6EBD4; + } +SB_L_08A6EBD4: + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load8(ctx.gpr[16] + static_cast(482)))))); + ctx.gpr[4] = (ctx.gpr[4] & 64u); + ctx.gpr[4] = (0u < ctx.gpr[4] ? 1u : 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + { const bool branch_taken = ctx.gpr[4] == ctx.gpr[23]; + // nop + if (branch_taken) { + goto SB_L_08A6EC1C; + } + goto SB_L_08A6EBEC; + } +SB_L_08A6EBEC: + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[16] + static_cast(72))); + ctx.gpr[5] = (ctx.gpr[5] & 2048u); + { const bool branch_taken = ctx.gpr[5] != 0u; + ctx.gpr[4] = (0u | 0u); + if (branch_taken) { + goto SB_L_08A6EC0C; + } + goto SB_L_08A6EBFC; + } +SB_L_08A6EBFC: + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[16] + static_cast(76))); + ctx.gpr[5] = (ctx.gpr[5] & 2048u); + { const bool branch_taken = ctx.gpr[5] == 0u; + ctx.gpr[4] = (ctx.gpr[4] & 255u); + if (branch_taken) { + goto SB_L_08A6EC14; + } + goto SB_L_08A6EC0C; + } +SB_L_08A6EC0C: + ctx.gpr[4] = (ctx.gpr[23] | 0u); + ctx.gpr[4] = (ctx.gpr[4] & 255u); + goto SB_L_08A6EC14; +SB_L_08A6EC14: + { const bool branch_taken = ctx.gpr[4] != 0u; + // nop + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6EC1C; + } +SB_L_08A6EC1C: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[16] + static_cast(444))); + { const bool branch_taken = ctx.gpr[4] != ctx.gpr[17]; + // nop + if (branch_taken) { + goto SB_L_08A6EC30; + } + goto SB_L_08A6EC28; + } +SB_L_08A6EC28: + { const bool branch_taken = 0u == 0u; + ctx.gpr[18] = (ctx.gpr[23] | 0u); + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6EC30; + } +SB_L_08A6EC30: + ctx.gpr[4] = (aot_mem.aot_load8(ctx.gpr[16] + static_cast(473))); + ctx.gpr[4] = (static_cast(ctx.gpr[4]) < 3 ? 1u : 0u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6EC40; + } +SB_L_08A6EC40: + aot_mem.aot_store32(ctx.gpr[29] + static_cast(128), 0u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(136))); + ctx.gpr[4] = (ctx.gpr[4] & ctx.gpr[9]); + aot_mem.aot_store32(ctx.gpr[29] + static_cast(136), ctx.gpr[4]); + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load16(ctx.gpr[19] + static_cast(86)))))); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(7656))); + ctx.gpr[6] = (static_cast(ctx.gpr[4]) < static_cast(ctx.gpr[6]) ? 1u : 0u); + { const bool branch_taken = ctx.gpr[6] == 0u; + ctx.gpr[5] = (0u | 0u); + if (branch_taken) { + goto SB_L_08A6EC74; + } + goto SB_L_08A6EC64; + } +SB_L_08A6EC64: + ctx.gpr[4] = (ctx.gpr[4] << 2u); + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(24))); + ctx.gpr[4] = (ctx.gpr[5] + ctx.gpr[4]); + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); + goto SB_L_08A6EC74; +SB_L_08A6EC74: + ctx.gpr[8] = (aot_mem.aot_load32(ctx.gpr[5] + static_cast(20))); + ctx.gpr[8] = (ctx.gpr[8] + static_cast(32)); + ctx.gpr[4] = (static_cast(static_cast(static_cast(aot_mem.aot_load16(ctx.gpr[19] + static_cast(86)))))); + ctx.gpr[6] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(7656))); + ctx.gpr[6] = (static_cast(ctx.gpr[4]) < static_cast(ctx.gpr[6]) ? 1u : 0u); + { const bool branch_taken = ctx.gpr[6] == 0u; + ctx.gpr[5] = (0u | 0u); + if (branch_taken) { + goto SB_L_08A6ECA0; + } + goto SB_L_08A6EC90; + } +SB_L_08A6EC90: + ctx.gpr[4] = (ctx.gpr[4] << 2u); + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(24))); + ctx.gpr[4] = (ctx.gpr[5] + ctx.gpr[4]); + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(0))); + goto SB_L_08A6ECA0; +SB_L_08A6ECA0: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[5] + static_cast(20))); + ctx.gpr[4] = (ctx.gpr[4] + static_cast(16)); + { const std::uint32_t vfpu_address = ctx.gpr[8] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<0u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<1u, 4u>(vfpu_value); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<0u, 3u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<1u, 3u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 3u; ++i) vfpu_d[i] = vfpu_s[i] - vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<0u, 3u>(vfpu_d); } + ctx.gpr[21] = (ctx.gpr[29] + static_cast(144)); + { float vfpu_value[4]{}; ctx.read_vfpu_vector_ct<0u, 4u>(vfpu_value); + const std::uint32_t vfpu_address = ctx.gpr[21] + static_cast(0); + aot_mem.aot_store32(vfpu_address + 0u, std::bit_cast(vfpu_value[0])); + aot_mem.aot_store32(vfpu_address + 4u, std::bit_cast(vfpu_value[1])); + aot_mem.aot_store32(vfpu_address + 8u, std::bit_cast(vfpu_value[2])); + aot_mem.aot_store32(vfpu_address + 12u, std::bit_cast(vfpu_value[3])); } + { const std::uint32_t vfpu_address = ctx.gpr[21] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<1u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[19] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<4u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[19] + static_cast(16); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<5u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[19] + static_cast(32); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<6u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[19] + static_cast(48); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<7u, 4u>(vfpu_value); } + { float vfpu_matrix[16]{}, vfpu_target_raw[4]{}, vfpu_target[4]{}, vfpu_result[4]{}; + ctx.read_vfpu_matrix_ct<36u, 3u>(vfpu_matrix); + ctx.read_vfpu_vector_ct<1u, 3u>(vfpu_target_raw); + constexpr std::uint32_t vfpu_side = 3u; + constexpr std::uint32_t vfpu_input_length = 3u; + for (std::uint32_t i = 0; i < 4u; ++i) vfpu_target[i] = i < vfpu_input_length ? vfpu_target_raw[i] : 0.0f; + if (vfpu_side - 1u >= vfpu_input_length) vfpu_target[vfpu_side - 1u] = 1.0f; + for (std::uint32_t row = 0; row + 1u < vfpu_side; ++row) { + float sum = 0.0f; + for (std::uint32_t column = 0; column < vfpu_side; ++column) sum += vfpu_matrix[row * 4u + column] * vfpu_target[column]; + vfpu_result[row] = sum; + } + float vfpu_final_row[4]{vfpu_matrix[(vfpu_side - 1u) * 4u + 0u], vfpu_matrix[(vfpu_side - 1u) * 4u + 1u], + vfpu_matrix[(vfpu_side - 1u) * 4u + 2u], vfpu_matrix[(vfpu_side - 1u) * 4u + 3u]}; + ctx.apply_vfpu_source_prefix_ct<4u, 0u>(vfpu_final_row); + ctx.apply_vfpu_source_prefix_ct<4u, 1u>(vfpu_target); + for (std::uint32_t column = 0; column < 4u; ++column) vfpu_result[vfpu_side - 1u] += vfpu_final_row[column] * vfpu_target[column]; + const std::uint32_t vfpu_destination_prefix = ctx.vfpu_ctrl[2]; + const std::uint32_t vfpu_last_lane = vfpu_side - 1u; + ctx.vfpu_ctrl[2] = ((vfpu_destination_prefix & (1u << 8u)) << vfpu_last_lane) | + ((vfpu_destination_prefix & 3u) << (vfpu_last_lane * 2u)); + ctx.write_vfpu_vector_with_destination_prefix_ct<0u, 3u>(vfpu_result); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<0u, 3u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<7u, 3u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 3u; ++i) vfpu_d[i] = vfpu_s[i] + vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<0u, 3u>(vfpu_d); } + ctx.gpr[4] = (ctx.gpr[29] + static_cast(48)); + { float vfpu_value[4]{}; ctx.read_vfpu_vector_ct<0u, 4u>(vfpu_value); + const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + aot_mem.aot_store32(vfpu_address + 0u, std::bit_cast(vfpu_value[0])); + aot_mem.aot_store32(vfpu_address + 4u, std::bit_cast(vfpu_value[1])); + aot_mem.aot_store32(vfpu_address + 8u, std::bit_cast(vfpu_value[2])); + aot_mem.aot_store32(vfpu_address + 12u, std::bit_cast(vfpu_value[3])); } + { const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<0u, 4u>(vfpu_value); } + { float vfpu_value[4]{}; ctx.read_vfpu_vector_ct<0u, 4u>(vfpu_value); + const std::uint32_t vfpu_address = ctx.gpr[21] + static_cast(0); + aot_mem.aot_store32(vfpu_address + 0u, std::bit_cast(vfpu_value[0])); + aot_mem.aot_store32(vfpu_address + 4u, std::bit_cast(vfpu_value[1])); + aot_mem.aot_store32(vfpu_address + 8u, std::bit_cast(vfpu_value[2])); + aot_mem.aot_store32(vfpu_address + 12u, std::bit_cast(vfpu_value[3])); } + ctx.fpr[12] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[29] + static_cast(152))); + ctx.gpr[4] = (ctx.gpr[17] + static_cast(48)); + ctx.fpr[13] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[4] + static_cast(8))); + ctx.fcr31 = (ctx.fcr31 & ~0x00800000u) | (((ctx.fpr[12] < ctx.fpr[13])) ? 0x00800000u : 0u); + // nop + { const bool branch_taken = !((ctx.fcr31 & 0x00800000u) != 0u); + // nop + if (branch_taken) { + goto SB_L_08A6ED10; + } + goto SB_L_08A6ED04; + } +SB_L_08A6ED04: + ctx.gpr[18] = (ctx.gpr[23] | 0u); + { const bool branch_taken = 0u == 0u; + aot_mem.aot_store32(ctx.gpr[16] + static_cast(444), ctx.gpr[17]); + if (branch_taken) { + goto SB_L_08A6ED6C; + } + goto SB_L_08A6ED10; + } +SB_L_08A6ED10: + ctx.gpr[30] = (ctx.gpr[29] + static_cast(64)); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + ctx.gpr[31] = (0x08A6ED20u); + ctx.gpr[5] = (ctx.gpr[30] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0022_entry, 22u, 934u, 0x0885FE8Cu>(ctx, &aot_mem) && ctx.pc == 0x08A6ED20u) goto SB_L_08A6ED20; + TIER2_SB_RETURN(); +SB_L_08A6ED20: + ctx.gpr[4] = (ctx.gpr[30] | 0u); + ctx.gpr[31] = (0x08A6ED2Cu); + ctx.gpr[5] = (ctx.gpr[2] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0023_entry, 23u, 7u, 0x08860358u>(ctx, &aot_mem) && ctx.pc == 0x08A6ED2Cu) goto SB_L_08A6ED2C; + TIER2_SB_RETURN(); +SB_L_08A6ED2C: + { const std::uint32_t vfpu_address = ctx.gpr[21] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<1u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[30] + static_cast(0); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<4u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[30] + static_cast(16); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<5u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[30] + static_cast(32); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<6u, 4u>(vfpu_value); } + { const std::uint32_t vfpu_address = ctx.gpr[30] + static_cast(48); + float vfpu_value[4]{ + std::bit_cast(aot_mem.aot_load32(vfpu_address + 0u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 4u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 8u)), + std::bit_cast(aot_mem.aot_load32(vfpu_address + 12u))}; + ctx.write_vfpu_vector_ct<7u, 4u>(vfpu_value); } + { float vfpu_matrix[16]{}, vfpu_target_raw[4]{}, vfpu_target[4]{}, vfpu_result[4]{}; + ctx.read_vfpu_matrix_ct<36u, 3u>(vfpu_matrix); + ctx.read_vfpu_vector_ct<1u, 3u>(vfpu_target_raw); + constexpr std::uint32_t vfpu_side = 3u; + constexpr std::uint32_t vfpu_input_length = 3u; + for (std::uint32_t i = 0; i < 4u; ++i) vfpu_target[i] = i < vfpu_input_length ? vfpu_target_raw[i] : 0.0f; + if (vfpu_side - 1u >= vfpu_input_length) vfpu_target[vfpu_side - 1u] = 1.0f; + for (std::uint32_t row = 0; row + 1u < vfpu_side; ++row) { + float sum = 0.0f; + for (std::uint32_t column = 0; column < vfpu_side; ++column) sum += vfpu_matrix[row * 4u + column] * vfpu_target[column]; + vfpu_result[row] = sum; + } + float vfpu_final_row[4]{vfpu_matrix[(vfpu_side - 1u) * 4u + 0u], vfpu_matrix[(vfpu_side - 1u) * 4u + 1u], + vfpu_matrix[(vfpu_side - 1u) * 4u + 2u], vfpu_matrix[(vfpu_side - 1u) * 4u + 3u]}; + ctx.apply_vfpu_source_prefix_ct<4u, 0u>(vfpu_final_row); + ctx.apply_vfpu_source_prefix_ct<4u, 1u>(vfpu_target); + for (std::uint32_t column = 0; column < 4u; ++column) vfpu_result[vfpu_side - 1u] += vfpu_final_row[column] * vfpu_target[column]; + const std::uint32_t vfpu_destination_prefix = ctx.vfpu_ctrl[2]; + const std::uint32_t vfpu_last_lane = vfpu_side - 1u; + ctx.vfpu_ctrl[2] = ((vfpu_destination_prefix & (1u << 8u)) << vfpu_last_lane) | + ((vfpu_destination_prefix & 3u) << (vfpu_last_lane * 2u)); + ctx.write_vfpu_vector_with_destination_prefix_ct<0u, 3u>(vfpu_result); } + { float vfpu_s[4]{}, vfpu_t[4]{}, vfpu_d[4]{}; + ctx.read_vfpu_vector_with_source_prefix_ct<0u, 3u, 0u>(vfpu_s); + ctx.read_vfpu_vector_with_source_prefix_ct<7u, 3u, 1u>(vfpu_t); + for (std::uint32_t i = 0; i < 3u; ++i) vfpu_d[i] = vfpu_s[i] + vfpu_t[i]; + ctx.write_vfpu_vector_with_destination_prefix_ct<0u, 3u>(vfpu_d); } + ctx.gpr[4] = (ctx.gpr[29] + static_cast(160)); + { float vfpu_value[4]{}; ctx.read_vfpu_vector_ct<0u, 4u>(vfpu_value); + const std::uint32_t vfpu_address = ctx.gpr[4] + static_cast(0); + aot_mem.aot_store32(vfpu_address + 0u, std::bit_cast(vfpu_value[0])); + aot_mem.aot_store32(vfpu_address + 4u, std::bit_cast(vfpu_value[1])); + aot_mem.aot_store32(vfpu_address + 8u, std::bit_cast(vfpu_value[2])); + aot_mem.aot_store32(vfpu_address + 12u, std::bit_cast(vfpu_value[3])); } + ctx.fpr[12] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[29] + static_cast(168))); + ctx.fcr31 = (ctx.fcr31 & ~0x00800000u) | (((ctx.fpr[12] < ctx.fpr[26])) ? 0x00800000u : 0u); + // nop + { const bool branch_taken = !((ctx.fcr31 & 0x00800000u) != 0u); + // nop + if (branch_taken) { + goto SB_L_08A6ED6C; + } + goto SB_L_08A6ED64; + } +SB_L_08A6ED64: + ctx.gpr[18] = (ctx.gpr[23] | 0u); + aot_mem.aot_store32(ctx.gpr[16] + static_cast(444), ctx.gpr[17]); + goto SB_L_08A6ED6C; +SB_L_08A6ED6C: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(136))); + ctx.gpr[4] = (ctx.gpr[4] & 1u); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6ED7C; + } +SB_L_08A6ED7C: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(128))); + { const bool branch_taken = ctx.gpr[4] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A6ED90; + } + goto SB_L_08A6ED88; + } +SB_L_08A6ED88: + ctx.gpr[31] = (0x08A6ED90u); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(128))); + if (rt.invoke_chained_direct<&recomp_unit_0117_entry, 117u, 129u, 0x089D89E0u>(ctx, &aot_mem) && ctx.pc == 0x08A6ED90u) goto SB_L_08A6ED90; + TIER2_SB_RETURN(); +SB_L_08A6ED90: + { const bool branch_taken = 0u == 0u; + // nop + if (branch_taken) { + recomp_unit_0154_entry(rt, ctx, 388u, aot_mem); TIER2_SB_RETURN(); + } + recomp_unit_0154_entry(rt, ctx, 301u, aot_mem); TIER2_SB_RETURN(); + } + +SB_L_08A71100: + { const bool branch_taken = ctx.gpr[4] != 0u; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A71108; + } +SB_L_08A71108: + ctx.gpr[31] = (0x08A71110u); + ctx.gpr[4] = (ctx.gpr[19] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0152_entry, 152u, 433u, 0x08A65EB4u>(ctx, &aot_mem) && ctx.pc == 0x08A71110u) goto SB_L_08A71110; + TIER2_SB_RETURN(); +SB_L_08A71110: + { const bool branch_taken = ctx.gpr[2] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A7113C; + } + goto SB_L_08A71118; + } +SB_L_08A71118: + ctx.gpr[31] = (0x08A71120u); + ctx.gpr[4] = (ctx.gpr[19] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 100u, 0x08A68CBCu>(ctx, &aot_mem) && ctx.pc == 0x08A71120u) goto SB_L_08A71120; + TIER2_SB_RETURN(); +SB_L_08A71120: + { const bool branch_taken = ctx.gpr[2] != ctx.gpr[17]; + // nop + if (branch_taken) { + goto SB_L_08A7113C; + } + goto SB_L_08A71128; + } +SB_L_08A71128: + ctx.gpr[4] = (ctx.gpr[19] | 0u); + ctx.gpr[31] = (0x08A71134u); + ctx.gpr[5] = (0u | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 101u, 0x08A68CC4u>(ctx, &aot_mem) && ctx.pc == 0x08A71134u) goto SB_L_08A71134; + TIER2_SB_RETURN(); +SB_L_08A71134: + { const bool branch_taken = 0u == 0u; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A7113C; + } +SB_L_08A7113C: + ctx.gpr[31] = (0x08A71144u); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0152_entry, 152u, 433u, 0x08A65EB4u>(ctx, &aot_mem) && ctx.pc == 0x08A71144u) goto SB_L_08A71144; + TIER2_SB_RETURN(); +SB_L_08A71144: + { const bool branch_taken = ctx.gpr[2] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A71180; + } + goto SB_L_08A7114C; + } +SB_L_08A7114C: + ctx.gpr[31] = (0x08A71154u); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 100u, 0x08A68CBCu>(ctx, &aot_mem) && ctx.pc == 0x08A71154u) goto SB_L_08A71154; + TIER2_SB_RETURN(); +SB_L_08A71154: + { const bool branch_taken = ctx.gpr[2] != ctx.gpr[19]; + // nop + if (branch_taken) { + goto SB_L_08A71180; + } + goto SB_L_08A7115C; + } +SB_L_08A7115C: + ctx.gpr[31] = (0x08A71164u); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 102u, 0x08A68CCCu>(ctx, &aot_mem) && ctx.pc == 0x08A71164u) goto SB_L_08A71164; + TIER2_SB_RETURN(); +SB_L_08A71164: + { const bool branch_taken = ctx.gpr[2] == ctx.gpr[19]; + // nop + if (branch_taken) { + goto SB_L_08A71180; + } + goto SB_L_08A7116C; + } +SB_L_08A7116C: + ctx.gpr[4] = (ctx.gpr[17] | 0u); + ctx.gpr[31] = (0x08A71178u); + ctx.gpr[5] = (0u | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 101u, 0x08A68CC4u>(ctx, &aot_mem) && ctx.pc == 0x08A71178u) goto SB_L_08A71178; + TIER2_SB_RETURN(); +SB_L_08A71178: + { const bool branch_taken = 0u == 0u; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A71180; + } +SB_L_08A71180: + ctx.gpr[31] = (0x08A71188u); + ctx.gpr[4] = (ctx.gpr[19] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0152_entry, 152u, 432u, 0x08A65EA0u>(ctx, &aot_mem) && ctx.pc == 0x08A71188u) goto SB_L_08A71188; + TIER2_SB_RETURN(); +SB_L_08A71188: + { const bool branch_taken = ctx.gpr[2] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A711B4; + } + goto SB_L_08A71190; + } +SB_L_08A71190: + ctx.gpr[31] = (0x08A71198u); + ctx.gpr[4] = (ctx.gpr[19] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 103u, 0x08A68CD4u>(ctx, &aot_mem) && ctx.pc == 0x08A71198u) goto SB_L_08A71198; + TIER2_SB_RETURN(); +SB_L_08A71198: + { const bool branch_taken = ctx.gpr[2] != ctx.gpr[17]; + // nop + if (branch_taken) { + goto SB_L_08A711B4; + } + goto SB_L_08A711A0; + } +SB_L_08A711A0: + ctx.gpr[4] = (ctx.gpr[19] | 0u); + ctx.gpr[31] = (0x08A711ACu); + ctx.gpr[5] = (0u | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 104u, 0x08A68CDCu>(ctx, &aot_mem) && ctx.pc == 0x08A711ACu) goto SB_L_08A711AC; + TIER2_SB_RETURN(); +SB_L_08A711AC: + { const bool branch_taken = 0u == 0u; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A711B4; + } +SB_L_08A711B4: + ctx.gpr[31] = (0x08A711BCu); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0152_entry, 152u, 432u, 0x08A65EA0u>(ctx, &aot_mem) && ctx.pc == 0x08A711BCu) goto SB_L_08A711BC; + TIER2_SB_RETURN(); +SB_L_08A711BC: + { const bool branch_taken = ctx.gpr[2] == 0u; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A711C4; + } +SB_L_08A711C4: + ctx.gpr[31] = (0x08A711CCu); + ctx.gpr[4] = (ctx.gpr[17] | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 103u, 0x08A68CD4u>(ctx, &aot_mem) && ctx.pc == 0x08A711CCu) goto SB_L_08A711CC; + TIER2_SB_RETURN(); +SB_L_08A711CC: + { const bool branch_taken = ctx.gpr[2] != ctx.gpr[19]; + // nop + if (branch_taken) { + goto SB_L_08A711E0; + } + goto SB_L_08A711D4; + } +SB_L_08A711D4: + ctx.gpr[4] = (ctx.gpr[17] | 0u); + ctx.gpr[31] = (0x08A711E0u); + ctx.gpr[5] = (0u | 0u); + if (rt.invoke_chained_direct<&recomp_unit_0153_entry, 153u, 104u, 0x08A68CDCu>(ctx, &aot_mem) && ctx.pc == 0x08A711E0u) goto SB_L_08A711E0; + TIER2_SB_RETURN(); +SB_L_08A711E0: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1576))); + { const bool branch_taken = ctx.gpr[4] != 0u; + // nop + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<154u, 0x08A6E8A4u>(ctx)) { + ctx.pc = 0x08A6E8A4u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A6E8A4; + } + goto SB_L_08A711EC; + } +SB_L_08A711EC: + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1600))); + ctx.gpr[4] = (ctx.gpr[4] + static_cast(4)); + ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1596))); + aot_mem.aot_store32(ctx.gpr[29] + static_cast(1600), ctx.gpr[4]); + ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast(1604))); + ctx.gpr[4] = (ctx.gpr[4] + static_cast(-1)); + aot_mem.aot_store32(ctx.gpr[29] + static_cast(1596), ctx.gpr[4]); + { const bool branch_taken = ctx.gpr[5] != 0u; + aot_mem.aot_store32(ctx.gpr[29] + static_cast(1604), ctx.gpr[4]); + if (branch_taken) { + if (!rt.tier2_enter_fused_transfer<154u, 0x08A6E894u>(ctx)) { + ctx.pc = 0x08A6E894u; + TIER2_SB_RETURN(); + } + ++tier2_pending_transfers; + goto SB_L_08A6E894; + } + goto SB_L_08A71210; + } +SB_L_08A71210: + ctx.gpr[2] = (ctx.gpr[22] | 0u); + recomp_unit_0155_entry(rt, ctx, 269u, aot_mem); TIER2_SB_RETURN(); + +#undef TIER2_SB_RETURN +} + +} // namespace vcs diff --git a/profiles/vcs/host/vcs_tier2_superblocks.hpp b/profiles/vcs/host/vcs_tier2_superblocks.hpp new file mode 100644 index 0000000..89e74d3 --- /dev/null +++ b/profiles/vcs/host/vcs_tier2_superblocks.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "psprecomp/guest_memory.hpp" +#include + +namespace psprecomp { +class Runtime; +struct AllegrexContext; +} + +namespace vcs { + +bool tier2_superblocks_enabled() noexcept; +void tier2_superblock_154_155(psprecomp::Runtime &rt, + psprecomp::AllegrexContext &ctx, + psprecomp::GuestMemory::AotFastView &aot_mem, + std::uint32_t entry_pc); + +} // namespace vcs diff --git a/profiles/vcs/scripts/build_release_ninja.bat b/profiles/vcs/scripts/build_release_ninja.bat index aff2d9d..88d430b 100644 --- a/profiles/vcs/scripts/build_release_ninja.bat +++ b/profiles/vcs/scripts/build_release_ninja.bat @@ -92,6 +92,7 @@ if not exist "%CTEST_EXE%" set "CTEST_EXE=ctest.exe" set "NINJA_STATUS=[%%f/%%t %%p ^| %%e elapsed ^| %%r running] " set "BOOTFIX_STAMP=%BUILD%\.vcs_tier2_bootfix_20260816_v1" +set "SUPERBLOCK_STAMP=%BUILD%\.vcs_tier2_superblock_v1_20260816" echo ================================================================ echo VCS - NINJA PERFORMANCE INCREMENTAL BUILD @@ -103,15 +104,37 @@ echo CMake: %CMAKE_EXE% echo Ninja: %NINJA_EXE% echo Ninja workers: %JOBS% echo cl.exe /MP: OFF ^(Ninja owns compile parallelism^) -echo Generated AOT: O3, cold /Ob0, measured hot /Ob3, /GL- +echo Generated AOT: O3, cold /Ob0, measured hot /Ob3; Tier2 hot superblock host O2 /Ob3 echo Host/core LTCG: ON echo AVX2/fast paths: ON echo ================================================================ echo. -echo [0b/7] Reapplying BOOTFIX-safe Tier-2 source transforms... +echo [0b/7] Reapplying BOOTFIX-safe Tier-2 transforms (OPT1 semantic transforms disabled)... call "%PROFILE%\APPLY_TIER2_EXTREME.bat" if errorlevel 1 goto :FAIL +echo [0b2/7] Building profile-guided Tier-2 superblock cluster 0154+0155... +set "PYTHON3_CMD=" +py -3 -c "import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)" >nul 2>&1 +if not errorlevel 1 set "PYTHON3_CMD=py -3" +if not defined PYTHON3_CMD ( + python -c "import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)" >nul 2>&1 + if not errorlevel 1 set "PYTHON3_CMD=python" +) +if not defined PYTHON3_CMD goto :NO_PYTHON3 +echo Python 3: %PYTHON3_CMD% +%PYTHON3_CMD% "%PROFILE%\tools\build_tier2_superblocks.py" "%PROFILE%" +if errorlevel 1 goto :FAIL + +if exist "%BUILD%" if not exist "%SUPERBLOCK_STAMP%" ( + echo. + echo [0c-super/7] Tier2 SUPERBLOCK V1 revision changed - invalidating fused pair objects once... + del /s /q "%BUILD%\*generated_unit_0154*.obj" >nul 2>&1 + del /s /q "%BUILD%\*generated_unit_0155*.obj" >nul 2>&1 + del /s /q "%BUILD%\*vcs_tier2_superblocks*.obj" >nul 2>&1 + del /s /q "%BUILD%\*runtime*.obj" >nul 2>&1 +) + if exist "%BUILD%" if not exist "%BOOTFIX_STAMP%" ( echo. echo [0c/7] BOOTFIX revision changed - invalidating stale .obj/.pch once... @@ -146,6 +169,7 @@ echo [2/7] Building VCSNative with Ninja... "%CMAKE_EXE%" --build "%BUILD%" --parallel %JOBS% --target VCSNative if errorlevel 1 goto :FAIL >"%BOOTFIX_STAMP%" echo VCS Tier2 BOOTFIX 2026-08-16 v1 +>"%SUPERBLOCK_STAMP%" echo VCS Tier2 SUPERBLOCK V1 2026-08-16 echo. echo [2b/7] Building tests and DX12 probes... @@ -232,6 +256,13 @@ exit /b 5 echo ERROR: ninja.exe was not found. pause exit /b 6 + +:NO_PYTHON3 +echo ERROR: Python 3 was not found. +echo The Tier-2 superblock generator requires Python 3. +echo Tried: py -3 and python. +pause +exit /b 7 :TEST_FAIL echo ERROR: regression tests failed. pause diff --git a/profiles/vcs/tools/build_tier2_superblocks.py b/profiles/vcs/tools/build_tier2_superblocks.py new file mode 100644 index 0000000..d7e9aad --- /dev/null +++ b/profiles/vcs/tools/build_tier2_superblocks.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Generate the profile-guided VCS Tier-2 hot superblock V1. + +The second layer deliberately duplicates only the physically hot region, not +whole 16 KiB AOT units. Cold exits resume the original generated unit at +its existing direct-entry id without creating a new logical chain frame. +Logical cross-unit tail frames retain Runtime chain-depth and starvation +accounting through tier2_enter_fused_transfer/tier2_complete_fused_transfers. +""" +from __future__ import annotations +import argparse +import pathlib +import re + +U154_START = 0x08A6E894 +U154_END = 0x08A6ED98 # cold exit label, excluded +U155_START = 0x08A71100 +U155_END = 0x08A71214 # cold epilogue, excluded +HOOK_PCS = { + 154: (0x08A6E894, 0x08A6E8A4), + 155: (0x08A71100, 0x08A711E0, 0x08A711EC), +} +HOOK_BEGIN = '// TIER2_SUPERBLOCK_V1_HOOK_BEGIN\n' +HOOK_END = '// TIER2_SUPERBLOCK_V1_HOOK_END\n' + + +def strip_old_hooks(text: str) -> str: + text = re.sub( + re.escape(HOOK_BEGIN) + r'.*?' + re.escape(HOOK_END), + '', text, flags=re.S) + return text + + +def region(text: str, start: int, end: int) -> str: + a = text.find(f'L_{start:08X}:') + b = text.find(f'L_{end:08X}:') + if a < 0 or b < 0 or b <= a: + raise RuntimeError(f'cannot extract region {start:08X}-{end:08X}') + return text[a:b] + + +def direct_entry_map(full_text: str) -> dict[int, int]: + return {int(pc, 16): int(entry_id) for entry_id, pc in re.findall( + r'case (\d+)u: goto L_([0-9A-F]{8});', full_text)} + + +def transform_region(text: str, unit: int, partner: int, + start: int, end: int, + partner_start: int, partner_end: int, + self_entries: dict[int, int]) -> tuple[str, int, int, int]: + # Namespace labels so both source regions can live in one function. + text = re.sub(r'\bL_([0-9A-F]{8})\b', r'SB_L_\1', text) + + fused = 0 + # Only tail edges between the two selected regions are fused. JALs to other + # units remain invoke_chained_direct so HLE/thread-switch semantics are exact. + tail = re.compile( + rf' \(void\)rt\.invoke_chained_direct<&recomp_unit_{partner:04d}_entry, {partner}u, ' + r'(\d+)u, (0x[0-9A-F]+)u>\(ctx, &aot_mem\); return;' + ) + + def tail_repl(m: re.Match[str]) -> str: + nonlocal fused + entry_id = int(m.group(1)) + target = int(m.group(2), 16) + if not (partner_start <= target < partner_end): + return m.group(0) + fused += 1 + return ( + f' if (!rt.tier2_enter_fused_transfer<{partner}u, 0x{target:08X}u>(ctx)) {{\n' + f' ctx.pc = 0x{target:08X}u;\n' + f' TIER2_SB_RETURN();\n' + f' }}\n' + f' ++tier2_pending_transfers;\n' + f' goto SB_L_{target:08X};' + ) + + text = tail.sub(tail_repl, text) + + # Any ordinary local branch that leaves the selected hot region resumes the + # original generated unit at its existing direct-entry id. This is an + # ordinary native call, *not* invoke_chained_direct: the original local goto + # did not create a logical chain frame or scheduler work item either. + cold_exits: set[int] = set() + cold_dispatch_fallbacks: set[int] = set() + goto_re = re.compile(r'goto SB_L_([0-9A-F]{8});') + + def goto_repl(m: re.Match[str]) -> str: + target = int(m.group(1), 16) + in_self = start <= target < end + in_partner = partner_start <= target < partner_end + if in_self or in_partner: + return m.group(0) + cold_exits.add(target) + entry_id = self_entries.get(target) + if entry_id is not None: + return (f'recomp_unit_{unit:04d}_entry(rt, ctx, {entry_id}u, aot_mem); ' + f'TIER2_SB_RETURN();') + cold_dispatch_fallbacks.add(target) + return f'ctx.pc = 0x{target:08X}u; TIER2_SB_RETURN();' + + text = goto_re.sub(goto_repl, text) + + # Remaining returns are exits caused by non-fused calls, unsupported paths, + # or chain fallback. Unwind the logical fused tail frames first. + text = text.replace('return;', 'TIER2_SB_RETURN();') + return text, fused, len(cold_exits), len(cold_dispatch_fallbacks) + + +def patch_hooks(text: str, unit: int) -> str: + text = strip_old_hooks(text) + if '#include "vcs_tier2_superblocks.hpp"' not in text: + text = text.replace('#include "generated_units.hpp"\n', + '#include "generated_units.hpp"\n#include "vcs_tier2_superblocks.hpp"\n', 1) + for pc in HOOK_PCS[unit]: + label = f'L_{pc:08X}:\n' + if label not in text: + raise RuntimeError(f'missing hook label {pc:08X} in unit {unit:04d}') + hook = ( + label + HOOK_BEGIN + + ' if (vcs::tier2_superblocks_enabled()) {\n' + f' vcs::tier2_superblock_154_155(rt, ctx, aot_mem, 0x{pc:08X}u);\n' + ' return;\n' + ' }\n' + HOOK_END + ) + text = text.replace(label, hook, 1) + return text + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument('profile', nargs='?', type=pathlib.Path, + default=pathlib.Path(__file__).resolve().parents[1]) + args = ap.parse_args() + profile = args.profile.resolve() + generated = profile / 'generated' + host = profile / 'host' + + raw154 = strip_old_hooks((generated/'generated_unit_0154.cpp').read_text(encoding='utf-8')) + raw155 = strip_old_hooks((generated/'generated_unit_0155.cpp').read_text(encoding='utf-8')) + r154 = region(raw154, U154_START, U154_END) + r155 = region(raw155, U155_START, U155_END) + e154 = direct_entry_map(raw154) + e155 = direct_entry_map(raw155) + t154, f154, c154, d154 = transform_region( + r154, 154, 155, U154_START, U154_END, U155_START, U155_END, e154) + t155, f155, c155, d155 = transform_region( + r155, 155, 154, U155_START, U155_END, U154_START, U154_END, e155) + + cpp = f'''// AUTO-GENERATED by profiles/vcs/tools/build_tier2_superblocks.py. +// Profile-guided second-layer superblock: 0154/0155 dominant entity loop. +#include "vcs_tier2_superblocks.hpp" +#include "vcs_runtime_log.hpp" +#include "psprecomp/runtime.hpp" +#include "generated_units.hpp" + +#include +#include + +namespace vcs {{ + +// Generated AOT call targets are declared in namespace psprecomp. The copied +// region text intentionally stays byte-close to the original units, so make +// those names visible here instead of rewriting every external helper target. +using namespace psprecomp; + +bool tier2_superblocks_enabled() noexcept {{ + static const bool enabled = [] {{ + const char *value = std::getenv("PSPRECOMP_TIER2_SUPERBLOCKS"); + return value == nullptr || std::strcmp(value, "0") != 0; + }}(); + return enabled; +}} + +void tier2_superblock_154_155(psprecomp::Runtime &rt, + psprecomp::AllegrexContext &ctx, + psprecomp::GuestMemory::AotFastView &aot_mem, + std::uint32_t entry_pc) {{ + std::uint32_t tier2_pending_transfers = 0u; +#define TIER2_SB_RETURN() do {{ \\ + if (tier2_pending_transfers != 0u) {{ \\ + (void)rt.tier2_complete_fused_transfers(ctx, tier2_pending_transfers); \\ + tier2_pending_transfers = 0u; \\ + }} \\ + return; \\ + }} while (false) + + switch (entry_pc) {{ + case 0x08A6E894u: goto SB_L_08A6E894; + case 0x08A6E8A4u: goto SB_L_08A6E8A4; + case 0x08A71100u: goto SB_L_08A71100; + case 0x08A711E0u: goto SB_L_08A711E0; + case 0x08A711ECu: goto SB_L_08A711EC; + default: ctx.pc = entry_pc; TIER2_SB_RETURN(); + }} + +{t154} +{t155} +#undef TIER2_SB_RETURN +}} + +}} // namespace vcs +''' + out = host/'vcs_tier2_superblocks.cpp' + changed_cpp = not out.exists() or out.read_text(encoding='utf-8') != cpp + if changed_cpp: + out.write_text(cpp, encoding='utf-8', newline='\n') + + changes = 0 + for unit, raw in ((154, raw154), (155, raw155)): + patched = patch_hooks(raw, unit) + p = generated/f'generated_unit_{unit:04d}.cpp' + if p.read_text(encoding='utf-8') != patched: + p.write_text(patched, encoding='utf-8', newline='\n') + changes += 1 + + print('Tier2 SUPERBLOCK V1:', + f'host_changed={int(changed_cpp)} hook_units_changed={changes}', + f'fused_tail_edges={f154+f155} cold_exit_labels={c154+c155}', + f'cold_dispatch_fallbacks={d154+d155} lines={len(cpp.splitlines())}') + return 0 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/profiles/vcs/tools/optimize_generated_gpr_readonly.py b/profiles/vcs/tools/optimize_generated_gpr_readonly.py new file mode 100644 index 0000000..4c0f94a --- /dev/null +++ b/profiles/vcs/tools/optimize_generated_gpr_readonly.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Read-only basic-block GPR cache for measured VCS AOT hotspots. + +Safety rule: ONLY cache GPRs that are read but never written anywhere in the +same guest basic block. There is therefore no dirty state and no writeback. +Blocks containing Runtime calls or AllegrexContext member-function calls are +skipped because those calls may observe/mutate architectural GPR state. +Guest-memory AotFastView calls are allowed: they operate on guest memory and do +not receive AllegrexContext. +""" +from __future__ import annotations +import argparse, collections, pathlib, re +from dataclasses import dataclass + +LABEL_RE = re.compile(r"(?m)^L_[0-9A-F]+:\n") +GPR_RE = re.compile(r"ctx\.gpr\[([1-9]|[12][0-9]|3[01])\]") +CTX_METHOD_RE = re.compile(r"ctx\.[A-Za-z_]\w*\s*\(") +MARKER = "// PSPRECOMP_TIER2_GPR_READONLY_CACHE_V1" + +@dataclass +class Stats: + blocks_cached: int = 0 + registers_cached: int = 0 + occurrences_replaced: int = 0 + blocks_skipped_runtime: int = 0 + blocks_skipped_ctx_method: int = 0 + def add(self, other: 'Stats') -> None: + for name in self.__dataclass_fields__: + setattr(self, name, getattr(self, name) + getattr(other, name)) + +def _written(block: str, reg: int) -> bool: + tok = re.escape(f"ctx.gpr[{reg}]") + # Direct/compound assignment, increment/decrement, or prefix increment. + if re.search(tok + r"\s*(?:=|\+=|-=|\*=|/=|%=|&=|\|=|\^=|<<=|>>=|\+\+|--)", block): + return True + if re.search(r"(?:\+\+|--)\s*" + tok, block): + return True + return False + +def _transform_block(label: str, block: str, threshold: int) -> tuple[str, Stats]: + s = Stats() + if "rt." in block or "ctx.execute_" in block: + s.blocks_skipped_runtime = 1 + return label + block, s + # Any AllegrexContext method call is conservatively a synchronization + # boundary. Direct ctx.gpr/fpr/vfpu field accesses are not matched here. + if CTX_METHOD_RE.search(block): + s.blocks_skipped_ctx_method = 1 + return label + block, s + + counts = collections.Counter(GPR_RE.findall(block)) + selected = [] + for reg_s, count in counts.items(): + reg = int(reg_s) + if count >= threshold and not _written(block, reg): + selected.append(reg) + selected.sort() + if not selected: + return label + block, s + + original_counts = {r: block.count(f"ctx.gpr[{r}]") for r in selected} + for r in selected: + block = block.replace(f"ctx.gpr[{r}]", f"g{r}_ro") + + init = ''.join(f" const std::uint32_t g{r}_ro = ctx.gpr[{r}];\n" for r in selected) + s.blocks_cached = 1 + s.registers_cached = len(selected) + s.occurrences_replaced = sum(original_counts.values()) + # Per-label scope prevents any generated goto from bypassing a C++ local + # initialization belonging to a different guest basic block. + return label + "{\n" + init + block + "}\n", s + +def transform_text(text: str, threshold: int = 4) -> tuple[str, Stats]: + if MARKER in text: + return text, Stats() + matches = list(LABEL_RE.finditer(text)) + if not matches: + return text, Stats() + out=[]; last=0; total=Stats() + for i,m in enumerate(matches): + start=m.end() + if i+1 < len(matches): + end=matches[i+1].start() + else: + end=text.find("\n}\n\nvoid ", start) + if end < 0: end=len(text) + out.append(text[last:m.start()]) + transformed, s = _transform_block(text[m.start():start], text[start:end], threshold) + out.append(transformed); total.add(s); last=end + out.append(text[last:]) + result=''.join(out) + if result != text: + result = MARKER + "\n" + result + return result, total + +def optimize_file(path: pathlib.Path, threshold: int, check: bool) -> Stats: + original=path.read_text(encoding='utf-8') + transformed,s=transform_text(original, threshold) + if transformed != original and not check: + path.write_text(transformed, encoding='utf-8', newline='\n') + return s + +def main() -> int: + ap=argparse.ArgumentParser() + ap.add_argument('paths', nargs='+', type=pathlib.Path) + ap.add_argument('--threshold', type=int, default=4) + ap.add_argument('--check', action='store_true') + a=ap.parse_args(); total=Stats(); files=[] + for p in a.paths: + files.extend(sorted(p.glob('generated_unit_*.cpp'))) if p.is_dir() else files.append(p) + changed=0 + for p in files: + before=p.read_text(encoding='utf-8') + s=optimize_file(p,a.threshold,a.check); total.add(s) + if MARKER not in before and s.blocks_cached: changed += 1 + print(f"GPR-RO cache: files={len(files)} changed={changed} blocks={total.blocks_cached} " + f"locals={total.registers_cached} occurrences={total.occurrences_replaced} " + f"skip_runtime={total.blocks_skipped_runtime} skip_ctx_method={total.blocks_skipped_ctx_method} " + f"threshold={a.threshold} check={int(a.check)}") + return 0 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/profiles/vcs/tools/optimize_hot_leaf_calls.py b/profiles/vcs/tools/optimize_hot_leaf_calls.py new file mode 100644 index 0000000..0c318d3 --- /dev/null +++ b/profiles/vcs/tools/optimize_hot_leaf_calls.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Profile-guided lowering of tiny, side-effect-known VCS AOT leaf helpers. + +These helpers were confirmed from the generated corpus and the 2026-08-16 +GUESTHOT trace. They contain only one RAM load/store (or one masked flag test) +plus the guest return. Replacing the cross-unit call with the exact leaf +semantics removes Runtime chaining/local-dispatch overhead while preserving the +same starvation-boundary accounting via account_inlined_generated_leaf(). The continuation PC is materialized first, exactly as the original generated callee does on return. + +The pass is intentionally exact and idempotent: only the known generated call +shapes are rewritten. +""" +from __future__ import annotations +import argparse +import pathlib +import re + +MARKER = "// PSPRECOMP_VCS_HOT_LEAF_LOWERING_V1" + +# (target unit, entry id, target PC) -> statement body before scheduler account. +LEAVES = { + (152, 432, 0x08A65EA0): ( + "ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(72)));\n" + " ctx.gpr[4] = (ctx.gpr[4] & 14u);\n" + " ctx.gpr[2] = ((ctx.gpr[4] ^ 6u) < static_cast(1) ? 1u : 0u);" + ), + (152, 433, 0x08A65EB4): ( + "ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(72)));\n" + " ctx.gpr[4] = (ctx.gpr[4] & 14u);\n" + " ctx.gpr[2] = ((ctx.gpr[4] ^ 8u) < static_cast(1) ? 1u : 0u);" + ), + (153, 100, 0x08A68CBC): + "ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(444)));", + (153, 101, 0x08A68CC4): + "aot_mem.aot_store32(ctx.gpr[4] + static_cast(444), ctx.gpr[5]);", + (153, 102, 0x08A68CCC): + "ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(448)));", + (153, 103, 0x08A68CD4): + "ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast(2116)));", + (153, 104, 0x08A68CDC): + "aot_mem.aot_store32(ctx.gpr[4] + static_cast(2116), ctx.gpr[5]);", + (44, 665, 0x088B7AD8): ( + "ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast(-24624)));\n" + " aot_mem.aot_store8(ctx.gpr[4] + static_cast(2), static_cast(0u));" + ), +} + +CALL_RE = re.compile( + r"^(?P\s*)if \(rt\.invoke_chained_direct<&recomp_unit_(?P\d+)_entry, " + r"(?P\d+)u, (?P\d+)u, 0x(?P[0-9A-F]+)u>\(ctx, &aot_mem\) && " + r"ctx\.pc == 0x(?P[0-9A-F]+)u\) goto L_(?P=ret);\s*$", + re.M, +) + +def transform(text: str) -> tuple[str, int]: + if MARKER in text: + return text, 0 + count = 0 + def repl(m: re.Match[str]) -> str: + nonlocal count + key=(int(m.group('unit')), int(m.group('entry')), int(m.group('pc'),16)) + body=LEAVES.get(key) + if body is None: + return m.group(0) + count += 1 + indent=m.group('indent') + ret=m.group('ret') + body=body.replace('\n ', '\n'+indent) + return (f"{indent}{{ // Tier-2 inlined generated leaf {key[0]}:{key[1]}\n" + f"{indent} {body}\n" + f"{indent} ctx.pc = 0x{ret}u;\n" + f"{indent} if (!rt.account_inlined_generated_leaf(ctx)) return;\n" + f"{indent} goto L_{ret};\n" + f"{indent}}}") + out=CALL_RE.sub(repl,text) + if count: + out=MARKER+'\n'+out + return out,count + +def main() -> int: + ap=argparse.ArgumentParser() + ap.add_argument('generated',type=pathlib.Path) + ap.add_argument('--check',action='store_true') + args=ap.parse_args() + total=files=0 + for name in ('generated_unit_0155.cpp','generated_unit_0085.cpp'): + p=args.generated/name + if not p.exists(): + raise FileNotFoundError(p) + src=p.read_text(encoding='utf-8') + out,n=transform(src) + total += n + files += int(n>0) + if n and not args.check: + p.write_text(out,encoding='utf-8',newline='\n') + print(f'hot_leaf_lowering: files={files} calls_inlined={total} check={int(args.check)}') + return 0 + +if __name__=='__main__': + raise SystemExit(main()) diff --git a/src/runtime.cpp b/src/runtime.cpp index c2d5f63..c030537 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -135,6 +136,108 @@ bool runtime_thread_switch_generation_matches(std::uint64_t generation) noexcept return generation == g_runtime_thread_switch_generation_fast; } +bool g_guest_hotspot_profile_enabled = false; +std::uint32_t g_guest_hotspot_sample_mask = 0xFFu; +std::uint64_t g_guest_hotspot_unit_calls[kUnitProfileCapacity]{}; +namespace { +struct GuestHotspotPcSlot { + std::uint32_t pc{}; + std::uint16_t unit{}; + std::uint16_t occupied{}; + std::uint64_t samples{}; + std::uint64_t inclusive_sample_ns{}; +}; +std::uint64_t g_guest_hotspot_unit_samples[kUnitProfileCapacity]{}; +std::uint64_t g_guest_hotspot_unit_ns[kUnitProfileCapacity]{}; +std::array g_guest_hotspot_pc_slots{}; +std::uint64_t g_guest_hotspot_total_samples = 0u; + +std::size_t guest_hotspot_hash(std::uint32_t pc) noexcept { + std::uint32_t x = pc >> 2u; + x ^= x >> 16u; + x *= 0x7FEB352Du; + x ^= x >> 15u; + return static_cast(x) & (kGuestHotspotPcCapacity - 1u); +} +} + +void set_guest_hotspot_profile(bool enabled, std::uint32_t sample_shift) noexcept { + if (sample_shift > 16u) sample_shift = 16u; + g_guest_hotspot_sample_mask = sample_shift == 0u ? 0u : ((1u << sample_shift) - 1u); + g_guest_hotspot_profile_enabled = enabled; +} + +std::uint64_t guest_hotspot_clock_ns() noexcept { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + +void guest_hotspot_record_sample(std::uint32_t unit, std::uint32_t pc, + std::uint64_t elapsed_ns) noexcept { + if (!g_guest_hotspot_profile_enabled || unit >= kUnitProfileCapacity) return; + ++g_guest_hotspot_unit_samples[unit]; + g_guest_hotspot_unit_ns[unit] += elapsed_ns; + ++g_guest_hotspot_total_samples; + if (pc == 0u) return; + + std::size_t slot = guest_hotspot_hash(pc); + for (std::size_t probe = 0u; probe < 12u; ++probe) { + GuestHotspotPcSlot &entry = g_guest_hotspot_pc_slots[(slot + probe) & (kGuestHotspotPcCapacity - 1u)]; + if (entry.occupied == 0u) { + entry.pc = pc; + entry.unit = static_cast(unit); + entry.occupied = 1u; + } + if (entry.pc == pc && entry.unit == unit) { + ++entry.samples; + entry.inclusive_sample_ns += elapsed_ns; + return; + } + } + // The table is intentionally fixed and allocation-free. Extremely unlikely + // collision overflow drops only this sampled PC; unit totals remain exact. +} + +GuestHotspotSnapshot consume_guest_hotspot_profile(std::size_t unit_limit, std::size_t pc_limit) { + GuestHotspotSnapshot out; + out.sample_stride = g_guest_hotspot_sample_mask + 1u; + out.total_samples = g_guest_hotspot_total_samples; + for (std::size_t unit = 0u; unit < kUnitProfileCapacity; ++unit) { + const std::uint64_t calls = g_guest_hotspot_unit_calls[unit]; + const std::uint64_t samples = g_guest_hotspot_unit_samples[unit]; + const std::uint64_t ns = g_guest_hotspot_unit_ns[unit]; + out.total_unit_calls += calls; + if (calls != 0u || samples != 0u) + out.units.push_back(GuestHotspotUnitEntry{static_cast(unit), calls, samples, ns}); + g_guest_hotspot_unit_calls[unit] = 0u; + g_guest_hotspot_unit_samples[unit] = 0u; + g_guest_hotspot_unit_ns[unit] = 0u; + } + for (GuestHotspotPcSlot &slot : g_guest_hotspot_pc_slots) { + if (slot.occupied != 0u && slot.samples != 0u) + out.pcs.push_back(GuestHotspotPcEntry{slot.unit, slot.pc, slot.samples, slot.inclusive_sample_ns}); + slot = {}; + } + g_guest_hotspot_total_samples = 0u; + + const auto estimated_cost = [stride = static_cast(out.sample_stride)](const auto &e) { + return e.samples == 0u ? 0u : e.inclusive_sample_ns * stride; + }; + std::sort(out.units.begin(), out.units.end(), [&](const auto &a, const auto &b) { + const auto ac = estimated_cost(a), bc = estimated_cost(b); + if (ac != bc) return ac > bc; + return a.calls > b.calls; + }); + std::sort(out.pcs.begin(), out.pcs.end(), [&](const auto &a, const auto &b) { + const auto ac = estimated_cost(a), bc = estimated_cost(b); + if (ac != bc) return ac > bc; + return a.samples > b.samples; + }); + if (out.units.size() > unit_limit) out.units.resize(unit_limit); + if (out.pcs.size() > pc_limit) out.pcs.resize(pc_limit); + return out; +} + Runtime::Runtime(std::uint32_t ram_size) : memory_(ram_size) { // Most commercial PSP titles use a few hundred import stubs. Seed a small // binding table so first use of a late-numbered import does not reallocate @@ -257,6 +360,25 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView } const std::uint32_t target_pc = ctx.pc; + std::uint32_t guest_hotspot_unit = static_cast(kUnitProfileCapacity); + bool guest_hotspot_sample = false; + std::uint64_t guest_hotspot_start_ns = 0u; + if (g_guest_hotspot_profile_enabled && generated_unit_layout_valid_ && generated_unit_span_ != 0u) { + const std::uint32_t canonical_pc = memory_.canonical(target_pc); + if (canonical_pc >= generated_unit_base_) { + const std::uint32_t delta = canonical_pc - generated_unit_base_; + const std::uint32_t unit_index = generated_unit_span_ == 16384u + ? (delta >> 14u) : (delta / generated_unit_span_); + if (unit_index < kUnitProfileCapacity) { + guest_hotspot_unit = unit_index; + ++g_guest_hotspot_unit_calls[unit_index]; + const std::uint64_t ticket = ++guest_hotspot_ticket_; + guest_hotspot_sample = + (ticket & static_cast(g_guest_hotspot_sample_mask)) == 0u; + if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns(); + } + } + } const std::uint32_t native_depth = chain_depth_; // Always guard execution-context ownership. Even a clean generated unit can // reach a nested direct chain whose scheduler boundary switches PSP thread. @@ -280,6 +402,12 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView } else { function(*this, ctx); } + if (guest_hotspot_sample) { + const std::uint64_t end_ns = guest_hotspot_clock_ns(); + guest_hotspot_record_sample(guest_hotspot_unit, target_pc, + end_ns >= guest_hotspot_start_ns + ? end_ns - guest_hotspot_start_ns : 0u); + } #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_post_chained_call_hook != nullptr) g_post_chained_call_hook(*this, ctx, target_pc, native_depth); @@ -309,6 +437,15 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index if (g_unit_profile_enabled) ++g_unit_profile_counts[unit_index]; const std::uint32_t target_pc = ctx.pc; + bool guest_hotspot_sample = false; + std::uint64_t guest_hotspot_start_ns = 0u; + if (g_guest_hotspot_profile_enabled) { + ++g_guest_hotspot_unit_calls[unit_index]; + const std::uint64_t ticket = ++guest_hotspot_ticket_; + guest_hotspot_sample = + (ticket & static_cast(g_guest_hotspot_sample_mask)) == 0u; + if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns(); + } const std::uint32_t native_depth = chain_depth_; const std::uint64_t caller_generation = g_runtime_thread_switch_generation_fast; #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) @@ -325,6 +462,12 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index } else { function(*this, ctx); } + if (guest_hotspot_sample) { + const std::uint64_t end_ns = guest_hotspot_clock_ns(); + guest_hotspot_record_sample(unit_index, target_pc, + end_ns >= guest_hotspot_start_ns + ? end_ns - guest_hotspot_start_ns : 0u); + } #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_post_chained_call_hook != nullptr) g_post_chained_call_hook(*this, ctx, target_pc, native_depth); diff --git a/tests/test_main.cpp b/tests/test_main.cpp index ebed7e0..2eb0ebb 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -154,6 +154,62 @@ static void nested_direct_middle(psprecomp::Runtime &runtime, psprecomp::Allegre if (!runtime.invoke_chained_direct<&nested_direct_leaf, 1u>(ctx)) return; nested_direct_middle_resumed = true; } + +static void test_tier2_fused_transfer_accounting() { + // A Tier-2 fused edge removes an invoke_chained_direct native frame but must + // preserve both chain depth and the exact scheduler-work count when that + // logical frame unwinds. + { + psprecomp::Runtime runtime; + psprecomp::AllegrexContext ctx{}; + chained_tick_count = 0u; + chained_tick_switch_context = false; + psprecomp::set_runtime_starvation_hook(&chained_tick, 2u); + psprecomp::set_runtime_thread_identity(3, "tier2-superblock"); + + require(runtime.tier2_enter_fused_transfer<154u, 0x08A6E8A4u>(ctx), + "Tier2 fused transfer rejected first logical chain frame"); + require(runtime.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx), + "Tier2 fused transfer rejected second logical chain frame"); + require(runtime.tier2_complete_fused_transfers(ctx, 2u), + "Tier2 fused unwind rejected same PSP execution context"); + require(chained_tick_count == 1u, + "Tier2 fused unwind did not preserve scheduler dispatch cadence"); + } + + { + psprecomp::Runtime runtime; + psprecomp::AllegrexContext ctx{}; + chained_tick_count = 0u; + chained_tick_switch_context = true; + psprecomp::set_runtime_starvation_hook(&chained_tick, 1u); + psprecomp::set_runtime_thread_identity(3, "tier2-superblock"); + + require(runtime.tier2_enter_fused_transfer<154u, 0x08A6E8A4u>(ctx), + "Tier2 fused switch test rejected frame 1"); + require(runtime.tier2_enter_fused_transfer<155u, 0x08A71100u>(ctx), + "Tier2 fused switch test rejected frame 2"); + require(runtime.tier2_enter_fused_transfer<154u, 0x08A6E8A4u>(ctx), + "Tier2 fused switch test rejected frame 3"); + require(!runtime.tier2_complete_fused_transfers(ctx, 3u), + "Tier2 fused unwind retained a stale native superblock after PSP context switch"); + require(chained_tick_count == 1u && psprecomp::runtime_thread_uid() == 7, + "Tier2 fused unwind ran another scheduler boundary after losing PSP ownership"); + + // All three logical DepthGuards must have unwound even on the switched + // path; otherwise future generated chains would eventually hit a false + // depth limit. + ctx.pc = 0u; + require(runtime.tier2_enter_fused_transfer<154u, 0x08A6E8A4u>(ctx), + "Tier2 fused unwind leaked logical chain depth after context switch"); + (void)runtime.tier2_complete_fused_transfers(ctx, 1u); + } + + psprecomp::set_runtime_starvation_hook(nullptr, 0u); + chained_tick_switch_context = false; + psprecomp::set_runtime_thread_identity(-1, "none"); +} + static void test_nested_direct_chain_context_guard() { psprecomp::Runtime runtime; constexpr std::uint32_t base = 0x08800000u; @@ -804,6 +860,7 @@ int main() { try { test_import_return_context_guard(); test_chained_call_context_guard(); + test_tier2_fused_transfer_accounting(); test_nested_direct_chain_context_guard(); psprecomp::GuestMemory mem; @@ -1780,6 +1837,25 @@ int main() { require(nids.resolve("SysMemUserForUser", 0x7591C7DBu) == "sceKernelSetCompiledSdkVersion", "PSP boot NID registry failed"); + // Guest hotspot sampler: exact unit census + sparse PC timing snapshot. + psprecomp::set_guest_hotspot_profile(true, 2u); // stride 4 for deterministic fixture + psprecomp::g_guest_hotspot_unit_calls[23u] = 8u; + psprecomp::g_guest_hotspot_unit_calls[24u] = 4u; + psprecomp::guest_hotspot_record_sample(23u, 0x0890C000u, 100u); + psprecomp::guest_hotspot_record_sample(23u, 0x0890C000u, 300u); + psprecomp::guest_hotspot_record_sample(24u, 0x08910000u, 50u); + const auto hotspot = psprecomp::consume_guest_hotspot_profile(8u, 8u); + require(hotspot.sample_stride == 4u && hotspot.total_unit_calls == 12u && hotspot.total_samples == 3u, + "guest hotspot summary failed"); + require(!hotspot.units.empty() && hotspot.units.front().unit == 23u && hotspot.units.front().calls == 8u, + "guest hotspot unit ranking failed"); + require(!hotspot.pcs.empty() && hotspot.pcs.front().pc == 0x0890C000u && hotspot.pcs.front().samples == 2u, + "guest hotspot PC ranking failed"); + const auto hotspot_reset = psprecomp::consume_guest_hotspot_profile(8u, 8u); + require(hotspot_reset.total_unit_calls == 0u && hotspot_reset.total_samples == 0u, + "guest hotspot reset failed"); + psprecomp::set_guest_hotspot_profile(false); + psprecomp::Runtime runtime; runtime.set_game_root(std::filesystem::current_path()); const auto translated = runtime.translate_path("disc0:/PSP_GAME/USRDIR/data.bin");