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