mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-27 08:55:30 -04:00
otimizações round 4
otimizações round 4
This commit is contained in:
@@ -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<GuestHotspotUnitEntry> units;
|
||||
std::vector<GuestHotspotPcEntry> 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<std::uint64_t>(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 <std::uint32_t UnitIndex, std::uint32_t TargetPc>
|
||||
[[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_{};
|
||||
|
||||
@@ -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%
|
||||
|
||||
@@ -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
|
||||
"$<$<CONFIG:Release>:/Ob3>;$<$<CONFIG:RelWithDebInfo>:/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)
|
||||
|
||||
|
||||
@@ -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 = <continuation PC>;
|
||||
if (!rt.account_inlined_generated_leaf(ctx)) return;
|
||||
goto <continuation label>;
|
||||
```
|
||||
|
||||
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%
|
||||
@@ -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
|
||||
...
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "psprecomp/runtime.hpp"
|
||||
#include "generated_units.hpp"
|
||||
#include "vcs_tier2_superblocks.hpp"
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -6791,6 +6792,12 @@ L_08A6E828:
|
||||
aot_mem.aot_store32(ctx.gpr[29] + static_cast<std::uint32_t>(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<std::uint32_t>(1600)));
|
||||
ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(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<std::uint32_t>(1576)));
|
||||
ctx.gpr[17] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(0)));
|
||||
ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(8)));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "psprecomp/runtime.hpp"
|
||||
#include "generated_units.hpp"
|
||||
#include "vcs_tier2_superblocks.hpp"
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -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<std::uint32_t>(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<std::uint32_t>(1600)));
|
||||
ctx.gpr[4] = (ctx.gpr[4] + static_cast<std::uint32_t>(4));
|
||||
ctx.gpr[5] = (aot_mem.aot_load32(ctx.gpr[29] + static_cast<std::uint32_t>(1596)));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<std::uint64_t>(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<std::byte> 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<std::uint64_t>(s.report.dx12_srv_high_water, s.next_srv);
|
||||
return s.report;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<std::uint64_t>(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<GuestCallbackInvocation> &callbacks,
|
||||
const std::atomic<std::uint32_t> *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<std::uint64_t>(
|
||||
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::uint64_t>(std::max<std::int64_t>(0, frame_us));
|
||||
a.frame_us_min = std::min(a.frame_us_min, static_cast<std::uint64_t>(std::max<std::int64_t>(0, frame_us)));
|
||||
a.frame_us_max = std::max(a.frame_us_max, static_cast<std::uint64_t>(std::max<std::int64_t>(0, frame_us)));
|
||||
a.guest_cpu_us_sum += guest_cpu_us;
|
||||
a.ge_us_sum += static_cast<std::uint64_t>(std::max<std::int64_t>(0, ge_us));
|
||||
a.ge_wait_us_sum += static_cast<std::uint64_t>(std::max<std::int64_t>(0, ge_async_wait_us));
|
||||
a.present_us_sum += static_cast<std::uint64_t>(std::max<std::int64_t>(0, present_us));
|
||||
a.io_us_sum += static_cast<std::uint64_t>(std::max<std::int64_t>(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<char *>(guest_destination),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "psprecomp/guest_memory.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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 <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
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())
|
||||
@@ -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())
|
||||
@@ -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<std::uint32_t>(72)));\n"
|
||||
" ctx.gpr[4] = (ctx.gpr[4] & 14u);\n"
|
||||
" ctx.gpr[2] = ((ctx.gpr[4] ^ 6u) < static_cast<std::uint32_t>(1) ? 1u : 0u);"
|
||||
),
|
||||
(152, 433, 0x08A65EB4): (
|
||||
"ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(72)));\n"
|
||||
" ctx.gpr[4] = (ctx.gpr[4] & 14u);\n"
|
||||
" ctx.gpr[2] = ((ctx.gpr[4] ^ 8u) < static_cast<std::uint32_t>(1) ? 1u : 0u);"
|
||||
),
|
||||
(153, 100, 0x08A68CBC):
|
||||
"ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(444)));",
|
||||
(153, 101, 0x08A68CC4):
|
||||
"aot_mem.aot_store32(ctx.gpr[4] + static_cast<std::uint32_t>(444), ctx.gpr[5]);",
|
||||
(153, 102, 0x08A68CCC):
|
||||
"ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(448)));",
|
||||
(153, 103, 0x08A68CD4):
|
||||
"ctx.gpr[2] = (aot_mem.aot_load32(ctx.gpr[4] + static_cast<std::uint32_t>(2116)));",
|
||||
(153, 104, 0x08A68CDC):
|
||||
"aot_mem.aot_store32(ctx.gpr[4] + static_cast<std::uint32_t>(2116), ctx.gpr[5]);",
|
||||
(44, 665, 0x088B7AD8): (
|
||||
"ctx.gpr[4] = (aot_mem.aot_load32(ctx.gpr[28] + static_cast<std::uint32_t>(-24624)));\n"
|
||||
" aot_mem.aot_store8(ctx.gpr[4] + static_cast<std::uint32_t>(2), static_cast<std::uint8_t>(0u));"
|
||||
),
|
||||
}
|
||||
|
||||
CALL_RE = re.compile(
|
||||
r"^(?P<indent>\s*)if \(rt\.invoke_chained_direct<&recomp_unit_(?P<sym>\d+)_entry, "
|
||||
r"(?P<unit>\d+)u, (?P<entry>\d+)u, 0x(?P<pc>[0-9A-F]+)u>\(ctx, &aot_mem\) && "
|
||||
r"ctx\.pc == 0x(?P<ret>[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())
|
||||
+143
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
@@ -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<GuestHotspotPcSlot, kGuestHotspotPcCapacity> 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<std::size_t>(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::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
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<std::uint16_t>(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<std::uint32_t>(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<std::uint64_t>(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<std::uint32_t>(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<std::uint64_t>(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<std::uint64_t>(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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user