otimizações round 4

otimizações round 4
This commit is contained in:
Jessica_Natalia
2026-08-16 20:23:12 -03:00
parent 11ba9fe222
commit 44e4b0183b
22 changed files with 2360 additions and 16 deletions
+125
View File
@@ -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_{};
+12
View File
@@ -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%
+15 -2
View File
@@ -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)));
+6
View File
@@ -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.
+14 -2
View File
@@ -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
+3
View File
@@ -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;
+30
View File
@@ -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));
+7
View File
@@ -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
+146 -8
View File
@@ -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),
+8 -2
View File
@@ -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
+33 -2
View File
@@ -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
View File
@@ -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);
+76
View File
@@ -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");