1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
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// A fullscreen dmabuf the kernel takes is scanned out: nothing renders.
16func TestSubmitFrameScanout(t *testing.T) {
17 o, k, commits := testOutput(t)
18 o.cursor = nil
19 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
20 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
21 s, c := fullscreenScene()
22 r := portsmocks.NewMockRenderer(t)
23 direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
24 if err != nil || !direct {
25 t.Fatalf("direct %v err %v", direct, err)
26 }
27 if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 88 {
28 t.Fatalf("primary fb %d", v)
29 }
30 if o.queued != 9 {
31 t.Fatalf("queued %d", o.queued)
32 }
33}
34
35// A scanout flip the kernel refuses composes the same frame instead.
36func TestSubmitFrameScanoutRefusedComposes(t *testing.T) {
37 o, k, commits := testOutput(t, unix.EINVAL)
38 o.cursor, o.tearing, o.vrrProp = nil, false, 0
39 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
40 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
41 s, c := fullscreenScene()
42 r := portsmocks.NewMockRenderer(t)
43 r.EXPECT().UseTarget(0).Return().Once()
44 r.EXPECT().Render(s, c).Return(nil, nil).Once()
45 direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
46 if err != nil || direct {
47 t.Fatalf("direct %v err %v", direct, err)
48 }
49 if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 70 || o.back != 1 {
50 t.Fatalf("primary fb %d back %d", v, o.back)
51 }
52 if o.reason != "flip_refused" {
53 t.Fatalf("reason %q", o.reason)
54 }
55}
56
57// An overlay the kernel refuses in its test commit is composed too.
58func TestSubmitFrameOverlayRefusedComposesAll(t *testing.T) {
59 o, k, commits := overlayOutput(t, unix.EINVAL)
60 o.cursor = nil
61 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
62 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
63 s, c := overlayScene()
64 r := portsmocks.NewMockRenderer(t)
65 r.EXPECT().UseTarget(0).Return().Once()
66 var drawn []int
67 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
68 drawn = append(drawn, len(s.Windows))
69 return nil, nil
70 }).Twice()
71 direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
72 if err != nil || direct {
73 t.Fatalf("direct %v err %v", direct, err)
74 }
75 // First without the overlay window, then the whole scene.
76 if len(drawn) != 2 || drawn[0] != 1 || drawn[1] != 2 {
77 t.Fatalf("rendered windows %v", drawn)
78 }
79 if v, ok := (*commits)[len(*commits)-1].req.value(tOverlay, pFB); ok && v != 0 {
80 t.Fatalf("overlay committed: fb %d", v)
81 }
82}
83
84// An overlay commit the kernel refuses after its test passed drops the
85// overlay: the caller asks for a new, fully composed frame.
86func TestSubmitFrameOverlayCommitRefusedDropsOverlay(t *testing.T) {
87 o, k, _ := overlayOutput(t, nil, unix.EINVAL)
88 o.cursor, o.vrrProp = nil, 0
89 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
90 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
91 s, c := overlayScene()
92 r := portsmocks.NewMockRenderer(t)
93 r.EXPECT().UseTarget(0).Return().Once()
94 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
95 if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); !errors.Is(err, errOverlayDropped) {
96 t.Fatalf("err %v", err)
97 }
98 if o.back != 0 || o.clientFBs[9].overlayFailed != "cursor_conflict" {
99 t.Fatalf("back %d reason %q", o.back, o.clientFBs[9].overlayFailed)
100 }
101}
102
103// A render failure stops the output and is not a commit error.
104func TestSubmitFrameRenderError(t *testing.T) {
105 o, _, commits := testOutput(t)
106 boom := errors.New("boom")
107 r := portsmocks.NewMockRenderer(t)
108 r.EXPECT().UseTarget(0).Return().Once()
109 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, boom).Once()
110 _, err := o.submitFrame(context.Background(), r, ports.Scene{Seq: 1}, nil, map[ports.WindowID]uint64{}, nil, nil)
111 var fatal renderError
112 if !errors.As(err, &fatal) || !errors.Is(err, boom) {
113 t.Fatalf("err %v", err)
114 }
115 if len(*commits) != 0 || o.back != 0 {
116 t.Fatalf("committed %d back %d", len(*commits), o.back)
117 }
118}
119
120func fullscreenScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
121 s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
122 {ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true},
123 }}
124 return s, map[ports.WindowID]ports.SurfaceContent{
125 1: {ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccXRGB}},
126 }
127}