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:
@@ -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};
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user