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