From ea079b8e9f5af310076814d98a4aea9b06343cb3 Mon Sep 17 00:00:00 2001 From: megboyzz Date: Sun, 20 Sep 2026 03:10:12 +0300 Subject: [PATCH] Stop emitting the exit-request check on every guest load and store Unicorn calls check_exit_request() from tcg_gen_qemu_ld_i32/st_i32/ld_i64/ st_i64 - i.e. on EVERY GUEST MEMORY ACCESS, not per basic block. Upstream QEMU does not do this. It exists so a uc_emu_stop() issued from a MEMORY hook callback takes effect immediately instead of at the next block boundary. Found by taking a profile contradiction seriously. In a clean in-game profile, helper_check_exit_request_arm was the largest single symbol at 8.71%, against helper_uc_tracecode's 0.98% - yet both are emitted together at the ARM translator's two hook sites and should have matched. Grepping every emission site across the tree, rather than just target/arm, found the six in tcg/tcg-op.c. This engine never stops emulation from a memory callback: mem_fault_hook_cb only logs and returns false, and the one accelerator that does call uc_emu_stop (FnvHashAccelHookCb) is a UC_HOOK_CODE hook, where the check is still emitted. So on the load/store path it is pure overhead. helper_check_exit_request_arm 8.71% -> 4.07% The remainder is the legitimate per-hook emission. Game still loads, reaches OnCarLoaded, zero MEM FAULTs. EVIDENCE LEVEL, stated plainly: one clean interleaved A/B pair, 33.13s without the check vs 35.89s with it (7.7%), plus the profile share above. That is weaker than this project's usual bar - the follow-up pairs were lost when the Pixel's screen dozed off mid-run and the engine stopped rendering (glClear=0, no faults), and later attempts to reset the save between runs were blocked. The change should be re-A/B'd on a woken, freshly-booted device before being treated as settled. Kept as a named constant rather than deleted because this is a vendored tree: anyone adding a memory hook that calls uc_emu_stop MUST set kEmitExitCheckOnMemoryAccess back to 1, or that stop will be deferred to the next block boundary. Also turns off the task #56 instrumentation (kLogPerfMetrics, kCountStubDispatches): both took a steady_clock reading per draw and per shim crossing, which showed up as __kernel_clock_gettime at 3.67% of a profile - the measurement was costing nearly as much as some of the things being measured. Co-Authored-By: Claude --- mpcore/src/main/cpp/emu/gles_shim.cpp | 2 +- mpcore/src/main/cpp/emu/guest_engine.cpp | 2 +- .../cpp/third_party/unicorn/qemu/tcg/tcg-op.c | 34 +++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/mpcore/src/main/cpp/emu/gles_shim.cpp b/mpcore/src/main/cpp/emu/gles_shim.cpp index f43846b..bcb9e31 100644 --- a/mpcore/src/main/cpp/emu/gles_shim.cpp +++ b/mpcore/src/main/cpp/emu/gles_shim.cpp @@ -110,7 +110,7 @@ void NoteDrawForSwapGate() { // by timestamp afterwards. Frames are counted as synthesized-frame // boundaries would be - here, default-framebuffer draws reset per swap - so // drawsPerFrame is the number that matters. -constexpr bool kLogPerfMetrics = true; +constexpr bool kLogPerfMetrics = false; std::atomic g_totalDraws{0}; std::atomic g_totalIndices{0}; std::atomic g_framesSeen{0}; diff --git a/mpcore/src/main/cpp/emu/guest_engine.cpp b/mpcore/src/main/cpp/emu/guest_engine.cpp index 95d0bae..bc58a1c 100644 --- a/mpcore/src/main/cpp/emu/guest_engine.cpp +++ b/mpcore/src/main/cpp/emu/guest_engine.cpp @@ -133,7 +133,7 @@ constexpr uint32_t kGuardPageSize = kPageSize; // TCG to re-translate (tb_invalidate_phys_page_fast_arm, 3.15%). If this // reports zero, the invalidation comes from somewhere else and that lead // is dead. -constexpr bool kCountStubDispatches = true; +constexpr bool kCountStubDispatches = false; constexpr bool kCountTextWrites = false; // kCountArenaWrites - task #54. tb_invalidate_phys_page_fast_arm is 5.56% of // the in-race profile and its cause is open: the .text write counter above diff --git a/mpcore/src/main/cpp/third_party/unicorn/qemu/tcg/tcg-op.c b/mpcore/src/main/cpp/third_party/unicorn/qemu/tcg/tcg-op.c index d0223ea..df33255 100644 --- a/mpcore/src/main/cpp/third_party/unicorn/qemu/tcg/tcg-op.c +++ b/mpcore/src/main/cpp/third_party/unicorn/qemu/tcg/tcg-op.c @@ -2850,6 +2850,28 @@ static void gen_ldst_i64(TCGContext *tcg_ctx, TCGOpcode opc, TCGv_i64 val, TCGv // Unicorn engine // check if the last memory access was invalid // if so, we jump to the block epilogue to quit immediately. +// PROJECT PATCH (2026-09-19, NFSMW arm64-poc). Unicorn emits an exit-request +// check inside tcg_gen_qemu_ld/st - i.e. on EVERY GUEST LOAD AND STORE, not +// per basic block. Upstream QEMU does not do this; it exists so that a +// uc_emu_stop() issued from a MEMORY hook callback takes effect immediately +// rather than at the next block boundary. +// +// Measured cost in this project: helper_check_exit_request_arm was the single +// largest symbol in a clean in-game profile at 8.71%, against 0.98% for +// helper_uc_tracecode - a ratio that made no sense until these call sites +// were found, because the two are emitted together at the ARM translator's +// hook sites and should have matched. +// +// This engine never stops emulation from a memory callback: mem_fault_hook_cb +// returns false (it only logs), and the one accelerator that does call +// uc_emu_stop (FnvHashAccelHookCb) is a UC_HOOK_CODE hook, where the check is +// still emitted. So the per-access check is pure overhead here. +// +// Kept as a named constant rather than deleted, because this is a vendored +// tree: anyone adding a memory hook that calls uc_emu_stop MUST flip this +// back, or that stop will be deferred to the next block boundary. +static const int kEmitExitCheckOnMemoryAccess = 0; + void check_exit_request(TCGContext *tcg_ctx) { // Unicorn: @@ -2917,7 +2939,7 @@ void tcg_gen_qemu_ld_i32(TCGContext *tcg_ctx, TCGv_i32 val, TCGv addr, TCGArg id } } - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); } void tcg_gen_qemu_st_i32(TCGContext *tcg_ctx, TCGv_i32 val, TCGv addr, TCGArg idx, MemOp memop) @@ -2952,7 +2974,7 @@ void tcg_gen_qemu_st_i32(TCGContext *tcg_ctx, TCGv_i32 val, TCGv addr, TCGArg id tcg_temp_free_i32(tcg_ctx, swap); } - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); } void tcg_gen_qemu_ld_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg idx, MemOp memop) @@ -2967,7 +2989,7 @@ void tcg_gen_qemu_ld_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg id } else { tcg_gen_movi_i32(tcg_ctx, TCGV_HIGH(tcg_ctx, val), 0); } - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); return; } #endif @@ -3009,7 +3031,7 @@ void tcg_gen_qemu_ld_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg id g_assert_not_reached(); } } - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); } void tcg_gen_qemu_st_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg idx, MemOp memop) @@ -3019,7 +3041,7 @@ void tcg_gen_qemu_st_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg id #if TCG_TARGET_REG_BITS == 32 if ((memop & MO_SIZE) < MO_64) { tcg_gen_qemu_st_i32(tcg_ctx, TCGV_LOW(tcg_ctx, val), addr, idx, memop); - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); return; } #endif @@ -3055,7 +3077,7 @@ void tcg_gen_qemu_st_i64(TCGContext *tcg_ctx, TCGv_i64 val, TCGv addr, TCGArg id if (swap) { tcg_temp_free_i64(tcg_ctx, swap); } - check_exit_request(tcg_ctx); + if (kEmitExitCheckOnMemoryAccess) check_exit_request(tcg_ctx); } static void tcg_gen_ext_i32(TCGContext *tcg_ctx, TCGv_i32 ret, TCGv_i32 val, MemOp opc)