Skip to content

fix: improve unfocused window frame pacing - #3493

Open
mvanhorn wants to merge 2 commits into
hajimehoshi:mainfrom
mvanhorn:fix/3397-fps-of-unfocused-window-is-56-not-60
Open

fix: improve unfocused window frame pacing#3493
mvanhorn wants to merge 2 commits into
hajimehoshi:mainfrom
mvanhorn:fix/3397-fps-of-unfocused-window-is-56-not-60

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

What issue is this addressing?

In internal/ui/ui_glfw.go, extract the unfocused sleep-budget calculation used by glfwBackend.updateGame and reduce its target interval from 1/60 second to the maintainer-suggested 1/120 second, continuing to subtract time already spent rendering the frame. Keep the existing focus detection, buffer swap, and time.Sleep wiring intact, and skip sleeping when frame work has already consumed the shorter interval. Add a desktop-scoped unit test in internal/ui/ui_glfw_test.go for the duration calculation so boundary behavior is deterministic without relying on wall-clock timing or a live X server.

Closes #3397

What type of issue is this addressing?

What this PR does | solves

Comment thread internal/ui/ui_glfw.go Outdated
Comment thread internal/ui/ui_glfw.go Outdated
Comment thread internal/ui/ui_glfw.go Outdated
Applies the review suggestions: both clamps now use the builtin max, and
unfocusedSleepDuration is max(wait-elapsed-sleepOverhead, 0) without the
separate branch. Extends the unit test to the boundaries that rewrite touches:
elapsed+overhead exactly equal to wait, overhead larger than wait, and negative
elapsed or overhead.

Adds TestTimeSleepOvershoot behind the ebitengine_sleep_measurement build tag,
so normal runs and CI skip its ~9s of sampling. It reports min/median/mean/p95/
max overshoot of time.Sleep(time.Second/60) over 500 samples, which is the
measurement needed to judge whether compensating for the overshoot is warranted.
@mvanhorn

mvanhorn commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Both max() suggestions applied in 36e1fea (go.mod is 1.25, so the builtin is available), and I extended the unit test to the boundaries that rewrite touches — elapsed+overhead exactly equal to wait, overhead larger than wait, and negative inputs.

On your actual question — how much is unfocusedSleepOverhead, and does this really fix it — I measured rather than guessed. Added TestTimeSleepOvershoot behind the ebitengine_sleep_measurement build tag so it doesn't run in normal test or CI runs. 500 samples of time.Sleep(time.Second/60):

min=23.8µs  median=2.010ms  mean=1.738ms  p95=2.018ms  max=2.045ms

That is ~10.4% of a 16.667ms frame. Feeding it through 60/(1+overhead/wait) gives ~54.3 FPS for the old code. The 56 FPS in #3397 corresponds to ~1.19ms of overhead, so the effect is the right size and, on this machine, somewhat larger than what was reported.

What that does not show: this is one run in a headless macOS sandbox (macOS 26.3, arm64), not an Ebitengine desktop session, and it says nothing about Windows or Linux scheduler behaviour. It establishes that the overshoot is big enough to matter at this frame budget. It does not demonstrate end-to-end that carrying the previous frame's overshoot forward restores 60 FPS in a real unfocused window — that needs a desktop-session measurement of the whole frame loop, ideally per OS.

So: your skepticism about whether the overhead matters is answered, I think — 1.7ms out of 16.7ms is not noise. Whether this particular compensation is the right fix is a separate question, and I'd rather you have the numbers than my opinion. If you'd prefer the measurement test not live in the tree, I'll drop it and keep the numbers here in the thread.

@hajimehoshi

Copy link
Copy Markdown
Owner

Thank you for elaborating. The overshoot seems a real.

I suggest a way to record absolute time to wait like this in order to treat overheads correctly:

const wait = time.Second / 60
now := time.Now()
if next := u.unfocusedNextWake.Add(wait); next.After(now) {
    u.unfocusedNextWake = next
    time.Sleep(time.Until(next))
} else {
    // Fell behind, or just became unfocused: resync instead of chasing the past.
    u.unfocusedNextWake = now
}

Also, drop the tests. Especially, TestTimeSleepOvershoot was useful for this thread discussion, but we no longer need it.

Thanks,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FPS of unfocused window is ~56 not ~60

2 participants