diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 9329dd16ad..9fd41529c0 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -257,6 +257,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(instance), CmdParamType::Int, "instance", '\0', "Run as instance N of several talking to each other over ad hoc (own 127.0.0.N address and MAC)", CmdLineMode::Headless}, {POFF(realtime), CmdParamType::Bool, "realtime", '\0', "Run at PSP speed instead of as fast as possible", CmdLineMode::Headless}, {POFF(adhocServer), CmdParamType::Bool, "adhoc-server", '\0', "Run the built-in ad hoc server"}, + {POFF(lagSync), CmdParamType::Bool, "lag-sync", '\0', "Keep emulated time in step with the wall clock every millisecond, not every frame (the Force real clock sync setting)"}, {POFF(frameStats), CmdParamType::Bool, "frame-stats", '\0', "Print flips per emulated second at the end of the run, counted from --state's load", CmdLineMode::Headless}, {POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless}, {POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1: when booting an ELF/PBP or --vsh"}, @@ -573,6 +574,10 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.bEnableAdhocServer = adhocServer.value(); g_Config.DoNotSaveSetting(&g_Config.bEnableAdhocServer); } + if (lagSync.has_value()) { + g_Config.bForceLagSync = lagSync.value(); + g_Config.DoNotSaveSetting(&g_Config.bForceLagSync); + } if (fuseId.has_value()) { g_Config.sFuseID = fuseId.value(); g_Config.DoNotSaveSetting(&g_Config.sFuseID); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index d7e913624d..d2a6dce1d3 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -200,6 +200,7 @@ struct CommandLineOptions { std::optional instance; std::optional realtime; std::optional adhocServer; + std::optional lagSync; std::optional screenshotFilename; std::optional screenshotFilenameSave; diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index d9708edfca..03eec4b974 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -100,8 +100,16 @@ static std::vector vblankWakePending; static int afterFlipEvent = -1; static int lagSyncEvent = -1; -static double lastLagSync = 0.0; +// Lag sync keeps emulated time in step with the wall clock: lagSyncEmuBase (emulated us) was reached at +// lagSyncRealBase, and each sync waits for the wall clock to catch up from there. +static double lagSyncRealBase = 0.0; +static u64 lagSyncEmuBase = 0; +static float lagSyncScale = 1.0f; static bool lagSyncScheduled = false; +static const int LAG_SYNC_INTERVAL_US = 1000; +// Further behind than this (a pause, or loading), start over from now instead of catching up. Like the +// frame throttle's limit, it lets the odd slow frame be made up for. +static const double LAG_SYNC_MAX_BEHIND = 0.1; static int numSkippedFrames; static bool hasSetMode; @@ -178,16 +186,18 @@ static bool UseLagSync() { return g_Config.bForceLagSync && !UseAutoFrameSkip(); } -static void ScheduleLagSync(int over = 0) { +static void ResetLagSyncBase() { + lagSyncRealBase = time_now_d(); + lagSyncEmuBase = CoreTiming::GetGlobalTimeUs(); +} + +// Starts or stops lag sync to match the setting. +static void ScheduleLagSync() { + CoreTiming::UnscheduleEvent(lagSyncEvent, 0); lagSyncScheduled = UseLagSync(); if (lagSyncScheduled) { - // Reset over if it became too high, such as after pausing or initial loading. - // There's no real sense in it being more than 1/60th of a second. - if (over > 1000000 / framerate) { - over = 0; - } - CoreTiming::ScheduleEvent(usToCycles(1000 + over), lagSyncEvent, 0); - lastLagSync = time_now_d(); + ResetLagSyncBase(); + CoreTiming::ScheduleEvent(usToCycles(LAG_SYNC_INTERVAL_US), lagSyncEvent, 0); } } @@ -294,9 +304,10 @@ void __DisplayDoState(PointerWrap &p) { Do(p, lagSyncScheduled); CoreTiming::RestoreRegisterEvent(lagSyncEvent, "LagSync", &hleLagSync); if (p.mode == p.MODE_READ) { - lastLagSync = time_now_d(); if (lagSyncScheduled != UseLagSync()) { ScheduleLagSync(); + } else { + ResetLagSyncBase(); } } } else { @@ -500,7 +511,9 @@ static void DoFrameTiming(bool throttle, bool *skipFrame, float scaledTimestep, } } - if (curFrameTime < nextFrameTime && throttle) { + // Lag sync paces emulation already. A wait here too would be out of phase with it, and it would race + // to catch up afterwards. + if (curFrameTime < nextFrameTime && throttle && !lagSyncScheduled) { // If time gap is huge just jump (somebody fast-forwarded) if (nextFrameTime - curFrameTime > 2*scaledTimestep) { nextFrameTime = curFrameTime; @@ -523,7 +536,7 @@ static void DoFrameTiming(bool throttle, bool *skipFrame, float scaledTimestep, static void DoFrameIdleTiming() { PROFILE_THIS_SCOPE("timing"); - if (!FrameTimingThrottled() || !g_Config.bEnableSound || wasPaused) { + if (!FrameTimingThrottled() || !g_Config.bEnableSound || wasPaused || lagSyncScheduled) { return; } @@ -835,27 +848,26 @@ void hleLagSync(u64 userdata, int cyclesLate) { scale = (float)framerate / fpsLimit; } - const double goal = lastLagSync + (scale / 1000.0f); - double before = time_now_d(); - // Don't lag too long ever, if they leave it paused. - double now = before; - while (now < goal && goal < now + 0.01) { - // Tight loop on win32 - intentionally, as timing is otherwise not precise enough. - // TODO: Use the precise waits if available -#ifndef _WIN32 - const double left = goal - now; - if (left > 0.0f && left < 1.0f) { // Sanity check - usleep((long)(left * 1000000.0)); - } -#else - yield(); -#endif - now = time_now_d(); + if (scale != lagSyncScale) { + lagSyncScale = scale; + ResetLagSyncBase(); } - const int emuOver = (int)cyclesToUs(cyclesLate); - const int over = (int)((now - goal) * 1000000); - ScheduleLagSync(over - emuOver); + // Measured from a fixed base rather than from the previous sync, so oversleeping doesn't add up. + const double emulated = (double)(CoreTiming::GetGlobalTimeUs() - lagSyncEmuBase) / 1000000.0; + const double goal = lagSyncRealBase + emulated * scale; + const double before = time_now_d(); + if (before > goal + LAG_SYNC_MAX_BEHIND || goal > before + LAG_SYNC_MAX_BEHIND) { + ResetLagSyncBase(); + } else if (before < goal) { + WaitUntil(before, goal, "lag-sync"); + } + const double now = time_now_d(); + + lagSyncScheduled = UseLagSync(); + if (lagSyncScheduled) { + CoreTiming::ScheduleEvent(usToCycles(LAG_SYNC_INTERVAL_US) - cyclesLate, lagSyncEvent, 0); + } if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::FRAME_GRAPH || g_coreCollectDebugStats) { DisplayNotifySleep(now - before); diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index de56320abb..e06df83568 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -2133,14 +2133,12 @@ void __NetAdhocInit() { while (adhocServerRunning) { auto start = std::chrono::steady_clock::now(); auto pump_status = relay_server.pump(); - auto wait_time = std::chrono::milliseconds(8); if (pump_status == aemu_postoffice_server::ServerPumpStatus::LISTEN_SOCK_DEAD) { break; } - if (pump_status == aemu_postoffice_server::ServerPumpStatus::IDLE) { - wait_time = std::chrono::milliseconds(100); - } - std::this_thread::sleep_until(start + wait_time); + // Every hop through the relay waits for the next pump, so keep them close together. Idle + // (nobody connected) too, since that's when a game's first connection comes in. + std::this_thread::sleep_until(start + std::chrono::milliseconds(1)); } INFO_LOG(Log::sceNet, "RelayServer: Shutdown complete"); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 8c7bfbb7d6..5f4fd0ed4e 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -662,10 +662,13 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar int blockTicks = (int)usToCycles(1000000 / (opt.presses.empty() ? 10 : 60)); if (opt.realtime) { // Display flips are the only thing that paces headless otherwise, and a test that doesn't - // draw never flips, so the emulated clock would race ahead of instances it talks to. + // draw never flips, so the emulated clock would race ahead of instances it talks to. The JIT + // cores only return at the end of a frame, so like the app, this paces once per frame. blockTicks = (int)usToCycles(1000); const double ahead = emulatedElapsed - (time_now_d() - startTime); - if (ahead > 0.001) { + // With --lag-sync, lag sync paces every emulated millisecond instead, and two pacers out of + // phase would undo each other. + if (ahead > 0.001 && !g_Config.bForceLagSync) { sleep_ms((int)(std::min(ahead, 0.05) * 1000.0), "realtime"); } }