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 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 03:10:12 +03:00
co-authored by Claude
parent 633e99c3da
commit ea079b8e9f
3 changed files with 30 additions and 8 deletions
+1 -1
View File
@@ -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<uint64_t> g_totalDraws{0};
std::atomic<uint64_t> g_totalIndices{0};
std::atomic<uint64_t> g_framesSeen{0};
+1 -1
View File
@@ -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
+28 -6
View File
@@ -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)