1package drm
2
3import (
4 "context"
5 "os"
6 "sync"
7 "sync/atomic"
8 "testing"
9 "testing/synctest"
10 "time"
11
12 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/stretchr/testify/mock"
15 "golang.org/x/sys/unix"
16)
17
18// runStuck runs an output whose commits return errs (after the start
19// modeset test and modeset) and whose CRTC never sends an event. It
20// returns the recorded commits once n were made, failing after 2s.
21func runStuck(t *testing.T, n int, errs ...error) []commitRec {
22 t.Helper()
23 h := startStuck(t, nil, errs...)
24 h.scenes <- ports.Scene{Seq: 1}
25 waitFor(t, func() bool { return h.count() >= n })
26 return h.stop()
27}
28
29// stuckRun is an output run by startStuck.
30type stuckRun struct {
31 t *testing.T
32 scenes chan ports.Scene
33 contents chan ports.SurfaceContent
34 active chan bool
35 presented chan ports.OutputPresented
36 count func() int
37 stop func() []commitRec
38}
39
40// startStuck runs an output whose commits return errs (after the start
41// modeset test and modeset) and whose CRTC never sends an event. render
42// makes each frame's fence once the output started (nil: none).
43func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
44 t.Helper()
45 o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)
46 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
47 o.frame.stuckAfter = 200 * time.Millisecond
48 o.flipped = make(chan flipEvent) // no event ever
49 r := portsmocks.NewMockRenderer(t)
50 buf := func() ports.DMABuf {
51 f, w, _ := os.Pipe()
52 w.Close()
53 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
54 }
55 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
56 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
57 r.EXPECT().UseTarget(mock.Anything).Return()
58 var started atomic.Bool
59 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
60 if render == nil || !started.Load() {
61 return nil, nil
62 }
63 return render(), nil
64 })
65 r.EXPECT().Close().Return().Once()
66 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
67 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
68 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
69 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
70 h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)}
71 ctx, cancel := context.WithCancel(context.Background())
72 done := make(chan error, 1)
73 go func() {
74 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented, nil, nil)
75 }()
76 h.count = func() int {
77 commitMu.Lock()
78 defer commitMu.Unlock()
79 return len(*commits)
80 }
81 h.stop = func() []commitRec {
82 cancel()
83 if err := <-done; err != nil {
84 t.Fatal(err)
85 }
86 commitMu.Lock()
87 defer commitMu.Unlock()
88 return append([]commitRec(nil), (*commits)...)
89 }
90 waitFor(t, func() bool { return h.count() == 2 })
91 started.Store(true)
92 return h
93}
94
95// A commit whose event never comes must not stop the output: it is
96// abandoned with a modeset, then the scene is committed again.
97func TestMissingEventRecoversWithModeset(t *testing.T) {
98 c := runStuck(t, 5)
99 if c[2].user&3 != userFrame {
100 t.Fatalf("commit 2 is not a frame: %+v", c[2])
101 }
102 if c[3].flags&atomicAllowModes == 0 {
103 t.Fatalf("no recovery modeset after the missing event: flags %#x", c[3].flags)
104 }
105 if c[4].user&3 != userFrame || c[4].user>>userKindBits <= c[2].user>>userKindBits {
106 t.Fatalf("scene not committed again after the modeset: %+v", c[4])
107 }
108}
109
110// EBUSY with no commit of ours in flight gets no event: the commit is
111// retried without one.
112func TestBusyWithoutEventRetries(t *testing.T) {
113 c := runStuck(t, 4, unix.EBUSY)
114 if c[2].user&3 != userFrame || c[3].user&3 != userFrame || c[3].flags&atomicAllowModes != 0 {
115 t.Fatalf("busy frame not retried: %+v", c[2:])
116 }
117}
118
119// EBUSY that lasts past the limit ends in a modeset, and a modeset
120// refused as busy is retried too.
121func TestLastingBusyModesets(t *testing.T) {
122 errs := make([]error, 8)
123 for i := range errs {
124 errs[i] = unix.EBUSY
125 }
126 c := runStuck(t, 2+len(errs)+1, errs...)
127 found := false
128 for _, r := range c[2:] {
129 if r.flags&atomicAllowModes != 0 {
130 found = true
131 }
132 }
133 if !found {
134 t.Fatal("no modeset while EBUSY lasted")
135 }
136}
137
138// fencePipe is a fence that signals when its write end is written.
139func fencePipe(t *testing.T) (fence, signal *os.File) {
140 t.Helper()
141 r, w, err := os.Pipe()
142 if err != nil {
143 t.Fatal(err)
144 }
145 t.Cleanup(func() { r.Close(); w.Close() })
146 return r, w
147}
148
149// A frame whose GPU fence has not signalled is slow, not lost: it keeps
150// waiting past the limit; once the fence signalled and still no event
151// came, the output modesets.
152func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
153 synctest.Test(t, unsignalledFenceDelaysRecovery)
154}
155
156func unsignalledFenceDelaysRecovery(t *testing.T) {
157 var mu sync.Mutex
158 var signals []*os.File
159 h := startStuck(t, func() *os.File {
160 f, w := fencePipe(t)
161 mu.Lock()
162 signals = append(signals, w)
163 mu.Unlock()
164 return f
165 })
166 h.scenes <- ports.Scene{Seq: 1}
167 waitFor(t, func() bool { return h.count() == 3 })
168 time.Sleep(3 * 200 * time.Millisecond)
169 synctest.Wait()
170 if n := h.count(); n != 3 {
171 t.Fatalf("%d commits while the frame's fence is unsignalled", n)
172 }
173 mu.Lock()
174 _, _ = signals[0].Write([]byte{1})
175 mu.Unlock()
176 waitFor(t, func() bool { return h.count() >= 4 })
177 if c := h.stop(); c[3].flags&atomicAllowModes == 0 {
178 t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags)
179 }
180}
181
182// A frame refused with EBUSY may still be read by the GPU: what was seen
183// is reported only once its fence signalled.
184func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
185 var mu sync.Mutex
186 var signals []*os.File
187 h := startStuck(t, func() *os.File {
188 f, w := fencePipe(t)
189 mu.Lock()
190 signals = append(signals, w)
191 mu.Unlock()
192 return f
193 }, unix.EBUSY, unix.EACCES)
194 h.scenes <- ports.Scene{Seq: 1}
195 // EBUSY, then the retry loses DRM master: nothing is pending.
196 waitFor(t, func() bool { return h.count() == 4 })
197 h.contents <- ports.SurfaceContent{ID: 1, Seq: 7}
198 select {
199 case p := <-h.presented:
200 t.Fatalf("seen reported while refused frames may be read: %+v", p)
201 case <-time.After(50 * time.Millisecond):
202 }
203 mu.Lock()
204 for _, w := range signals {
205 _, _ = w.Write([]byte{1})
206 }
207 mu.Unlock()
208 select {
209 case p := <-h.presented:
210 if p.Seen[1] != 7 {
211 t.Fatalf("report %+v", p)
212 }
213 case <-time.After(2 * time.Second):
214 t.Fatal("seen not reported once the fences signalled")
215 }
216 h.stop()
217}
218
219// An EBUSY run ends when commits stop being refused: a later EBUSY is
220// retried, not taken as the same run lasting past the limit.
221func TestLaterBusyStartsNewRun(t *testing.T) { synctest.Test(t, laterBusyStartsNewRun) }
222
223func laterBusyStartsNewRun(t *testing.T) {
224 o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)
225 o.frame.stuckAfter = 100 * time.Millisecond
226 enabled := true
227 o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
228 time.Sleep(busyRetry)
229 if o.expire() {
230 t.Fatal("first EBUSY retry modesets")
231 }
232 time.Sleep(2 * o.frame.stuckAfter)
233 o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
234 time.Sleep(busyRetry)
235 if o.expire() {
236 t.Fatal("a new EBUSY taken as the old run: modeset")
237 }
238}
239
240// A modeset that fails keeps what is on screen: buffers still shown are
241// not released under the plane.
242func TestFailedModesetKeepsShown(t *testing.T) {
243 o, k, _ := testOutput(t, unix.EACCES)
244 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
245 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
246 o.shown, o.queued = 5, 6
247 if err := o.modeset(); err == nil {
248 t.Fatal("modeset succeeded")
249 }
250 if o.shown != 5 || o.queued != 6 {
251 t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued)
252 }
253}