1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
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// overlayFrames submits the overlay scene once (composed), then with the
18// overlay window's next buffers: the frames after the first only flip the
19// overlay buffer.
20func overlayFrames(t *testing.T, o *Output, r *portsmocks.MockRenderer, s ports.Scene, c map[ports.WindowID]ports.SurfaceContent, frames int) {
21 t.Helper()
22 for i := range frames {
23 win := c[2]
24 buf := *win.DMABuf
25 buf.ID = uint64(9 + i)
26 win.DMABuf, win.Seq = &buf, uint64(1+i)
27 c[2] = win
28 if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
29 t.Fatalf("frame %d: %v", i, err)
30 }
31 o.frame.endPending()
32 }
33}
34
35// When only the overlay window's buffer changes, the image on screen is
36// committed again with the new buffer: no render, no probe once KMS took
37// the same frame.
38func TestSubmitFrameOverlayOnlySkipsRender(t *testing.T) {
39 o, k, commits := overlayOutput(t)
40 o.cursor, o.vrrProp = nil, 0
41 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
42 return uint32(80 + b.ID), nil
43 }).Times(4)
44 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
45 s, c := overlayScene()
46 r := portsmocks.NewMockRenderer(t)
47 r.EXPECT().UseTarget(0).Return().Once()
48 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
49 overlayFrames(t, o, r, s, c, 4)
50 var probes, frames int
51 for _, cm := range *commits {
52 if cm.flags&atomicTestOnly != 0 {
53 probes++
54 continue
55 }
56 frames++
57 if v, _ := cm.req.value(tPrimary, pFB); v != 70 {
58 t.Fatalf("frame %d primary fb %d, want the composed image 70", frames, v)
59 }
60 if _, ok := cm.req.value(tPrimary, pFence); ok {
61 t.Fatalf("frame %d waits on a render fence", frames)
62 }
63 }
64 if frames != 4 {
65 t.Fatalf("%d frame commits", frames)
66 }
67 // Each new overlay buffer is probed once; nothing else is.
68 if probes != 4 {
69 t.Fatalf("%d probes", probes)
70 }
71 if last := (*commits)[len(*commits)-1]; o.queued != 12 {
72 t.Fatalf("queued %d", o.queued)
73 } else if v, _ := last.req.value(tOverlay, pFB); v != 92 {
74 t.Fatalf("overlay fb %d", v)
75 }
76 if o.back != 1 {
77 t.Fatalf("back %d: the skipped frames drew nothing", o.back)
78 }
79}
80
81// An uncommitted render that may still read client buffers forces
82// composition: the overlay-only flip carries no fence to order it after
83// those reads, and its report would let wayland release them.
84func TestSubmitFrameOverlayOnlyWaitsForHeldReads(t *testing.T) {
85 o, k, _ := overlayOutput(t)
86 o.cursor, o.vrrProp = nil, 0
87 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
88 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
89 s, c := overlayScene()
90 r := portsmocks.NewMockRenderer(t)
91 r.EXPECT().UseTarget(mock.Anything).Return().Twice()
92 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Twice()
93 overlayFrames(t, o, r, s, c, 1)
94 // An unsignalled fence of a render whose commit failed.
95 fence, w, err := os.Pipe()
96 if err != nil {
97 t.Fatal(err)
98 }
99 defer fence.Close()
100 defer w.Close()
101 o.holdRead(fence)
102 defer o.dropRead()
103 overlayFrames(t, o, r, s, c, 1)
104}
105
106// The desktop case through Run: a video (a dmabuf window on the overlay)
107// next to a static window. Once the overlay is up, each new video buffer is
108// one frame commit of the same composed image with no render, and each flip
109// reports the video shown zero-copy at its latest content.
110func TestRunVideoOnOverlayDoesNotRender(t *testing.T) {
111 o, k, commits, commitMu := testOutputMu(t)
112 o.cursor, o.tearing, o.vrrProp = nil, false, 0
113 props := map[string]uint32{"zpos": pZpos}
114 for n, v := range planeProps {
115 props[n] = v
116 }
117 o.primary.props = props
118 o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
119 o.primary.formats = nil
120 o.pickOverlay()
121 flips := make(chan flipEvent, 1)
122 o.flipped = flips
123 r := portsmocks.NewMockRenderer(t)
124 pipeBuf := func() ports.DMABuf {
125 f, w, err := os.Pipe()
126 if err != nil {
127 t.Fatal(err)
128 }
129 w.Close()
130 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
131 }
132 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
133 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
134 r.EXPECT().UseTarget(mock.Anything).Return()
135 var renders atomic.Int32
136 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
137 renders.Add(1)
138 return nil, nil
139 })
140 r.EXPECT().Close().Return().Once()
141 var images atomic.Uint32
142 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
143 if b.ID == 0 {
144 return 70 + images.Add(1) - 1, nil // the output images
145 }
146 return uint32(100 + b.ID), nil
147 })
148 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
149 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
150 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
151 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
152 frameCommits := func() []commitRec {
153 commitMu.Lock()
154 defer commitMu.Unlock()
155 var out []commitRec
156 for _, c := range *commits {
157 if c.user&3 == userFrame {
158 out = append(out, c)
159 }
160 }
161 return out
162 }
163 scenes := make(chan ports.Scene, 1)
164 contents := newContentFeed(t, 1)
165 sink, presented := presentedSink(t, 64)
166 ctx, cancel := context.WithCancel(context.Background())
167 done := make(chan error, 1)
168 go func() {
169 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
170 }()
171 // Stop Run on every path, before the mocks check their calls.
172 t.Cleanup(func() {
173 cancel()
174 select {
175 case err := <-done:
176 if err != nil {
177 t.Error(err)
178 }
179 case <-time.After(2 * time.Second):
180 t.Error("Run did not stop")
181 }
182 })
183 static := ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
184 video := func(n int) ports.SurfaceContent {
185 return ports.SurfaceContent{ID: 2, Seq: uint64(n), Width: 100, Height: 100, Opaque: true,
186 DMABuf: &ports.DMABuf{ID: uint64(n%3 + 1), Width: 100, Height: 100, Format: fourccXRGB}}
187 }
188 contents.send(static, video(1))
189 scenes <- ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
190 {ID: 1, Rect: ports.Rect{W: 100, H: 100}},
191 {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
192 }}
193 // play delivers video frame n and flips every frame commit until a flip
194 // shows it; it returns that commit and its report. (Seen is what the
195 // output took in, which may run ahead of the flip.)
196 flipped := 0
197 vseq := uint32(0)
198 play := func(n int) (commitRec, ports.OutputPresented) {
199 t.Helper()
200 contents.send(video(n))
201 var c commitRec
202 var got ports.OutputPresented
203 waitFor(t, func() bool {
204 if cs := frameCommits(); len(cs) > flipped {
205 c = cs[len(cs)-1]
206 flipped = len(cs)
207 vseq++
208 flips <- flipEvent{crtc: tCrtc, user: c.user, when: time.Duration(vseq) * time.Millisecond, seq: vseq}
209 }
210 for {
211 select {
212 case p := <-presented:
213 if p.Flip != nil && p.Flip.Shows[2] == uint64(n) {
214 got = p
215 return true
216 }
217 default:
218 return false
219 }
220 }
221 })
222 return c, got
223 }
224 // Warm up until the video is on the overlay over a composed image.
225 n := 1
226 for ; n < 10; n++ {
227 c, _ := play(n)
228 if v, _ := c.req.value(tOverlay, pFB); v != 0 {
229 break
230 }
231 }
232 base, commitsBefore := renders.Load(), len(frameCommits())
233 const frames = 60
234 for i := range frames {
235 n++
236 c, p := play(n)
237 if v, _ := c.req.value(tOverlay, pFB); v != uint64(100+n%3+1) {
238 t.Fatalf("video frame %d: overlay fb %d", i, v)
239 }
240 if _, ok := c.req.value(tPrimary, pFence); ok {
241 t.Fatalf("video frame %d waits on a render", i)
242 }
243 // The flip reports the video shown zero-copy at its content.
244 if p.Flip.ZeroCopy != 2 || p.Flip.Shows[2] != uint64(n) || p.Seen[1] != 1 {
245 t.Fatalf("video frame %d: report %+v flip %+v", i, p, *p.Flip)
246 }
247 }
248 if got := len(frameCommits()) - commitsBefore; got != frames {
249 t.Fatalf("%d frame commits for %d video frames", got, frames)
250 }
251 if got := renders.Load() - base; got != 0 {
252 t.Fatalf("%d renders for %d video frames, want 0", got, frames)
253 }
254 // Three buffers in rotation: each probed once, then remembered.
255 commitMu.Lock()
256 probes := 0
257 for _, c := range *commits {
258 if c.flags&atomicTestOnly != 0 {
259 probes++
260 }
261 }
262 commitMu.Unlock()
263 if probes > 4 {
264 t.Fatalf("%d overlay probes for %d video frames of 3 buffers", probes, frames)
265 }
266}
267
268// Refusals on the overlay-only path: a refused probe composes the whole
269// scene at once; a refused commit drops the overlay and asks for a new frame,
270// leaving the images and the queue as they were.
271func TestSubmitFrameOverlayOnlyRefused(t *testing.T) {
272 for _, tc := range []struct {
273 name string
274 refuse func(r *atomicReq, flags uint32) bool
275 wantErr error
276 renders int
277 }{
278 {"probe", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly != 0 }, nil, 1},
279 {"commit", func(_ *atomicReq, flags uint32) bool { return flags&atomicTestOnly == 0 }, errOverlayDropped, 0},
280 } {
281 t.Run(tc.name, func(t *testing.T) {
282 refusing := false
283 o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
284 if refusing && tc.refuse(r, flags) {
285 return unix.EINVAL
286 }
287 return nil
288 })
289 props := map[string]uint32{"zpos": pZpos}
290 for n, v := range planeProps {
291 props[n] = v
292 }
293 o.primary.props, o.primary.formats = props, nil
294 o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}}
295 o.pickOverlay()
296 o.cursor, o.vrrProp = nil, 0
297 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).RunAndReturn(func(b *ports.DMABuf, _ uint32) (uint32, error) {
298 return uint32(80 + b.ID), nil
299 })
300 s, c := overlayScene()
301 r := portsmocks.NewMockRenderer(t)
302 r.EXPECT().UseTarget(mock.Anything).Return()
303 var drawn []int
304 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
305 drawn = append(drawn, len(s.Windows))
306 return nil, nil
307 })
308 overlayFrames(t, o, r, s, c, 1) // composed, front image held
309 back, queued := o.back, o.queued
310 refusing = true
311 win := c[2]
312 buf := *win.DMABuf
313 buf.ID, win.DMABuf, win.Seq = 20, &buf, 2
314 c[2] = win
315 _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
316 if !errors.Is(err, tc.wantErr) && err != tc.wantErr {
317 t.Fatalf("err %v, want %v", err, tc.wantErr)
318 }
319 if got := len(drawn) - 1; got != tc.renders {
320 t.Fatalf("%d renders, want %d", got, tc.renders)
321 }
322 if tc.renders == 1 && drawn[1] != 2 {
323 t.Fatalf("composed %d windows, want the whole scene", drawn[1])
324 }
325 if tc.wantErr != nil {
326 if o.back != back || o.queued != queued || o.clientFBs[20].overlayFailed != "cursor_conflict" {
327 t.Fatalf("back %d queued %d reason %q", o.back, o.queued, o.clientFBs[20].overlayFailed)
328 }
329 // The retry composes the window.
330 refusing = false
331 if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); err != nil {
332 t.Fatal(err)
333 }
334 if drawn[len(drawn)-1] != 2 {
335 t.Fatalf("retry composed %d windows", drawn[len(drawn)-1])
336 }
337 }
338 })
339 }
340}
341
342// A commit result with the overlay on: refusals the overlay causes ask for a
343// frame without it and leave the queue; a commit queues the buffer.
344func TestFinishOverlayCommit(t *testing.T) {
345 boom := errors.New("boom")
346 for _, tc := range []struct {
347 name string
348 err error
349 want error
350 wantQueued uint64
351 reason string
352 }{
353 {"committed", nil, nil, 9, ""},
354 {"colour refused", errColorRefused, errOverlayDropped, 1, "color_refused"},
355 {"overlay conflict", unix.EINVAL, errOverlayDropped, 1, "cursor_conflict"},
356 {"other", boom, boom, 1, ""},
357 } {
358 t.Run(tc.name, func(t *testing.T) {
359 o, _, _ := testOutput(t)
360 o.queued = 1
361 o.clientFBs[9] = &clientFB{}
362 if err := o.finishOverlayCommit(tc.err, 9); err != tc.want {
363 t.Fatalf("err %v, want %v", err, tc.want)
364 }
365 if o.queued != tc.wantQueued || o.overlayReason != tc.reason {
366 t.Fatalf("queued %d reason %q", o.queued, o.overlayReason)
367 }
368 })
369 }
370}
371
372// A frame whose buffer KMS took before is not probed again.
373func TestOverlayProbeCached(t *testing.T) {
374 o, k, commits := overlayOutput(t)
375 o.cursor.image = true
376 o.cursor.Move(10, 10)
377 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
378 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
379 s, c := overlayScene()
380 ov, _ := o.overlayFrame(s, c)
381 for range 3 {
382 if !o.testOverlay(70, ov) {
383 t.Fatal("refused")
384 }
385 }
386 if len(*commits) != 1 {
387 t.Fatalf("%d probes, want 1", len(*commits))
388 }
389 // Another place, primary image or cursor is probed.
390 moved := ov
391 moved.rect.X++
392 o.testOverlay(70, moved)
393 o.testOverlay(71, ov)
394 o.cursor.Move(20, 10)
395 o.testOverlay(70, ov)
396 if len(*commits) != 4 {
397 t.Fatalf("%d probes, want 4", len(*commits))
398 }
399 // The probe tested the cursor where the cache remembers it.
400 if v, _ := (*commits)[3].req.value(tCursor, 14); v != 20-uint64(o.cursor.hotX) {
401 t.Fatalf("probe cursor x %d", v)
402 }
403 // New colour state (HDR brightness) is probed again.
404 o.colorMult++
405 o.testOverlay(70, ov)
406 if len(*commits) != 5 {
407 t.Fatalf("%d probes after a colour change, want 5", len(*commits))
408 }
409 // A removed client framebuffer may give its id to another buffer.
410 k.EXPECT().rmFB(uint32(88)).Return(nil).Once()
411 o.dropClientFBs(time.Now(), true)
412 o.testOverlay(70, ov)
413 if len(*commits) != 6 {
414 t.Fatal("probe cached across a framebuffer removal")
415 }
416 // A modeset (VT resume) forgets them.
417 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
418 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Maybe()
419 if err := o.modeset(); err != nil {
420 t.Fatal(err)
421 }
422 before := len(*commits)
423 o.testOverlay(70, ov)
424 if len(*commits) != before+1 {
425 t.Fatal("probe cached across a modeset")
426 }
427}
428
429// Anything but the overlay window changing composes: another window's
430// content, the scene, or an image drawn by another path.
431func TestSubmitFrameOverlayOnlyNeedsSameImage(t *testing.T) {
432 for name, edit := range map[string]func(o *Output, s *ports.Scene, c map[ports.WindowID]ports.SurfaceContent){
433 "other window content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
434 w := c[1]
435 w.Seq++
436 c[1] = w
437 },
438 "scene": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
439 s.Background = "#ffffff"
440 },
441 "image forgotten": func(o *Output, _ *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
442 o.front.forget()
443 },
444 "layer content": func(_ *Output, _ *ports.Scene, c map[ports.WindowID]ports.SurfaceContent) {
445 l := c[5]
446 l.Seq++
447 c[5] = l
448 },
449 // The scene's slices rewritten in place by their owner: what the
450 // image holds is a copy.
451 "scene slice rewritten": func(_ *Output, s *ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) {
452 s.Windows[0].Rect.W = 90
453 },
454 } {
455 t.Run(name, func(t *testing.T) {
456 o, k, _ := overlayOutput(t)
457 o.cursor, o.vrrProp = nil, 0
458 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Maybe()
459 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
460 s, c := overlayScene()
461 s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerBottom, Rect: ports.Rect{W: 10, H: 10}}}
462 c[1] = ports.SurfaceContent{ID: 1, Seq: 1, Width: 100, Height: 100, Opaque: true, Solid: &ports.SolidColor{A: 1}}
463 c[5] = ports.SurfaceContent{ID: 5, Seq: 1, Width: 10, Height: 10, Opaque: true, Solid: &ports.SolidColor{A: 1}}
464 r := portsmocks.NewMockRenderer(t)
465 r.EXPECT().UseTarget(mock.Anything).Return().Twice()
466 var drawn []int
467 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
468 drawn = append(drawn, len(s.Windows))
469 return nil, nil
470 }).Twice()
471 overlayFrames(t, o, r, s, c, 1)
472 edit(o, &s, c)
473 overlayFrames(t, o, r, s, c, 1)
474 if len(drawn) != 2 || drawn[1] != 1 {
475 t.Fatalf("rendered %v", drawn)
476 }
477 })
478 }
479}