Merge pull request #22838 from hrydgard/lag-sync-master

Lag sync: Fix drift and pacing. Set built-in relay to pump every millisecond
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-29 09:16:22 +01:00
commit 5ceca071fe
5 files changed
+57 -38

No files matched your search

+5
View File
@@ -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);
+1
View File
@@ -200,6 +200,7 @@ struct CommandLineOptions {
std::optional<int> instance;
std::optional<bool> realtime;
std::optional<bool> adhocServer;
std::optional<bool> lagSync;
std::optional<std::string> screenshotFilename;
std::optional<std::string> screenshotFilenameSave;
+43 -31
View File
@@ -100,8 +100,16 @@ static std::vector<SceUID> 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);
+3 -5
View File
@@ -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");
+5 -2
View File
@@ -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");
}
}