1package drm
2
3import (
4 "context"
5 "os"
6 "testing"
7 "time"
8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/stretchr/testify/mock"
12)
13
14// The gap starts only after a game frame's flip under VRR.
15func TestStartFlipGap(t *testing.T) {
16 for _, tc := range []struct {
17 name string
18 gap time.Duration
19 frame, vrr, game bool
20 want bool
21 }{
22 {"game frame under VRR", time.Millisecond, true, true, true, true},
23 {"gap off", 0, true, true, true, false},
24 {"state commit keeps the gap", time.Millisecond, false, true, true, true},
25 {"state commit keeps no gap", time.Millisecond, false, false, false, false},
26 {"VRR off", time.Millisecond, true, false, true, false},
27 {"desktop under VRR hold", time.Millisecond, true, true, false, false},
28 } {
29 // A gap is pending from the last game frame when it applies.
30 pending := time.Time{}
31 if tc.vrr && tc.game {
32 pending = time.Now().Add(time.Hour)
33 }
34 o := &Output{vrrFlipGap: tc.gap, vrrOn: tc.vrr, vrrGame: tc.game, flipGapUntil: pending}
35 o.startFlipGap(pendingFrame{frame: tc.frame}, monotonic(), time.Now())
36 if got := !o.flipGapUntil.IsZero(); got != tc.want {
37 t.Errorf("%s: gap started %v, want %v", tc.name, got, tc.want)
38 }
39 }
40}
41
42// A cursor commit that completes inside the gap does not end it.
43func TestStateCommitKeepsFlipGap(t *testing.T) {
44 o, _, commits := testOutput(t)
45 o.vrrFlipGap, o.vrrOn, o.vrrGame = time.Hour, true, true
46 seen := map[ports.WindowID]uint64{}
47 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
48 t.Fatal(err)
49 }
50 o.completed(eventOf((*commits)[0]), seen)
51 gap := o.flipGapUntil
52 if gap.IsZero() {
53 t.Fatal("no gap after a game frame")
54 }
55 o.cursor.image = true
56 o.lastFrame = time.Time{} // the cursor commits alone
57 o.cursor.Move(3, 3)
58 if err := o.commitState(o.stateVRR()); err != nil || len(*commits) != 2 {
59 t.Fatalf("cursor commit: %v, %d commits", err, len(*commits))
60 }
61 o.completed(eventOf((*commits)[1]), seen)
62 if o.flipGapUntil != gap {
63 t.Fatalf("gap %v after a cursor commit, want %v", o.flipGapUntil, gap)
64 }
65}
66
67// A modeset drops a pending gap: it belonged to the old display state.
68func TestModesetClearsFlipGap(t *testing.T) {
69 o, k, _ := testOutput(t)
70 o.flipGapUntil = time.Now().Add(time.Hour)
71 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
72 if err := o.modeset(); err != nil {
73 t.Fatal(err)
74 }
75 if !o.flipGapUntil.IsZero() {
76 t.Fatalf("gap until %v after modeset", o.flipGapUntil)
77 }
78}
79
80// Run commits the next game frame no sooner than the gap after the flip
81// event, then commits it on its own when the gap ends.
82func TestRunWaitsVRRFlipGap(t *testing.T) {
83 o, k, commits, commitMu := testOutputMu(t)
84 o.cursor, o.tearing = nil, false
85 o.vrrFlipGap = 30 * time.Millisecond
86 flips := make(chan flipEvent, 1)
87 o.flipped = flips
88 r := portsmocks.NewMockRenderer(t)
89 pipeBuf := func() ports.DMABuf {
90 f, w, _ := os.Pipe()
91 w.Close()
92 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
93 }
94 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
95 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
96 r.EXPECT().UseTarget(mock.Anything).Return()
97 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
98 r.EXPECT().Close().Return().Once()
99 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
100 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
101 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
102 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
103 frames := func() (commitRec, int) {
104 commitMu.Lock()
105 defer commitMu.Unlock()
106 n, last := 0, commitRec{}
107 for _, c := range *commits {
108 if c.user&3 == userFrame {
109 n, last = n+1, c
110 }
111 }
112 return last, n
113 }
114 scenes := make(chan ports.Scene, 1)
115 ctx, cancel := context.WithCancel(context.Background())
116 done := make(chan error, 1)
117 go func() {
118 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), nil, nil)
119 }()
120 // A composed fullscreen window: a game frame under VRR.
121 game := ports.Scene{OutputWidth: 200, OutputHeight: 100,
122 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
123 Layers: []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}}
124 scenes <- game
125 waitFor(t, func() bool { _, n := frames(); return n == 1 })
126 last, _ := frames()
127 // The next frame is already waiting when the flip arrives.
128 game.Seq = 1
129 scenes <- game
130 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
131 // Run handles the flip after it is sent: its commit is at least the
132 // gap after this point, whatever the scheduling delay.
133 sent := time.Now()
134 waitFor(t, func() bool { _, n := frames(); return n == 2 })
135 commitMu.Lock()
136 second := (*commits)[len(*commits)-1].at
137 commitMu.Unlock()
138 if waited := second.Sub(sent); waited < o.vrrFlipGap-time.Millisecond {
139 t.Fatalf("next frame committed %s after the flip, gap %s", waited, o.vrrFlipGap)
140 }
141 cancel()
142 if err := <-done; err != nil {
143 t.Fatal(err)
144 }
145}