From 5c6406c1e791199438bd6cf59abda55378efa453 Mon Sep 17 00:00:00 2001 From: sergystepanov Date: Fri, 21 Nov 2025 20:13:27 +0300 Subject: [PATCH] Implemented a busy loop for the emulation ticker. This replaces the low-precision, OS-dependent time ticker with a CPU spin loop that performs continuous target frame time checks and corrections. --- pkg/worker/caged/libretro/frontend.go | 57 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go index 8d0a73ac..5fc05a61 100644 --- a/pkg/worker/caged/libretro/frontend.go +++ b/pkg/worker/caged/libretro/frontend.go @@ -283,20 +283,67 @@ func (f *Frontend) Start() { } } - ticker := time.NewTicker(time.Second / time.Duration(f.nano.VideoFramerate())) - defer ticker.Stop() - if f.conf.AutosaveSec > 0 { // !to sync both for loops, can crash if the emulator starts later go f.autosave(f.conf.AutosaveSec) } + // The main loop of Libretro + + // calculate the exact duration required for a frame (e.g., 16.666ms = 60 FPS) + targetFrameTime := time.Second / time.Duration(f.nano.VideoFramerate()) + + // stop sleeping and start spinning in the remaining 1ms + const spinThreshold = 1 * time.Millisecond + + // how many frames will be considered not normal + const lateFramesThreshold = 4 + + lastFrameStart := time.Now() + for { select { - case <-ticker.C: - f.Tick() case <-f.done: return + default: + // run one tick of the emulation + f.Tick() + + elapsed := time.Since(lastFrameStart) + sleepTime := targetFrameTime - elapsed + + if sleepTime > 0 { + // SLEEP + // if we have plenty of time, sleep to save CPU and + // wake up slightly before the target time + if sleepTime > spinThreshold { + time.Sleep(sleepTime - spinThreshold) + } + + // SPIN + // if we are close to the target, + // burn CPU and check the clock with ns resolution + for time.Since(lastFrameStart) < targetFrameTime { + // CPU burn! + } + } else { + // lagging behind the target framerate so we don't sleep + f.log.Debug().Msgf("[] Frame drop: %v", elapsed) + } + + // timer reset + // + // adding targetFrameTime to the previous start + // prevents drift, if one frame was late, + // we try to catch up in the next frame + lastFrameStart = lastFrameStart.Add(targetFrameTime) + + // if execution was paused or heavily delayed, + // reset lastFrameStart so we don't try to run + // a bunch of frames instantly to catch up + if time.Since(lastFrameStart) > targetFrameTime*lateFramesThreshold { + lastFrameStart = time.Now() + } } } }