1package drm
2
3import (
4 "os"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/stretchr/testify/mock"
10 "golang.org/x/sys/unix"
11)
12
13const pZpos = 30
14
15// overlayOutput is a test output with an overlay plane above the primary.
16func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
17 o, k, commits := testOutput(t, errs...)
18 props := map[string]uint32{"zpos": pZpos}
19 for n, v := range planeProps {
20 props[n] = v
21 }
22 o.primary.props = props
23 o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}}
24 o.primary.formats = nil // the overlay's own formats decide
25 o.pickOverlay()
26 if o.overlay == nil {
27 t.Fatal("no overlay picked")
28 }
29 return o, k, commits
30}
31
32// A 1:1 opaque dmabuf window with nothing above goes on the overlay.
33func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
34 s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
35 {ID: 1, Rect: ports.Rect{W: 100, H: 100}},
36 {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
37 }}
38 return s, map[ports.WindowID]ports.SurfaceContent{
39 2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}},
40 }
41}
42
43func TestOverlayCandidate(t *testing.T) {
44 s, c := overlayScene()
45 if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 {
46 t.Fatalf("candidate %v %q", w.ID, reason)
47 }
48 s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}})
49 if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
50 t.Fatalf("reason %q", reason)
51 }
52 s, _ = overlayScene()
53 s.Scale = 1.5
54 if _, _, reason := overlayCandidate(s, c); reason != "scaled" {
55 t.Fatalf("reason %q", reason)
56 }
57 s, _ = overlayScene()
58 s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}}
59 if _, _, reason := overlayCandidate(s, c); reason != "layer_above" {
60 t.Fatalf("reason %q", reason)
61 }
62}
63
64// The overlay is chosen and the composed scene leaves its window out;
65// the frame commit carries the plane at the window's place.
66func TestOverlayChosen(t *testing.T) {
67 o, k, commits := overlayOutput(t)
68 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
69 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
70 s, c := overlayScene()
71 ov, rest := o.overlayFrame(s, c)
72 if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
73 t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
74 }
75 if !o.testOverlay(70, ov) {
76 t.Fatal("test refused")
77 }
78 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
79 t.Fatal(err)
80 }
81 last := (*commits)[len(*commits)-1]
82 if v, _ := last.req.value(tOverlay, pFB); v != 88 {
83 t.Fatalf("overlay fb %d", v)
84 }
85 if v, _ := last.req.value(tOverlay, 14); v != 100 {
86 t.Fatalf("overlay x %d", v)
87 }
88 if o.overlayOn != 9 {
89 t.Fatalf("overlayOn %d", o.overlayOn)
90 }
91}
92
93// A TEST_ONLY refusal composes the window and is cached on the buffer.
94func TestOverlayTestRefusedFallsBack(t *testing.T) {
95 o, k, _ := overlayOutput(t, unix.EINVAL)
96 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
97 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
98 s, c := overlayScene()
99 ov, _ := o.overlayFrame(s, c)
100 if o.testOverlay(70, ov) {
101 t.Fatal("refusal taken")
102 }
103 if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
104 t.Fatal("refused buffer tried again")
105 }
106 // The overlay's refusal does not bar direct scanout of the buffer.
107 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
108 if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
109 t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
110 }
111}
112
113// Popups are drawn above every window: one listed first still blocks the
114// overlay; a bordered window does too.
115func TestOverlayPopupAndBorder(t *testing.T) {
116 s, c := overlayScene()
117 s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...)
118 if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
119 t.Fatalf("popup: %q", reason)
120 }
121 s, _ = overlayScene()
122 s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}}
123 if _, _, reason := overlayCandidate(s, c); reason != "border" {
124 t.Fatalf("border: %q", reason)
125 }
126}
127
128// A cursor commit refused with the overlay on drops the overlay: the
129// next frame is composed.
130func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
131 o, k, _ := overlayOutput(t, nil, unix.EINVAL)
132 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
133 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
134 s, c := overlayScene()
135 ov, _ := o.overlayFrame(s, c)
136 if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
137 t.Fatal(err)
138 }
139 o.pending = false
140 o.cursor.image = true
141 o.cursor.Move(150, 50)
142 if err := o.commitState(false); err != errOverlayDropped {
143 t.Fatalf("err %v", err)
144 }
145 if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
146 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
147 }
148}
149
150// An explicit-sync window on the overlay: the plane carries a duplicate of
151// the client's acquire fence, closed once the frame is committed.
152func TestOverlayCarriesAcquireFence(t *testing.T) {
153 o, k, commits := overlayOutput(t)
154 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
155 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
156 s, c := overlayScene()
157 r, w, err := os.Pipe()
158 if err != nil {
159 t.Fatal(err)
160 }
161 defer r.Close()
162 defer w.Close()
163 win := c[2]
164 win.Acquire = r
165 c[2] = win
166 ov, _ := o.overlayFrame(s, c)
167 if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
168 t.Fatal("overlay has no duplicate of the acquire fence")
169 }
170 fd := ov.acquire.Fd()
171 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
172 t.Fatal(err)
173 }
174 ov.close()
175 last := (*commits)[len(*commits)-1]
176 if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
177 t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
178 }
179 if _, err := ov.acquire.Stat(); err == nil {
180 t.Fatal("duplicate left open")
181 }
182}