Core: Handle CORE_REENTER_DISPATCH in Core_NextFrame

"Core_NextFrame called with wrong core state N/A" is CORE_REENTER_DISPATCH -
the one CoreState missing from CoreStateToString, so it printed as N/A.

Core_ReenterDispatcher() sets that state to bounce out of the dispatcher and
back in, so pending jit cache invalidations get applied, and normally nothing
can observe it: both run loops have coreState == CORE_RUNNING_CPU in their loop
condition, so they bail at once and Core_RunLoopUntil resets it.

CoreTiming::ProcessEvents() is the exception. It drains every due event in one
loop without returning to the dispatcher in between, so if one callback
invalidates the icache and EnterVBlank is behind it in the same batch,
__DisplayFlip reaches Core_NextFrame while we're still in REENTER_DISPATCH.
The likeliest source is hleCheat: for the JitInvalidationHack games it
invalidates on every tick, at a 2ms interval against a 16.7ms vblank.

Before, that meant Core_NextFrame refused, so the flip was skipped entirely -
a dropped frame plus an alarming error log. Treat it like CORE_RUNNING_CPU
instead: CORE_NEXTFRAME bounces out of the dispatcher just as well, and
MIPSState::RunLoopUntil() calls ProcessPendingInvalidates() on every entry
regardless of state, so nothing is lost.

Also add the missing case to CoreStateToString.
This commit is contained in:
Henrik Rydgård committed 2026-09-02 16:45:30 -06:00
1 parent 2820fa9467
commit 097788aaea
1 file changed
+11 -1
+11 -1
View File
@@ -275,6 +275,7 @@ const char *CoreStateToString(CoreState state) {
switch (state) {
case CORE_RUNNING_CPU: return "RUNNING_CPU";
case CORE_NEXTFRAME: return "NEXTFRAME";
case CORE_REENTER_DISPATCH: return "REENTER_DISPATCH";
case CORE_STEPPING_CPU: return "STEPPING_CPU";
case CORE_POWERDOWN: return "POWERDOWN";
case CORE_RUNTIME_ERROR: return "RUNTIME_ERROR";
@@ -727,7 +728,16 @@ bool Core_NextFrame() {
_dbg_assert_(coreState != CORE_STEPPING_GE && coreState != CORE_RUNNING_GE);
if (coreState == CORE_RUNNING_CPU) {
if (coreState == CORE_RUNNING_CPU || coreState == CORE_REENTER_DISPATCH) {
// REENTER_DISPATCH is just a request to bounce out of the dispatcher and come back in,
// so that pending jit cache invalidations get applied. Switching to NEXTFRAME instead
// bounces out just as well, and MIPSState::RunLoopUntil() calls ProcessPendingInvalidates()
// on every entry regardless of state, so they're still applied when we resume.
//
// This happens when a CoreTiming event invalidates the icache (a cheat writing code, an
// async io notify, ...) and the vblank event follows it in the same ProcessEvents() batch:
// event callbacks run back to back without returning to the dispatcher in between, so the
// state is still REENTER_DISPATCH when __DisplayFlip gets here.
::coreState = CORE_NEXTFRAME;
return true;
} else if (coreState == CORE_STEPPING_CPU) {