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 "golang.org/x/sys/unix"
13)
14
15// An event queued before a modeset (or for an earlier output on the CRTC)
16// must not complete the commit that follows.
17func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
18 o, _, commits := testOutput(t)
19 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
20 t.Fatal(err)
21 }
22 old := eventOf((*commits)[0])
23 o.frame.endPending() // a modeset dropped the commit; its event is queued
24 if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
25 t.Fatal(err)
26 }
27 if o.completed(old, nil) || !o.frame.pendingCommit() {
28 t.Fatal("stale event completed the new commit")
29 }
30 if !o.completed(eventOf((*commits)[1]), nil) || o.frame.pendingCommit() {
31 t.Fatal("own event did not complete the commit")
32 }
33}
34
35// After EBUSY no commit of ours is known: the next event of the CRTC ends
36// the wait, then the frame commits.
37func TestBusyFrameCommitsAfterEvent(t *testing.T) {
38 o, _, commits := testOutput(t, unix.EBUSY)
39 enabled := true
40 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
41 t.Fatalf("EBUSY not handled: %v", err)
42 }
43 if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.frame.pendingCommit() {
44 t.Fatal("EBUSY wait not ended by the CRTC's event")
45 }
46 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.frame.pendingCommit() {
47 t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
48 }
49}
50
51// A cursor image change writes only an image the plane neither shows nor
52// is about to show; with both busy it waits for the event.
53func TestCursorImageNeverOverwritesScreen(t *testing.T) {
54 o, _, _ := testOutput(t)
55 c := o.cursor
56 r := portsmocks.NewMockRenderer(t)
57 var written []int
58 r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
59 written = append(written, i)
60 return nil
61 })
62 px := []byte{1, 2, 3, 4}
63 // Image 0 on screen: two changes before a commit both go to image 1.
64 c.screen, c.cur, c.image = 90, 0, true
65 for range 2 {
66 if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
67 t.Fatal(err)
68 }
69 }
70 if len(written) != 2 || written[0] != 1 || written[1] != 1 {
71 t.Fatalf("written %v", written)
72 }
73 // Image 1 in flight, image 0 on screen: nothing is free.
74 c.committed(cursorState{on: true, fb: 91})
75 if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
76 t.Fatalf("wrote a busy image: %v %v", err, written)
77 }
78 // The flip frees image 0.
79 c.landed()
80 if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
81 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
82 }
83}
84
85// KMS refusing the cursor plane turns the hardware cursor off and the
86// frame commits without it; the output keeps running.
87func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
88 o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
89 o.cursor.image = true
90 o.cursor.Move(5, 5)
91 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
92 t.Fatal(err)
93 }
94 if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
95 t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
96 }
97 if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
98 t.Fatalf("retry shows cursor fb %d", v)
99 }
100}
101
102// A refused cursor is not blamed on the client buffer.
103func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
104 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
105 o.cursor.image = true
106 o.cursor.Move(5, 5)
107 o.clientFBs[9] = &clientFB{fbID: 80}
108 direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
109 if err != nil || !direct || o.clientFBs[9].failed != "" {
110 t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
111 }
112}
113
114// showImages validates the images with a TEST_ONLY modeset and falls back
115// to linear images when KMS refuses the driver's.
116func TestShowImagesTestsBeforeModeset(t *testing.T) {
117 o, k, commits := testOutput(t, unix.EINVAL)
118 o.cursor = nil
119 r := portsmocks.NewMockRenderer(t)
120 buf := func() ports.DMABuf {
121 f, w, _ := os.Pipe()
122 w.Close()
123 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
124 }
125 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
126 r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
127 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
128 r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
129 r.EXPECT().UseTarget(mock.Anything).Return()
130 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
131 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
132 k.EXPECT().rmFB(mock.Anything).Return(nil)
133 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
134 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
135 if err := o.showImages(r, imagesDriver, nil); err != nil {
136 t.Fatal(err)
137 }
138 want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
139 if len(*commits) != len(want) {
140 t.Fatalf("%d commits", len(*commits))
141 }
142 for i, f := range want {
143 if (*commits)[i].flags != f {
144 t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
145 }
146 }
147 if o.kind != imagesLinear {
148 t.Fatalf("kind %d", o.kind)
149 }
150}
151
152// Enabling an output whose images were not made yet (started switched
153// away) tests them and modesets once.
154func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
155 o, k, commits, commitMu := testOutputMu(t)
156 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
157 r := portsmocks.NewMockRenderer(t)
158 buf := func() ports.DMABuf {
159 f, w, _ := os.Pipe()
160 w.Close()
161 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
162 }
163 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
164 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
165 r.EXPECT().UseTarget(mock.Anything).Return()
166 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
167 r.EXPECT().Close().Return().Once()
168 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
169 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
170 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
171 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
172 active := make(chan bool, 1)
173 active <- false
174 ctx, cancel := context.WithCancel(context.Background())
175 done := make(chan error, 1)
176 go func() {
177 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
178 }()
179 count := func(flags uint32) int {
180 commitMu.Lock()
181 defer commitMu.Unlock()
182 n := 0
183 for _, c := range *commits {
184 if c.flags == flags {
185 n++
186 }
187 }
188 return n
189 }
190 time.Sleep(20 * time.Millisecond)
191 if n := count(atomicAllowModes); n != 0 {
192 t.Fatalf("%d modesets while away", n)
193 }
194 active <- true
195 waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
196 cancel()
197 if err := <-done; err != nil {
198 t.Fatal(err)
199 }
200 if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
201 t.Fatalf("%d test modesets", n)
202 }
203}
204
205// Close without a mode to restore turns the CRTC off.
206func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
207 o, k, commits := testOutput(t)
208 k.EXPECT().rmFB(mock.Anything).Return(nil)
209 o.Close()
210 c := (*commits)[0]
211 for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
212 if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
213 t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
214 }
215 }
216}
217
218// Close restores the saved mode and gives the connector its CRTC back.
219func TestCloseRestoresSavedMode(t *testing.T) {
220 o, k, commits := testOutput(t)
221 o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
222 k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
223 k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
224 k.EXPECT().rmFB(mock.Anything).Return(nil)
225 o.Close()
226 c := (*commits)[0]
227 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
228 t.Fatalf("connector crtc %d", v)
229 }
230 if v, _ := c.req.value(tPrimary, pFB); v != 5 {
231 t.Fatalf("primary fb %d", v)
232 }
233}
234
235// With the cursor off no image is loaded (no warning per change).
236func TestCursorOffSkipsLoads(t *testing.T) {
237 o, _, _ := testOutput(t)
238 o.cursor.off = true
239 loaded := false
240 o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
241 loaded = true
242 return ports.CursorImage{}, nil
243 }, ports.CursorChange{}, 1, 0)
244 if loaded || o.cursor.desired().on {
245 t.Fatal("cursor used while off")
246 }
247}
248
249// Content read while only a cursor commit is in flight is reported when
250// that commit's event arrives, not held until a later frame.
251func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
252 o, k, commits, commitMu := testOutputMu(t)
253 o.tearing, o.cursor.fbs = false, [2]uint32{}
254 flips := make(chan flipEvent, 1)
255 o.flipped = flips
256 r := portsmocks.NewMockRenderer(t)
257 buf := func() ports.DMABuf {
258 f, w, _ := os.Pipe()
259 w.Close()
260 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
261 }
262 r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
263 r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
264 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
265 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
266 r.EXPECT().UseTarget(mock.Anything).Return()
267 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
268 r.EXPECT().Close().Return().Once()
269 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
270 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
271 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
272 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
273 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
274 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
275 load := func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
276 return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
277 }
278 last := func(kind uint64) (commitRec, bool) {
279 commitMu.Lock()
280 defer commitMu.Unlock()
281 for i := len(*commits) - 1; i >= 0; i-- {
282 if c := (*commits)[i]; c.user&3 == kind {
283 return c, true
284 }
285 }
286 return commitRec{}, false
287 }
288 scenes := make(chan ports.Scene, 1)
289 contents := make(chan ports.SurfaceContent, 1)
290 presented := make(chan ports.OutputPresented, 8)
291 ctx, cancel := context.WithCancel(context.Background())
292 done := make(chan error, 1)
293 go func() {
294 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented, nil, nil)
295 }()
296 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
297 var frame commitRec
298 waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
299 flips <- eventOf(frame)
300 <-presented // the frame's flip
301 o.cursor.Move(3, 3)
302 var cur commitRec
303 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
304 // A window this output does not show commits while the cursor flies.
305 contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
306 time.Sleep(20 * time.Millisecond)
307 flips <- eventOf(cur)
308 select {
309 case p := <-presented:
310 if p.Seen[7] != 2 {
311 t.Fatalf("report %+v", p)
312 }
313 case <-time.After(time.Second):
314 t.Fatal("Seen not reported after the cursor commit")
315 }
316 cancel()
317 if err := <-done; err != nil {
318 t.Fatal(err)
319 }
320}
321
322// Frame fences are closed on every path: flipped, EBUSY (re-rendered
323// after the event) and lost master (re-rendered after the modeset). The
324// fd count is unchanged after many frames.
325func TestRunClosesFrameFences(t *testing.T) {
326 fds := func() int {
327 ents, err := os.ReadDir("/proc/self/fd")
328 if err != nil {
329 t.Skip(err)
330 }
331 return len(ents)
332 }
333 const rounds = 30
334 // Commit results: modeset test and modeset, then per round
335 // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
336 errs := []error{nil, nil}
337 for i := range rounds {
338 switch i % 3 {
339 case 0:
340 errs = append(errs, unix.EBUSY, nil)
341 case 1:
342 errs = append(errs, unix.EACCES, nil, nil)
343 default:
344 errs = append(errs, nil)
345 }
346 }
347 o, k, commits, commitMu := testOutputMu(t, errs...)
348 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
349 flips := make(chan flipEvent, 1)
350 o.flipped = flips
351 r := portsmocks.NewMockRenderer(t)
352 buf := func() ports.DMABuf {
353 f, w, _ := os.Pipe()
354 w.Close()
355 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
356 }
357 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
358 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
359 r.EXPECT().UseTarget(mock.Anything).Return()
360 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
361 f, w, _ := os.Pipe()
362 _, _ = w.Write([]byte{1})
363 w.Close()
364 return f, nil
365 })
366 r.EXPECT().Close().Return().Once()
367 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
368 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
369 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
370 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
371 before := fds()
372 active := make(chan bool, 1)
373 scenes := make(chan ports.Scene, 1)
374 presented := make(chan ports.OutputPresented, 4*rounds)
375 ctx, cancel := context.WithCancel(context.Background())
376 done := make(chan error, 1)
377 go func() {
378 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented, nil, nil)
379 }()
380 count := func() int {
381 commitMu.Lock()
382 defer commitMu.Unlock()
383 return len(*commits)
384 }
385 last := func() commitRec {
386 commitMu.Lock()
387 defer commitMu.Unlock()
388 return (*commits)[len(*commits)-1]
389 }
390 waitFor(t, func() bool { return count() == 2 })
391 for i := range rounds {
392 n := count()
393 scenes <- ports.Scene{Seq: uint64(i)}
394 switch i % 3 {
395 case 0:
396 waitFor(t, func() bool { return count() == n+1 })
397 flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
398 waitFor(t, func() bool { return count() == n+2 })
399 case 1:
400 waitFor(t, func() bool { return count() == n+1 })
401 active <- true
402 waitFor(t, func() bool { return count() == n+3 })
403 default:
404 waitFor(t, func() bool { return count() == n+1 })
405 }
406 flips <- eventOf(last())
407 }
408 cancel()
409 if err := <-done; err != nil {
410 t.Fatal(err)
411 }
412 if after := fds(); after > before {
413 t.Fatalf("fds %d -> %d", before, after)
414 }
415}