vrr_flip_gap.go RawHistoryFolder
1package drm
2
3import "time"
4
5// VRR flip gap: a workaround for game frames that hit the panel's slowest
6// refresh while the game renders fast.
7//
8// Symptom: fullscreen game, direct scanout, VRR on, 120-165 fps. Now and
9// then, runs of 5 to 30 consecutive flips each take ~20.8 ms: the panel's
10// maximum VRR frame time (1/48 Hz on a 48-165 Hz panel). The game's FPS
11// counter stays steady, but the screen visibly drops to 48 Hz for a
12// fraction of a second.
13//
14// Root cause, measured with --debug=drm-flip on amdgpu (DP, 48-165 Hz):
15// - the client's acquire fence had already signalled at commit, and
16// the atomic ioctl returned at once: neither the game nor our commit
17// path is late;
18// - every slow flip was committed within ~0.3 ms after the previous
19// flip event, i.e. right at the start of the next refresh cycle;
20// - the flip event then comes ~20.8 ms after the commit: the flip
21// misses the early-refresh window and the display only refreshes when
22// the VRR timeout forces it (vtotal at the minimum rate);
23// - since Output.Run commits the next frame as soon as that late event
24// arrives, the next commit lands at the same point of the cycle, and
25// the output stays locked at the minimum rate until the game's timing
26// drifts.
27//
28// Why the kernel misses these flips is not established (amdgpu DC VRR
29// flip programming around vblank is the likely place). What is measured:
30// committing the next game frame a short time after the flip event keeps
31// it out of that window. With a 1 ms gap a 3-minute run had 0 slow flips
32// in gameplay, against bursts of up to 30 without it and ~10 isolated
33// slow flips per minute with a 2 ms delay applied only after a slow flip.
34// The cost is at most the gap of added latency per frame, well under one
35// 165 Hz refresh (6 ms), and it applies only while VRR runs for a game.
36//
37// render.vrr-flip-gap sets the gap (default 1 ms, 0 turns the workaround
38// off); drop it once the driver no longer needs it.
39
40// startFlipGap is called on each completed commit f: after a game frame's
41// flip under VRR, the next frame waits vrrFlipGap before it commits. A
42// state commit (cursor, VRR toggle) completing inside the gap keeps it:
43// the next frame still follows the last frame flip.
44func (o *Output) startFlipGap(f pendingFrame) {
45 if !f.frame {
46 return
47 }
48 o.flipGapUntil = time.Time{}
49 if o.vrrFlipGap > 0 && o.vrrOn && o.vrrGame {
50 o.flipGapUntil = time.Now().Add(o.vrrFlipGap)
51 }
52}