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